WEBGL编程指南

WebGL编程指南/(美)松田浩一(Matsuda,K.),(美)李(Lea,R.)著;谢光磊译。

一北京:电子工业出 版社,2014.6

书名原文:WebGL programming guide: interactive 3D graphics programming with WebGL

ISBN 978-7-121-22942-8

温故而知新,可以为师矣?

第1章 WebGL 概述

这一章简要介绍了WebGL 技术的若干关键特性和WebGL程序(网页)的结构。总之,这一章最重要的内容是,WebGL程序使用三种语言开发:HTML、JavaScript 和 GLSL ES——然而,由于着色器代码GLSLES 内嵌在JavaScript中,所以WebGL网页的 文件结构和传统网页一样。下一章将通过一些简单的WebGL示例,一步一步把你带进 WebGL的大门。

image-20210910133823201

OpenGL、OpenGL ES 1.1//2.0/3.0 和 WebGL之间的关系

第2章 WebGL 入门

本章内容:

  • WebGL如何获取canvas元素,如何在其上绘图。
  • HTML文件如何引人WebGL JavaScript 文件。
  • 简单的WebGL绘图函数。
  • WebGL 中的着色器程序。

2D canvas

var ctx = canvas.getContext ('2d');

var ctx = canvas.getContext (‘2d’);

//绘制蓝色矩形

ctx.fillStyle = ‘rgba(0,0,255,1.0)’; //设置填充颜色为蓝色

ctx.fillRect(120,10,150,150);//用这个颜色填充矩形

3D canvas

最短的WebGL程序:清空绘图区

1
2
3
4
5
6
7
//获取WebGL绘图上下文 
var canvas = document.getElementById("webgl");//一般需要兼容处理 不一定都是webgl
var gl = canvas.getContext("webgl");
//指定清空 canvas的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空canvas
g1.clear (gl.COLOR_BUFFER_BIT);

关于WebGLRenderingContext的文档可以看这里:WebGLRenderingContext:

  • gl.clearColor(red, green, blue, alpha) 指定绘图区域的背景色:

示例程序执行了gl.clearColor(0.0, 0.0, 0.0, 1.0),背景色被指定为黑色。

一旦指定了背景色之后,背景色就会驻存在WebGL系统(WebGL System)中,在下一次调用g1.clearColor()方法前不会改变。

最后,你可以调用g1.clear()函数,用之前指定的背景色清空(即用背景色填充, 擦除已经绘制的内容)绘图区域。

  • gl.clear(gl.COLOR_BUFFER_ BIT);

注意,函数的参数是g1.COLOR_BUFFER_BIT,而不是(你可能认为会是)表示绘图区域的canvas。这是因为 WebGL中的g1.clear()方法实际上继承自OpenGL,它基于基本缓冲区模型,这可比二维绘图上下文复杂得多。清空绘图区域,实际上是在清空颜色缓冲区 (color buffer),传递参数g1.COLOR_BUFFER_BIT就是在告诉WebGL清空颜色缓冲区。除了颜色缓冲区,WebGL还会使用其他种类的缓冲区,比如深度缓冲区和模板缓冲区。

1
void gl.clear(mask);

mask
一个用于指定需要清除的缓冲区的 GLbitfield (en-US) 。可能的值有:
gl.COLOR_BUFFER_BIT //颜色缓冲区
gl.DEPTH_BUFFER_BIT //深度缓冲区
gl.STENCIL_BUFFER_BIT //模板缓冲区
错误抛出 如果mask不是以上列出的值,会抛出 gl.INVALID_ENUM 错误。

返回值 无

如果没有指定背景色(也就是说,你没有调用g1.clearColor()),那么使用的默认。

绘制一个点:

封装的一个initShaders函数,方便复用。

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
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
}

gl.useProgram(program);
gl.program = program;

return true;
}
function createProgram(gl, vshader, fshader) {
// Create shader object
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}

// Create a program object
var program = gl.createProgram();
if (!program) {
return null;
}

// Attach the shader objects
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

// Link the program object
gl.linkProgram(program);

// Check the result of linking
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
}
function loadShader(gl, type, source) {
// Create shader object
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
}

// Set the shader program
gl.shaderSource(shader, source);

// Compile the shader
gl.compileShader(shader);

// Check the result of compilation
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log('Failed to compile shader: ' + error);
gl.deleteShader(shader);
return null;
}

return shader;
}

啰嗦一通等价下面11行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
let vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);

let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);

let program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

没有传入顶点坐标,但是在顶点着色器中写死了顶点位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var canvas = document.getElementById("point1");
var gl = canvas.getContext("webgl");
var vertexShaderSource = `
void main() {
gl_Position = vec4(0.0,0.0,0.0,1.0);
gl_PointSize = 10.0;
}
`;
var fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
initShaders(gl, vertexShaderSource, fragmentShaderSource);
gl.drawArrays(gl.POINTS,0,1);
  • gl.drawArrays(mode,first,count)

gl.drawarrays()是一个强大的函数,它可以用来绘制各种图形,该函数的规范如下表所示。

https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/drawArrays

mode
GLenum (en-US) 类型,指定绘制图元的方式,可能值如下。
gl.POINTS: 绘制一系列点。
gl.LINE_STRIP: 绘制一个线条。即,绘制一系列线段,上一点连接下一点。
gl.LINE_LOOP: 绘制一个线圈。即,绘制一系列线段,上一点连接下一点,并且最后一点与第一个点相连。
gl.LINES: 绘制一系列单独线段。每两个点作为端点,线段之间不连接。
gl.TRIANGLE_STRIP:绘制一个三角带。
gl.TRIANGLE_FAN:绘制一个三角扇。
gl.TRIANGLES: 绘制一系列三角形。每三个点作为顶点。

first
GLint (en-US) 类型 ,指定从哪个点开始绘制。

count
GLsizei (en-US) 类型,指定绘制需要使用到多少个点。

返回值 无。

异常
如果 mode 不是一个可接受值,将会抛出 gl.INVALID_ENUM 异常。
如果 first 或者 count 是负值,会抛出 gl.INVALID_VALUE 异常。
如果 gl.CURRENT_PROGRAM 为 null,会抛出 gl.INVALID_OPERATION 异常。

使用attribute传递顶点坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var gl = canvas.getContext("webgl");
var vertexShaderSource = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`;
var fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
initShaders(gl, vertexShaderSource, fragmentShaderSource);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
  • gl.getattribLocation (program, name)

获取由 name参数指定的attribute变量的存储地址。

参数 program 指定包含顶点着色器和片元着色器的着色器程序对象
name 指定想要获取其存储地址的attribute变量的名称
返回值 大于等于0 attribute变量的存储地址
-1 指定的attribute变量不存在
  • gi.vertexAttrib3f (location, v0, v1, v2)

将数据(v0.v1,v2)传给由location参数指定的attribute变量

该函数的第1个参数是 attribute变量的存储地址,即gl.getAttribLocation()的返 回值;第2、3、4个参数是三个浮点型数值,即点的x、y和z坐标值。函数被调用后,这三个值被一起传给顶点着色器中的a_Position变量。

gl.vertexAttrib3f()的同族函数

gl.vertexAttrib3f()是一系列同族函数中的一个,该系列函数的任务就是从 JavaScript向顶点着色器中的attribute变量传值。

  1. gl.vertexAttrib1f(location, v0)
  2. gl.vertexAttrib2t(location, v0, v1)
  3. gl.vertexAttrib3f(location, v0, v1, v2)
  4. gl.vertexAttrib4f(location, v0, vl, v2, v3)

鼠标点击绘制点。

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
let canvas = document.getElementById("point2");
let gl = canvas.getContext("webgl");
let vertexShaderSource = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`;
let fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

initShaders(gl, vertexShaderSource, fragmentShaderSource);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
let g_points = [];

canvas.onmousedown = function (ev) {
let x = ev.clientX;
let y = ev.clientY;
let rect = ev.target.getBoundingClientRect();
x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2);
y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2);
g_points.push(x, y);
gl.clear(gl.COLOR_BUFFER_BIT);
for (let i = 0; i < g_points.length; i += 2) {
gl.vertexAttrib3f(a_Position, g_points[i], g_points[i + 1], 0.0);
gl.drawArrays(gl.POINTS, 0, 1);
}
}

webgl默认是右手坐标系

x轴:大拇指朝右。y轴:食指朝上。z轴:中指朝向自己

image-20210910151752699

鼠标位置改变点的颜色

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
let canvas = document.getElementById("point3");
let gl = canvas.getContext("webgl");
let vertexShaderSource = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`;
//precision mediump float; 不能少
let fragmentShaderSource = `
precision mediump float;
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
`;

initShaders(gl, vertexShaderSource, fragmentShaderSource);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
let g_points = [];
let u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
let g_colors = [];

canvas.onmousedown = function (ev) {
let x = ev.clientX;
let y = ev.clientY;
let rect = ev.target.getBoundingClientRect();
x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2);
y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2);
g_points.push([x, y]);
g_colors.push([(x+1)/2,(y+1)/1, 1.0, 1.0]);
gl.clear(gl.COLOR_BUFFER_BIT);
for (let i = 0; i < g_points.length; i ++ ) {
gl.vertexAttrib3f(a_Position, g_points[i][0], g_points[i][1], 0.0);
gl.uniform4f(u_FragColor, g_colors[i][0], g_colors[i][1], g_colors[i][2], g_colors[i][3]);
gl.drawArrays(gl.POINTS, 0, 1);
}
}

第3章 绘制和变换三角形

本章内容:

  • 三角形在三维图形学中的重要地位,以及WebGL如何绘制三角形。
  • 使用多个三角形绘制其他类型的基本图形。
  • 利用简单的方程对三角形做基本的变换,如移动、旋转和缩放。
  • 利用矩阵简化变换。

WebGL提供了一种很方便的机制,即缓冲区对象(buffer object),它可以一次性地向 着色器传入多个顶点的数据。缓冲区对象是WebGL系统中的一块内存区域,我们可以 一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。

使用缓冲区对象向顶点着色器传入多个顶点的数据,需要遵循以下五个步骤。处理 其他对象,如纹理对象(第4章)、帧缓冲区对象(第8章“光照””)时的步骤也比较类似, 我们来仔细研究一下:

  1. 创建缓冲区对象(gl.createBuffer())。
  2. 绑定缓冲区对象(g1.bindBuffer())。
  3. 将数据写人缓冲区对象(g1.bufferData())。
  4. 将缓冲区对象分配给一个attribute变量(g1.vertexAttribPointer())。
  5. 开启 attribute变量(g1.enableVertexAttribArray())。
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
let vertexShaderSource = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
}
`;
let fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

// let vertexShader = gl.createShader(gl.VERTEX_SHADER);
// gl.shaderSource(vertexShader, vertexShaderSource);
// gl.compileShader(vertexShader);

// let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// gl.shaderSource(fragmentShader, fragmentShaderSource);
// gl.compileShader(fragmentShader);

// let program = gl.createProgram();
// gl.attachShader(program, vertexShader);
// gl.attachShader(program, fragmentShader);
// gl.linkProgram(program);
// gl.useProgram(program);
// 上面11行代码可以封装一下initShaders
initShaders(gl, vertexShaderSource, fragmentShaderSource);
//
let a_Position = gl.getAttribLocation(gl.program, "a_Position");
let data = new Float32Array([
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5
]);
// 创建缓冲区对象
let buffer = gl.createBuffer();
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 向缓冲区对象中写入数据
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// 将缓冲区对象分配给a_Position变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 启用变量
gl.enableVertexAttribArray(a_Position);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);

g1.bufferData(target, data, usage)

开辟存储空间,向绑定在target上的缓冲区对象中写入数据data

target gl.ARRAY_BUFFER 或g1.ELEMENT ARRAY_ BUFFER

data 写入缓冲区对象的数据(类型化数组,参阅下一节)

usage 表示程序将如何使用存储在缓冲区对象中的数据。该参数 将帮助WebGL优化操作,但是就算你传入了错误的值,也 不会终止程序(仅仅是降低程序的效率)

​ gl.STATIC DRAW 只会向缓冲区对象中写入一次数据,但需要绘制很多次

​ g1.STREAM DRAW 只会向缓冲区对象中写入一次数据,然后绘制若干次

​ g1.DYNAMIC_DRAW 会向缓冲区对象中多次写入数据,并绘制很多次

返回值 无

错误 INVALID ENUM target不是上述值之一,这时将保持原有的绑定情况不变

类型化数组

为了绘制三维图形,WebGL通常需要同时处理大量相同类型的数据,例如顶点的坐 标和颜色数据。为了优化性能,WebGL为每种基本数据类型引入了一种特殊的数组(类 型化数组)。浏览器事先知道数组中的数据类型,所以处理起来也更加有效率。

与JavaScript 中的Array数组相似,类型化数组也有一系列方法和属性(包括一个常 量属性),如表3.2所示。注意,与普通的Array数组不同,类型化数组不支持push()和 pop()方法

image-20210913154741883

image-20210913154903937

image-20210913154950476

平移

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
let vertexShaderSource = `
attribute vec4 a_Position;
uniform vec4 u_Translate;
void main() {
gl_Position = a_Position+u_Translate;
gl_PointSize = 10.0;
}
`;
let fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

initShaders(gl, vertexShaderSource, fragmentShaderSource);

let a_Position = gl.getAttribLocation(gl.program, "a_Position");
let data = new Float32Array([
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5
]);

let buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);

let u_Translate = gl.getUniformLocation(gl.program, "u_Translate");
gl.uniform4f(u_Translate, 0.0, 0.5, 0.0, 0.0);

gl.drawArrays(gl.POINTS, 0, 3);

image-20210913162101853

旋转矩阵

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
let vertexShaderSource = `
attribute vec4 a_Position;
uniform mat4 u_Rotate;
void main() {
gl_Position = u_Rotate*a_Position;
gl_PointSize = 40.0;
}
`;
let fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
`;

initShaders(gl, vertexShaderSource, fragmentShaderSource);

let a_Position = gl.getAttribLocation(gl.program, "a_Position");
//居中的正三角形
let k = .3, sqtr3 = Math.sqrt(3);
let data = new Float32Array([
-sqtr3 * k, -k,
sqtr3 * k, -k,
0, 2 * k
]);

let buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);

let u_Translate = gl.getUniformLocation(gl.program, "u_Rotate");
let ang = 0;
let rad = ang * Math.PI / 180;
let cosB = Math.cos(rad), sinB = Math.sin(rad);
let xformMatrix = new Float32Array([
cosB, sinB, 0, 0,
-sinB, cosB, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
gl.uniformMatrix4fv(u_Translate, false, xformMatrix);
gl.drawArrays(gl.TRIANGLES, 0, 3);

setInterval(() => {
ang -= 2;
let rad = ang * Math.PI / 180;
let cosB = Math.cos(rad), sinB = Math.sin(rad);
let xformMatrix = new Float32Array([
cosB, sinB, 0, 0,
-sinB, cosB, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
gl.uniformMatrix4fv(u_Translate, false, xformMatrix);
gl.drawArrays(gl.POINTS, 0, 3);
}, 40);

注意WebGL中矩阵是列主序的,就是和公式的行列是反的,构建数组的时候注意顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 //旋转矩阵 
let xformMatrix = new Float32Array([
cosB, sinB, 0, 0,
-sinB, cosB, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
//平移矩阵
let xformMatrix = new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
Tx, Ty, Tz, 1
]);
//缩放矩阵
let xformMatrix = new Float32Array([
Sx, 0, 0, 0,
0, Sy, 0, 0,
0, 0, Sz, 0,
0, 0, 0, 1
]);

第4章高级变换与动画基础

本章内容:

  • 学习使用一个矩阵变换库,该库封装了矩阵运算的数学细节。
  • 快速上手使用该矩阵库,对图形进行复合变换。
  • 在该矩阵库的帮助下,实现简单的动画效果。

坐车提供的一个矩阵函数库:

image-20210913180945765

image-20210913181007161

第5章颜色与纹理

第6章OpenGL ES着色器语言 (GLSL ES)

第7章进入三维世界

第8章光照

第9章层次模型

第10章高级技术

在线案例地址: https://sites.google.com/site/webglbook/home