WebGL 中的灯光

正如现在应该清楚的那样,WebGL 没有太多内置的知识。它只运行您提供的两个函数——顶点着色器和片段着色器——并期望您编写创意函数来获得您想要的结果。换句话说,如果您想要灯光,您必须自己计算它。幸运的是,这并不难,本文将介绍一些基础知识。

模拟 3D 中的灯光和阴影

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

有三种基本的光照类型

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

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

点光是指从一个点发出的光,向各个方向辐射。这就是许多现实世界中的光源通常的工作原理。例如,灯泡会向各个方向发射光。

为了我们的目的,我们将简化光照模型,只考虑简单的方向光和环境光;在这个场景中,我们将不会有任何 镜面高光 或点光源。相反,我们将拥有我们的环境光以及一个指向旋转立方体的单个方向光源 之前的演示

一旦您放弃了点光源和镜面光照的概念,我们只需要两条信息来实现我们的方向光照

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

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

注意:将以下代码添加到您的“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一致变量的位置。

注意:像这样更新您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"),
  },
};

就这样!

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

读者的练习

显然,这是一个简单的示例,实现了基本的每顶点光照。对于更高级的图形,您需要实现每像素光照,但这将使您朝着正确的方向前进。

您还可以尝试试验光源的方向、光源的颜色等等。