WebGL之二维变换与矩阵

730 阅读4分钟

平移

在webgl中要想平移物体我们可以有两种方法:

  • 改变传递给顶点着色器的数据
  • 在顶点着色器中改变传进来的数据

先来看第一种方法:

  var translation = [0, 0];   // 偏移量
  var width = 100; // 矩形宽度
  var height = 30; // 矩形高度
  var color = [Math.random(), Math.random(), Math.random(), 1]; // 矩形颜色值
  
    // 绘制场景
  function drawScene() {
    webglUtils.resizeCanvasToDisplaySize(gl.canvas);
 
    // 告诉WebGL如何从裁剪空间对应到像素
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
 
    // 清空画布
    gl.clear(gl.COLOR_BUFFER_BIT);
 
    // 使用我们的程序
    gl.useProgram(program);
 
    // 启用属性
    gl.enableVertexAttribArray(positionLocation);
 
    // 绑定位置缓冲
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
 
    // 设置矩形参数
    setRectangle(gl, translation[0], translation[1], width, height);
 
    // 告诉属性怎么从positionBuffer中读取数据 (ARRAY_BUFFER)
    var size = 2;          // 每次迭代运行提取两个单位数据
    var type = gl.FLOAT;   // 每个单位的数据类型是32位浮点型
    var normalize = false; // 不需要归一化数据
    var stride = 0;        // 0 = 移动单位数量 * 每个单位占用内存(sizeof(type))
    var offset = 0;        // 从缓冲起始位置开始读取
    gl.vertexAttribPointer(
        positionLocation, size, type, normalize, stride, offset)
 
    // 设置分辨率
    gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
 
    // 设置颜色
    gl.uniform4fv(colorLocation, color);
 
    // 绘制矩形
    var primitiveType = gl.TRIANGLES;
    var offset = 0;
    var count = 6;
    gl.drawArrays(primitiveType, offset, count);
  }

这种方法咋一看似乎挺方便的,平移的时候我们只要改变translation的值,然后调用drawScene方法就可以了。但是假如现在一个物体由几百个三角形组成呢?那我们就不得不为三角形的每个顶点加上偏移量了,这可能会让代码显得不那么优雅。那这时第二种方法就显得简洁许多,我们在着色器中加上一个偏移量,在WebGL之入门这一章我们已经知道webgl在生成每个顶点的时候都会调用顶点着色器,在这里为每个顶点加上一个偏移量就可以实现图像的平移。

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
 
uniform vec2 u_resolution;
uniform vec2 u_translation;
 
void main() {
   // 加上平移量
   vec2 position = a_position + u_translation;
 
   // 从像素坐标转换到 0.0 到 1.0
   vec2 zeroToOne = position / u_resolution;
   ...

旋转

坐标旋转公式的数学推导可以看一下这篇文章过程很简单,这里就不赘述了。总之最后我们可以得到这样一个公式:

//angle为旋转的角度,x、y是旋转前的坐标
x1=cos(angle)*x-sin(angle)*y;
y1=cos(angle)*y+sin(angle)*x;

加上旋转我们再来修改一下我们的顶点着色器:

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
 
uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
 
void main() {
  // 旋转位置
  vec2 rotatedPosition = vec2(
     a_position.x * u_rotation.y + a_position.y * u_rotation.x,
     a_position.y * u_rotation.y - a_position.x * u_rotation.x);
 
  // 加上平移
  vec2 position = rotatedPosition + u_translation;

缩放

缩放更简单,我们只需要将坐标位置乘上缩放值就可以了。

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
 
uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
uniform vec2 u_scale;
 
void main() {
  // 缩放
  vec2 scaledPosition = a_position * u_scale;
 
  // 旋转
  vec2 rotatedPosition = vec2(
     scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x,
     scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x);
 
  // 平移
  vec2 position = rotatedPosition + u_translation;

变换矩阵

之前我们讲了平移、旋转、缩放三种变化,不知道大家注意到没有这三种变换的先后顺序不同最后的结果也是不同的。例如:

  1. 缩放 2, 1 ,旋转30度,然后平移 100, 0

  1. 平移 100, 0 ,旋转30度,然后缩放 2, 1

不同的情况需要编写不同的着色器,就会变得十分麻烦。针对这样的情况就需要用到变化矩阵了,在二维中我们使用3*3的矩阵。由于二维的点只有x,y所以我们将第三个点设置为1.所以我们的点现在可以认为是(x,y,1);假如我们现在有这样一个矩阵。

1.0	0.0	0.0
0.0	1.0	0.0
tx	ty	1.0

与(x,y,1)相乘最后的结果是 (x+tx, y+ty, 1),这不就是我们想要的平移么。同理假如矩阵是:

c	-s	0.0
s	c	0.0
0.0	0.0	1.0

与(x,y,1)相乘最后的结果是 (x * c + y * s, x * -s + y * c, 1),这是是缩放的结果。再假如矩阵是:

sx	0.0	0.0
0.0	sy	0.0
0.0	0.0	1.0

与(x,y,1)相乘最后的结果是 (x * sx, y * sy, 1)这是缩放的结果。

看上去挺神奇的,但是好像没什么用。其实不然,我们再来修改一下我们的顶点着色器:

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
 
uniform vec2 u_resolution;
uniform mat3 u_matrix;
 
void main() {
  // 将位置乘以矩阵
  vec2 position = (u_matrix * vec3(a_position, 1)).xy;
  ...

这次我们将顶点位置和矩阵相乘,所有的变换信息都包含的在传入的矩阵里面,这样我们就解决了上次提出的问题,我们就可以在js中根据需要随意改变变换矩阵相乘的顺序,只把变换之后的结果传入顶点着色器。