WebGL 入门笔记

前言

因为计算机图形学课程作业的需要,我在使用过 OpenGL 的基础上学习了使用 WebGL 进行二维图形渲染。

WebGL 拥有与 OpenGL ES 相似的 API(前者基于后者),但与 OpenGL 相比,两者缺少基础图形的渲染管线,而是需要手写 shader 进行渲染。

基础

请阅读 WebGL Fundamentals 以获得对于 WebGL 的大体了解。

Shader

在 WebGL 中需要两种 shader 进行渲染。第一种是 vertex shader,负责对顶点进行变换,例如将像素坐标映射到 clip space。而第二种是 fragment shader,负责对像素点进行着色。

Shader 可以有两种参数。第一种是 attribute,在每次调用中不同,例如当前顶点的位置;第二种是 uniform,在渲染过程中共享,例如渲染的变换矩阵。

以下是两个实用的简单 2D shader:

Vertex shader:

1
2
3
4
5
6
7
attribute vec2 a_position;

uniform mat3 u_transformation;

void main() {
gl_Position = vec4((u_transformation * vec3(a_position, 1)).xy, 0, 1);
}

Fragment shader:

1
2
3
4
5
6
7
precision mediump float;

uniform vec4 u_color;

void main() {
gl_FragColor = u_color;
}

初始化

以下是我的一些工具函数,参考了 WebGL Boilerplate 和一部分 WebGL - Less Code, More Fun

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function createAndCompileShader(gl, id) {
var element = document.getElementById(id);
var type = null;
switch (element.type) {
case 'x-shader/x-vertex':
type = gl.VERTEX_SHADER;
break;
case 'x-shader/x-fragment':
type = gl.FRAGMENT_SHADER;
break;
default:
throw 'Unknown shader type:' + element.type;
}
var source = element.textContent;
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw 'Error compiling shader:' + gl.getShaderInfoLog(shader);
}
return shader;
}

function createAndLinkProgram(gl, vertexShaderId, fragmentShaderId) {
var program = gl.createProgram();
gl.attachShader(program, createAndCompileShader(gl, vertexShaderId));
gl.attachShader(program, createAndCompileShader(gl, fragmentShaderId));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw 'Error linking program:' + gl.getProgramInfoLog(program);
}
return program;
}

提供数据

以下是我自己写作的一些工具函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @param {[float]} value
*/
function setUniformFv(gl, program, name, value) {
gl['uniform' + value.length + 'fv'](gl.getUniformLocation(program, name), value);
}

/**
* @param {string} color '#RRGGBB'
* @param {float=} alpha \in [0, 1]
*/
function setUniformColor(gl, program, name, color, alpha) {
if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
throw 'Invalid color:' + color;
}
setUniformFv(gl, program, name, [
parseInt(color.substr(1, 2), 16) / 0xff,
parseInt(color.substr(3, 2), 16) / 0xff,
parseInt(color.substr(5, 2), 16) / 0xff,
alpha || 1
]);
}

function setUniformMatrix(gl, program, name, value) {
gl['uniformMatrix' + Math.sqrt(value.length) + 'fv'](gl.getUniformLocation(program, name), false, value);
}

/**
* @param {[[float]]} value
*/
function setAttributeArrayFva(gl, program, name, value) {
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
var data = new Float32Array(Array.prototype.concat.apply([], value));
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
var location = gl.getAttribLocation(program, name);
gl.enableVertexAttribArray(location);
var size = value[0].length;
gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
}

变换矩阵

以下代码参考了 WebGL 2D TranslationWebGL Orthographic 3D。需要注意的是,原教程的代码中有部分矩阵错误地以行优先而非列有限的方式进行表示或运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
function makeIdentityMatrix() {
return [
1, 0, 0,
0, 1, 0,
0, 0, 1
];
}

function makeTranslationMatrix(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}

function makeScalingMatrix(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}

function degreeToRadian(angle) {
return angle / 180 * Math.PI;
}

function makeRotationMatrix(angle) {
angle = degreeToRadian(angle);
var cosa = Math.cos(angle);
var sina = Math.sin(angle);
return [
cosa, sina, 0,
-sina, cosa, 0,
0, 0, 1
];
}

function makeSkewXMatrix(angle) {
angle = degreeToRadian(angle);
var tana = Math.tan(angle);
return [
1, 0, 0,
tana, 1, 0,
0, 0, 1
];
}

function makeSkewYMatrix(angle) {
angle = degreeToRadian(angle);
var tana = Math.tan(angle);
return [
1, tana, 0,
0, 1, 0,
0, 0, 1
];
}

function makeProjectionMatrix(width, height) {
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}

function multiplyMatrix(a, b) {
// A \cdot B = (B^T \cdot A^T)^T
var a00 = a[0], a01 = a[1], a02 = a[2];
var a10 = a[3], a11 = a[4], a12 = a[5];
var a20 = a[6], a21 = a[7], a22 = a[8];
var b00 = b[0], b01 = b[1], b02 = b[2];
var b10 = b[3], b11 = b[4], b12 = b[5];
var b20 = b[6], b21 = b[7], b22 = b[8];
return [
b00 * a00 + b01 * a10 + b02 * a20,
b00 * a01 + b01 * a11 + b02 * a21,
b00 * a02 + b01 * a12 + b02 * a22,
b10 * a00 + b11 * a10 + b12 * a20,
b10 * a01 + b11 * a11 + b12 * a21,
b10 * a02 + b11 * a12 + b12 * a22,
b20 * a00 + b21 * a10 + b22 * a20,
b20 * a01 + b21 * a11 + b22 * a21,
b20 * a02 + b21 * a12 + b22 * a22
];
}

近似于 OpenGL 的 API 封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var _gl = null;
var _program = null;

function initialize(gl, program) {
_gl = gl;
_program = program;
}

function setColor(color, alpha) {
setUniformColor(_gl, _program, 'u_color', color, alpha);
}

var _transformations = [];

function getTransformation() {
return _transformations[_transformations.length - 1];
}

function _setTransformation(transformation) {
setUniformMatrix(_gl, _program, 'u_transformation', transformation);
}

function setTransformation(transformation) {
_setTransformation(transformation);
_transformations = [transformation];
}

function pushTransformation(transformation) {
transformation = multiplyMatrix(getTransformation(), transformation);
_setTransformation(transformation);
_transformations.push(transformation);
}

function popTransformation() {
_transformations.pop();
_setTransformation(getTransformation());
}

function drawTriangles(positions) {
setAttributeArrayFva(_gl, _program, 'a_position', positions);
_gl.drawArrays(_gl.TRIANGLES, 0, positions.length);
}

function drawRectangle(x, y, width, height) {
var x1 = x;
var y1= y;
var x2 = x + width;
var y2 = y + height;
drawTriangles([
[x1, y1],
[x2, y1],
[x1, y2],
[x1, y2],
[x2, y1],
[x2, y2]
]);
}

绘制和应对大小改变

以下代码参考了 WebGL Resizing the CanvasWebGL Anti-Patterns

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function draw(gl) {

gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);

gl.clearColor(0xde / 0xff, 0x29 / 0xff, 0x10 / 0xff, 0xff / 0xff);
gl.clear(gl.COLOR_BUFFER_BIT);

var program = createAndLinkProgram(gl, 'vertex-shader', 'fragment-shader');
gl.useProgram(program);

initialize(gl, program);

setColor('#ffdd00');

setTransformation(makeProjectionMatrix(1000, 800));

// Draw here.

gl.flush();
}

function main() {

var canvas = document.getElementById('canvas');
var webgl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!webgl) {
alert("Error initializing WebGL. Your browser may not support it.");
return;
}

var needToDraw = true;
function resizeAndDraw() {
var resized = canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientWidth * 0.8;
if (resized) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientWidth * 0.8;
}
if (resized || needToDraw) {
needToDraw = false;
draw(webgl);
}
requestAnimationFrame(resizeAndDraw);
}
requestAnimationFrame(resizeAndDraw);
}

main();

结语

作为一个复杂度适中的示例,我的作业代码可以在 这里 看到。