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

就是這樣!

檢視完整程式碼 | 在新頁面中開啟此演示

讀者練習

顯然,這是一個簡單的例子,實現了基本的逐頂點光照。對於更高階的圖形,您可能需要實現逐畫素光照,但這將為您指明正確的方向。

您也可以嘗試調整光源的方向、光源的顏色等等。