【WebGL之巅】27-光照-运动物体的光照效果

By 大Van家 on 2021-08-08
阅读时间 14 分钟
文章共 3k
阅读量

对应《WebGL编程指南》第八章 39-LightedTranslatedRotatedCube

要点:平行光、漫反射+环境反射、魔法矩阵(逆转置矩阵)

知识点

一、坐标变换引起的法向量变化

在程序 LightedTranslatedRotatedCube 中,立方体先绕 z 轴顺时针旋转了90度,然后沿着 y 轴平移了0.9个单位。场景中的光照情况与前一节的 LightedCube_ambient 一样,既有平行光又有环境光。结果如下图:

LightedTranslatedRotatedCube

立方体旋转时,每个表面的法向量也会随之变化。在下图中,我们沿着 z 轴负方向观察一个立方体,最左边是立方体的初始状态,途中标出了立方体右侧面的法向量(1, 0, 0),它指向 x 轴正方向,然后对该立方体进行变换,观察右侧面法向量随之变化的情况。

坐标变换引起的法向量变化

由上图可知:

  • 平移变换不会改变法向量,因为平移不会改变物体的方向。
  • 旋转变换会改变法向量,因为旋转改变了物体的方向。
  • 缩放变换对法向量的影响较为复杂。最右侧的图显示了立方体先旋转了45度,再在y轴上拉伸至原来的2倍的情况。此时法向量改变了,因为表面的朝向改变了。但是,如果缩放比例在所有轴上都一致的话,那么法向量就不会变化。最后,即使物体在某些轴上的缩放比例并不一致,法向量也并不一定会变化,比如将最左侧图中的立方体在y轴方向上拉伸两倍,法向量就不会变化。

显然,在对物体进行不同变化时,法向量的变化情况较为复杂。这时候,数学公式就会派上用场了。

二、魔法矩阵:逆转置矩阵

2.1 定义

在第4章中曾讨论过,对顶点进行变换的矩阵称为模型矩阵。如何计算变换之后的法向量呢?只要将变换之前的法向量乘以模型矩阵的逆转置矩阵即可。所谓逆转置矩阵,就是逆矩阵的转置

逆矩阵的含义是,如果矩阵 M 的逆矩阵是 R,那么 R M 或 M R 的结果都是单位矩阵。转置的意思是,将矩阵的行列进行调换

2.2 使用规则

逆转置矩阵的用法总结如下:

规则:用法向量乘以模型矩阵的逆转置矩阵,就可以求得变换后的法向量。

求逆转置矩阵的两个步骤:

  • 求原矩阵的逆矩阵
  • 将上一步求得的逆矩阵进行转置

2.3 setInverseOf与transpose

Matrix4 对象提供了便捷的方法来完成上述任务:

方法 描述
Matrix4.setInverseOf(m) 使自身(调用该方法的Matrix4实例)成为矩阵m的逆矩阵
Matrix4.transpose() 对自身进行转置操作,并将自身设定为转置后的结果

假设模型矩阵存储在 modelMatrix 对象中,那么下面这段代码将会计算它的 逆转置矩阵,并将其存储在 normalMatrix 对象中:

1
2
3
4
5
6
7
var u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
// 用来变换法向量的矩阵
var normalMatrix = new Matrix4();
// 根据模型矩阵计算用来变换法向量的矩阵
normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);

三、程序分析

下面来看看示例程序 LightedTranslatedRotatedCube.js 的代码。该程序使立方体绕 z 轴顺时针旋转90度,然后沿 y 轴平移0.9个单位,并且处于平行光和环境光的照射下。立方体在变换之前,与 LightedCube_ambient 中的立方体完全相同。

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' + //表面基底色
'attribute vec4 a_Normal;\n' + //法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_NormalMatrix;\n' + //用来变换法向量的矩阵
'uniform vec3 u_LightColor;\n' + //光线颜色
'uniform vec3 u_LightDirection;\n' + //归一化的世界坐标(入射光方向)
'uniform vec3 u_AmbientLight;\n' + //环境光颜色
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' + //对法向量进行归一化
' float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' + //计算光线方向和法向量的点积
' vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;\n' + //计算漫反射光的颜色
' vec3 ambient = u_AmbientLight * vec3(a_Color);\n' + //计算环境光产生的反射光颜色
' v_Color = vec4(diffuse + ambient, a_Color.a);\n' +
'}\n';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main()

...

var u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');

var modelMatrix = new Matrix4(); //模型矩阵
var normalMatrix = new Matrix4(); // 用来变换法向量的矩阵

modelMatrix.setTranslate(0,0.9,0);//y轴平移
modelMatrix.rotate(90, 0,0,1);//绕z轴旋转

// 根据模型矩阵计算用来变换法向量的矩阵
normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();

gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);

顶点着色器的流程与 LightedCube_ambient 类似,区别在于,本例根据前述的规则先用模型矩阵的逆转置矩阵对 a_Normal 进行了变换,再赋值给 normal,而不是直接赋值:

1
2
3
4
5
// LightedCube_ambient
' vec3 normal = normalize(vec3(a_Normal));\n' + //对法向量进行归一化

// 本节代码
' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' + //对法向量进行归一化

a_Normal 是 vec4 类型的,a_NormalMatrixmat4 类型的,两者可以相乘,其结果也是 vec4 类型。我们只需要知道结果的前三个分量,所以使用 vec3()函数取其前3个分量,转为 vec3 类型。你也可以使用 .xyz 来这样做,比如这样写:(u_NomalMatrix*a_Normal).xyz

现在你已经了解了在物体旋转和平移时,如何变换每个顶点的法向量了。下面来看在 JS 代码中如何计算传给着色器的 u_NormalMatrix变量的矩阵。

u_NormalMatrix 是模型矩阵的逆转置矩阵。实例中立方体先绕 z 轴旋转再沿 y 轴平移,所以首先使用 setTranslate()rotate()计算出模型矩阵;接着求模型矩阵的逆矩阵,再对结果进行转置,得到逆转置矩阵 normalMatrix;最后,将逆转置军阵传给着色器中的 u_NormalMatrix 变量。gl.uniformMatrix4fv()函数的第2个参数指定是否对矩阵矩形转置。

1
2
3
4
5
// 根据模型矩阵计算用来变换法向量的矩阵
normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();
// 将用来变换法向量的矩阵传给u_NormalMatrix变量
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);

与 LightedCube_ambient 相比,立方体各个表面的颜色没有改变,只是位置上移动了一段距离,这是因为:

  • 平移没有改变法向量
  • 旋转虽然改变了法向量,但这里恰好旋转了90度,原来的前面现在处在右侧面的位置上,所以立方体看上去没有变化
  • 场景中的光照条件不会随着立方体位置的变化而变化
  • 漫反射光在个方向上是均匀的

实例

代码

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//LightedTranslatedRotatedCube.js
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' + //表面基底色
'attribute vec4 a_Normal;\n' + //法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_NormalMatrix;\n' + //用来变换法向量的矩阵
'uniform vec3 u_LightColor;\n' + //光线颜色
'uniform vec3 u_LightDirection;\n' + //归一化的世界坐标(入射光方向)
'uniform vec3 u_AmbientLight;\n' + //环境光颜色
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' + //对法向量进行归一化
' float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' + //计算光线方向和法向量的点积
' vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;\n' + //计算漫反射光的颜色
' vec3 ambient = u_AmbientLight * vec3(a_Color);\n' + //计算环境光产生的反射光颜色
' v_Color = vec4(diffuse + ambient, a_Color.a);\n' +
'}\n';

var FSHADER_SOURCE=
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\n' +
'varying vec4 v_Color;\n' +
'void main(){\n'+
' gl_FragColor = v_Color;\n'+
'}\n';

function main() {

var canvas = document.getElementById("webgl");

var gl = getWebGLContext(canvas);

if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log("Failed to initialize shaders.");
return;
}

//设置顶点位置
var n = initVertexBuffers(gl);

// Set the clear color and enable the depth test
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);

var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');//模型视图投影矩阵
var u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
var u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');

gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0); //设置光线颜色为白色
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2); //设置环境光颜色

var lightDirection = new Vector3([0.5, 3.0, 4.0]);//设置光线方向(世界坐标系下)
lightDirection.normalize();//归一化
gl.uniform3fv(u_LightDirection, lightDirection.elements);

var modelMatrix = new Matrix4(); //模型矩阵
var mvpMatrix = new Matrix4(); //模型视图投影矩阵
var normalMatrix = new Matrix4(); //用来变换法向量的矩阵

modelMatrix.setTranslate(0,0.9,0);//y轴平移
modelMatrix.rotate(90, 0,0,1);//绕z轴旋转

mvpMatrix.setPerspective(30, canvas.width/canvas.height, 1 ,100);
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
mvpMatrix.multiply(modelMatrix);
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

//根据模型矩阵计算用来变换法向量的矩阵
normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);


gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

function initVertexBuffers(gl) {
// Create a cube
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
var vertices = new Float32Array([ // Vertex 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([ // Indices of the vertices
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
]);

// Write the vertex coordinates and color to the buffer object
if (!initArrayBuffer(gl, vertices, 3, gl.FLOAT, 'a_Position'))
return -1;

if (!initArrayBuffer(gl, colors, 3, gl.FLOAT, 'a_Color'))
return -1;

if (!initArrayBuffer(gl, normals, 3, gl.FLOAT, 'a_Normal'))
return -1;

// Create a buffer object
var indexBuffer = gl.createBuffer();

// Write the indices to the buffer object
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

return indices.length;
}

function initArrayBuffer(gl, data, num, type, attribute) {
var buffer = gl.createBuffer(); // Create a buffer object

// Write date into the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// Assign the buffer object to the attribute variable
var a_attribute = gl.getAttribLocation(gl.program, attribute);
if (a_attribute < 0) {
console.log('Failed to get the storage location of ' + attribute);
return false;
}
gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
// Enable the assignment of the buffer object to the attribute variable
gl.enableVertexAttribArray(a_attribute);

gl.bindBuffer(gl.ARRAY_BUFFER, null);

return true;
}

效果

见顶部


Tips: Please indicate the source and original author when reprinting or quoting this article.