WebGL 中的光照

到现在, il est clair que WebGL n'a pas beaucoup de connaissances intégrées. Il exécute simplement deux fonctions que vous fournissez — un vertex shader et un fragment shader — et s'attend à ce que vous écriviez des fonctions créatives pour obtenir les résultats souhaités. En d'autres termes, si vous voulez de l'éclairage, vous devez le calculer vous-même. Heureusement, ce n'est pas si difficile à faire, et cet article couvrira quelques-unes des bases.

在 3D 中模拟光照和着色

尽管详细讨论 3D 图形中模拟光照背后的理论远远超出了本文的范围,但了解其工作原理还是很有帮助的。与其在这里深入探讨,不如看看维基百科上关于Phong 着色的文章,它对最常用的光照模型进行了很好的概述。或者,如果您想查看基于 WebGL 的解释,请阅读WebGL 3D - 点光源

有三种基本类型的光照

环境光 是弥漫在场景中的光;它不具方向性,平等地影响场景中的每个面,无论其朝向哪个方向。

平行光 是从特定方向发出的光。这是来自遥远的光,以至于每个光子都与其他所有光子平行移动。例如,阳光被认为是平行光。

点光源 是从一个点发出的光,向所有方向辐射。许多现实世界的光源通常就是这样工作的。例如,灯泡向所有方向发出光。

就我们而言,我们将通过仅考虑简单的平行光和环境光来简化光照模型;在此场景中,我们将没有任何镜面高光或点光源。相反,我们将拥有环境光加上一个单一的平行光源,该光源对来自上一个演示的旋转立方体进行照射。

一旦您抛弃了点光源和镜面光照的概念,我们将需要两条信息来实施我们的平行光照

  1. 我们需要将一个表面法线与每个顶点相关联。这是一个垂直于该顶点处面的向量。
  2. 我们需要知道光线传播的方向;这由方向向量定义。

然后,我们更新顶点着色器,根据环境光以及平行光照射到面的角度来调整每个顶点的颜色。当查看着色器的代码时,我们将看到如何做到这一点。

为顶点构建法线

我们需要做的第一件事是生成构成我们立方体的所有顶点的法线数组。由于立方体是一个非常简单的对象,这很容易做到;显然,对于更复杂的对象,计算法线将更加复杂。

注意:将此函数添加到您的“init-buffer.js”模块

js
function initNormalBuffer(gl) {
  const normalBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);

  const vertexNormals = [
    // Front
    0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,

    // Back
    0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0,

    // Top
    0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,

    // Bottom
    0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0,

    // Right
    1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,

    // Left
    -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0,
  ];

  gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array(vertexNormals),
    gl.STATIC_DRAW,
  );

  return normalBuffer;
}

这现在应该看起来很熟悉了;我们创建一个新缓冲区,将其绑定为我们正在处理的缓冲区,然后通过调用 bufferData() 将我们的顶点法线数组发送到缓冲区。

和以前一样,我们更新了 initBuffers() 以调用我们的新函数,并返回它创建的缓冲区。

注意:在您的 initBuffers() 函数的末尾,添加以下代码,替换现有的 return 语句

js
const normalBuffer = initNormalBuffer(gl);

return {
  position: positionBuffer,
  normal: normalBuffer,
  textureCoord: textureCoordBuffer,
  indices: indexBuffer,
};

然后,我们将代码添加到“draw-scene.js”模块中,将法线数组绑定到一个着色器属性,以便着色器代码可以访问它。

注意:将此函数添加到您的“draw-scene.js”模块

js
// Tell WebGL how to pull out the normals from
// the normal buffer into the vertexNormal attribute.
function setNormalAttribute(gl, buffers, programInfo) {
  const numComponents = 3;
  const type = gl.FLOAT;
  const normalize = false;
  const stride = 0;
  const offset = 0;
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal);
  gl.vertexAttribPointer(
    programInfo.attribLocations.vertexNormal,
    numComponents,
    type,
    normalize,
    stride,
    offset,
  );
  gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal);
}

注意:在您的“draw-scene.js”模块的 drawScene() 函数中,在 gl.useProgram() 行之前添加此行

js
setNormalAttribute(gl, buffers, programInfo);

最后,我们需要更新构建 uniform 矩阵的代码,以生成一个法线矩阵并将其传递给着色器,该矩阵用于在处理立方体相对于光源的当前方向时变换法线。

注意:在您的“draw-scene.js”模块的 drawScene() 函数中,在三个 mat4.rotate() 调用之后添加以下代码

js
const normalMatrix = mat4.create();
mat4.invert(normalMatrix, modelViewMatrix);
mat4.transpose(normalMatrix, normalMatrix);

注意:在您的“draw-scene.js”模块的 drawScene() 函数中,在前面两个 gl.uniformMatrix4fv() 调用之后添加以下代码

js
gl.uniformMatrix4fv(
  programInfo.uniformLocations.normalMatrix,
  false,
  normalMatrix,
);

更新着色器

现在所有着色器所需的数据都可用了,我们需要更新着色器本身的代码。

顶点着色器

要做的第一件事是更新顶点着色器,以便它根据环境光和方向光生成每个顶点的着色值。

注意:在您的 main() 函数中,像这样更新 vsSource 声明

js
const vsSource = `
    attribute vec4 aVertexPosition;
    attribute vec3 aVertexNormal;
    attribute vec2 aTextureCoord;

    uniform mat4 uNormalMatrix;
    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;

    varying highp vec2 vTextureCoord;
    varying highp vec3 vLighting;

    void main(void) {
      gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
      vTextureCoord = aTextureCoord;

      // Apply lighting effect

      highp vec3 ambientLight = vec3(0.3, 0.3, 0.3);
      highp vec3 directionalLightColor = vec3(1, 1, 1);
      highp vec3 directionalVector = normalize(vec3(0.85, 0.8, 0.75));

      highp vec4 transformedNormal = uNormalMatrix * vec4(aVertexNormal, 1.0);

      highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0);
      vLighting = ambientLight + (directionalLightColor * directional);
    }
  `;

一旦计算出顶点的位置,并且我们将与顶点对应的纹理单元格的坐标传递给片段着色器,我们就可以着手计算顶点的着色。

我们做的第一件事是根据立方体的当前方向变换法线,通过将顶点的法线乘以法线矩阵。然后,我们可以通过计算变换后的法线与方向向量(即光线来源的方向)的点积来计算需要应用于顶点的方向光量。如果此值小于零,则我们将该值固定为零,因为光线不可能小于零。

一旦计算出方向光的量,我们就可以通过取环境光并加上方向光的颜色与其提供的方向光量的乘积来生成光照值。结果是,我们现在有了一个 RGB 值,片段着色器将使用它来调整我们渲染的每个像素的颜色。

片段着色器

现在需要更新片段着色器,以考虑由顶点着色器计算的光照值。

注意:在您的 main() 函数中,像这样更新 fsSource 声明

js
const fsSource = `
    varying highp vec2 vTextureCoord;
    varying highp vec3 vLighting;

    uniform sampler2D uSampler;

    void main(void) {
      highp vec4 texelColor = texture2D(uSampler, vTextureCoord);

      gl_FragColor = vec4(texelColor.rgb * vLighting, texelColor.a);
    }
  `;

在这里,我们像在上一个示例中一样获取纹理单元格的颜色,但在设置片段颜色之前,我们将纹理单元格的颜色乘以光照值,以调整纹理单元格的颜色,使其考虑我们光源的影响。

剩下的唯一一件事就是查找 aVertexNormal 属性和 uNormalMatrix uniform 的位置。

注意:在您的 main() 函数中,像这样更新 programInfo 声明

js
const programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
    vertexNormal: gl.getAttribLocation(shaderProgram, "aVertexNormal"),
    textureCoord: gl.getAttribLocation(shaderProgram, "aTextureCoord"),
  },
  uniformLocations: {
    projectionMatrix: gl.getUniformLocation(shaderProgram, "uProjectionMatrix"),
    modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
    normalMatrix: gl.getUniformLocation(shaderProgram, "uNormalMatrix"),
    uSampler: gl.getUniformLocation(shaderProgram, "uSampler"),
  },
};

就是这样!

查看完整代码 | 在新页面中打开此演示

读者练习

显然,这是一个简单的例子,实现了基本的逐顶点光照。对于更高级的图形,您可能需要实现逐像素光照,但这将为您指明正确的方向。

您也可以尝试调整光源的方向、光源的颜色等等。