本文目录
[[toc]]
WebGL 简介
WebGL 中有两个重要概念: 顶点着色器 ( Vertex Shader) 、 片元着色器 ( Fragment Shader )
着色器用于 GPU 绘图,其中顶点着色器用于绘制图形的边框,片元着色器用于给图形染色,其基本工作流程如下:
// 创建 WebGL 上下文
const canvas = document.querySelector("#canvas");
const gl = canvas.getContext("webgl");
// 创建着色器 ( Shader )
const vertex = `
attribute vec2 position;
void main() {
gl_PointSize = 1.0;
gl_Position = vec4(position, 1.0, 1.0);
}
`;
const fragment = `
precision mediump float;
void main()
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
// 实例化着色器 ( Shader ) 对象
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragment);
gl.compileShader(fragmentShader);
// 连接到 Program
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// 数据写入缓冲区
const points = new Float32Array([-1, -1, 0, 1, 1, -1]);
const bufferId = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
// GPU 读取缓冲区数据
const vPosition = gl.getAttribLocation(program, "position"); // 获取顶点着色器中的position变量的地址
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0); // 给变量设置长度和类型
gl.enableVertexAttribArray(vPosition); // 激活这个变量
// 开始着色绘制
gl.clear(gl.COLOR_BUFFER_BIT);
// 声明绘制的是三角形
gl.drawArrays(gl.TRIANGLES, 0, points.length / 2);WebGL 的基本单元
WebGL 的基本单元是图元,相当于 RGB 中的三原色的地位。
WebGL 支持的图元类型有 7 种,分别是:
gl.POINTS: 点gl.LINES: 线段gl.LINE_STRIP: 线条gl.LINE_LOOP: 回路gl.TRIANGLES: 三角形gl.TRIANGLE_STRIP: 三角带gl.TRIANGLE_FAN: 三角扇
坐标系
不同于 HTML 、 Canvas 、 CSS 的左上角为坐标原点 X 轴从左向右, Y 轴从上向下, WebGL 采用的是中心点为坐标原点,并且 Y 轴是从下向上。
由于坐标系不同,在 Canvas 绘制 WebGL 时常常遇到坐标系转换问题,一个不注意就容易导致 bug 。
所以在进行开发之前,往往会预先统一坐标系原点,对 Canvas 进行坐标系改造,使其原点也在画面中央:
const canvas = document.querySelector('canvas');
const ctx = canvas.ctx;
const ctxWidth = canvas.width;
const ctxHeight = canvas.height;
// 进行坐标系转换
// 处理原点位置
ctx.translate(ctxWidth / 2, ctxHeight / 2);
// 反转 Y 轴方向
ctx.scale(1, -1);向量
向量即为 N 维空间中的有向线段,这里需要理清几个问题:
- 向量可以任意平移,不改变这个向量: 说明了向量的原点是可以随意改变的,不影响向量及其计算。
- 一般认为这是一维向量的扩展,比如 2 + 5 ,就是先走 2 单位,再走 5 单位。
- 向量相加时,可以视为两个向量起点与终点相连,最终形成的从起点到终点的有向线段。
- 向量乘以一个常数时,等于各个维度均会被放大常数倍。
- 单位向量是某个维度(方向)上的最小向量,其在其他维度的投影长度为 0 ,即单位向量垂直于其他所有的面。
- 由于单位向量存在,所以向量的各个坐标轴(维度)必然互相垂直,否则不存在单位向量。
- 向量本质上是不同的单位向量进行缩放后,相加得出来的有向线段,其值受各个单位向量影响,只要单位向量改变,则向量值也会改变。
上述的坐标系变化,可以视为单位向量发生变化, Y 轴单位向量从 j 变为 -j 。
/**
* 基础向量类
*/
class Vector {
private x: number;
private y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
/**
* 向量长度
*/
get length() {
return Math.hypot(this.x, this.y);
}
/**
* 向量与 X 轴夹角
*/
get dir() {
return Math.atan2(this.y, this.x);
}
/**
* 向量旋转
*/
rotate(dir: number) {
const { x, y, length: len } = this;
const cosDir = len * Math.cos(dir);
const sinDir = len * Math.sin(dir);
this.x = x * cosDir + y * -sinDir;
this.y = x * sinDir + y + cosDir;
}
/**
* 判断向量是否平行
*/
parallel(v: Vector) {
return this.x * v.y === this.y * v.x;
}
/**
* 判断向量是否垂直
*/
perpendicular(v: Vector) {
return this.x * v.x + this.y * v.y === 0;
}
}
const v = new Vector(3, 4)
// 向量的 x 可以根据 length 与 dir 计算
const x = v.length * Math.cos(v.dir);
// 向量的 y 可以根据 length 与 dir 计算
const y = v.length * Math.sin(v.dir);矩阵
简单来说,矩阵通常代表一个线性变换。
const matrix = [
[1, 1],
[1, 0],
]以列为单位,可以拆分为 [ matrix[0][0], matrix[1][0] ] 和 [ matrix[0][1], matrix[1][1] ] ,这就是线性变换后,向量的单位向量(基向量)。
基向量的变化也会让向量随之变换,比如有 [ x, y ] 的向量,进行 matrix 变换后,新的向量为:
const v1 = [
x * matrix[0][0],
x * matrix[1][0],
]
const v2 = [
y * matrix[0][1],
y * matrix[1][1],
]
// v = v1 + v2
const v = [
x * matrix[0][0] + y * matrix[0][1],
x * matrix[1][0] + y * matrix[1][1],
]同理,矩阵相乘,就是先应用一个线性变换,再应用另一个线性变换,所以往往称为复合变换。
从计算角度上看,就是对于一对基向量,进行线性变换。
例如有以下矩阵:
// 剪切矩阵
const shearMatrix = [
[1, 1],
[0, 1],
]
// 旋转矩阵
const rotateMatrix = [
[0, -1],
[1, 0],
]shearMatrix * rotateMatrix 变换过程如下:
// 矩阵进行左结合,即先进行 rotateMatrix 变换,再进行 shearMatrix 变换
// 记复合变换后的 X 轴单位向量为 i
// 记复合变换后的 Y 轴单位向量为 j
// 先计算 i
// const i = shearMatrix * [ rotateMatrix[0][0], rotateMatrix[1][0] ]
const ix = rotateMatrix[0][0]
const iy = rotateMatrix[1][0]
const i1 = [
// 0 * 1 = 0
ix * shearMatrix[0][0],
// 0 * 0 = 0
ix * shearMatrix[1][0],
]
const i2 = [
// 1 * 1 = 1
iy * shearMatrix[0][1],
// 1 * 1 = 1
iy * shearMatrix[1][1],
]
// i = i1 + i2
const i = [
1,
1,
]
// 再计算 j
// const j = shearMatrix * [ rotateMatrix[0][1], rotateMatrix[1][1] ]
const jx = rotateMatrix[0][0]
const jy = rotateMatrix[1][0]
const j1 = [
// -1 * 1 = -1
jx * shearMatrix[0][0],
// -1 * 0 = 0
jx * shearMatrix[1][0],
]
const j2 = [
// 0 * 1 = 0
jy * shearMatrix[0][1],
// 0 * 1 = 0
jy * shearMatrix[1][1],
]
// j = j1 + j2
const j = [
-1,
0,
]
// 最终复合后的矩阵为:
[
[1, -1],
[1, 0],
]对于行列式来说,行列式就是求 i 与 j 围成的平行四边形的面积,以上述复合矩阵为例,可以视为 [1, 1] 与 [-1, 0] 围成的面积,为了方便计算面积,可以平移 [-1, 0] 为 [1, 0] ,所以向量围成的面积就是 1 * 1 = 1 。
当然,也有更加学术的计算方式,那就是计算 ad - bc ,也就是 1 * 0 - -1 * 1 = 0 - -1 = 1 ,通常我们会用这个式子进行计算,但是它并不好理解为什么这么算,所以在抽象领域更加倾向于声明其象征意义,而不是如何计算。
如果想要知道如何计算的话,可以参考如下图所示,通过大矩形面积,减去边缘的三角形、矩形的面积,就可以得到最终的算式。

严格意义来说,行列式是向量围成的面积这一说法并不准确,因为行列式可以为负数与 0 ,这里的负号主要表示 X/Y 发生了翻转。
以传统的平面直角坐标系为例, X 为从左向右, Y 为从下向上,通常我们会认为, X 在 Y 的左边(看正数轴围成的角度较小的角确定的方向),而行列式的符号正是表现了这一关系是否发生了翻转,即当行列式值为负数时,指 X 位于 Y 的右边,面积为行列式的绝对值。
当行列式的为 0 时,表示维度发生了坍缩,由 N 维坍缩到 N - k 维,具体坍缩了多少并不能直接从行列式值中看出,因为乘法的计算过程中只要有一个为 0 ,则值为 0 。