WebGL编程指南之光照

205 阅读18分钟

前言

本文将讨论如何在三维场景中实现不同类型的光照,以及其产生的效果。光照使场景更具有层次感。

主要内容如下:

  1. 明暗、阴影、不同类型的光:点光源光、平行光和散射光
  2. 物体表面反射光线的方式:漫反射和环境反射
  3. 编写代码实现光照效果,使三维模型看上去更逼真

1 光照原理

现实世界中的物体被光线照射时,会反射一部分光。只有当反射光线进入你的眼睛时,你才能够看到物体并辨认出它的颜色。

在现实世界中,当光线照射到物体上时,会发生两个重要的现象:

  1. 根据光源和光线方向,物体不同表面的明暗程度变得不一致
  2. 根据光源和光线方向,物体向地面投下了影子

在三维图形学中着色(shading)的真正含义就是,根据光照条件重建物体各表面明暗不一的效果的过程。物体向地面投下影子的现象,又被称为阴影(shadowing)。

在考虑着色之前,考虑两件事:

  1. 发出光线的光源的类型
  2. 物体表面如何反射光线

2 光源类型

真实世界中的光主要有两种类型:

平行光(directional light),类似于自然中的太阳光,光线是相互平行的;点光源(point light),类似于人造灯泡的光源,由一个点向周围所有方向发出的光。此外,还用 环境光(ambient light) 来模拟真实世界中的非直射光(也就是由光源发出后经过墙壁或其它物体反射后的光)。三维图形学中还使用一些其它类型的光,比如聚光灯光(spot light) 来模拟电筒、车前灯等。

3 反射类型

物体向哪个方向反射光,反射的光是什么颜色,取决于以下两个因素:入射光和物体表面的类型。入射光的信息包括入射光的方向和颜色,而物体表面的信息包括表面的固有颜色(也称基底色)和反射特性。

物体表面反射光线的方式有两种: 漫反射(dinusereection)环境反射(enviromentambient refection)。重点是如何根据上述两种信息(人射光和物体表面特性)来计算出反射光的颜色。

3.1 漫反射

漫反射是针对平行光或点光源而言的。漫反射的反射光在各个方向上是均匀的,如下图所示。如果物体表面像镜子一样光滑,那么光线就会以特定的角度反射出去:但是现实中的大部分材质,比如纸张、岩石、塑料等,其表面都是粗糙的,在这种情况下反射光就会以不固定的角度反射出去。漫反射就是针对后一种情况而建立的理想反射模型。

在漫反射中,反射光的颜色取决于人射光的颜色、表面的基底色、人射光与表面形成的人射角。我们将人射角定义为人射光与表面的法线形成的夹角,并用a表示,那么漫反射光的颜色可以通过以下公式得到:

<漫反射光颜色> = <入射光颜色><表面基低色>cosa

<入射光颜色>指的是点光源或平行光的颜色,乘法操作是在颜色矢量上逐分量(R、G、B)进行的。因为漫反射光在各个方向上都是“均匀”的,所以从任何角度看上去其强度都相等。

3.2 环境反射

环境反射是针对环境光而言的。在环境反射中,反射光的方向可以认为就是人射光的反方向。由于环境光照射物体的方式就是各方向均匀、强度相等的,所以反射光也是各向均匀的,如下图所示。我们可以这样来描述它:

<表面的反射光颜色>=<入射光颜色>x<表面基底色>

当漫反射和环境反射同时存在时,将两者加起来,就会得到物体最终被观察到的颜色:

<表面的反射光颜色>=<漫反射光颜色>+<环境反射光颜色>

注意,两种反射光并不一定总是存在,也并不一定要完全按照上述公式来计算。渲染三维模型时,你可以修改这些公式以达到想要的效果。

4 根据光线和表面的方向计算入射角

我们没法直接直接说“人射角是多少多少度"。我们必须根据入射光的方向和物体表面的朝向(即法线方向)来计算出入射角。这并不简单,因为在创建三维模型的时候,我们无法预先确定光线将以怎样的角度照射到每个表面上。但是,我们可以确定每个表面的朝向。在指定光源的时候,再确定光的方向,就可以用这两项信息来计算出人射角了。

cosa=<光线方向>・<法线方向>

这里有两点需要注意:其一,光线方向矢量和表面法线矢量的长度必须为1,否则反射光的颜色就会过暗或过亮。将一个矢量的长度调整为1,同时保持方向不变的过程称为之为归一化(normalization)。GLSLES 提供了内置的归一化函数,你可以直接使用。

其二,这里(包括后面)所谓的“光线方向”,实际上是入射方向的反方向,即从入射点指向光源方向(因为这样,该方向与法线方向的夹角才是入射角)。

5 法向量

法向量只代表方向,不代表长度,一个平面有两个法向量,即正面和背面,比如x-y平面,法向量正面(0,0,1),负面(0,0,-1)。法向量与位置无关,只与平面有关,换句话说,与平面上的点无关,在平面上的任何位置法向量都一样,进一步讲,即使有两个不同的平面,只要朝向相同,法向量也相同。

立方体法向量

6 平行光下的漫反射代码实现

6.1 代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    #webgl {
      width: 600px;
      height: 600px;
      position: absolute;
      top: calc(50% - 300px);
      left: calc(50% - 300px);
      background-color: black;
    }
  </style>
  <body onload="main()">
    <canvas id="webgl" width="600" height="600">
      Please use a browser that supports "canvas"
    </canvas>
    <script src="./matrix.js"></script>
    <script>
      // 顶点着色器
      var VSHADER_SOURCE =
      'attribute vec4 a_Position;\n' + 
      'attribute vec4 a_Color;\n' + 
      'attribute vec4 a_Normal;\n' +        //法向量
      'uniform mat4 u_MvpMatrix;\n' +
      'uniform vec3 u_LightColor;\n' +     //光照颜色
      'uniform vec3 u_LightDirection;\n' + //光方向(世界坐标,归一化)
      'varying vec4 v_Color;\n' +
      'void main() {\n' +
      '  gl_Position = u_MvpMatrix * a_Position ;\n' +
      // 正则化
      '  vec3 normal = normalize(a_Normal.xyz);\n' +
      // 光方向与曲面方向(法线)的点积,获取角度
      '  float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +
      // 计算漫反射颜色
      '  vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
      '  v_Color = vec4(diffuse, 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 createProgram(gl, vshader, fshader) {
        var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
        var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
        return program;
      }

      function initShaders(gl, vshader, fshader) {
        var program = createProgram(gl, vshader, fshader);
        gl.useProgram(program);
        gl.program = program;
        return true;
      }

      function loadShader(gl, type, source) {
        var shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
        return shader;
      }

      function main() {
        var canvas = document.getElementById("webgl");
        const gl = canvas.getContext("webgl");

        // 初始化着色器
        initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

        // 初始化buffer
        var n = initVertexBuffers(gl);

        // 清除canvas
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.enable(gl.DEPTH_TEST);
        var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
        var u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
        var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
         // 设置光照颜色 白色
        gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);
        // 设置光照方向
        var lightDirection = new Vector3([0.5, 3.0, 4.0]);
        lightDirection.normalize();     //正则化
        gl.uniform3fv(u_LightDirection, lightDirection.elements);

        var mvpMatrix = new Matrix4(); 
        mvpMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
        mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);

        gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

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

        gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);   // Draw the cube
      }

      function initVertexBuffers(gl) {
         // Create a cube
        //    v6----- v5
        //   /|      /|
        //  v1------v0|
        //  | |     | |
        //  | |v7---|-|v4
        //  |/      |/
        //  v2------v3
        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
       ]);


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

        var indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

        return indices.length;
      }

      function initArrayBuffer(gl, attribute, data, num, type) {
        var buffer = gl.createBuffer();   
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
        var a_attribute = gl.getAttribLocation(gl.program, attribute);
        gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
        gl.enableVertexAttribArray(a_attribute);
        return true;
      }
    </script>
  </body>
</html>

6.2 效果图

7 环境光下的漫反射

前面的立方体和现实中的还是有点不一样,特别是右侧表面是全黑的,仿佛不存在一样。在现实中,那些背面其实是被非直射光(其它物体、如墙壁的反射光)照亮的。前面的环境光就起到了这部分非直射光的作用。

前面提过

<环境反射光颜色>=<入射光颜色>x<表面基底色>

<表面的反射光颜色>=<漫反射光颜色>+<环境反射光颜色>

将环境光加入上述代码示例中

7.1 代码

var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'attribute vec4 a_Normal;\n' +       // 法向量
  'uniform mat4 u_MvpMatrix;\n' +
  'uniform vec3 u_DiffuseLight;\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(a_Normal.xyz);\n' +
     // 光照方向与法线(曲面的方向)的点积
  '  float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +
     // 计算由于漫反射的颜色
  '  vec3 diffuse = u_DiffuseLight * a_Color.rgb * nDotL;\n' +
     // 计算由于环境反射的颜色
  '  vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
     //由于漫反射和环境反射,添加表面颜色
  '  v_Color = vec4(diffuse + ambient, a_Color.a);\n' + 
  '}\n';

7.2 效果

8 运动物体的光照效果

当立方体旋转时,法向量也会跟着旋转,法向量会跟随立方体的几何变换发生变化:

由上图可知

  • 平移变换不会改变法向量,因为平移不会改变物体的方向;
  • 旋转变换会改变法向量,因为旋转改变了物体的方向;
  • 缩放对法向量的影响比较复杂。如果所有轴的缩放一致,则不会影响法向量,但是某些轴上的法向量的缩放比例不一致,法向量也不一定会发生变化,如果在上图3的y轴放大两倍,法向量就不会发生变化。

对立方体进行变换的矩阵称之为模型矩阵,如何计算变换之后的立方体法向量呢?只要将变换之前的法向量乘以模型矩阵的逆转置矩阵(inverse transpose matrix)即可。

为什么要乘以逆转置矩阵?

其实这取决于模型矩阵,如果模型矩阵只是涉及旋转,那么就可以直接使用模型矩阵乘以法向量即可。如果模型矩阵中包含了平移变换,法向量就会被当作顶点坐标平移,从而导致法向量与原有的表面朝向不一致,比如说(1,0,0)沿着y轴平移2个单位,就变为(1,2,0)。事实上,如果从4x4的模型矩阵的左上角抽出3x3子矩阵,然后乘以法向量,就可以避免该问题,因为变换矩阵,最后一列代表的就是平移。

我们看如果模型矩阵涉及了旋转和平移如何证明针对法向量的变换矩阵:

我们令模型矩阵为M,令初始的法向量为n,令变换矩阵为M',也就是用来正确变换法向量n的矩阵,令垂直于n的向量为s,此外定义n'和s’:

n' = M' x n

s' = M x s

如上图所示,我们需要计算M',使得n'和s'的相对角度垂直。我们知道两个垂直矢量的点积为0

(n' . s' )= 0

(M' x n). (M x s) = 0

(M' x n)T x (M x s) = 0 //(A . B) = AT X B

(nT x M'T x M x s) = 0 //(A x B)T = BT X A

n和s互相垂直,所以它们的点积是0,因此M'T x M必须是单位矩阵

M'T x M = I

M' = (M-1)T

8.1 顶点着色器代码实现

    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" +
      // 根据模型矩阵重新计算法线,使其长度为1。
      "  vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n" +
      // 计算光线方向与表面方向(法线)的点积
      "  float nDotL = max(dot(u_LightDirection, normal), 0.0);\n" +
      // 计算漫反射的颜色
      "  vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n" +
      // 根据环境反射计算颜色
      "  vec3 ambient = u_AmbientLight * a_Color.rgb;\n" +
      // 根据漫反射和环境反射添加表面颜色
      "  v_Color = vec4(diffuse + ambient, a_Color.a);\n" +
      "}\n";

9 点光源光

与平行光相比,点光源光发出的光,在三维空间的不同位置上其方向也不同。所以,在对点光源光下的物体进行着色时,需要在每个人射点计算点光源光在该处的方向。

平行光根据每个顶点的法向量和平行光人射方向来计算反射光的颜色,而点光源光的方向不再是恒定不变的,要根据每个顶点的位置逐一计算。着色器需要知道点光源光自身的所在位置,而不是光的方向。

9.1 代码实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    #webgl {
      width: 600px;
      height: 600px;
      position: absolute;
      top: calc(50% - 300px);
      left: calc(50% - 300px);
      background-color: black;
    }
  </style>
  <body onload="main()">
    <canvas id="webgl" width="600" height="600">
      Please use a browser that supports "canvas"
    </canvas>
    <script src="./matrix.js"></script>
    <script>
      // 顶点着色器
      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_ModelMatrix;\n' +    // 模型矩阵
        'uniform mat4 u_NormalMatrix;\n' +   // 法线的坐标变换矩阵
        'uniform vec3 u_LightColor;\n' +     // 光色
        'uniform vec3 u_LightPosition;\n' +  // 光源位置
        'uniform vec3 u_AmbientLight;\n' +   // 环境光颜色
        'varying vec4 v_Color;\n' +
        'void main() {\n' +
        '  gl_Position = u_MvpMatrix * a_Position;\n' +
        // 根据模型矩阵重新计算法线,使其长度为1
        '  vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
        // 计算顶点的世界坐标
        '  vec4 vertexPosition = u_ModelMatrix * a_Position;\n' +
        // 计算光线方向,使其长度为1.0
        '  vec3 lightDirection = normalize(u_LightPosition - vec3(vertexPosition));\n' +
        // 计算法线方向和光方向的点积
        '  float nDotL = max(dot(normal, lightDirection), 0.0);\n' +
        // 计算漫反射的颜色
        '  vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
        // 根据环境反射计算颜色
        '  vec3 ambient = u_AmbientLight * a_Color.rgb;\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 createProgram(gl, vshader, fshader) {
        var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
        var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
        return program;
      }

      function initShaders(gl, vshader, fshader) {
        var program = createProgram(gl, vshader, fshader);
        gl.useProgram(program);
        gl.program = program;
        return true;
      }

      function loadShader(gl, type, source) {
        var shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
        return shader;
      }

      function main() {
      var canvas = document.getElementById("webgl");
        const gl = canvas.getContext("webgl");

        // 初始化着色器
        initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

        // 初始化buffer
        var n = initVertexBuffers(gl);

        // 清除canvas
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.enable(gl.DEPTH_TEST);

        var u_ModelMatrix = gl.getUniformLocation(gl.program, "u_ModelMatrix");
        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_LightPosition = gl.getUniformLocation(
          gl.program,
          "u_LightPosition"
        );
        var u_AmbientLight = gl.getUniformLocation(
          gl.program,
          "u_AmbientLight"
        );

        var vpMatrix = new Matrix4(); // 视图投影矩阵
        // 设置投影矩阵
        vpMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100);
        // 设置视图矩阵
        vpMatrix.lookAt(6, 6, 14, 0, 0, 0, 0, 1, 0);

        // 设置灯光颜色(白色)
        gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);
        // 设置光照方向(世界坐标)
        gl.uniform3f(u_LightPosition, 2.3, 4.0, 3.5);
        //设置环境光
        gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);

        var currentAngle = 0.0; // 当前旋转角度
        var modelMatrix = new Matrix4(); // 模型矩阵
        var mvpMatrix = new Matrix4(); //模型视图投影矩阵
        var normalMatrix = new Matrix4(); // 法向量的变换矩阵
        var tick = function () {
          currentAngle = animate(currentAngle); // 旋转角度

          // 计算模型矩阵
          modelMatrix.setRotate(currentAngle, 0, 1, 0); // Rotate around the y-axis
          //将模型矩阵传递给u_ModelMatrix
          gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);

          //将模型视图投影矩阵传递给u_MvpMatrix
          mvpMatrix.set(vpMatrix).multiply(modelMatrix);
          gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

          //通过矩阵将基于模型矩阵的法线转换为u_NormalMatrix
          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);

          requestAnimationFrame(tick, canvas);
        };
        tick();
      }

      function initVertexBuffers(gl) {
        // Create a cube
        //    v6----- v5
        //   /|      /|
        //  v1------v0|
        //  | |     | |
        //  | |v7---|-|v4
        //  |/      |/
        //  v2------v3
        // Coordinates
        var vertices = new Float32Array([
          2.0, 2.0, 2.0,  -2.0, 2.0, 2.0,  -2.0,-2.0, 2.0,   2.0,-2.0, 2.0, // v0-v1-v2-v3 front
          2.0, 2.0, 2.0,   2.0,-2.0, 2.0,   2.0,-2.0,-2.0,   2.0, 2.0,-2.0, // v0-v3-v4-v5 right
          2.0, 2.0, 2.0,   2.0, 2.0,-2.0,  -2.0, 2.0,-2.0,  -2.0, 2.0, 2.0, // v0-v5-v6-v1 up
          -2.0, 2.0, 2.0,  -2.0, 2.0,-2.0,  -2.0,-2.0,-2.0,  -2.0,-2.0, 2.0, // v1-v6-v7-v2 left
          -2.0,-2.0,-2.0,   2.0,-2.0,-2.0,   2.0,-2.0, 2.0,  -2.0,-2.0, 2.0, // v7-v4-v3-v2 down
          2.0,-2.0,-2.0,  -2.0,-2.0,-2.0,  -2.0, 2.0,-2.0,   2.0, 2.0,-2.0  // v4-v7-v6-v5 back
        ]);

        // Colors
        var colors = new Float32Array([
          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
        ]);

        // Normal
        var normals = new Float32Array([
          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
        ]);

        // Indices of the vertices
        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
        ]);

        
        if (!initArrayBuffer(gl, "a_Position", vertices, 3, gl.FLOAT))
          return -1;
        if (!initArrayBuffer(gl, "a_Color", colors, 3, gl.FLOAT)) return -1;
        if (!initArrayBuffer(gl, "a_Normal", normals, 3, gl.FLOAT)) return -1;

        var indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

        return indices.length;
      }

      function initArrayBuffer(gl, attribute, data, num, type) {
        var buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
        var a_attribute = gl.getAttribLocation(gl.program, attribute);
        gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
        gl.enableVertexAttribArray(a_attribute);
        return true;
      }

      //旋转角度
      var ANGLE_STEP = 30.0;
      var g_last = Date.now();
      function animate(angle) {
        var now = Date.now();
        var elapsed = now - g_last;
        g_last = now;
        var newAngle = angle + (ANGLE_STEP * elapsed) / 1000.0;
        return (newAngle %= 360);
      }
    </script>
  </body>
</html>

9.2 预览效果

9.3 逐片元光照

9.2中最关键的变化在顶点着色器中,首先需要使用模型矩阵变换顶点坐标,获得顶点在世界坐标系中的坐标,以计算点光源光在顶点出的方向。根据上图,出现的一个现象是:里面提表面上有不自然的线条。这是因为,点光源光照射到一个表面上所产生的效果与简单使用4个顶点颜色内插出的效果并不相同,如果想要效果更加逼真,需要对表面上每一点计算光照效果,即逐片元计算。

9.3.1 代码实现

// 顶点着色器
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_ModelMatrix;\n" + // 模型矩阵
  "uniform mat4 u_NormalMatrix;\n" + // 法向量变换矩阵
  "varying vec4 v_Color;\n" +
  "varying vec3 v_Normal;\n" +
  "varying vec3 v_Position;\n" +
  "void main() {\n" +
  "  gl_Position = u_MvpMatrix * a_Position;\n" +
  // 计算世界坐标系中顶点坐标
  "  v_Position = vec3(u_ModelMatrix * a_Position);\n" +
  "  v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));\n" +
  "  v_Color = a_Color;\n" +
  "}\n";

// 片元着色器
var FSHADER_SOURCE =
  "#ifdef GL_ES\n" +
  "precision mediump float;\n" +
  "#endif\n" +
  "uniform vec3 u_LightColor;\n" + // 光色
  "uniform vec3 u_LightPosition;\n" + // 光源位置
  "uniform vec3 u_AmbientLight;\n" + // 环境光
  "varying vec3 v_Normal;\n" +
  "varying vec3 v_Position;\n" +
  "varying vec4 v_Color;\n" +
  "void main() {\n" +
  // 规范化法线,因为它是插值的,不再是1.0长度
  "  vec3 normal = normalize(v_Normal);\n" +
  // 计算光的方向,使其长度为1。
  "  vec3 lightDirection = normalize(u_LightPosition - v_Position);\n" +
  // 光方向与曲面方向(法线)的点积
  "  float nDotL = max(dot(lightDirection, normal), 0.0);\n" +
  // 根据漫反射和环境反射计算最终颜色
  "  vec3 diffuse = u_LightColor * v_Color.rgb * nDotL;\n" +
  "  vec3 ambient = u_AmbientLight * v_Color.rgb;\n" +
  "  gl_FragColor = vec4(diffuse + ambient, v_Color.a);\n" +
  "}\n";

9.3.2 效果图

为了逐片元地计算光照,需要知道:(1)片元在世界坐标系下的坐标,(2)片元处表面的法向量。可以在顶点着色器中,将顶点的世界坐标和法向量以varying 变量的形式传人片元着色器,片元着色器中的同名变量就已经是内插后的逐片元值了。