您现在的位置是:首页 >技术交流 >一起学 WebGL:图形变形以及矩阵变换网站首页技术交流

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

前端西瓜哥 2023-05-14 04:00:02
简介一起学 WebGL:图形变形以及矩阵变换

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

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

变形常见的有位移、选择、缩放。位移,其实就是给每个顶点的各个坐标值加上偏移量 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 ] egin{aligned} \ &egin{bmatrix} 1 & 0 & 0 & dx \ 0 & 1 & 0 & dy \ 0 & 0 & 1 & dz\ 0 & 0 & 0 & 1\ end{bmatrix} egin{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 ] egin{aligned} \ egin{bmatrix} cos heta &-sin heta & 0 & 0 \ sin heta &cos heta & 0 & 0 \ 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 1 \ end{bmatrix} egin{bmatrix} x \ y \ z \ 1 end{bmatrix} end{aligned} cosθsinθ00sinθ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 ] egin{aligned} \ egin{bmatrix} sx & 0 & 0 & 0 \ 0 & sy & 0 & 0 \ 0 & 0 & sz & 0 \ 0 & 0 & 0 & 1 \ end{bmatrix} egin{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 非常重要的一部分。

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

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。