Your First Step with WebGL

What Is a Canvas

DrawRectangle.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Draw A Rectangle</title>
</head>

<body onload="main()">
<!-- 创建canvas -->
<canvas id = "example" width = "400" height="400">
<!-- 如果浏览器不支持canvas显示的信息 -->
Please use a brwoser that supprots "canvas"
</canvas>
<script src = "DrawRectangle.js"></script>
</body>
</html>

DrawRectangle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function main()
{
// 取得canvas组件
var canvas = document.getElementById('example');
if(!canvas)
{
console.log('Failed to retrieve the <canvas> element');
return;
}

// 向canvas请求2D“绘制上下文”
var ctx = canvas.getContext('2d');

// 在绘制上下文上调用绘图函数
ctx.fillStyle = 'rgba(0, 0, 255, 1)'; // rgb从0-255,A从0.0-1.0
ctx.fillRect(120, 10, 150, 150); // Point1X, Point1Y, Point2X, Point2Y
}

最终效果

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/2_YourFirstStepwithWebGL/DrawRectangle.html

canvas的坐标系:以左上角为原点,x正半轴向右,y正半轴向下。

The World’s Shortest WebGL Program: Clean Drawing Area

需要下载一些库:https://github.com/hushhw/WebGL-Programming-Guide/tree/master/lib

HelloCanvas.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Clear Canvas</title>
</head>

<body onload="main()">
<!-- 创建canvas -->
<canvas id = 'webgl' width="400" height="400">
Please Use the Browser supporting canvas
</canvas>

<!-- 包含文件 -->
<script src = './lib/webgl-utils.js'></script>
<script src = './lib/webgl-debug.js'></script>
<script src = './lib/cuon-utils.js'></script>

<script src = 'HelloCanvas.js'></script>
</body>
</html>

HelloCanvas.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function main() {
//获取<canvas>元素
var canvas = document.getElementById('webgl');
if(!canvas)
{
console.log('Failed to retrieve the <canvas> element');
return;
}

//获取WebGl绘图上下文
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}

//清空canvas
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}

getWebGLContext()函数是三个库文件定义的方法,防止一些浏览器间的差异

WebGL有三个Buffer:COLOR_BUFFER_BIT DEPTH_BUFFER_BIT STENCIL_BUFFER_BIT

Buffer Name Default Value Setting Method
Color Buffer (0, 0, 0, 0) clearColor(r, g, b, a)
Depth Buffer 1.0 clearDepth(depth)
Stencil Buffer 0 clearStencil(s)

Draw a Point (V1)

需要用shader写:

HelloPoint.js

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
// Vertex Shader
var VSHADER_SOURCE =
'void main() {\n' +
' gl_Position = vec4(0, 0, 0, 1);\n' +
' gl_PointSize = 10.0;\n' +
'}\n';

//Fragment Shader
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1, 0, 0, 1);\n' +
'}\n';

function main()
{
// 获取Canvas
var canvas = document.getElementById('webgl');

// 获取WebGL
var gl = getWebGLContext(canvas);
if(!gl)
{
console.log("Failed to get rendering context for WebGL");
return;
}

// 初始化Shader
if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE))
{
console.log('Failed to initialize shaders.');
return;
}

//设置清屏颜色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

// 绘制点
gl.drawArrays(gl.POINTS, 0, 1);
}

vertex shader确定点的位置和半径,fragment shader设置点的颜色,drawArrays绘制

drawArrays(mode, first, count)

  • mode: POINTS, LINES, LINE_STRIP, LINE_LOOP, TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN
  • first: 开始绘制的顶点
  • count: 绘制的总顶点数量

执行这个函数的时候,顶点着色器被执行count次,,每次计算一个顶点。

最终结果

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/2_YourFirstStepwithWebGL/HelloPoint1.html

从文件读入shader

不过这么用字符串写shader也太复杂了,所以我自己实现新建了一个库函数文件,在里面实现了从文件读入shader的函数(书的附录F):

MyLoadShader.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Load shader from file
function loadShaderFile(gl, fileName, shader) {
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState === 4 && request.status !== 404) {
OnLoadShader(gl, request.responseText, shader);
}
}
request.open('GET', fileName, true);
request.send();
}

function OnLoadShader(gl, fileString, type) {
if (type == gl.VERTEX_SHADER) {
VSHADER_SOURCE = fileString;
}
else if (type == gl.FRAGMENT_SHADER) {
FSHADER_SOURCE = fileString;
}
if (VSHADER_SOURCE && FSHADER_SOURCE) {
// 开始渲染函数
start(gl);
}
}

将原来的js文件进行更改(调用函数,实现start函数):

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
// Vertex Shader
var VSHADER_SOURCE = null;
//Fragment Shader
var FSHADER_SOURCE = null;

function main()
{
// 获取Canvas
var canvas = document.getElementById('webgl');

// 获取WebGL
var gl = getWebGLContext(canvas);
if(!gl)
{
console.log("Failed to get rendering context for WebGL");
return;
}

//设置清屏颜色
gl.clearColor(0, 0, 0, 1);

// 加载Shader
loadShaderFile(gl, 'HelloPoint1.vert', gl.VERTEX_SHADER);
loadShaderFile(gl, 'HelloPoint1.frag', gl.FRAGMENT_SHADER);
}

// 开始渲染
function start(gl) {
// 初始化Shader
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to initialize shaders.');
return;
}

// 清屏
gl.clear(gl.COLOR_BUFFER_BIT);

// 绘制点
gl.drawArrays(gl.POINTS, 0, 1);
}

再使用glsl实现顶点着色器和片元着色器:

HelloPoint1.vert

1
2
3
4
5
void main()
{
gl_Position = vec4(0, 0, 0, 1);
gl_PointSize = 10.0;
}

HelloPoint1.frag

1
2
3
4
void main()
{
gl_FragColor = vec4(1, 0, 0, 1);
}

这时,使用chorme直接打开,按F12在控制台会报错,出现加载本地文件跨域的情况。按照 https://zhuanlan.zhihu.com/p/359881121 解决就好了。

或者也可以开一个本地 server。

WebGL坐标系

对于画布而言,Y轴向上的右手系。画布最左边X为-1,最右边为1,上下则是Y。

Draw a Point (V2)

通过外部传入将参数传入shader

  1. 在shader中声明全局变量:

    1
    2
    3
    4
    5
    6
    attribute vec4 a_Position;
    void main()
    {
    gl_Position = a_Position;
    gl_PointSize = 10.0;
    }

    其中 attribute 是储存占位符

    • attribute:与顶点有关的数据,只能在顶点着色其中使用;
    • uniform:与顶点无关,或者所有顶点都相同的数据;
  2. JS中获得变量储存地址

    1
    var a_Position = gl.getAttribLocation(gl.program, 'a_Position');

    返回值为-1时表示变量不存在,或者命名有 gl_ 或 webgl_ 前缀

  3. 传入值

    1
    gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
    • gl.vertexAttrib1f (location, v0);
    • gl.vertexAttrib2f (location, v0, v1);
    • gl.vertexAttrib3f (location, v0, v1, v2);
    • gl.vertexAttrib4f (location, v0, v1, v2, v3);

    使用v时,传入一个数组,前面的数字代表数组长度;

Draw a Point with Mouse Click

创建结构体:

1
2
3
4
5
function Point(x, y)
{
this.x = x;
this.y = y;
}

绑定点击事件:

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
canvas.onmousedown = function(event){ click(event, a_Position); };

···

var g_Points = [];
function click(event, a_Position)
{

var x = event.clientX;
var y = event.clientY;
var rect = event.target.getBoundingClientRect();

// 映射坐标到WebGL坐标系
x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2);
y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2);

var point = new Point(x, y);
g_Points.push(point);

gl.clear(gl.COLOR_BUFFER_BIT);

// 绘制循环
for(var i = 0; i < g_Points.length; i++)
{
gl.vertexAttrib3f(a_Position, g_Points[i].x, g_Points[i].y, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);
}
}

结果:

Click Points

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/2_YourFirstStepwithWebGL/ClickPoints.html

Change the Point Color

通过声明在 Fragment Shader 中的 Uniform 变量,控制点的颜色。

1
2
3
4
5
6
precision mediump float;
uniform vec4 u_PointColor;
void main()
{
gl_FragColor = u_PointColor;
}
1
2
3
4
5
var u_PointColor = gl.getUniformLocation(gl.program, 'u_PointColor');

···

gl.uniform4f(u_PointColor, color[0], color[1], color[2], color[3]);

其中 getUniformLocation 返回空时(不是-1)表示变量获取失败。

这里简单的根据坐标上色:

Colored Points

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/2_YourFirstStepwithWebGL/ColoredPoints.html

Drawing and Transforming Triangles

Drawing Multiple Points

使用 Buffer 对象储存顶点:

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
function initVertexBuffers(gl)
{
var vertices = new Float32Array([
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5
])
var n = 3;

// 创建 Buffer 对象
var vertexBuffer = gl.createBuffer();
if(!vertexBuffer)
{
console.log('Create Buffer Object Failed');
return -1;
}

// 将 Buffer 对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if(a_Position < 0)
{
console.log('Get Attribute a_Position Failed');
return -1;
}

gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);

return n;
}

结果:

Multiple Points

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/3_DrawingandTransformingTriangles/1_MultiPoints.html

通过 Buffer 对象传递多个数据的步骤:

  1. 创建 Buffer 对象

    1
    2
    gl.createBuffer();			// 创建一个 Buffer Object
    gl.deleteBuffer(buffer); // 删除一个 Buffer Object
  2. 绑定对象到目标

    1
    gl.bindBuffer(target, buffer);

    target 用于表示 Buffer 对象中包含的数据类型:

    • gl.ARRAY_BUFFER: 包含顶点数据
    • gl.ELEMENT_ARRAY_BUFFER: 包含顶点索引值
  3. 向对象中写入数据

    1
    gl.bufferData(target, buffer, usage);

    其中usage表示程序该如何优化性能,不会导致程序错误:

    • gl.STATIC_DRAW: 数据被传入一次, 绘制很多次。
    • gl.STREAM_DRAW: 数据传入一次, 绘制若干次。
    • gl.DYNAMIC_DRAW: 数据不断传入,不断绘制。

    JS原生的 Array 没有对处理大量同类型数据做优化,所以这里引入了类型化数组

  4. 将对象分配给 attribute 变量

    gl.vertexAttribPointer(location, size, type, normalized, stride, offset)

    将整个 Buffer 对象的指针分配给 attribute 变量。

    • location: 待分配的 attribute 变量的位置

    • size: 每个顶点的分量个数(1 - 4)

    • type:

      gl.UNSIGNED_BYTE: Uint8Array

      gll.SHORT: Int16Arrat

      gl.UNSIGNED_SHORT: Uint16Array

      gl.INT: Int32Array

      gl.UUNSIGNED_INT: Uint32Array

      gl.FLOAT: Float32Array

    • normalized: 是否被归一化

    • stride: 不同顶点间的字节数量,默认是0

    • offset: attribute 变量的值从何处开始储存,默认是0

  5. 启用 attribute 变量

    gl.enableVertexAttribArray(position):开启数组(作用于 Buffer 对象)。

    gl.disableVertexAttribArray(location):关闭数组

Hello Triangle

只需要将 drawArray 的第一个参数更改为gl.TRIANGLES,然后将顶点着色器中的 PointSize 行删除:

Hello Triangle

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/3_DrawingandTransformingTriangles/2_HelloTriangle.html

基础形状

模式 介绍
gl.POINTS 不相关的点,绘制顺序:v0, v1, v2···
gl.LINES 不相关的线,绘制顺序:(v0, v1), (v2, v3)··· 如果有奇数个点最后一个点会被忽略
gl.LINE_STRIP 相连的线,绘制顺序:(v0, v1), (v1, v2)···
gl.LINE_LOOP 与strip类似,只是最后一个点会与第一个点相连
gl.TRIANGLES 不相关的三角形, 绘制顺序:(v0, v1, v2), (v3, v4, v5)··· 剩下的点会被忽略
gl.TRIANGLE_STRIP 三角形条,绘制顺序:(v0, v1, v2), (v2, v1, v3), (v2, v3, v4)···
gl.TRIANGLE_FAN 三角形扇,绘制顺序:(v0, v1, v2), (v0, v2, v3), (v0, v3, v4)···

类型化数组

Typed Array Number of Bytes per Element Description(C)
Int8Array 1 8-bit signed integer
Uint8Array 1 8-bit unsigned integer
Int16Array 2 16-bit signed integer
Uint16Array 2 16-bit unsigned integer
Int32Array 4 32-bit signed integer
Uint32Array 4 32-bit unsigned integer
Float32Array 4 32-bit floating point number
Float64Array 8 64-bit floating point number

类型化数组不支持push和pop方法。

方法、属性和常量 描述
get(i) 获取第 i 个元素
set(i, value) 设置第i各元素的值为value
set(array, offset) 从第offset个元素开始将array数组填充进去
length 长度
BYTE_PER_ELEMENT 每个元素的字节数

More Transformations and Basic Animation

Translate and Then Rotate

首先手写旋转矩阵实现一下:

先在顶点着色其中引入变换矩阵:

1
2
3
4
5
6
attribute vec4 a_Position;
uniform mat4 u_TransformMatrix;
void main()
{
gl_Position = u_TransformMatrix * a_Position;
}

然后在JS中引入变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var angle = 45;

var n = initVertexBuffers(gl);

···

var radian = Math.PI * angle / 180.0;
var sin = Math.sin(radian);
var cos = Math.cos(radian);
var matrix = new Float32Array([
cos, sin, 0, 0,
-sin, cos, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
])

var u_TransformMatrix = gl.getUniformLocation(gl.program, 'u_TransformMatrix');
gl.uniformMatrix4fv(u_TransformMatrix, false, matrix);

Rotate Triangle

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/4_MoreTransformationsandBasicAnimation/1_RotateTriangle.html

然后引入矩阵变换。由于WebGL没有自带的矩阵库,所以这里使用为本书制作的矩阵库:cuon-matrix.js。

1
2
3
4
5
var transformMatrix = new Matrix4();
transformMatrix.setRotate(angle, 0, 0, 1);

var u_TransformMatrix = gl.getUniformLocation(gl.program, 'u_TransformMatrix');
gl.uniformMatrix4fv(u_TransformMatrix, false, transformMatrix.elements);

注意这个库里的矩阵变换是在右边乘,所以实际顺序是反过来的。

Animation

制作每帧运行的动画效果

1
2
3
4
5
6
7
8
9
var tick = function()
{
currentAngle = animate(currentAngle);
modelMartrix.setRotate(currentAngle, 0, 0, 1);
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMartrix.elements);
draw(gl, n);
requestAnimationFrame(tick);
}
tick();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function animate(angle)
{
var deltaTime = Date.now() - lastTime;
lastTime = Date.now();

var newAngle = angle + (angleSpeed * deltaTime) / 1000.0;
return newAngle % 360;
}

function draw(gl, n)
{
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);
}

现在三角形就可以动起来了。

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/4_MoreTransformationsandBasicAnimation/3_RotatingTriangle.html

Using Colors and Texture Images

Passing Other Types of Information to Vertex Shader

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
function initVertexBuffers(gl)
{
var n = 3;
var vertices = new Float32Array
([
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5
]);
var size = new Float32Array
(
[10.0, 20.0, 30.0]
)

// 创建 Buffer 对象
var vertexBuffer = gl.createBuffer();
var sizeBuffer = gl.createBuffer();
if(!vertexBuffer || !sizeBuffer)
{
console.log('Create Buffer Object Failed');
return -1;
}

// 将 Buffer 对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

// 写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if(a_Position < 0)
{
console.log('Get Attribute a_Position Failed');
return -1;
}

gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);

// 写入顶点大小
gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
gl.bufferData(gl.ARRAY_BUFFER, size, gl.STATIC_DRAW);
var a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
if (a_PointSize < 0) {
console.log('Get Attribute a_Position Failed');
return -1;
}

gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_PointSize);

return n;
}

Mult-Attribute Size

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/5_UsingColorsandTextureImages/1_MultiAttributeSize.html

或者也可以这么写:

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
var n = 3;
var verticesSize = new Float32Array
([
0.0, 0.5, 10.0,
-0.5, -0.5, 20.0,
0.5, -0.5, 30.0
]);

// 创建 Buffer 对象
var vertexSizeBuffer = gl.createBuffer();
if (!vertexSizeBuffer)
{
console.log('Create Buffer Object Failed');
return -1;
}

// 将 Buffer 对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexSizeBuffer);

// 写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesSize, gl.STATIC_DRAW);

var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
var a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
if (a_Position < 0 || a_PointSize < 0)
{
console.log('Get Attribute Failed');
return -1;
}

var FSIZE = verticesSize.BYTES_PER_ELEMENT;

gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2);
gl.enableVertexAttribArray(a_PointSize);

return n;

Color Triangle

通过varying变量从顶点着色器传值到片元。

1
2
3
4
5
6
7
8
9
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main()
{
gl_Position = a_Position;
gl_PointSize = 10.0;
v_Color = a_Color; // 变量赋值
}
1
2
3
4
5
6
precision mediump float;
varying vec4 v_Color;
void main()
{
gl_FragColor = v_Color;
}

Multi-Attribute Color

三角形结果(自动插值)

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/5_UsingColorsandTextureImages/2_MultiAttributeColor.html

Pasting an Image onto a Rectangle

在顶点和片元着色其中传递 UV 坐标

1
2
3
4
5
6
7
8
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main()
{
gl_Position = a_Position;
v_TexCoord = a_TexCoord;
}
1
2
3
4
5
6
7
precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main()
{
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}

将顶点坐标和顶点 UV 坐标传入shader

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
var n = 4;
// 顶点坐标和 UV 值
var verticesTexCoord = new Float32Array
([
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.0
]);

// 创建 Buffer 对象
var vertexTexCoordBuffer = gl.createBuffer();

···

// 将 Buffer 对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);

// 写入
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoord, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');

···

var FSIZE = verticesTexCoord.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord);
return n;

创建贴图并且加载贴图:

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
function initAndLoadTexture(gl, n) {
// 创建 Texture 对象
var texture = gl.createTexture();
if (!texture) {
console.log('Create Texture Failed');
return false;
}

var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
if (!u_Sampler) {
console.log('Get Uniform Failed');
return false;
}

// 创建 Image 对象并加载
var image = new Image();
image.onload = function () {
loadTexture(gl, n, texture, u_Sampler, image);
}
image.src = '../resource/test.jpg';

return true;
}
// 处理 Texture 对象
function loadTexture(gl, n, texture, u_Sampler, image) {
// 翻转 Y 轴
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 开启 0 号纹理单元
gl.activeTexture(gl.TEXTURE0);
// 指定贴图对象为 2D 贴图
gl.bindTexture(gl.TEXTURE_2D, texture);

// 设定贴图属性(缩小采样为线性)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 为贴图对象指定图片
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);

// 把 0 号纹理单元指定给变量
gl.uniform1i(u_Sampler, 0);

StartRender(gl, n);
}
  1. 使用 gl.CreatTexture() 创建 Texture 对象,然后让浏览器加载图片

  2. 因为 WebGl的纵轴和图片纵轴相反,所以需要翻转Y轴:

    gl.pixelStorei(pname, param)

    • pname

      gl.UNPACK_FLIP_Y_WEBGL:加载图像后,翻转图像Y轴

      gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL:把RGB分别乘以A

    • param:0位假,非0为真。

  3. 开启贴图单元:gl.activeTexture(texUnit)

    • texUnit:gl.TEXTURE0 - gl.TEXTURE7(可能会更多)
  4. 指定贴图类型:gl.bindTexture(target, texture)

    • target:gl.TEXTURE_2D, gl.TEXTURE_CUBE_MAP
  5. 设置贴图参数:gl.texParameteri(target, pname, param)

    • target: gl.TEXTURE_2D, gl.TEXTURE_CUBE_MAP

    • pname:

      pname description default value
      gl.TEXTURE_MAG_FLITER 贴图放大采样方法 gl.LINEAR
      gl.TEXTURE_MIN_FLITER 贴图缩小采样方法 gl.NEAREST_MIPMAP_LINEAR
      gl.TEXTURE_WRAP_S 纵向 warp 模式 gl.REPEAT
      gl.TEXTURE_WRAP_TA 横向 warp 模式 gl.REPEAT
    • param:

      对于贴图采样方法:

      gl.LINEAR, gl.NEAREST

      gl.NEAREST_MIPMAP_NEAREST, gl.LINEAR_MIPMAP_NEAREST, gl.NEAREST_MIPMAP_LINEAR, gl,LINEAR_MIPMAP_LINEAR

      对于 Wrap 模式:gl.REPEAT, gl.MIRRORED_REPEAT, gl.CLAMP_TO_EDGE

  6. 为贴图对象指定图片: gl.texImage(target, level, internalformat, format, type, image)

    • target: gl.TEXTURE_2D, gl.TEXTURE_CUBE_MAP

    • level: MipMap层级,一般都是0

    • internalformat: 设置图片格式

      Format 纹素
      gl.RGB (R, G, B, 1)
      gl.RGBA (R, G, B, A)
      gl.ALPHA (0, 0, 0, A)
      gl.LUMINANCE (L, L, L, 1)
      gl.LUMINANCE_ALPHA (L, L, L, A)
    • format: 纹素格式,要与 internal format 一样。

    • type: 纹素的数据类型

      Type 介绍
      gl.UNSIGNED_BYTE 无符号字节,每个元素有1字节
      gl.UNSIGNED_SHORT_5_6_5 RGB 分别有5, 6, 5字节
      gl.UNSIGNED_SHORT_4_4_4_4 RGBA 每个4字节
      gl.UNSIGNED_SHORT_5_5_5_1 RGB 每个5字节,A 1字节

      image: 图像

  7. 将纹理单元分配给uniform变量:gl.uniform1i(paramLoc, num)

Texture Quad

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/5_UsingColorsandTextureImages/3_TextureQuad.html

Pasting Multiple Textures to a Shape

传入多张贴图:

1
2
3
4
5
6
7
8
9
10
precision mediump float;
uniform sampler2D u_Sampler_Base;
uniform sampler2D u_Sampler_Mask;
varying vec2 v_TexCoord;
void main()
{
vec3 baseColor = texture2D(u_Sampler_Base,v_TexCoord).rgb;
float mask = texture2D(u_Sampler_Mask, v_TexCoord).r;
gl_FragColor = vec4(baseColor * mask, 1);
}
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
// 创建 Texture 对象
var textureBase = gl.createTexture();
var textureMask = gl.createTexture();
if (!textureBase || !textureMask) {
console.log('Create Texture Failed');
return false;
}

var u_Sampler_Base = gl.getUniformLocation(gl.program, 'u_Sampler_Base');
var u_Sampler_Mask = gl.getUniformLocation(gl.program, 'u_Sampler_Mask');
if (!u_Sampler_Base || !u_Sampler_Mask) {
console.log('Get Uniform Failed');
return false;
}

// 创建 Image 对象并加载
var imageBase = new Image();
var imageMask = new Image();
imageBase.onload = function () {
loadTexture(gl, n, textureBase, u_Sampler_Base, imageBase, 0);
}
imageMask.onload = function () {
loadTexture(gl, n, textureMask, u_Sampler_Mask, imageMask, 1);
}
imageBase.src = '../resource/test.jpg';
imageMask.src = '../resource/Drop.png';
return true;

在 loadTexture 函数中分配 Texture Unit 即可。

Multi-Texture

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/5_UsingColorsandTextureImages/4_MultiTexture.html

The OpenGL ES Shading Language (GLSL ES)

官方文档:https://www.khronos.org/registry/OpenGL-Refpages/

自己写的文章:https://1keven1.github.io/2022/01/11/%E3%80%90OpenGL%E3%80%91OpenGL%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/

Toward the 3D World

Specifying the Viewing Direction

可以使用cuon-matrix中的函数setLookAt来创建一个视图矩阵,然后输入三维坐标就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');

···

var viewMatrix = new Matrix4();
viewMatrix.setLookAt(0.2, 0.25, 0.25, 0, 0, 0, 0, 1, 0);
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);

···

var verticesColors = new Float32Array
([
0.0, 0.5, -0.4, 0.4, 1.0, 0.4,
-0.5, -0.5, -0.4, 0.4, 1.0, 0.4,
0.5, -0.5, -0.4, 1.0, 0.4, 0.4,

0.5, 0.4, -0.2, 1.0, 0.4, 0.4,
-0.5, 0.4, -0.2, 1.0, 1.0, 0.4,
0.0, -0.6, -0.2, 1.0, 1.0, 0.4,

0.0, 0.5, 0.0, 0.4, 0.4, 1.0,
-0.5, -0.5, 0.0, 0.4, 0.4, 1.0,
0.5, -0.5, 0.0, 1.0, 0.4, 0.4
]);

Look At Triangles

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/7_Towardthe3DWorld/1_LookAtTriangles.html

多个矩阵时在CPU算出结果后一并传入GPU:

1
2
3
4
5
6
7
var viewMatrix = new Matrix4();
viewMatrix.setLookAt(0.2, 0.25, 0.25, 0, 0, 0, 0, 1, 0);
var modelMatrix = new Matrix4();
modelMatrix.setRotate(45, 0, 0, 1);

var modelViewMatrix = viewMatrix.multiply(modelMatrix);
gl.uniformMatrix4fv(u_Matrix_MV, false, modelViewMatrix.elements);

写按键控制摄像机位置:

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
// 注册按键按下事件
document.onkeydown = function (event) {
keydown(event, gl, n, u_ViewMatrix, viewMatrix);
}

···

function keydown(event, gl, n, u_ViewMatrix, viewMatrix) {
// 按下左右改变摄像机X坐标
if (event.keyCode == 39 && eyeX < 0.25){
eyeX += 0.01;
}
else if (event.keyCode == 37 && eyeX > -0.25){
eyeX -=0.01;
}
// 按下上下改变摄像机Y坐标
else if (event.keyCode == 38 && eyeY < 0.25){
eyeY +=0.01;
}
else if (event.keyCode == 40 && eyeY > -0.25){
eyeY -=0.01;
}
else { return; }

viewMatrix.setLookAt(eyeX, eyeY, eyeZ, 0, 0, 0, 0, 1, 0)
draw(gl, n, u_ViewMatrix, viewMatrix);
}

可以用键盘上下左右键移动摄像机了

Specifying the Visible Range (Box Type)

WebGL只会显示在摄像机前2*2*1的标准立方体中的东西,所以可以用投影矩阵把点变换到这个空间里。

使用cuon_matrix中的函数 setOrtho(left, right, bottom, top, near, far) 即可:

1
projMatrix.setOrtho(-1, 1, -1, 1, near, far);

Specifying the Visible Range Using a Quadrangular Pyramid

使用cuon_matrix中的函数 setPerspective(fov, aspect, near, far) 即可:

1
projMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100);

Perspective View

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/7_Towardthe3DWorld/5_PerspectiveView.html

Correct Handling Foreground and Background Objects

WebGL默认是按照顶点输入顺序渲染的,所以有会造成前后遮挡错误。

因此我们需要开启深度测试:

1
2
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.DEPTH_BUFFER_BIT);

Hello Cube

一个一个指定顶点很复杂,因为模型有很多共用顶点。

可以使用顶点索引的方式绘制模型

顶点索引数据结构

需要用到gl.drawElements(mode, num, type, offset)

修改InitVertexBuffer函数,

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
function initVertexBuffers(gl)
{
// 所有的顶点坐标
var verticesColors = new Float32Array
([
1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
-1.0, 1.0, 1.0, 1.0, 0.0, 1.0,
-1.0, -1.0, 1.0, 1.0, 0.0, 0.0,
1.0, -1.0, 1.0, 1.0, 1.0, 0.0,
1.0, -1.0, -1.0, 0.0, 1.0, 0.0,
1.0, 1.0, -1.0, 0.0, 1.0, 1.0,
-1.0, 1.0, -1.0, 0.0, 0.0, 1.0,
-1.0, -1.0, -1.0, 0.0, 0.0, 0.0,
]);
var FSIZE = verticesColors.BYTES_PER_ELEMENT;

// 所有的三角形顶点索引值
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3,
0, 3, 4, 0, 4, 5,
0, 5, 6, 0, 6, 1,
1, 6, 7, 1, 7, 2,
7, 4, 3, 7, 3, 2,
4, 7, 6, 4, 6, 5
])

// 将顶点坐标和颜色写入buffer对象
var vertexColorBuffer = gl.createBuffer();
···

gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if (a_Position < 0 || a_Color < 0)
···

gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
gl.enableVertexAttribArray(a_Color);

// 将顶点索引写入Element Buffer
var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

return indices.length;
}

使用drawElement函数

1
2
3
4
5
function draw(gl, n)
{
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

注意,如果定点数量大于256个,则需要使用Uint16Array,同时数据类型为gl.USIGNED_SHORT。

Hello Cube

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/7_Towardthe3DWorld/6_HelloCube.html

Lighting Objects

Lighting 3D Objects

通过外部传入的光线颜色方向和顶点法线计算最终颜色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Vertex Attribute
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal;
// Matrix
uniform mat4 u_Matrix_M;
uniform mat4 u_Matrix_MVP;
//Lighting
uniform vec3 u_LightColor;
uniform vec3 u_LightDir;
uniform vec3 u_AmibientColor;

varying vec4 v_Color;
void main()
{
gl_Position = u_Matrix_MVP * a_Position;
vec3 worldNormal = normalize(u_Matrix_M * vec4(a_Normal.xyz, 0.0)).xyz;// 这里计算有问题 懒得求逆矩阵了 就这样吧 好孩子不要学
float nDotL = max(0.0, dot(u_LightDir, worldNormal));
vec3 diffuse = u_LightColor * a_Color.xyz * nDotL;
vec3 ambient = u_AmibientColor * a_Color.rgb;

vec3 finalColor = diffuse + ambient;
v_Color = vec4(finalColor, a_Color.a);
}

在JS中传入更多矩阵和顶点信息:

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
var mMatrix = new Matrix4();
var mvpMatrix = new Matrix4();
var lightDir = new Vector3([0.5, 3.0, 4.0]);
lightDir = lightDir.normalize();

gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);
gl.uniform3fv(u_LightDir, lightDir.elements);
gl.uniform3f(u_AmibientColor, 0.2, 0.2, 0.2);

currentAngle = 0;
var tick = function ()
{
currentAngle = animate(currentAngle, rotateSpeed);

mvpMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100);
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
mvpMatrix.rotate(currentAngle, 0, 1, 0);
mMatrix.setRotate(currentAngle, 0, 1, 0);

gl.uniformMatrix4fv(u_Matrix_MVP, false, mvpMatrix.elements);
gl.uniformMatrix4fv(u_Matrix_M, false, mMatrix.elements);

draw(gl, n);
requestAnimationFrame(tick);
}
tick()

···

// 顶点坐标、颜色、法线
var vertices = new Float32Array([ // Coordinates
1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, // v0-v1-v2-v3 front
1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, // v0-v3-v4-v5 right
1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, // v0-v5-v6-v1 up
-1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, // v1-v6-v7-v2 left
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, // v7-v4-v3-v2 down
1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0 // v4-v7-v6-v5 back
]);

var colors = new Float32Array([ // Colors
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v1-v2-v3 front
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v3-v4-v5 right
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v5-v6-v1 up
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v1-v6-v7-v2 left
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v7-v4-v3-v2 down
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0  // v4-v7-v6-v5 back
]);

var normals = new Float32Array([ // Normal
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v0-v1-v2-v3 front
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // v0-v3-v4-v5 right
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v5-v6-v1 up
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, // v1-v6-v7-v2 left
0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, // v7-v4-v3-v2 down
0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0 // v4-v7-v6-v5 back
]);

// 顶点编号
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9, 10, 8, 10, 11, // up
12, 13, 14, 12, 14, 15, // left
16, 17, 18, 16, 18, 19, // down
20, 21, 22, 20, 22, 23 // back
]);

Lighted Cube

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/8_LightingObjects/1_LightedCube.html

Lighting the Translated-Rotated Object

在含有缩放的变换中,法线变换会出问题。

使用M矩阵的逆转置矩阵变换法线即可。

Using a Point Light Object

需要计算光照方向了(现在还没计算衰减)

1
2
3
4
5
6
7
8
9
10
11
gl_Position = u_Matrix_MVP * a_Position;
vec3 worldPos = (u_Matrix_M * a_Position).xyz;
vec3 worldNormal = normalize(u_Matrix_M_IT * vec4(a_Normal.xyz, 0.0)).xyz;
vec3 lightDir = normalize(u_LightPos - worldPos);

float nDotL = max(0.0, dot(lightDir, worldNormal));
vec3 diffuse = u_LightColor * a_Color.xyz * nDotL;
vec3 ambient = u_AmibientColor * a_Color.rgb;

vec3 finalColor = diffuse + ambient;
v_Color = vec4(finalColor, a_Color.a);

Point Lighted Cube

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/8_LightingObjects/2_PointLightedCube.html

把光照计算挪到片元着色器更好

1
2
3
4
5
6
7
8
9
10
void main()
{
gl_Position = u_Matrix_MVP * a_Position;
vec3 worldPos = (u_Matrix_M * a_Position).xyz;
vec3 worldNormal = normalize(u_Matrix_M_IT * vec4(a_Normal.xyz, 0.0)).xyz;

v_Color = a_Color;
v_WorldPos = worldPos;
v_WorldNormal = worldNormal;
}
1
2
3
4
5
6
7
8
9
10
11
12
void main()
{
vec3 worldNormal = normalize(v_WorldNormal);
vec3 lightDir = normalize(u_LightPos - v_WorldPos);

float nDotL = max(0.0, dot(worldNormal, lightDir));
vec3 diffuse = v_Color.rgb * nDotL * u_LightColor;
vec3 ambient = u_AmibientColor * v_Color.rgb;

vec3 finalColor = diffuse + ambient;
gl_FragColor = vec4(finalColor, 1);
}

光照更加细腻

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/8_LightingObjects/3_PointLightedCube_Frag.html

Hierarchical Objects

前面没啥用

Shader and Program Objects: The Role of initShaders()

在cuon-utils.js中定义,大概流程:

  1. gl.createShader(type);
  2. gl.shaderSource(shader, source);
  3. gl.compileShader(shader);
  4. gl.createProgram();
  5. gl.attachShader(program, shader);
  6. gl.linkProgram(program);
  7. gl.useProgram(program);

Advanced Techniques

Rotate an Object with the Mouse

用鼠标事件就好了

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 initEventHandler(canvas)
{
var drag = false;
var lastX = -1;
var lastY = -1;

canvas.onmousedown = function (event)
{
var x = event.clientX;
var y = event.clientY;
drag = true;
lastX = x;
lastY = y;
};

canvas.onmouseup = function (event) { drag = false; };
canvas.onmouseout = function (event) { drag = false; };

canvas.onmousemove = function (event)
{
if (drag)
{
var x = event.clientX;
var y = event.clientY;
var factor = 100 / canvas.height;
var dx = (x - lastX) * factor;
var dy = (y - lastY) * factor;
currentAngle += dx;
lastX = x;
lastY = y;
}
}
}

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/1_RotateObject.html

Select an Object

点击时绘制一个单色图片,并使用gl.readPixel读取鼠标位置像素颜色值即可。不如添加bounding box用cpu做射线检测。

HUD

只要两个canvas即可,一个渲染3D一个2D,用z-index排序;

1
2
3
4
<canvas id="webgl" height="400" width="400" style="position: absolute; z-index: 0;">
Please use a Browser that support WebGL
</canvas>
<canvas id="hud" width="400" height="400" style="position: absolute; z-index: 1;"></canvas>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hud = document.getElementById('hud');
ctx = hud.getContext('2d');
···
function drawHUD(ctx, currentAngle)
{
ctx.clearRect(0, 0, 400, 400);
// 绘制三角形
ctx.beginPath();
ctx.moveTo(120, 10);
ctx.lineTo(200, 150);
ctx.lineTo(40, 150);
ctx.closePath();
ctx.strokeStyle = 'rgba(255, 100, 255, 1)';
ctx.stroke();
// 绘制字体
ctx.font = '18px "Times New Roman"';
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
ctx.fillText('This is a hud', 40, 180);
ctx.fillText('Current Angle: '+Math.floor(currentAngle), 40, 200);
}

HUD结果

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/2_HUD.html

把gl.clearColor的A设置为0即可做出3D浮在网页表面的效果了

Alpha Blending

使用以下命令:

1
2
gl.enable(gl.BLEND);	// 开启混合
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // 混合模式

结果(懒得设置投影矩阵了 所以有被剪裁)

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/3_BlendTriangles.html

使用混合要关闭深度测试:

1
gl.depthMask(false);	// 关闭深度写入

Switching Shaders

先把program都创建出来,然后运行时使用gl.useProgram()切换

这里我大幅度重构了代码,并使用object储存两个材质以及模型(这里之后肯定要做成类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 材质1
let materialFlat = new Object();
materialFlat.VSHADER_SOURCE = null;
materialFlat.FSHADER_SOURCE = null;
materialFlat.shaderReadOver = false;
materialFlat.program = null;
// 材质2
let materialTex = new Object();
materialTex.VSHADER_SOURCE = null;
materialTex.FSHADER_SOURCE = null;
materialTex.shaderReadOver = false;
materialTex.program = null;
// 模型
let modelCube = new Object();
modelCube.vertexBuffer = null;
modelCube.texCoordBuffer = null;
modelCube.normalBuffer = null;
modelCube.indexBuffer = null;
modelCube.indexNum = -1;

记得传入变量的时候也要useProgram

经历一系列初始化后,draw中useProgram:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function draw(gl, model, material, vpMatrix, transform)
{
gl.useProgram(material.program);
gl.enable(gl.DEPTH_TEST);

bindAttributeToBuffer(gl, material.program.a_Position, model.vertexBuffer);
bindAttributeToBuffer(gl, material.program.a_TexCoord, model.texCoordBuffer);
bindAttributeToBuffer(gl, material.program.a_Normal, model.normalBuffer);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.indexBuffer);

if(material.program.u_LightPos) gl.uniform4f(materialFlat.program.u_LightPos, 0.5, 3.0, 4.0, 0.0);
if(material.program.u_LightColor) gl.uniform4f(materialFlat.program.u_LightColor, 1.0, 1.0, 1.0, 1.0);

let mMatrix = new Matrix4().setTranslate(transform, 0, 0).scale(0.7, 0.7, 0.7).rotate(0, 0, 1, 0);
let mIMatrix = new Matrix4().setInverseOf(mMatrix);
let mvpMatrix = new Matrix4().set(vpMatrix).multiply(mMatrix);

gl.uniformMatrix4fv(material.program.u_Matrix_MVP, false, mvpMatrix.elements);
gl.uniformMatrix4fv(material.program.u_Matrix_M_I, false, mIMatrix.elements);

gl.drawElements(gl.TRIANGLES, model.indexNum, model.indexBuffer.dataType, 0);
}

结果

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/4_SwitchShader.html

Use What You’ve Drawn as a Texture Image

Color Buffer直接显示在屏幕上 Frame Buffer不是

  1. 创建FrameBuffer对象
  2. 创建贴图,设置大小和参数
  3. 创建renderbuffer
  4. 设置renderbuffer的参数
  5. 把贴图绑到FrameBuffer的颜色上
  6. 把RenderBuffer绑到FrameBuffer的深度上
  7. 检查FrameBuffer是否完善
  8. 绘制

代码如下:

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
function initFrameBufferObject(gl)
{
let framebuffer, texture, depthBuffer;
// 错误返回函数
var error = function ()
{
if (framebuffer) gl.deleteFramebuffer(framebuffer);
if (texture) gl.deleteTexture(texture);
if (depthBuffer) gl.deleteRenderbuffer(depthBuffer);
return null;
}

// 创建
framebuffer = gl.createFramebuffer();
if(!framebuffer){
console.log("Create Framebuffer Fail");
error();
}

// 创建贴图,设置大小和参数
texture = gl.createTexture();
if(!texture){
console.log("Create Framebuffer texture Fail");
error();
}

gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, bufferWidth, bufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
framebuffer.texture = texture;

// 创建renderbuffer
depthBuffer = gl.createRenderbuffer();
if(!depthBuffer){
console.log('Create Framebuffer Renderbuffer Fail');
error();
}
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, bufferWidth, bufferHeight);

// 把贴图和renderbuffer绑到FrameBuffer上
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);

// 检查FrameBuffer是否完善
let e = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if(e !== gl.FRAMEBUFFER_COMPLETE){
console.log('Framebuffer is not complete: '+e.toString());
error();
}

gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);

return framebuffer;
}

渲染的时候绘制两次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var tick = function ()
{
currentAngle = animate(currentAngle, rotateSpeed);

// 绘制Framebuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.viewport(0, 0, bufferWidth, bufferHeight);
gl.clearColor(0.2, 0.2, 0.2, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

gl.uniform1i(materialPlane.program.u_Sampler_Tex, 0);
draw(gl, modelCube, materialPlane, vpMatrix, 1, currentAngle);

// 绘制Colorbuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

gl.uniform1i(materialPlane.program.u_Sampler_Tex, 1);
draw(gl, modelPlane, materialPlane, vpMatrix, 1.7, currentAngle * 0.5);

requestAnimationFrame(tick);
}

结果

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/5_FrameBufferObject.html

Display Shadows

  1. 到光源的位置绘制一张深度贴图,当做阴影贴图
  2. 正常绘制的时候对比当前像素距离光源的距离与阴影贴图的大小,以得知当前像素在不在阴影中

ShadowCater shader

1
2
3
4
void main()
{
gl_Position = u_Matrix_MVP * a_Position;
}
1
2
3
4
void main()
{
gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 1.0);
}

正常shader中采样阴影贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
attribute vec4 a_Position;
attribute vec4 a_TexCoord;
attribute vec4 a_Normal;

uniform mat4 u_Matrix_M_I;
uniform mat4 u_Matrix_MVP;
uniform vec4 u_LightPos;
uniform vec4 u_LightColor;
uniform mat4 u_Matrix_Light;

varying vec4 v_TexCoord;
varying vec3 v_WorldNormal;
varying vec4 v_PositionFromLight;
void main()
{
gl_Position = u_Matrix_MVP * a_Position;
vec3 worldNormal = (vec4(a_Normal.xyz, 0.0) * u_Matrix_M_I).xyz;

v_TexCoord = a_TexCoord;
v_WorldNormal = worldNormal;
v_PositionFromLight = u_Matrix_Light * a_Position;
}
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
#version 100

···

uniform mat4 u_Matrix_M_I;
uniform mat4 u_Matrix_MVP;
uniform vec4 u_LightPos;
uniform vec4 u_LightColor;
uniform sampler2D u_ShadowMap;

uniform vec3 u_AmbientColor;

varying vec4 v_TexCoord;
varying vec3 v_WorldNormal;
varying vec4 v_PositionFromLight;
void main()
{
vec3 albedo = vec3(0.5, 0.5, 0.5);
vec3 worldNormal = normalize(v_WorldNormal);
vec3 lightDir = normalize(u_LightPos.xyz);
float nDotL = max(0.0, dot(worldNormal, lightDir));
vec3 diffuse = albedo * nDotL * u_LightColor.xyz;
vec3 ambient = u_AmbientColor * albedo;

// 阴影
vec3 shadowCoord = (v_PositionFromLight.xyz / v_PositionFromLight.w) / 2.0 + 0.5;
float depth = texture2D(u_ShadowMap, shadowCoord.xy).r;
float shadow = (shadowCoord.z > depth + 0.01) ? 0.0 : 1.0;

vec3 finalColor = diffuse * shadow + ambient;
gl_FragColor = vec4(finalColor, 1.0);
}

然后先绘制阴影贴图 再渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 绘制ShadowMap
gl.bindFramebuffer(gl.FRAMEBUFFER, shadowMap);
gl.viewport(0, 0, bufferWidth, bufferHeight);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

draw(gl, modelCube, shadowCaster, lightVPMatrix, lightVPMatrix, 0.5, currentAngle, 0)
draw(gl, modelPlane, shadowCaster, lightVPMatrix, lightVPMatrix, 2, 0, -90, -0.5);

// 绘制
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

draw(gl, modelCube, myMaterial, vpMatrix, lightVPMatrix, 0.5, currentAngle, 0)
draw(gl, modelPlane, myMaterial, vpMatrix, lightVPMatrix, 2, 0, -90, -0.5);

结果

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/6_Shadow.html

不过这个阴影精准度不高,因为只用了一个R通道存,只有8bit的精准度。

可以使用rgba四个通道就有4字节32bit的储存空间了,先在ShadowCaster中给深度信息编码存在rgba中

1
2
3
4
5
6
7
8
void main()
{
const vec4 bitShift = vec4(1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0);
const vec4 bitMask = vec4(1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0, 0.0);
vec4 rgbaDepth = fract(gl_FragCoord.z * bitShift);
rgbaDepth -= rgbaDepth.gbaa * bitMask;
gl_FragColor = rgbaDepth;
}

然后使用的时候解码:

1
2
3
4
5
6
float unpackDepth(const in vec4 rgbaDepth)
{
const vec4 bitShift = vec4(1.0, 1.0 / 256.0, 1.0 / (256.0 * 256.0), 1.0 / (256.0 * 256.0 * 256.0));
float depth = dot(rgbaDepth, bitShift);
return depth;
}

这样就有精度极高的阴影了,与深度比对的时候基本不需要给深度做任何偏移。

1
float shadow = (shadowCoord.z > depth + 0.0001) ? 0.0 : 1.0;

Load and Display 3D Models

  1. 以文本文件形式读取OBJ文件
  2. 根据每行开头分别储存模型信息
  3. 根据模型信息生成buffer数据数组
  4. 然后使用数组渲染

读取OBJ文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function initOBJ(fileName, gl, model, scale, reverse)
{
let request = new XMLHttpRequest();
request.onreadystatechange = function ()
{
if (request.readyState === 4 && request.status !== 404)
{
let objDoc = new OBJDoc(fileName);
// 读取OBJ数据
if (!objDoc.parse(request.responseText, scale, reverse))
{
console.log('读入OBJ文件失败');
return;
}
// 解码数据为数组
let bufferArrays = objDoc.DecodeBufferArrays();
initModelBuffers(gl, model, bufferArrays);
model.loaded = true;
}
}
request.open('GET', fileName, true);
request.send();
}

OBJDoc类 解码OBJ文件

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
let OBJDoc = function (fileName)
{
this.fileName = fileName;
this.vertices = new Array(0);
this.texCoords = new Array(0);
this.normals = new Array(0);
this.faces = new Array(0);
}

OBJDoc.prototype.parse = function (fileString, scale, reverseNormal)
{
let lines = fileString.split('\n');
lines.push(null);
let index = 0;

let line;
let sp = new StringParser();
while ((line = lines[index++]) != null)
{
sp.init(line);
let command = sp.getWord();
if (command == null) continue;

switch (command)
{
case '#':
continue;
case 'mtllib':
continue;
case 'usemtl':
continue;
case 's':
continue;
case 'o':
console.log('More Than 1 Object in OBJ: ', this.fileName);
return false;
case 'g':
console.log('More Than 1 Object in OBJ: ', this.fileName);
return false;
case 'v':
let vertex = this.parseVertex(sp, scale);
this.vertices.push(vertex);
continue;
case 'vt':
let texCoord = this.parseTexCoord(sp);
this.texCoords.push(texCoord);
continue;
case 'vn':
let normal = this.parseNormal(sp);
this.normals.push(normal);
continue;
case 'f':
let face = this.parseFace(sp);
this.faces.push(face);
continue;
default:
console.log('Undefined OBJ Head: ', command);
return false;
}
}
return true;
}

生成Buffer数组:

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
OBJDoc.prototype.DecodeBufferArrays = function ()
{
// Create an arrays for vertex coordinates, normals, colors, and indices
let numIndices = this.faces.length * 3;

let numVertices = numIndices;
let vertices = new Float32Array(numVertices * 3);
let texCoords = new Float32Array(numVertices * 2);
let normals = new Float32Array(numVertices * 3);
let indices = new Uint16Array(numIndices);

// Set vertex, normal and color
let index_indices = 0;
for (let i = 0; i < this.faces.length; i++)
{
let face = this.faces[i];
for (let j = 0; j < 3; j++)
{
// Set index
indices[index_indices] = index_indices;
// Copy vertex
let vIndex = face.vIndex[j];
let vertex = this.vertices[vIndex];
vertices[index_indices * 3 + 0] = vertex.x;
vertices[index_indices * 3 + 1] = vertex.y;
vertices[index_indices * 3 + 2] = vertex.z;
// UV坐标
let tIndex = face.tIndex[j];
let texCoord;
if (tIndex >= 0) texCoord = this.texCoords[tIndex];
else texCoord = new TexCoord(0, 0);
texCoords[index_indices * 2 + 0] = texCoord.u;
texCoords[index_indices * 2 + 1] = texCoord.v;
// Copy normal
let nIdx = face.nIndex[j];
let normal = this.normals[nIdx];
normals[index_indices * 3 + 0] = normal.x;
normals[index_indices * 3 + 1] = normal.y;
normals[index_indices * 3 + 2] = normal.z;
index_indices++;
}
}

return new BufferArrays(vertices, texCoords, normals, indices);
}

结果

链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/7_ReadOBJ.html

Handling Lost Context

为了防止打开网页后休眠设备导致的上下文丢失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function main()
{
canvas = document.getElementById("webgl");

canvas.addEventListener('webglcontextlost', contextLost, false);
canvas.addEventListener('webglcontextrestored', function(event){
let gl = getWebGLContext(canvas);
start(gl);
})

let gl = getWebGLContext(canvas);

···

// 读取shader文件到material中
readShaderFile(gl, '7_ReadOBJ.vert', '7_ReadOBJ.frag', myMaterial);
}
···
function contextLost(event)
{
cancelAnimationFrame(frameRequest);
event.preventDefault();
}

其他

WebGL不需要Swap Buffer,浏览器自动完成了这步操作

WebGL在剪裁空间是左手系,所以开启深度测试后会变为左手系,也就是Y轴正半轴指向屏幕内部。需要使用投影矩阵转换坐标系、