实施光照的方式有很多种,最简单的可能就是方向光源了。
方向光是指光照均匀地来自某一个方向,晴朗天气下的太阳经常被当作方向光源, 它距离太远所以光线被看作是平行的照到地面上。
计算方向光非常简单,将方向光的方向和面的朝向点乘就可以得到两个方向的余弦值。
随意拖动其中的点,如果两点方向刚好相反,点乘结果则为 -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);}
在代码中使用它们,先移除顶点颜色以便观察光照效果。

现在让着色器使用它
首先在顶点着色器中只将法向量传递给片断着色器
然后在片断着色器中将法向量和光照方向点乘
然后找到 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, worldViewProjectionMatrix);// 设置使用的颜色gl.uniform4fv(colorLocation, [0.2, 1, 0.2, 1]); // green// 设置光线方向gl.uniform3fv(reverseLightDirectionLocation, m4.normalize([0.5, 0.7, 1]));

如果你旋转了 F 就会发现,F 虽然旋转了但是光照没变, 我们希望随着 F 的旋转正面总是被照亮的。
为了解决这个问题就需要在物体重定向时重定向法向量, 和位置一样我们也可以将向量和矩阵相乘,这个矩阵显然是 world 矩阵, 现在我们只传了一个矩阵 u_matrix,所以先来改成传递两个矩阵, 一个叫做 u_world 的世界矩阵,另一个叫做 u_worldViewProjection 也就是我们现在的 u_matrix。
attribute vec4 a_position;attribute 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 worldViewProjectionLocation =gl.getUniformLocation(program, "u_worldViewProjection");var worldLocation = gl.getUniformLocation(program, "u_world");// 设置矩阵gl.uniformMatrix4fv(worldViewProjectionLocation, false,worldViewProjectionMatrix);gl.uniformMatrix4fv(worldLocation, false, worldMatrix);

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

解决办法就是对世界矩阵求逆并转置, 用这个矩阵就会得到正确的结果。
在图解里中间的 紫色球体是未缩放的, 左边的红色球体用的世界矩阵并缩放了, 你可以看出有些不太对劲。右边蓝色 的球体用的是世界矩阵求逆并转置后的矩阵。
点击图解循环观察不同的表示形式,你会发现在缩放严重时左边的(世界矩阵) 法向量和表面没有保持垂直关系,而右边的(世界矩阵求逆并转置) 一直保持垂直。最后一种模式是将它们渲染成红色,你会发现两个球体的光照结果相差非常大, 基于可视化的结果可以得出使用世界矩阵求逆转置是对的。
修改代码让示例使用这种矩阵,首先更新着色器,理论上我们可以直接更新 u_world 的值,但是最好将它重命名以表示它真正的含义,防止混淆。
attribute vec4 a_position;attribute vec3 a_normal;uniform mat4 u_worldViewProjection;uniform mat4 u_worldInverseTranspose;varying vec3 v_normal;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 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);
二. 全部代码
"use strict";function main() {// Get A WebGL context/** @type {HTMLCanvasElement} */var canvas = document.querySelector("#canvas");var gl = canvas.getContext("webgl");if (!gl) {return;}// setup GLSL programvar program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-3d", "fragment-shader-3d"]);// look up where the vertex data needs to go.var positionLocation = gl.getAttribLocation(program, "a_position");var normalLocation = gl.getAttribLocation(program, "a_normal");// lookup uniformsvar worldViewProjectionLocation = gl.getUniformLocation(program, "u_worldViewProjection");var worldInverseTransposeLocation = gl.getUniformLocation(program, "u_worldInverseTranspose");var colorLocation = gl.getUniformLocation(program, "u_color");var reverseLightDirectionLocation =gl.getUniformLocation(program, "u_reverseLightDirection");// Create a buffer to put positions invar positionBuffer = gl.createBuffer();// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);// Put geometry data into buffersetGeometry(gl);// Create a buffer to put normals invar normalBuffer = gl.createBuffer();// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = normalBuffer)gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);// Put normals data into buffersetNormals(gl);function radToDeg(r) {return r * 180 / Math.PI;}function degToRad(d) {return d * Math.PI / 180;}var fieldOfViewRadians = degToRad(60);var fRotationRadians = 0;drawScene();// Setup a ui.webglLessonsUI.setupSlider("#fRotation", {value: radToDeg(fRotationRadians), slide: updateRotation, min: -360, max: 360});function updateRotation(event, ui) {fRotationRadians = degToRad(ui.value);drawScene();}// Draw the scene.function drawScene() {webglUtils.resizeCanvasToDisplaySize(gl.canvas);// Tell WebGL how to convert from clip space to pixelsgl.viewport(0, 0, gl.canvas.width, gl.canvas.height);// Clear the canvas AND the depth buffer.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);// Turn on culling. By default backfacing triangles// will be culled.gl.enable(gl.CULL_FACE);// Enable the depth buffergl.enable(gl.DEPTH_TEST);// Tell it to use our program (pair of shaders)gl.useProgram(program);// Turn on the position attributegl.enableVertexAttribArray(positionLocation);// Bind the position buffer.gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);// Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)var size = 3; // 3 components per iterationvar type = gl.FLOAT; // the data is 32bit floatsvar normalize = false; // don't normalize the datavar stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next positionvar offset = 0; // start at the beginning of the buffergl.vertexAttribPointer(positionLocation, size, type, normalize, stride, offset);// Turn on the normal attributegl.enableVertexAttribArray(normalLocation);// Bind the normal buffer.gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);// Tell the attribute how to get data out of normalBuffer (ARRAY_BUFFER)var size = 3; // 3 components per iterationvar type = gl.FLOAT; // the data is 32bit floating point valuesvar normalize = false; // normalize the data (convert from 0-255 to 0-1)var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next positionvar offset = 0; // start at the beginning of the buffergl.vertexAttribPointer(normalLocation, size, type, normalize, stride, offset);// Compute the projection matrixvar aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;var zNear = 1;var zFar = 2000;var projectionMatrix = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);// Compute the camera's matrixvar camera = [100, 150, 200];var target = [0, 35, 0];var up = [0, 1, 0];var cameraMatrix = m4.lookAt(camera, target, up);// Make a view matrix from the camera matrix.var viewMatrix = m4.inverse(cameraMatrix);// Compute a view projection matrixvar viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);// Draw a F at the originvar worldMatrix = m4.yRotation(fRotationRadians);// Multiply the matrices.var worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix, worldMatrix);var worldInverseMatrix = m4.inverse(worldMatrix);var worldInverseTransposeMatrix = m4.transpose(worldInverseMatrix);// Set the matricesgl.uniformMatrix4fv(worldViewProjectionLocation, false, worldViewProjectionMatrix);gl.uniformMatrix4fv(worldInverseTransposeLocation, false, worldInverseTransposeMatrix);// Set the color to usegl.uniform4fv(colorLocation, [0.2, 1, 0.2, 1]); // green// set the light direction.gl.uniform3fv(reverseLightDirectionLocation, m4.normalize([0.5, 0.7, 1]));// Draw the geometry.var primitiveType = gl.TRIANGLES;var offset = 0;var count = 16 * 6;gl.drawArrays(primitiveType, offset, count);}}// Fill the buffer with the values that define a letter 'F'.function setGeometry(gl) {var positions = new Float32Array([// left column front0, 0, 0,0, 150, 0,30, 0, 0,0, 150, 0,30, 150, 0,30, 0, 0,// top rung front30, 0, 0,30, 30, 0,100, 0, 0,30, 30, 0,100, 30, 0,100, 0, 0,// middle rung front30, 60, 0,30, 90, 0,67, 60, 0,30, 90, 0,67, 90, 0,67, 60, 0,// left column back0, 0, 30,30, 0, 30,0, 150, 30,0, 150, 30,30, 0, 30,30, 150, 30,// top rung back30, 0, 30,100, 0, 30,30, 30, 30,30, 30, 30,100, 0, 30,100, 30, 30,// middle rung back30, 60, 30,67, 60, 30,30, 90, 30,30, 90, 30,67, 60, 30,67, 90, 30,// top0, 0, 0,100, 0, 0,100, 0, 30,0, 0, 0,100, 0, 30,0, 0, 30,// top rung right100, 0, 0,100, 30, 0,100, 30, 30,100, 0, 0,100, 30, 30,100, 0, 30,// under top rung30, 30, 0,30, 30, 30,100, 30, 30,30, 30, 0,100, 30, 30,100, 30, 0,// between top rung and middle30, 30, 0,30, 60, 30,30, 30, 30,30, 30, 0,30, 60, 0,30, 60, 30,// top of middle rung30, 60, 0,67, 60, 30,30, 60, 30,30, 60, 0,67, 60, 0,67, 60, 30,// right of middle rung67, 60, 0,67, 90, 30,67, 60, 30,67, 60, 0,67, 90, 0,67, 90, 30,// bottom of middle rung.30, 90, 0,30, 90, 30,67, 90, 30,30, 90, 0,67, 90, 30,67, 90, 0,// right of bottom30, 90, 0,30, 150, 30,30, 90, 30,30, 90, 0,30, 150, 0,30, 150, 30,// bottom0, 150, 0,0, 150, 30,30, 150, 30,0, 150, 0,30, 150, 30,30, 150, 0,// left side0, 0, 0,0, 0, 30,0, 150, 30,0, 0, 0,0, 150, 30,0, 150, 0]);// Center the F around the origin and Flip it around. We do this because// we're in 3D now with and +Y is up where as before when we started with 2D// we had +Y as down.// We could do by changing all the values above but I'm lazy.// We could also do it with a matrix at draw time but you should// never do stuff at draw time if you can do it at init time.var matrix = m4.xRotation(Math.PI);matrix = m4.translate(matrix, -50, -75, -15);for (var ii = 0; ii < positions.length; ii += 3) {var vector = m4.transformPoint(matrix, [positions[ii + 0], positions[ii + 1], positions[ii + 2], 1]);positions[ii + 0] = vector[0];positions[ii + 1] = vector[1];positions[ii + 2] = vector[2];}gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);}function setNormals(gl) {var normals = new Float32Array([// left column front0, 0, 1,0, 0, 1,0, 0, 1,0, 0, 1,0, 0, 1,0, 0, 1,// top rung front0, 0, 1,0, 0, 1,0, 0, 1,0, 0, 1,0, 0, 1,0, 0, 1,// middle rung front0, 0, 1,0, 0, 1,0, 0, 1,0, 0, 1,0, 0, 1,0, 0, 1,// left column back0, 0, -1,0, 0, -1,0, 0, -1,0, 0, -1,0, 0, -1,0, 0, -1,// top rung back0, 0, -1,0, 0, -1,0, 0, -1,0, 0, -1,0, 0, -1,0, 0, -1,// middle rung back0, 0, -1,0, 0, -1,0, 0, -1,0, 0, -1,0, 0, -1,0, 0, -1,// top0, 1, 0,0, 1, 0,0, 1, 0,0, 1, 0,0, 1, 0,0, 1, 0,// top rung right1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,// under top rung0, -1, 0,0, -1, 0,0, -1, 0,0, -1, 0,0, -1, 0,0, -1, 0,// between top rung and middle1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,// top of middle rung0, 1, 0,0, 1, 0,0, 1, 0,0, 1, 0,0, 1, 0,0, 1, 0,// right of middle rung1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,// bottom of middle rung.0, -1, 0,0, -1, 0,0, -1, 0,0, -1, 0,0, -1, 0,0, -1, 0,// right of bottom1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,1, 0, 0,// bottom0, -1, 0,0, -1, 0,0, -1, 0,0, -1, 0,0, -1, 0,0, -1, 0,// left side-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);}main();
