# 光照原理

在物体表面根据光照着色,需要考虑两件事:

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

# 光源类型

真实世界中的光主要有两种类型:平行光和点光源。此外还有环境光来模拟真实世界中的非直射光(也是就是由光源发出后经过墙壁或其他物体反射后的光)

avatar

  • 平行光:太阳光就是平行光,可以用一个方向和颜色来定义。
  • 点光源:类似灯泡,可以用光源的位置和颜色来定义。
  • 环境光:环境光从各个角度照射物体,其强度都是一致的。只需要指定颜色即可。

# 反射类型

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

物体表面反射光线的方式有:漫反射和环境反射。

# 漫反射

粗糙的物体表面向各个方向等强度低反射光,这种等同度地散射现象称为光的漫反射(Diffuse Reflection) 漫反射模型和视点无关于,与入射光、入射点、物体材质相关。漫反射的反射光在各个方向上是均匀的。

avatar

漫反射中,反射光的颜色取决于入射光的颜色、表面的基底色、入射光与表面形成的入射角。我们将入射角定义为入射光与表面的法线形成的夹角,用 θ 表示,那么漫反射光的颜色可以根据下式得到:

漫反射光颜色 = 入射光颜色 × 表面基底色 × cos θ

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

# 环境反射

环境反射是针对环境光而言的。在环境反射中,反射光的方向可以认为就是入射光的反方向。由于环境光照射物体的方式就是各方向均匀、强度相等的,所以反射光也是各向均匀的。可以用下面式子表述:

  • 环境反射光 = 入射光颜色(环境光的颜色) × 表面基底色

avatar

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

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

# 平行光下的漫反射

我们可以根据光线方向和法线方向(物体表面的方向),来计算反射光的夹角,根据矢量的点乘。

  • cos θ = 光线方向 . 法线方向

所以漫反射光的颜色可以通过下面的式子计算:

  • 漫反射光颜色 = 入射光颜色 × 表面基底色 × (光线方向 . 法线方向)

这里需要注意,光线方向矢量和表面法线矢量的长度必须为 1,否则反射光的颜色就会过暗或过亮。比如矢量 n 为(nx, ny, nz)则其长度为 (nx^2 + ny ^2 + nz ^2 )^1/2。将一个矢量的长度调整为 1,同时保持方向不变的过程称为归一化。GLSL ES 提供了内置的归一化函数。还有就是这里所谓的光线方向,实际上是入射方向的的反方向。即从入射点指向光源方向(因为这样,该方向与法线方向的夹角才是入射角);

avatar

归一化:对矢量 n 进行归一化的结果是 (nx / m, ny / m, nz / m),式中 m 为 n 的长度。比如矢量 (2.0, 2.0, 1.0) 的长度为 |n| = sqrt(9) = 3,那么归一化之后就是 (2.0 / 3, 2.0 / 3, 1.0 / 3)

# 法线:表面的朝向

物体表面的朝向,即垂直于表面的方向,又称法线或法向量。法向量有三个分量,向量(nx, ny, nz)表示从原点(0, 0, 0)指向点(nx, ny, nz)的方向。比如说,向量 (1, 0, 0) 表示 x 轴正方向,向量 (0, 0, 1) 表示 z 轴正方向。涉及到表面和法向量的问题时,必须考虑下面两点:

  • 一个表面具有两个法向量 每个表面都具有正反面,每个面各自具有一个法向量。 avatar

在三维图形学中,表面的正面和背面取决于绘制表面时的顶点顺序,当你按照 v0, v1, v2, v3 的顶点顺序绘制了一个平面,那么当你从正面观察这个表面时,这 4 个顶点是顺时针。通过右手法则来确定旋转方向。

  • 平面的法向量唯一 由于法向量表示的是方向,与位置无关,所以一个平面只有一个法向量。换句话说,平面的任意一点都具有相同的法向量。也就是说,即使有两个不同的平面,主要其朝向相同(平行),法向量也相同。

avatar

# 漫反射模型

lld = k * la + K * ll * Cosa k 是全局光的系数,la 是全局光的光强,K 漫反射的系数,ll 代表入射光线的光强,Cosa 入射光与法线之间夹角的 cos 值。

avatar

# 示例代码

下面代码实现一个在白色平行光照射下的一个红色三角形;

// 顶点着色器
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 main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }
  // 设置顶点的坐标、颜色和法向量
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }
  gl.clearColor(0, 0, 0, 1);
  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');
  if (!u_MvpMatrix || !u_LightColor || !u_LightDirection) { 
    console.log('Failed to get the storage location');
    return;
  }

  // 设置光线颜色--白色
  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);
}

function initVertexBuffers(gl) {
  //    v6----- v5
  //   /|      /|
  //  v1------v0|
  //  | |     | |
  //  | |v7---|-|v4
  //  |/      |/
  //  v2------v3
  var vertices = new Float32Array([   // 顶点坐标
     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([    // 颜色
    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([    // 法向量
    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();
  if (!indexBuffer) {
    console.log('Failed to create the buffer object');
    return false;
  }

  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();
  if (!buffer) {
    console.log('Failed to create the buffer object');
    return false;
  }
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
  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);
  gl.enableVertexAttribArray(a_attribute);

  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  return true;
}

  • a_Normal 变量是 vec4 类型的,使用前三个分量 x、y 和 z 表示法线方向,所以上面例子是将这三个分量进行归一化处理。

  • nDotL 是计算光线方向与法线方向的点乘,使用了内置的 max() 方法,如果点积大于 0 ,就将点积赋值给 nDotL 变量,如果小于 0,就将 0 赋给该变量。点积值小于 0,说明 θ 大于 90 °,也就是入射反方向与表面法向量的夹角,θ 大于 90 度说明光线照射在表面的背面上,如下图:

avatar

注意我们使用 a_Color 变量就是顶点的颜色,被从 vec4 对象转换成了 vec3 对象,因为其第 4 分负能量是透明度,与计算物体反射光线无关。实际上,物体表面的透明度确实会影响物体的外观。但这时光照的计算较为复杂。这里不进行计算。

# 环境光下的漫反射

上面通过平行光渲染的物体,可以看到他的背光面,几乎黑到看不见。实际上,那些背光的面试被非直射光照亮的,前面提到的环境光就起到了这部分非直射光的作用,他使得场景更加逼真。因为环境光均匀地从各个角度照在物体表面,所以由环境光反射产生的颜色只取决于光的颜色和表面的基底色。如下公式:

环境反射光颜色 = 入射光颜色 × 表面基底色

我们改进上面的例子,加上环境光所产生的反射光颜色,公式如下:

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

示例代码:

// 顶点着色器
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';

// 片元着色器同上
function main() {
  var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');

  // 设置环境光--较微弱的白色光
  gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
  // 其他代码同上
}

顶点着色器新增 u_AmbientLight 接收环境光的颜色值,然后与基底色 a_Color 计算产生的反射光的颜色存储在 ambient 变量中,然后与平行光产生的颜色 deffuse 进行相加,得到最终物体表面显示的颜色。

# 运动物体的光照效果

这里主要是将当物体通过模型矩阵等变化后,如何进行渲染光线,主要需要解决法线方向的变化,通过逆转置矩阵解决。

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

avatar

由上图可知:

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

显然,在对物体进行不同变换时,法向量的变化情况较为复杂,可以通过逆转置矩阵来进行变化。

# 逆转置矩阵

我们可以使用逆转置矩阵通过与变换之前的法向量相乘。

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

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

封装方法:

// 使自身成为矩阵 other 的逆矩阵
Matrix4.prototype.setInverseOf = function(other) {
  var i, s, d, inv, det;

  s = other.elements;
  d = this.elements;
  inv = new Float32Array(16);

  inv[0]  =   s[5]*s[10]*s[15] - s[5] *s[11]*s[14] - s[9] *s[6]*s[15]
            + s[9]*s[7] *s[14] + s[13]*s[6] *s[11] - s[13]*s[7]*s[10];
  inv[4]  = - s[4]*s[10]*s[15] + s[4] *s[11]*s[14] + s[8] *s[6]*s[15]
            - s[8]*s[7] *s[14] - s[12]*s[6] *s[11] + s[12]*s[7]*s[10];
  inv[8]  =   s[4]*s[9] *s[15] - s[4] *s[11]*s[13] - s[8] *s[5]*s[15]
            + s[8]*s[7] *s[13] + s[12]*s[5] *s[11] - s[12]*s[7]*s[9];
  inv[12] = - s[4]*s[9] *s[14] + s[4] *s[10]*s[13] + s[8] *s[5]*s[14]
            - s[8]*s[6] *s[13] - s[12]*s[5] *s[10] + s[12]*s[6]*s[9];

  inv[1]  = - s[1]*s[10]*s[15] + s[1] *s[11]*s[14] + s[9] *s[2]*s[15]
            - s[9]*s[3] *s[14] - s[13]*s[2] *s[11] + s[13]*s[3]*s[10];
  inv[5]  =   s[0]*s[10]*s[15] - s[0] *s[11]*s[14] - s[8] *s[2]*s[15]
            + s[8]*s[3] *s[14] + s[12]*s[2] *s[11] - s[12]*s[3]*s[10];
  inv[9]  = - s[0]*s[9] *s[15] + s[0] *s[11]*s[13] + s[8] *s[1]*s[15]
            - s[8]*s[3] *s[13] - s[12]*s[1] *s[11] + s[12]*s[3]*s[9];
  inv[13] =   s[0]*s[9] *s[14] - s[0] *s[10]*s[13] - s[8] *s[1]*s[14]
            + s[8]*s[2] *s[13] + s[12]*s[1] *s[10] - s[12]*s[2]*s[9];

  inv[2]  =   s[1]*s[6]*s[15] - s[1] *s[7]*s[14] - s[5] *s[2]*s[15]
            + s[5]*s[3]*s[14] + s[13]*s[2]*s[7]  - s[13]*s[3]*s[6];
  inv[6]  = - s[0]*s[6]*s[15] + s[0] *s[7]*s[14] + s[4] *s[2]*s[15]
            - s[4]*s[3]*s[14] - s[12]*s[2]*s[7]  + s[12]*s[3]*s[6];
  inv[10] =   s[0]*s[5]*s[15] - s[0] *s[7]*s[13] - s[4] *s[1]*s[15]
            + s[4]*s[3]*s[13] + s[12]*s[1]*s[7]  - s[12]*s[3]*s[5];
  inv[14] = - s[0]*s[5]*s[14] + s[0] *s[6]*s[13] + s[4] *s[1]*s[14]
            - s[4]*s[2]*s[13] - s[12]*s[1]*s[6]  + s[12]*s[2]*s[5];

  inv[3]  = - s[1]*s[6]*s[11] + s[1]*s[7]*s[10] + s[5]*s[2]*s[11]
            - s[5]*s[3]*s[10] - s[9]*s[2]*s[7]  + s[9]*s[3]*s[6];
  inv[7]  =   s[0]*s[6]*s[11] - s[0]*s[7]*s[10] - s[4]*s[2]*s[11]
            + s[4]*s[3]*s[10] + s[8]*s[2]*s[7]  - s[8]*s[3]*s[6];
  inv[11] = - s[0]*s[5]*s[11] + s[0]*s[7]*s[9]  + s[4]*s[1]*s[11]
            - s[4]*s[3]*s[9]  - s[8]*s[1]*s[7]  + s[8]*s[3]*s[5];
  inv[15] =   s[0]*s[5]*s[10] - s[0]*s[6]*s[9]  - s[4]*s[1]*s[10]
            + s[4]*s[2]*s[9]  + s[8]*s[1]*s[6]  - s[8]*s[2]*s[5];

  det = s[0]*inv[0] + s[1]*inv[4] + s[2]*inv[8] + s[3]*inv[12];
  if (det === 0) {
    return this;
  }

  det = 1 / det;
  for (i = 0; i < 16; i++) {
    d[i] = inv[i] * det;
  }

  return this;
};

// 将自身进行转置操作,并将自身设为转置后的结果
Matrix4.prototype.transpose = function() {
  var e, t;

  e = this.elements;

  t = e[ 1];  e[ 1] = e[ 4];  e[ 4] = t;
  t = e[ 2];  e[ 2] = e[ 8];  e[ 8] = t;
  t = e[ 3];  e[ 3] = e[12];  e[12] = t;
  t = e[ 6];  e[ 6] = e[ 9];  e[ 9] = t;
  t = e[ 7];  e[ 7] = e[13];  e[13] = t;
  t = e[11];  e[11] = e[14];  e[14] = t;

  return this;
};

使用如下代码:

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

# 示例程序

下面代码展示了将立方体绕 z 轴顺时针旋转 90 °,然后沿 y 轴平移 0.9 个单位,并且处于平行光和环境光的照射下。

// LightedTranslatedRotatedCube.js (c) 2012 matsuda
// Vertex shader program
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 * a_Color.rgb * nDotL;\n' +
     // 计算环境光产生的反射光的颜色
  '  vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
     // 将以上两者相加作为最终的颜色
  '  v_Color = vec4(diffuse + ambient, a_Color.a);\n' + 
  '}\n';

// 片元着色器同上

function main() {

  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);
}

# 点光源

点光源与平行光是不同的,在对点光源下的物体进行着色时,需要在每个入射点计算点光源在该处的方向。

avatar

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

漫反射光颜色 = 入射光颜色 × 表面基底色 × cosθ 漫反射光颜色 = 入射光颜色 × 表面基底色 × (光线方向 . 法线方向)

# 示例代码

使用点光源下渲染一个红色立方体:

// 顶点着色器
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' +
     // 计算变换后的法向量归一化
  '  vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
     // 计算顶点的世界坐标
  '  vec4 vertexPosition = u_ModelMatrix * a_Position;\n' +
     // 计算光线方向并归一化
  '  vec3 lightDirection = normalize(u_LightPosition - vec3(vertexPosition));\n' +
     // 计算光线方向和法向量的点积
  '  float nDotL = max(dot(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';

// 片元着色器同上
// 这里只写一些关键代码
function main() {

  var u_LightPosition = gl.getUniformLocation(gl.program, 'u_LightPosition');
  // 设置光源位置
  gl.uniform3f(u_LightPosition, 2.3, 4.0, 3.5);

}

这里的代码只是增加了传入点光源的位置,最关键的代码是在顶点着色器中。首先使用模型矩阵变换顶点坐标,获得顶点在世界坐标系中的坐标(即变换后的坐标),以便计算点光源在顶点处的方向。这里的光线方向需要根据点光源坐标减去顶点坐标得到的矢量。这里需要注意的是,还是需要将计算出的光线方向矢量进行归一化,以保证光线方向矢量的长度为 1.0(是因为向量叉乘来计算 cos θ,所以保证向量的模为 1)最后,计算光线方向矢量与法向量的点积,从而算出每个顶点的颜色。

avatar

上面是渲染的结果,但是我们会法线立方体表面上会有一些不自然的线条。出现该现象的原因是因为我们之前说过颜色的内插引起的,webgl 系统会根据顶点的颜色,内插出表面上每个片元的颜色。实际上,点光源照射到一个表面上,所产生的效果(即每个片元获得的颜色)与简单实用 4 个顶点颜色(虽然这 4 个 顶点的颜色也是由点光源产生的)内插出的效果并不完全相同。如果是球体,差异会很大,如下图:

下图没有通过逐片元计算光照, avatar 下图通过逐片元计算光照 avatar

所以为了使效果更加逼真,我们需要对表面的每一点(而不仅仅是 4 个顶点)计算光照效果。

# 逐片元光照

我们并不是在表面的每一个点上计算光照产生的颜色,我们只需要逐片元地进行计算。

如下代码:

// 顶点着色器
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' +
     // 计算光线方向并归一化
  '  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';

// js 代码同上程序

为了逐片元地计算光照,主要知道:

  1. 片元在世界坐标系下的坐标
  2. 片元处表面的法向量。可以在顶点着色器中,将顶点的世界坐标和法向量以 varying 变量的形式传入片元着色器,片元着色器中的同名变量就已经是内插后的逐片元值了。

注意上面代码在片元着色器中获取到的 v_Position 是通过内插得到了片元的世界坐标。类似的 v_Normal 也是经过内插得到的逐片元的 v_Normal 变量,也就是片元的法向量。

片元着色器计算光照与之前的程序一样,首先对法向量 v_Normal 进行归一化处理,因为内插之后法向量可能不再是 1.0 了,然后,计算片元处的光线方向并对其归一化。接着计算法向量与光线方向的点积。最后分别计算点光源和环境光产生的反射光颜色。最终计算片元的颜色。

如果场景中有超过一个点光源,那么就需要在片元着色器中计算每一个点光源(当然还有环境光)对片元颜色的加成。并将他们全部加起来。

# Phong 光照模型

漫反射模型上的一个升级;漫反射有一个特点;没办法描述镜面反射。

avatar

specular 是高光。

漫反射可以表现出粗糙表面的光照现象,如墙壁,纸张等,但是无法很好地表现出光泽金属所有的镜面反射。

phong 模型认为镜面反射的光强与反射光线和视线的夹角相关。

avatar

specular = Ks * lightColor * (dot(V, R))^shininess

avatar

评 论:

更新: 11/21/2020, 7:00:56 PM