一起学 WebGL:图形变形以及矩阵变换

开发 前端
WebGL 用的是按列主序(column major order)规则,即按列填充矩阵,从左往右,属于主流。

之前绘制了三角形,我们现在给它做一个变形操作。

对一个三角形进行变形,其实就是重新这个三角形的三个顶点的位置,计算完后再绘制出来,相比原来就发生了变形。

变形常见的有位移、选择、缩放。位移,其实就是给每个顶点的各个坐标值加上偏移量 dx、dy、dz。旋转稍微复杂些,用到了三角函数。最后是缩放,就是简单地各个分量乘以缩放比例系数。

这些变换可以抽象简化成对应的变换矩阵,方便我们用统一的方式作表达,并配合矩阵乘法的结合律,将多个变形矩阵合并成一个复合矩阵,减少计算量。

直接进入正题,看看怎么用 WebGL 实现矩阵变换。

绘制三角形

我们先绘制一个普通的没做过变形的三角形。

demo 地址:

https://codesandbox.io/s/gbh1xf。

代码:

/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");

const vertexShaderSrc = `
attribute vec4 a_Position;
void main() {
 gl_Position = a_Position;
 gl_PointSize = 10.0;
}
`;

const fragmentShaderSrc = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

/**** 渲染器生成处理 ****/
// 创建顶点渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 创建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序对象
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;

// 顶点数据
const vertices = new Float32Array([
  // 第一个点
  0,
  0.5,
  // 第二个点
  -0.5,
  -0.5,
  // 第三个点
  0.5,
  -0.5
]);

// 创建缓存对象
const vertexBuffer = gl.createBuffer();
// 绑定缓存对象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向缓存区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 获取 a_Position 变量地址
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

// 允许访问缓存区
gl.enableVertexAttribArray(a_Position);

/*** 绘制 ***/
// 清空画布,并指定颜色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);

渲染效果:

图片

位移

位移,最简单的方式是再声明一个 u_Translation 向量,和 a_Position 相加就完事了:

但还是矩阵比较方便,具有可以统一格式,计算复合矩阵等优势。通常变形都是复杂的,旋转后平移然后再缩放一套下来,矩阵还是很重要的。

顶点着色器的代码修改为:

const vertexShaderSrc = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
 gl_Position = u_xformMatrix * a_Position;
}
`;

西瓜哥在这里加多了一个 u_xformMatrix 变量。

首先用了 uniform 类型修饰符,表示这个变量不会逐顶点发生变化,是固定的。mat4 表示一个 4x4 矩阵,一个有 16 个浮点数的一维数组。

来看看通过矩阵进行位移的实现。位移矩阵如下:

对应的 Float32Array 数组为:

/****** 位移矩阵 ****/
const dx = 0.5; // 向右移动
const dy = -0.3; // 向下移动
// z 先不管,没用到透视矩阵,设置值也看不到效果

const xformMatrix = new Float32Array([
  1, 0, 0, 0,

  0, 1, 0, 0,

  0, 0, 1, 0,

  dx, dy, 0, 1
]);

WebGL 用的是按列主序(column major order)规则,即按列填充矩阵,从左往右,属于主流。

还有一种是按行主序(row major order)的,也就是将遍历数组一行行填充到矩阵,从上往下。比较少见。

接着把这个数组怼到前面顶点着色器声明的 u_xformMatrix 变量中。

const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);

这里是用 gl.uniformMatrix4fv 来设置 4x4 矩阵的值。

完整代码:

/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");

const vertexShaderSrc = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
 gl_Position = u_xformMatrix * a_Position;
}
`;

const fragmentShaderSrc = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

/**** 渲染器生成处理 ****/
// 创建顶点渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 创建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序对象
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;

// 顶点数据
const vertices = new Float32Array([
  // 第一个点
  0,
  0.5,
  // 第二个点
  -0.5,
  -0.5,
  // 第三个点
  0.5,
  -0.5
]);

// 创建缓存对象
const vertexBuffer = gl.createBuffer();
// 绑定缓存对象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向缓存区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 获取 a_Position 变量地址
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

// 允许访问缓存区
gl.enableVertexAttribArray(a_Position);

/****** 位移矩阵 ****/
const dx = 0.5; // 向右移动
const dy = -0.3; // 向下移动
// z 先不管,没用到透视矩阵,设置值也看不到效果

const xformMatrix = new Float32Array([
  1,
  0,
  0,
  0,

  0,
  1,
  0,
  0,

  0,
  0,
  1,
  0,

  dx,
  dy,
  0,
  1
]);
const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);

/*** 绘制 ***/
// 清空画布,并指定颜色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);

demo 地址:

https://codesandbox.io/s/09ujp5?file=/index.js。

看看效果:

图片

成功向右并向下移动了一段距离。

旋转

顶点着色器的代码不用改,这次我们传一个旋转矩阵进去,就逆时针旋转 90 度吧,沿着 z 轴作旋转。

公式为:

这个公示是几何数学推导出来的。

数组数据为:

/****** 旋转矩阵 ****/
const angle = 90;
const radian = (angle * Math.PI) / 180;
const cos = Math.cos(radian);
const sin = Math.sin(radian);

const xformMatrix = new Float32Array([
  cos, sin, 0, 0,

  -sin, cos, 0, 0,

  0, 0, 1, 0,

  0, 0, 0, 1
]);

因为很多 API 只支持弧度制,所以我们需要将角度转弧度。

然后是旋转方向,提供一个正数,WebGL 是沿着逆时针旋转的。顺带一提, Canvas 2D 是顺时针旋转的。

完整代码:

/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");

const vertexShaderSrc = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
 gl_Position = u_xformMatrix * a_Position;
}
`;

const fragmentShaderSrc = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

/**** 渲染器生成处理 ****/
// 创建顶点渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 创建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序对象
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;

// 顶点数据
const vertices = new Float32Array([
  // 第一个点
  0,
  0.5,
  // 第二个点
  -0.5,
  -0.5,
  // 第三个点
  0.5,
  -0.5
]);

// 创建缓存对象
const vertexBuffer = gl.createBuffer();
// 绑定缓存对象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向缓存区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 获取 a_Position 变量地址
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

// 允许访问缓存区
gl.enableVertexAttribArray(a_Position);

/****** 旋转矩阵 ****/
const angle = 90;
const radian = (angle * Math.PI) / 180;
const cos = Math.cos(radian);
const sin = Math.sin(radian);

const xformMatrix = new Float32Array([
  cos,
  sin,
  0,
  0,

  -sin,
  cos,
  0,
  0,

  0,
  0,
  1,
  0,

  0,
  0,
  0,
  1
]);
const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);

/*** 绘制 ***/
// 清空画布,并指定颜色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);

demo 地址:

https://codesandbox.io/s/wx44l0?file=/index.js。

渲染效果:

图片

缩放

缩放公式为:

数组为:

/****** 缩放矩阵 ****/
const sx = 2;
const sy = 2;
const sz = 1;
// sz 先不管,没用到透视矩阵,设置了值也看不到效果

const xformMatrix = new Float32Array([
  sx, 0, 0, 0,

  0, sy, 0, 0,

  0, 0, sz, 0,

  0, 0, 0, 1
]);

完整代码:

/** @type {HTMLCanvasElement} */
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");

const vertexShaderSrc = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
 gl_Position = u_xformMatrix * a_Position;
}
`;

const fragmentShaderSrc = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

/**** 渲染器生成处理 ****/
// 创建顶点渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 创建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序对象
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;

// 顶点数据
const vertices = new Float32Array([
  // 第一个点
  0,
  0.5,
  // 第二个点
  -0.5,
  -0.5,
  // 第三个点
  0.5,
  -0.5
]);

// 创建缓存对象
const vertexBuffer = gl.createBuffer();
// 绑定缓存对象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向缓存区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 获取 a_Position 变量地址
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

// 允许访问缓存区
gl.enableVertexAttribArray(a_Position);

/****** 缩放矩阵 ****/
const sx = 2;
const sy = 2;
const sz = 2;
// z 先不管,没用到透视矩阵,设置了值也看不到效果

const xformMatrix = new Float32Array([
  sx,
  0,
  0,
  0,

  0,
  sy,
  0,
  0,

  0,
  0,
  sz,
  0,

  0,
  0,
  0,
  1
]);
const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);

/*** 绘制 ***/
// 清空画布,并指定颜色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);

demo 地址:

https://codesandbox.io/s/jsfdtr?file=/index.js。

绘制效果:

图片

结尾

矩阵变换是 WebGL 非常重要的一部分。

本节介绍了三种常见的变形矩阵,并展示了各自的效果,下节我们讲多个矩阵的组合,复合矩阵。

责任编辑:姜华 来源: 前端西瓜哥
相关推荐

2023-05-04 08:48:42

WebGL复合矩阵

2023-04-12 07:46:24

JavaScriptWebGL

2023-03-29 07:31:09

WebGL坐标系

2023-06-26 15:14:19

WebGL纹理对象学习

2023-04-26 07:42:16

WebGL图元的类型

2023-05-08 07:29:48

WebGL视图矩阵

2023-05-31 20:10:03

WebGL绘制立方体

2023-05-16 07:44:03

纹理映射WebGL

2023-04-13 07:45:15

WebGL片元着色器

2023-04-11 07:48:32

WebGLCanvas

2023-05-17 08:28:55

2023-04-17 09:01:01

WebGL绘制三角形

2023-02-23 08:40:09

Pixijs修改图形属性

2023-02-22 09:27:31

CanvasWebGL

2022-11-29 16:35:02

Tetris鸿蒙

2022-12-02 14:20:09

Tetris鸿蒙

2023-03-30 09:32:27

2022-11-14 17:01:34

游戏开发画布功能

2023-05-06 07:23:57

2024-02-28 12:12:20

Pipeline数据机制
点赞
收藏

51CTO技术栈公众号