此文上接WebGL 三维相机, 如果没读建议从那里开始。
实施光照的方式有很多种,最简单的可能就是方向光源了。
方向光是指光照均匀地来自某一个方向,晴朗天气下的太阳经常被当作方向光源, 它距离太远所以光线被看作是平行的照到地面上。
计算方向光非常简单,将方向光的方向和面的朝向点乘就可以得到两个方向的余弦值。
这有个例子
随意拖动其中的点,如果两点方向刚好相反,点乘结果则为 -1。 如果方向相同结果为 1。
这有什么用呢?如果将三维物体的朝向和光的方向点乘, 结果为 1 则物体朝向和光照方向相同,为 -1 则物体朝向和光照方向相反。
我们可以将颜色值和点乘结果相乘,BOOM!有光了!
还有一个问题,我们如何知道三维物体的朝向?
我不知道为什么叫法向量,但是在三维图形学中法向量就是描述面的朝向的单位向量。
这是正方体和球体的一些法向量。
这些插在物体上的线就是对应顶点的法向量。
注意到正方体在每个顶角有 3 个法向量。 这是因为需要 3 个法向量去描述相邻的每个面的朝向。
这里的法向量是基于他们的方向着色的,正 x 方向为 红色, 上方向为 绿色 ,正 z 方向为 蓝色.
让我们来给上节中的 F
添加法向量。
由于 F
非常规则并且朝向都是 x, y, z 轴,所以非常简单。正面的的部分法向量为 0, 0, 1
,
背面的部分法向量为 0, 0, -1
,左面为 -1, 0, 0
,右面为 1, 0, 0
,上面为 0, 1, 0
,
然后底面为 0, -1, 0
。
function setNormals(gl) {
var normals = new Float32Array([
// 正面左竖
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
// 正面上横
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
// 正面中横
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
// 背面左竖
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
// 背面上横
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
// 背面中横
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
// 顶部
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
// 上横右面
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
// 上横下面
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
// 上横和中横之间
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
// 中横上面
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
0, 1, 0,
// 中横右面
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
// 中横底面
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
// 底部右侧
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
1, 0, 0,
// 底面
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
0, -1, 0,
// 左面
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
-1, 0, 0,
]);
gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
}
在代码中使用它们,先移除顶点颜色以便观察光照效果。
// 找顶点着色器中的属性
var positionLocation = gl.getAttribLocation(program, "a_position");
-var colorLocation = gl.getAttribLocation(program, "a_color");
+var normalLocation = gl.getAttribLocation(program, "a_normal");
...
-// 创建一个缓冲存储颜色
-var buffer = gl.createBuffer();
-gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
-gl.enableVertexAttribArray(colorLocation);
-
-// 我们将提供 RGB 作为字节
-gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);
-
-// 设置颜色
-setColors(gl);
// 创建缓冲存储法向量
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(normalLocation);
gl.vertexAttribPointer(normalLocation, 3, gl.FLOAT, false, 0, 0);
// 设置法向量
setNormals(gl);
现在让着色器使用它
首先在顶点着色器中只将法向量传递给片断着色器
#version 300 es
// 属性是顶点着色器的输入(in),它将从缓冲区接收数据
in vec4 a_position;
-in vec4 a_color;
+in vec3 a_normal;
// 用于转换位置的矩阵
uniform mat4 u_matrix;
-// 定义颜色变量传递给片段着色器
-out vec4 v_color;
+// 定义法向量变量传递给片段着色器
+out vec3 v_normal;
// 所有着色器都有一个main函数
void main() {
// 将位置乘以矩阵
gl_Position = u_matrix * a_position;
- // 将颜色变量传递给片段着色器
- v_color = a_color;
+ // 将法向量传递给片段着色器
+ v_normal = a_normal;
}
然后在片断着色器中将法向量和光照方向点乘
#version 300 es
precision highp float;
-// 从顶点着色器中传入的颜色值
-in vec4 v_color;
+// 从顶点着色器中传入的法向量值
+in vec3 v_normal;
+
+uniform vec3 u_reverseLightDirection;
+uniform vec4 u_color;
// 我们需要为片段着色器声明一个输出
out vec4 outColor;
void main() {
- outColor = v_color;
+ // 因为 v_normal 是一个变化的插值所以它不会是一个单位向量。 归一化使它成为单位向量
+ vec3 normal = normalize(v_normal);
+
+ // 通过取法线与光线反向的点积计算光
+ float light = dot(normal, u_reverseLightDirection);
+
+ outColor = u_color;
+
+ // 让我们只将颜色部分(不是 alpha)乘以光
+ outColor.rgb *= light;
}
然后找到 u_color
和 u_reverseLightDirection
的位置。
// 寻找全局变量
var matrixLocation = gl.getUniformLocation(program, "u_matrix");
+ var colorLocation = gl.getUniformLocation(program, "u_color");
+ var reverseLightDirectionLocation =
+ gl.getUniformLocation(program, "u_reverseLightDirection");
为它们赋值
// 设置矩阵
gl.uniformMatrix4fv(matrixLocation, false, matrix);
+ // 设置使用的颜色
+ gl.uniform4fv(colorLocation, [0.2, 1, 0.2, 1]); // green
+
+ // 设置光线方向
+ gl.uniform3fv(reverseLightDirectionLocation, normalize([0.5, 0.7, 1]));
我们之前用到的 normalize
会将原向量转换为单位向量。
例子中的 x = 0.5
说明光线是从右往左照,y = 0.7
说明光线从上方往下照,
z = 1
说明光线从在场景前方。对应的值表示光源大多指向场景,在靠右方和上方一点的位置。
这是结果
如果你旋转了 F 就会发现,F 虽然旋转了但是光照没变, 我们希望随着 F 的旋转正面总是被照亮的。
为了解决这个问题就需要在物体重定向时重定向法向量,
和位置一样我们也可以将向量和矩阵相乘,这个矩阵显然是 world
矩阵,
现在我们只传了一个矩阵 u_matrix
,所以先来改成传递两个矩阵,
一个叫做 u_world
的世界矩阵,另一个叫做 u_worldViewProjection
也就是我们现在的 u_matrix
。
#version 300 es
// 属性是顶点着色器的输入(in),它将从缓冲区接收数据
in vec4 a_position;
in vec3 a_normal;
*uniform mat4 u_worldViewProjection;
+uniform mat4 u_world;
varying vec3 v_normal;
void main() {
// 将位置和矩阵相乘
* gl_Position = u_worldViewProjection * a_position;
* // 重定向法向量并传递给片断着色器
* v_normal = mat3(u_world) * a_normal;
}
注意到我们将 a_normal
与 mat3(u_world)
相乘,
那是因为法向量是方向所以不用关心位移,
矩阵的左上 3x3 部分才是控制姿态的。
找到全局变量
// 寻找全局变量
- var matrixLocation = gl.getUniformLocation(program, "u_matrix");
* var worldViewProjectionLocation =
* gl.getUniformLocation(program, "u_worldViewProjection");
+ var worldLocation = gl.getUniformLocation(program, "u_world");
然后更新它们
*var worldMatrix = m4.yRotation(fRotationRadians);
*var worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix,
worldMatrix);
*// 设置矩阵
*gl.uniformMatrix4fv(
* worldViewProjectionLocation, false,
* worldViewProjectionMatrix);
*gl.uniformMatrix4fv(worldLocation, false, worldMatrix);
结果
旋转后就会发现面对 F 的面总是被照亮的。
这里有一个问题我不知道如何表述所以就用图解展示。
我们用 normal
和 u_world
相乘去重定向法向量,
如果世界矩阵被缩放了怎么办?事实是会得到错误的法向量。
我从没想弄清为什么,但解决办法就是对世界矩阵求逆并转置, 用这个矩阵就会得到正确的结果。
在图解里中间的 紫色球体是未缩放的, 左边的红色球体用的世界矩阵并缩放了, 你可以看出有些不太对劲。右边蓝色 的球体用的是世界矩阵求逆并转置后的矩阵。
点击图解循环观察不同的表示形式,你会发现在缩放严重时左边的(世界矩阵) 法向量和表面没有保持垂直关系,而右边的(世界矩阵求逆并转置) 一直保持垂直。最后一种模式是将它们渲染成红色,你会发现两个球体的光照结果相差非常大, 基于可视化的结果可以得出使用世界矩阵求逆转置是对的。
修改代码让示例使用这种矩阵,首先更新着色器,理论上我们可以直接更新 u_world
的值,但是最好将它重命名以表示它真正的含义,防止混淆。
#version 300 es
// 属性是顶点着色器的输入(in),它将从缓冲区接收数据
in vec4 a_position;
in vec3 a_normal;
uniform mat4 u_worldViewProjection;
-uniform mat4 u_world
+uniform mat4 u_worldInverseTranspose;
// 定义传递到片段着色器的颜色变量和法向量
out vec4 v_color;
out vec3 v_normal;
// 所有的着色器都有一个 main 函数
void main() {
// 将位置和矩阵相乘
gl_Position = u_worldViewProjection * a_position;
// 重定向法向量并传递给片断着色器
* v_normal = mat3(u_worldInverseTranspose) * a_normal;
}
然后找到它
- var worldLocation = gl.getUniformLocation(program, "u_world");
+ var worldInverseTransposeLocation =
+ gl.getUniformLocation(program, "u_worldInverseTranspose");
更新它
var worldMatrix = m4.yRotation(fRotationRadians);
var worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix, worldMatrix);
+var worldInverseMatrix = m4.inverse(worldMatrix);
+var worldInverseTransposeMatrix = m4.transpose(worldInverseMatrix);
// 设置矩阵
gl.uniformMatrix4fv(
worldViewProjectionLocation, false,
worldViewProjectionMatrix);
-gl.uniformMatrix4fv(worldLocation, false, worldMatrix);
+gl.uniformMatrix4fv(
+ worldInverseTransposeLocation, false,
+ worldInverseTransposeMatrix);
这是转置的代码
var m4 = {
transpose: function(m) {
return [
m[0], m[4], m[8], m[12],
m[1], m[5], m[9], m[13],
m[2], m[6], m[10], m[14],
m[3], m[7], m[11], m[15],
];
},
...
由于我们并没有进行缩放,所以没有明显的变化,但防患于未然。
希望这光照的第一课解释的足够清楚,接下来是点光源。