当前位置: 首页 > 资讯 >

2D Transform

大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 9 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。讲完 WebGL 基本机制後,本章节讲述的是 texture 以及 2D transform,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容

在上篇加入了 u_offset 来控制物件的平移,如果我们现在想要进行缩放、旋转,那麽就得在传入更多 uniform,而且这些动作会有谁先作用的差别,比方说,先往右旋转 90 度、接着往右平移 30px,与先往右平移 30px、再往右旋转 90 度,这两着会获得不一样的结果,如果只是传平移量、缩放量、旋转度数,在 vertex shader 内得有一个写死的的先後作用顺序,那麽在应用层面上就会受到这个先後顺序的限制,因此在 vertex 运算座标的时候,通常会运用线性代数,传入一个矩阵,这个矩阵就能包含任意先後顺序的平移、缩放、旋转动作

矩阵运算

为什麽一个矩阵就能够包含任意先後顺序的平移、缩放、旋转动作?笔者觉得笔者不论怎麽用文字怎麽解释,都不会比这部 Youtube 影片解释得好:作为合成的矩阵乘法 by 3Blue1Brown,如果觉得有需要,也可以从这系列影片的第一部影片开始看。总之,所有的平移、缩放、旋转动作(现在开始应该说转换 -- transform)都可以用一个矩阵表示,而把这些 transform 矩阵与向量相乘後,可以得到平移、缩放或旋转後的结果,而且重点来了,我们也可以事先把多个矩阵相乘,这个相乘的结果与向量相乘与个别依序与向量相乘会得到相同的 transform;举例来说,先旋转 45 度、往右平移 30px、再旋转 45 度,这三个 transform 依序作用在一个向量上,与把这三个 transform 代表的矩阵先相乘,再跟向量相乘,会得到一样的结果,基於这个特性,我们只要传送这个相乘出来的矩阵即可

但是上面 3Blue1Brown 的影片使用了数学界的惯例,一个二维的向量是这样表示:

math-vec2

然而,在程序语言(如 Javascript)中也只能这样写:[x, y],因此在数学式与程序语言写法之间要把行列做对调,一个像这样的 2x2 矩阵在程序语言中的写法如蓝色文字所示:

cs-matrix

对向量进行 transform 的『矩阵乘以向量』右到左算的写法,假设向量为 [5, 6]:

math-matrix-product

在 shader 或是 Javascript 内一样是右到左,大致会变成这样写:

v = [5, 6]
m = [
  1, 2,
  3, 4,
]
multiply(m, v) => [23, 34]

在模拟电脑中的矩阵运算时,如果觉得在纸笔跟程序语言表示之间每次都要做行列转换很麻烦,那麽也可以改变一下矩阵计算的方式,下图中每个运算式的左方小格子是由右方相乘的两个矩阵的长条形区域相乘而成:

matrix-product-difference

调整 vertex shader 使用矩阵运算

建立 u_matrix 取代 u_offset 以及 u_resolution,然後让 u_matrixa_position 相乘,相乘的结果即为任意顺序的平移、缩放、旋转做完後的座标位置。没错,针对萤幕宽高调整 clip space 的 u_resolution 也可以在矩阵中顺便带着

 attribute vec2 a_position;
 attribute vec2 a_texcoord;
  
-uniform vec2 u_resolution;
-uniform vec2 u_offset;
+uniform mat3 u_matrix;
  
 varying vec2 v_texcoord;
  
 void main() {
-  vec2 position = a_position + u_offset;
-  gl_Position = vec4(
-    position / u_resolution * vec2(2, -2) + vec2(-1, 1),
-    0, 1
-  );
+  vec3 position = u_matrix * vec3(a_position.xy, 1);
+  gl_Position = vec4(position.xy, 0, 1);
   v_texcoord = a_texcoord;
 }
 `;

u_matrix 的资料型别为 mat3,为 3x3 矩阵,而且在 GLSL 中 * 运算子可以接受 mat3vec3 相乘,u_matrix * vec3(a_position.xy, 1) 就可以得到『使用矩阵转换向量』後的结果,可是说是把矩阵运算直接内建在语法中了;同时也可以注意到 u_matrix 为 3x3 矩阵,我们还在 2D,为何要用 mat3 呢?因为 2x2 矩阵是无法包含『平移 (translate)』的,要做到平移,必须在运算时增加一个维度,并填上 1,使得向量为 [x, y, 1],接着平移矩阵就是单位矩阵在多余的维度中放上要平移的量,举个例子,向量为 [4, 5],要平移 [2, 3]:

multiply(
  [
    1, 0, 0,
    0, 1, 0,
    2, 3, 1,
  ],
  [4, 5, 1]
) // =>
// [
//   6, 8 ,1
// ]

数学上写起来像是这样:

math-translate

稍微观察一下中间的计算过程,应该就能知道为什麽这样可以形成平移,在多余维度上的数字会与向量的 1 相乘加在原本的座标上

当然,uniform 位置的取得得修改一下:

   const uniforms = {
-    resolution: gl.getUniformLocation(program, 'u_resolution'),
+    matrix: gl.getUniformLocation(program, 'u_matrix'),
     texture: gl.getUniformLocation(program, 'u_texture'),
-    offset: gl.getUniformLocation(program, 'u_offset'),
   };

平移以及投影的矩阵

shader, uniform 部份准备好後,建立 lib/matrix.js,用来产生特定 transform 用的矩阵,同时也实做 Javascript 端的矩阵相乘运算,才能『事先』运算、合成好矩阵给 GPU 使用。除了矩阵相乘 matrix3.mulitply() 之外,也把上面提到的平移矩阵 matrix3.translate() 实做好:

export const matrix3 = {
  multiply: (a, b) => ([
    a[0]*b[0] + a[3]*b[1] + a[6]*b[2], /**/ a[1]*b[0] + a[4]*b[1] + a[7]*b[2], /**/ a[2]*b[0] + a[5]*b[1] + a[8]*b[2],
    a[0]*b[3] + a[3]*b[4] + a[6]*b[5], /**/ a[1]*b[3] + a[4]*b[4] + a[7]*b[5], /**/ a[2]*b[3] + a[5]*b[4] + a[8]*b[5],
    a[0]*b[6] + a[3]*b[7] + a[6]*b[8], /**/ a[1]*b[6] + a[4]*b[7] + a[7]*b[8], /**/ a[2]*b[6] + a[5]*b[7] + a[8]*b[8],
  ]),

  translate: (x, y) => ([
    1, 0, 0,
    0, 1, 0,
    x, y, 1,
  ]),
};

那麽就剩下针对萤幕宽高调整 clip space 的矩阵,这样的矩阵称为 projection,也就是把场景中一个宽高区域框起来,『投影』在 clip space -- 画布上,看着原本 shader 程序码拆解一下:

position / u_resolution * vec2(2, -2) + vec2(-1, 1)

可以发现,我们要分别对 x 座标缩放 2 / u_resolution.x 倍、对 y 轴缩放 -2 / u_resolution.y 倍,做完缩放後平移 vec2(-1, 1),平移已经知道要怎麽做了,那麽缩放的矩阵要怎麽产生呢?观察一下这个算式:

math-scale

单位矩阵中对应维度的数字即为缩放倍率,在这边把 x 座标乘以 2、y 座标乘以 3,其他栏位为零不会影响,因此缩放矩阵的产生 matrix3.scale() 这样实做:

  scale: (sx, sy) => ([
    sx, 0,  0,
    0,  sy, 0,
    0,  0,  1,
  ]),

最後投影矩阵的产生 matrix3.projection(),为平移与缩放相乘:

  projection: (width, height) => (
    matrix3.multiply(
      matrix3.translate(-1, 1),
      matrix3.scale(2 / width, -2 / height),
    )
  ),

记得矩阵运算与一般运算运算不同,向量放在最右边,向左运算,因此 matrix3.translate(-1, 1) 虽然放在前面,但是其 transform 是在 matrix3.translate(-1, 1) 之後的

这样一来 matrix3 就准备好,回到主程序引入:

import { matrix3 } from './lib/matrix.js';

画龙点睛的时候来了,在 render() function 设定 uniform 的地方产生、运算矩阵:

const viewMatrix = matrix3.projection(gl.canvas.width, gl.canvas.height);
const worldMatrix = matrix3.translate(...state.offset);

gl.uniformMatrix3fv(
  uniforms.matrix,
  false,
  matrix3.multiply(viewMatrix, worldMatrix),
);

笔者先制作名叫 viewMatrix 的矩阵,包含 matrix3.projection() 负责投影到 clip space;以及另一个矩阵称为 worldMatrix,表示该物件在场景中位置的 transform。最後把 viewMatrixworldMatrix 相乘,得到包含所有 transform 的矩阵,并使用 gl.uniformMatrix3fv() 设定到 uniform 上,其第二个参数表示要不要做转置 transpose,我们没有需要因此传入 false

存档重整,使用 matrix 做 transform 的版本看起来跟先前 offset 的版本一模一样:live 版本

2d-transform-penguin

总结一下 transform,首先 u_matrixviewMatrixworldMatrix 相乘viewMatrixmatrix3.projection()worldMatrixmatrix3.translate(offset),最後在 vertex shader 内 u_matrix * a_position,来展开一路从 a_positiongl_Position / clip space 的运算式(忽略维度的调整):

gl_Position = u_matrix * a_position;
=> gl_Position = viewMatrix * worldMatrix * a_position;
=> gl_Position = matrix3.projection() * matrix3.translate(offset) * a_position;

而 linear transform 在电脑中与数学上都是由右到左计算的,因此最後整个计算下来的效果就是:先对 a_position 平移 offset 量 (translate),接着投影萤幕范围到 clip space (projection)

完整程序码可以在这边找到:github.com/pastleo/webgl-ironman/commit/20f165c

虽然看似没有什麽新功能,且只有两个 transform,但是建构出来的流程让我们可以很容易地加入更多 transform,也更适合之後 3D 场景中复杂的物件位置到萤幕上位置的运算,待下篇再继续加入更多 2D transform


相关文章:

  • 2021 — 找工作 (上)
  • Day-28 番外篇:现身於电视上的掌中精灵 PSP
  • iOS APP 开发 OC 第十天,block
  • [Day11]PHP函数01
  • Day 12 - Length of Last Word
  • 【零基础成为 AI 解梦大师秘笈】Day30 - Django 整合部署 AI model
  • Day 28-ASP.NET & SQL资料库制作留言板(上)
  • 【後转前要多久】# Day10 CSS - CSS常用属性I (文字、背景)
  • [Day 26] LIFF InitPlugins
  • Day 4 - Array Cardio Day 1
  • Day07 - 【入门篇】什麽是OAuth
  • Day25:Dynamic Programming(DP) - 动态规划(上)
  • 成为工具人应有的工具包-27 ShellMenuView
  • 很难 vs 不可能
  • day9: CSS style 规划 - CSS in JS(emotion 使用 - 3)
  • 自建CDN教程:如何做一个自己的CDN
  • 软件分享:xshell6/xftp6个人版下载,无需破解,永久免费使用
  • 阿里云国际版怎么注册绑卡购买香港美国新加坡云主机【2020年最新教程】
  • 最便宜的国外VPS推荐:5美金以下的VPS大全
  • 财富自由怎么实现?如何做到财富自由
  • G口服务器推荐:G口服务器VPS哪家好
  • Git是什么?
  • 教程/魔改BBR 一键安装脚本 for CentOS/Debian 7+
  • vultr.com怎么申请退款教程和方法
  • 狗狗币怎么获得?狗狗币挖矿教程和狗狗币使用方法
  • Namesilo域名注册教程和域名注册流程方法
  • Vultr / Digitalocean开通教程和购买教程,Vultr / Digitalocean如何切换IP教程方法
  • 俄取消商品知识产权限制 盗版游戏、电影等商标合法化
  • 国外代发货教程:教你如何一件代发做跨境电商国外市场
  • HD钱包是什么?比特币钱包原理