实施光照的方式有很多种,最简单的可能就是方向光源了。
方向光是指光照均匀地来自某一个方向,晴朗天气下的太阳经常被当作方向光源, 它距离太远所以光线被看作是平行的照到地面上。
计算方向光非常简单,将方向光的方向和面的朝向点乘就可以得到两个方向的余弦值。
image.png

随意拖动其中的点,如果两点方向刚好相反,点乘结果则为 -1。 如果方向相同结果为 1。
这有什么用呢?如果将三维物体的朝向和光的方向点乘, 结果为 1 则物体朝向和光照方向相同,为 -1 则物体朝向和光照方向相反。
image.png
我们可以将颜色值和点乘结果相乘,BOOM!有光了!
还有一个问题,我们如何知道三维物体的朝向?

一.法向量

我不知道为什么叫法向量,但是在三维图形学中法向量就是描述面的朝向的单位向量。
这是正方体和球体的一些法向量。
image.png

这些插在物体上的线就是对应顶点的法向量。
注意到正方体在每个顶角有 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。

  1. function setNormals(gl) {
  2. var normals = new Float32Array([
  3. // 正面左竖
  4. 0, 0, 1,
  5. 0, 0, 1,
  6. 0, 0, 1,
  7. 0, 0, 1,
  8. 0, 0, 1,
  9. 0, 0, 1,
  10. // 正面上横
  11. 0, 0, 1,
  12. 0, 0, 1,
  13. 0, 0, 1,
  14. 0, 0, 1,
  15. 0, 0, 1,
  16. 0, 0, 1,
  17. // 正面中横
  18. 0, 0, 1,
  19. 0, 0, 1,
  20. 0, 0, 1,
  21. 0, 0, 1,
  22. 0, 0, 1,
  23. 0, 0, 1,
  24. // 背面左竖
  25. 0, 0, -1,
  26. 0, 0, -1,
  27. 0, 0, -1,
  28. 0, 0, -1,
  29. 0, 0, -1,
  30. 0, 0, -1,
  31. // 背面上横
  32. 0, 0, -1,
  33. 0, 0, -1,
  34. 0, 0, -1,
  35. 0, 0, -1,
  36. 0, 0, -1,
  37. 0, 0, -1,
  38. // 背面中横
  39. 0, 0, -1,
  40. 0, 0, -1,
  41. 0, 0, -1,
  42. 0, 0, -1,
  43. 0, 0, -1,
  44. 0, 0, -1,
  45. // 顶部
  46. 0, 1, 0,
  47. 0, 1, 0,
  48. 0, 1, 0,
  49. 0, 1, 0,
  50. 0, 1, 0,
  51. 0, 1, 0,
  52. // 上横右面
  53. 1, 0, 0,
  54. 1, 0, 0,
  55. 1, 0, 0,
  56. 1, 0, 0,
  57. 1, 0, 0,
  58. 1, 0, 0,
  59. // 上横下面
  60. 0, -1, 0,
  61. 0, -1, 0,
  62. 0, -1, 0,
  63. 0, -1, 0,
  64. 0, -1, 0,
  65. 0, -1, 0,
  66. // 上横和中横之间
  67. 1, 0, 0,
  68. 1, 0, 0,
  69. 1, 0, 0,
  70. 1, 0, 0,
  71. 1, 0, 0,
  72. 1, 0, 0,
  73. // 中横上面
  74. 0, 1, 0,
  75. 0, 1, 0,
  76. 0, 1, 0,
  77. 0, 1, 0,
  78. 0, 1, 0,
  79. 0, 1, 0,
  80. // 中横右面
  81. 1, 0, 0,
  82. 1, 0, 0,
  83. 1, 0, 0,
  84. 1, 0, 0,
  85. 1, 0, 0,
  86. 1, 0, 0,
  87. // 中横底面
  88. 0, -1, 0,
  89. 0, -1, 0,
  90. 0, -1, 0,
  91. 0, -1, 0,
  92. 0, -1, 0,
  93. 0, -1, 0,
  94. // 底部右侧
  95. 1, 0, 0,
  96. 1, 0, 0,
  97. 1, 0, 0,
  98. 1, 0, 0,
  99. 1, 0, 0,
  100. 1, 0, 0,
  101. // 底面
  102. 0, -1, 0,
  103. 0, -1, 0,
  104. 0, -1, 0,
  105. 0, -1, 0,
  106. 0, -1, 0,
  107. 0, -1, 0,
  108. // 左面
  109. -1, 0, 0,
  110. -1, 0, 0,
  111. -1, 0, 0,
  112. -1, 0, 0,
  113. -1, 0, 0,
  114. -1, 0, 0]);
  115. gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
  116. }

在代码中使用它们,先移除顶点颜色以便观察光照效果。
image.png

image.png
现在让着色器使用它
首先在顶点着色器中只将法向量传递给片断着色器
image.png
然后在片断着色器中将法向量和光照方向点乘
image.png
然后找到 u_color 和 u_reverseLightDirection 的位置。

  1. // 寻找全局变量
  2. var matrixLocation = gl.getUniformLocation(program, "u_matrix");
  3. var colorLocation = gl.getUniformLocation(program, "u_color");
  4. var reverseLightDirectionLocation =
  5. gl.getUniformLocation(program, "u_reverseLightDirection");
  6. // 设置矩阵
  7. gl.uniformMatrix4fv(matrixLocation, false, worldViewProjectionMatrix);
  8. // 设置使用的颜色
  9. gl.uniform4fv(colorLocation, [0.2, 1, 0.2, 1]); // green
  10. // 设置光线方向
  11. gl.uniform3fv(reverseLightDirectionLocation, m4.normalize([0.5, 0.7, 1]));

image.png
如果你旋转了 F 就会发现,F 虽然旋转了但是光照没变, 我们希望随着 F 的旋转正面总是被照亮的。
为了解决这个问题就需要在物体重定向时重定向法向量, 和位置一样我们也可以将向量和矩阵相乘,这个矩阵显然是 world 矩阵, 现在我们只传了一个矩阵 u_matrix,所以先来改成传递两个矩阵, 一个叫做 u_world 的世界矩阵,另一个叫做 u_worldViewProjection 也就是我们现在的 u_matrix。

  1. attribute vec4 a_position;
  2. attribute vec3 a_normal;
  3. uniform mat4 u_worldViewProjection;
  4. uniform mat4 u_world;
  5. varying vec3 v_normal;
  6. void main() {
  7. // 将位置和矩阵相乘
  8. gl_Position = u_worldViewProjection * a_position;
  9. // 重定向法向量并传递给片断着色器
  10. v_normal = mat3(u_world) * a_normal;
  11. }

注意到我们将 a_normal 与 mat3(u_world) 相乘, 那是因为法向量是方向所以不用关心位移, 矩阵的左上 3x3 部分才是控制姿态的。
找到全局变量

  1. // 寻找全局变量
  2. var worldViewProjectionLocation =
  3. gl.getUniformLocation(program, "u_worldViewProjection");
  4. var worldLocation = gl.getUniformLocation(program, "u_world");
  5. // 设置矩阵
  6. gl.uniformMatrix4fv(
  7. worldViewProjectionLocation, false,
  8. worldViewProjectionMatrix);
  9. gl.uniformMatrix4fv(worldLocation, false, worldMatrix);


image.png
旋转后就会发现面对 F 的面总是被照亮的。
这里有一个问题我不知道如何表述所以就用图解展示。 我们用 normal 和 u_world 相乘去重定向法向量, 如果世界矩阵被缩放了怎么办?事实是会得到错误的法向量

image.png

解决办法就是对世界矩阵求逆并转置, 用这个矩阵就会得到正确的结果。
在图解里中间的 紫色球体是未缩放的, 左边的红色球体用的世界矩阵并缩放了, 你可以看出有些不太对劲。右边蓝色 的球体用的是世界矩阵求逆并转置后的矩阵。
点击图解循环观察不同的表示形式,你会发现在缩放严重时左边的(世界矩阵) 法向量和表面没有保持垂直关系,而右边的(世界矩阵求逆并转置) 一直保持垂直。最后一种模式是将它们渲染成红色,你会发现两个球体的光照结果相差非常大, 基于可视化的结果可以得出使用世界矩阵求逆转置是对的。
修改代码让示例使用这种矩阵,首先更新着色器,理论上我们可以直接更新 u_world 的值,但是最好将它重命名以表示它真正的含义,防止混淆。

  1. attribute vec4 a_position;
  2. attribute vec3 a_normal;
  3. uniform mat4 u_worldViewProjection;
  4. uniform mat4 u_worldInverseTranspose;
  5. varying vec3 v_normal;
  6. void main() {
  7. // 将位置和矩阵相乘
  8. gl_Position = u_worldViewProjection * a_position;
  9. // 重定向法向量并传递给片断着色器
  10. v_normal = mat3(u_worldInverseTranspose) * a_normal;
  11. }
  12. // var worldLocation = gl.getUniformLocation(program, "u_world"); 弃用
  13. var worldInverseTransposeLocation =
  14. gl.getUniformLocation(program, "u_worldInverseTranspose");
  15. var worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix, worldMatrix);
  16. var worldInverseMatrix = m4.inverse(worldMatrix);
  17. var worldInverseTransposeMatrix = m4.transpose(worldInverseMatrix);
  18. // 设置矩阵
  19. gl.uniformMatrix4fv(
  20. worldViewProjectionLocation, false,
  21. worldViewProjectionMatrix);
  22. // gl.uniformMatrix4fv(worldLocation, false, worldMatrix); 弃用
  23. gl.uniformMatrix4fv(
  24. worldInverseTransposeLocation, false,
  25. worldInverseTransposeMatrix);

二. 全部代码

  1. "use strict";
  2. function main() {
  3. // Get A WebGL context
  4. /** @type {HTMLCanvasElement} */
  5. var canvas = document.querySelector("#canvas");
  6. var gl = canvas.getContext("webgl");
  7. if (!gl) {
  8. return;
  9. }
  10. // setup GLSL program
  11. var program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-3d", "fragment-shader-3d"]);
  12. // look up where the vertex data needs to go.
  13. var positionLocation = gl.getAttribLocation(program, "a_position");
  14. var normalLocation = gl.getAttribLocation(program, "a_normal");
  15. // lookup uniforms
  16. var worldViewProjectionLocation = gl.getUniformLocation(program, "u_worldViewProjection");
  17. var worldInverseTransposeLocation = gl.getUniformLocation(program, "u_worldInverseTranspose");
  18. var colorLocation = gl.getUniformLocation(program, "u_color");
  19. var reverseLightDirectionLocation =
  20. gl.getUniformLocation(program, "u_reverseLightDirection");
  21. // Create a buffer to put positions in
  22. var positionBuffer = gl.createBuffer();
  23. // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
  24. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  25. // Put geometry data into buffer
  26. setGeometry(gl);
  27. // Create a buffer to put normals in
  28. var normalBuffer = gl.createBuffer();
  29. // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = normalBuffer)
  30. gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  31. // Put normals data into buffer
  32. setNormals(gl);
  33. function radToDeg(r) {
  34. return r * 180 / Math.PI;
  35. }
  36. function degToRad(d) {
  37. return d * Math.PI / 180;
  38. }
  39. var fieldOfViewRadians = degToRad(60);
  40. var fRotationRadians = 0;
  41. drawScene();
  42. // Setup a ui.
  43. webglLessonsUI.setupSlider("#fRotation", {value: radToDeg(fRotationRadians), slide: updateRotation, min: -360, max: 360});
  44. function updateRotation(event, ui) {
  45. fRotationRadians = degToRad(ui.value);
  46. drawScene();
  47. }
  48. // Draw the scene.
  49. function drawScene() {
  50. webglUtils.resizeCanvasToDisplaySize(gl.canvas);
  51. // Tell WebGL how to convert from clip space to pixels
  52. gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  53. // Clear the canvas AND the depth buffer.
  54. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  55. // Turn on culling. By default backfacing triangles
  56. // will be culled.
  57. gl.enable(gl.CULL_FACE);
  58. // Enable the depth buffer
  59. gl.enable(gl.DEPTH_TEST);
  60. // Tell it to use our program (pair of shaders)
  61. gl.useProgram(program);
  62. // Turn on the position attribute
  63. gl.enableVertexAttribArray(positionLocation);
  64. // Bind the position buffer.
  65. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  66. // Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
  67. var size = 3; // 3 components per iteration
  68. var type = gl.FLOAT; // the data is 32bit floats
  69. var normalize = false; // don't normalize the data
  70. var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
  71. var offset = 0; // start at the beginning of the buffer
  72. gl.vertexAttribPointer(
  73. positionLocation, size, type, normalize, stride, offset);
  74. // Turn on the normal attribute
  75. gl.enableVertexAttribArray(normalLocation);
  76. // Bind the normal buffer.
  77. gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  78. // Tell the attribute how to get data out of normalBuffer (ARRAY_BUFFER)
  79. var size = 3; // 3 components per iteration
  80. var type = gl.FLOAT; // the data is 32bit floating point values
  81. var normalize = false; // normalize the data (convert from 0-255 to 0-1)
  82. var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
  83. var offset = 0; // start at the beginning of the buffer
  84. gl.vertexAttribPointer(
  85. normalLocation, size, type, normalize, stride, offset);
  86. // Compute the projection matrix
  87. var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  88. var zNear = 1;
  89. var zFar = 2000;
  90. var projectionMatrix = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);
  91. // Compute the camera's matrix
  92. var camera = [100, 150, 200];
  93. var target = [0, 35, 0];
  94. var up = [0, 1, 0];
  95. var cameraMatrix = m4.lookAt(camera, target, up);
  96. // Make a view matrix from the camera matrix.
  97. var viewMatrix = m4.inverse(cameraMatrix);
  98. // Compute a view projection matrix
  99. var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
  100. // Draw a F at the origin
  101. var worldMatrix = m4.yRotation(fRotationRadians);
  102. // Multiply the matrices.
  103. var worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix, worldMatrix);
  104. var worldInverseMatrix = m4.inverse(worldMatrix);
  105. var worldInverseTransposeMatrix = m4.transpose(worldInverseMatrix);
  106. // Set the matrices
  107. gl.uniformMatrix4fv(worldViewProjectionLocation, false, worldViewProjectionMatrix);
  108. gl.uniformMatrix4fv(worldInverseTransposeLocation, false, worldInverseTransposeMatrix);
  109. // Set the color to use
  110. gl.uniform4fv(colorLocation, [0.2, 1, 0.2, 1]); // green
  111. // set the light direction.
  112. gl.uniform3fv(reverseLightDirectionLocation, m4.normalize([0.5, 0.7, 1]));
  113. // Draw the geometry.
  114. var primitiveType = gl.TRIANGLES;
  115. var offset = 0;
  116. var count = 16 * 6;
  117. gl.drawArrays(primitiveType, offset, count);
  118. }
  119. }
  120. // Fill the buffer with the values that define a letter 'F'.
  121. function setGeometry(gl) {
  122. var positions = new Float32Array([
  123. // left column front
  124. 0, 0, 0,
  125. 0, 150, 0,
  126. 30, 0, 0,
  127. 0, 150, 0,
  128. 30, 150, 0,
  129. 30, 0, 0,
  130. // top rung front
  131. 30, 0, 0,
  132. 30, 30, 0,
  133. 100, 0, 0,
  134. 30, 30, 0,
  135. 100, 30, 0,
  136. 100, 0, 0,
  137. // middle rung front
  138. 30, 60, 0,
  139. 30, 90, 0,
  140. 67, 60, 0,
  141. 30, 90, 0,
  142. 67, 90, 0,
  143. 67, 60, 0,
  144. // left column back
  145. 0, 0, 30,
  146. 30, 0, 30,
  147. 0, 150, 30,
  148. 0, 150, 30,
  149. 30, 0, 30,
  150. 30, 150, 30,
  151. // top rung back
  152. 30, 0, 30,
  153. 100, 0, 30,
  154. 30, 30, 30,
  155. 30, 30, 30,
  156. 100, 0, 30,
  157. 100, 30, 30,
  158. // middle rung back
  159. 30, 60, 30,
  160. 67, 60, 30,
  161. 30, 90, 30,
  162. 30, 90, 30,
  163. 67, 60, 30,
  164. 67, 90, 30,
  165. // top
  166. 0, 0, 0,
  167. 100, 0, 0,
  168. 100, 0, 30,
  169. 0, 0, 0,
  170. 100, 0, 30,
  171. 0, 0, 30,
  172. // top rung right
  173. 100, 0, 0,
  174. 100, 30, 0,
  175. 100, 30, 30,
  176. 100, 0, 0,
  177. 100, 30, 30,
  178. 100, 0, 30,
  179. // under top rung
  180. 30, 30, 0,
  181. 30, 30, 30,
  182. 100, 30, 30,
  183. 30, 30, 0,
  184. 100, 30, 30,
  185. 100, 30, 0,
  186. // between top rung and middle
  187. 30, 30, 0,
  188. 30, 60, 30,
  189. 30, 30, 30,
  190. 30, 30, 0,
  191. 30, 60, 0,
  192. 30, 60, 30,
  193. // top of middle rung
  194. 30, 60, 0,
  195. 67, 60, 30,
  196. 30, 60, 30,
  197. 30, 60, 0,
  198. 67, 60, 0,
  199. 67, 60, 30,
  200. // right of middle rung
  201. 67, 60, 0,
  202. 67, 90, 30,
  203. 67, 60, 30,
  204. 67, 60, 0,
  205. 67, 90, 0,
  206. 67, 90, 30,
  207. // bottom of middle rung.
  208. 30, 90, 0,
  209. 30, 90, 30,
  210. 67, 90, 30,
  211. 30, 90, 0,
  212. 67, 90, 30,
  213. 67, 90, 0,
  214. // right of bottom
  215. 30, 90, 0,
  216. 30, 150, 30,
  217. 30, 90, 30,
  218. 30, 90, 0,
  219. 30, 150, 0,
  220. 30, 150, 30,
  221. // bottom
  222. 0, 150, 0,
  223. 0, 150, 30,
  224. 30, 150, 30,
  225. 0, 150, 0,
  226. 30, 150, 30,
  227. 30, 150, 0,
  228. // left side
  229. 0, 0, 0,
  230. 0, 0, 30,
  231. 0, 150, 30,
  232. 0, 0, 0,
  233. 0, 150, 30,
  234. 0, 150, 0]);
  235. // Center the F around the origin and Flip it around. We do this because
  236. // we're in 3D now with and +Y is up where as before when we started with 2D
  237. // we had +Y as down.
  238. // We could do by changing all the values above but I'm lazy.
  239. // We could also do it with a matrix at draw time but you should
  240. // never do stuff at draw time if you can do it at init time.
  241. var matrix = m4.xRotation(Math.PI);
  242. matrix = m4.translate(matrix, -50, -75, -15);
  243. for (var ii = 0; ii < positions.length; ii += 3) {
  244. var vector = m4.transformPoint(matrix, [positions[ii + 0], positions[ii + 1], positions[ii + 2], 1]);
  245. positions[ii + 0] = vector[0];
  246. positions[ii + 1] = vector[1];
  247. positions[ii + 2] = vector[2];
  248. }
  249. gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
  250. }
  251. function setNormals(gl) {
  252. var normals = new Float32Array([
  253. // left column front
  254. 0, 0, 1,
  255. 0, 0, 1,
  256. 0, 0, 1,
  257. 0, 0, 1,
  258. 0, 0, 1,
  259. 0, 0, 1,
  260. // top rung front
  261. 0, 0, 1,
  262. 0, 0, 1,
  263. 0, 0, 1,
  264. 0, 0, 1,
  265. 0, 0, 1,
  266. 0, 0, 1,
  267. // middle rung front
  268. 0, 0, 1,
  269. 0, 0, 1,
  270. 0, 0, 1,
  271. 0, 0, 1,
  272. 0, 0, 1,
  273. 0, 0, 1,
  274. // left column back
  275. 0, 0, -1,
  276. 0, 0, -1,
  277. 0, 0, -1,
  278. 0, 0, -1,
  279. 0, 0, -1,
  280. 0, 0, -1,
  281. // top rung back
  282. 0, 0, -1,
  283. 0, 0, -1,
  284. 0, 0, -1,
  285. 0, 0, -1,
  286. 0, 0, -1,
  287. 0, 0, -1,
  288. // middle rung back
  289. 0, 0, -1,
  290. 0, 0, -1,
  291. 0, 0, -1,
  292. 0, 0, -1,
  293. 0, 0, -1,
  294. 0, 0, -1,
  295. // top
  296. 0, 1, 0,
  297. 0, 1, 0,
  298. 0, 1, 0,
  299. 0, 1, 0,
  300. 0, 1, 0,
  301. 0, 1, 0,
  302. // top rung right
  303. 1, 0, 0,
  304. 1, 0, 0,
  305. 1, 0, 0,
  306. 1, 0, 0,
  307. 1, 0, 0,
  308. 1, 0, 0,
  309. // under top rung
  310. 0, -1, 0,
  311. 0, -1, 0,
  312. 0, -1, 0,
  313. 0, -1, 0,
  314. 0, -1, 0,
  315. 0, -1, 0,
  316. // between top rung and middle
  317. 1, 0, 0,
  318. 1, 0, 0,
  319. 1, 0, 0,
  320. 1, 0, 0,
  321. 1, 0, 0,
  322. 1, 0, 0,
  323. // top of middle rung
  324. 0, 1, 0,
  325. 0, 1, 0,
  326. 0, 1, 0,
  327. 0, 1, 0,
  328. 0, 1, 0,
  329. 0, 1, 0,
  330. // right of middle rung
  331. 1, 0, 0,
  332. 1, 0, 0,
  333. 1, 0, 0,
  334. 1, 0, 0,
  335. 1, 0, 0,
  336. 1, 0, 0,
  337. // bottom of middle rung.
  338. 0, -1, 0,
  339. 0, -1, 0,
  340. 0, -1, 0,
  341. 0, -1, 0,
  342. 0, -1, 0,
  343. 0, -1, 0,
  344. // right of bottom
  345. 1, 0, 0,
  346. 1, 0, 0,
  347. 1, 0, 0,
  348. 1, 0, 0,
  349. 1, 0, 0,
  350. 1, 0, 0,
  351. // bottom
  352. 0, -1, 0,
  353. 0, -1, 0,
  354. 0, -1, 0,
  355. 0, -1, 0,
  356. 0, -1, 0,
  357. 0, -1, 0,
  358. // left side
  359. -1, 0, 0,
  360. -1, 0, 0,
  361. -1, 0, 0,
  362. -1, 0, 0,
  363. -1, 0, 0,
  364. -1, 0, 0]);
  365. gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
  366. }
  367. main();