之前绘制了三角形,我们现在给它做一个变形操作。
对一个三角形进行变形,其实就是重新这个三角形的三个顶点的位置,计算完后再绘制出来,相比原来就发生了变形。
变形常见的有位移、选择、缩放。位移,其实就是给每个顶点的各个坐标值加上偏移量 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 个浮点数的一维数组。
来看看通过矩阵进行位移的实现。位移矩阵如下:
[ 1 0 0 d x 0 1 0 d y 0 0 1 d z 0 0 0 1 ] [ x y z 1 ] \begin{aligned} \\ &\begin{bmatrix} 1 & 0 & 0 & dx \\ 0 & 1 & 0 & dy \\ 0 & 0 & 1 & dz\\ 0 & 0 & 0 & 1\\ \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} \end{aligned} 100001000010dxdydz1 xyz1
对应的 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 轴作旋转。
公式为:
[ cos θ − sin θ 0 0 sin θ cos θ 0 0 0 0 1 0 0 0 0 1 ] [ x y z 1 ] \begin{aligned} \\ \begin{bmatrix} \cos \theta &-\sin \theta & 0 & 0 \\ \sin \theta &\cos \theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} \end{aligned} cosθsinθ00−sinθcosθ0000100001 xyz1
这个公示是几何数学推导出来的。
数组数据为:
/****** 旋转矩阵 ****/
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
渲染效果:
缩放公式为:
[ s x 0 0 0 0 s y 0 0 0 0 s z 0 0 0 0 1 ] [ x y z 1 ] \begin{aligned} \\ \begin{bmatrix} sx & 0 & 0 & 0 \\ 0 & sy & 0 & 0 \\ 0 & 0 & sz & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} \end{aligned} sx0000sy0000sz00001 xyz1
数组为:
/****** 缩放矩阵 ****/
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 知识。
矩阵变换是 WebGL 非常重要的一部分。
本节介绍了三种常见的变形矩阵,并展示了各自的效果,下节我们讲多个矩阵的组合,复合矩阵。