iT邦幫忙

2021 iThome 鐵人賽

DAY 20
0

大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 20 篇文章。本系列文章從 WebGL 基本運作機制以及使用的原理開始介紹,最後建構出繪製 3D、光影效果之網頁。繪製出簡易的 3D 場景後,本章節加入光照效果使得成像更加真實,如果在閱讀本文時覺得有什麼未知的東西被當成已知的,可能可以在前面的文章中找到相關的內容

從無限遠照射場景的平行光適合用來模擬太陽這類型的光源,如果是室內的燈泡光源呢?本篇將在場景中加入一個黃色自發光燈泡,並把平行光改成以這顆燈泡作為點光源

輸入光源位置並計算光線方向

在平行光的環境下,所有位置的光線方向都一樣,因此只需要一個 uniform u_lightDir 便可以,但是在點光源的情況下會因為頂點/表面位置不同而有不同的光線方向,而光線方向可以透過 vertex shader 計算,並利用平滑補間使得 fragment shader 得到對應表面所街收到的光線方向,因此在 vertex shader 中使用 uniform 接收光源位置 u_worldLightPosition,並且計算出光線方向使用 varying v_surfaceToLight 傳給 fragment shader 使用:

 attribute vec4 a_position;
 attribute vec2 a_texcoord;
 attribute vec4 a_normal;

 uniform mat4 u_matrix;
 uniform mat4 u_worldMatrix;
 uniform mat4 u_normalMatrix;
 uniform vec3 u_worldViewerPosition;
+uniform vec3 u_worldLightPosition;
  
 varying vec2 v_texcoord;
 varying vec3 v_normal;
 varying vec3 v_surfaceToViewer;
+varying vec3 v_surfaceToLight;

 void main() {
   gl_Position = u_matrix * a_position;
   v_texcoord = vec2(a_texcoord.x, 1.0 - a_texcoord.y);
   v_normal = (u_normalMatrix * a_normal).xyz;
   vec3 worldPosition = (u_worldMatrix * a_position).xyz;
   v_surfaceToViewer = u_worldViewerPosition - worldPosition;
+  v_surfaceToLight = u_worldLightPosition - worldPosition;
 }

在 fragment shader 上做的事情不會很困難,就只是從 u_lightDir 改用 v_surfaceToLight

 precision highp float;

 uniform vec3 u_diffuse;
 uniform sampler2D u_texture;
-uniform vec3 u_lightDir;
 uniform vec3 u_specular;
 uniform float u_specularExponent;
  
 varying vec2 v_texcoord;
 varying vec3 v_normal;
 varying vec3 v_surfaceToViewer;
+varying vec3 v_surfaceToLight;
  
 void main() {
   vec3 diffuse = u_diffuse + texture2D(u_texture, v_texcoord).rgb; 
   vec3 normal = normalize(v_normal);
-  vec3 surfaceToLightDir = normalize(-u_lightDir);
-  float diffuseBrightness = clamp(dot(surfaceToLightDir, normal), 0.0, 1.0);
+  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
+  float diffuseBrightness = clamp(dot(surfaceToLightDirection, normal), 0.0, 1.0);
  
   vec3 surfaceToViewerDirection = normalize(v_surfaceToViewer);
-  vec3 halfVector = normalize(surfaceToLightDir + surfaceToViewerDirection);
+  vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewerDirection);
   float specularBrightness = clamp(pow(dot(halfVector, normal), u_specularExponent), 0.0, 1.0);

   gl_FragColor = vec4(
     diffuse * diffuseBrightness +
     u_specular * specularBrightness,
     1
   );
 }

筆者設定光源的初始位置在 [0, 2, 0],並且設定該設定的 uniform:

 async function setup() {
   // ...
   return {
     // ...
     state: {
-      lightDir: [0, -1, 0],
+      lightPosition: [0, 2, 0],
     }
   }
 }

 function render(app) {
   twgl.setUniforms(programInfo, {
     u_worldViewerPosition: state.cameraPosition,
-    u_lightDir: state.lightDir,
+    u_worldLightPosition: state.lightPosition,
     u_specular: [1, 1, 1],
 });

同時也將從 DOM 進行的使用者控制部份調整好,程式碼比較瑣碎筆者就不列了,改完之後可以調整光源的 y 軸位置,觀察接近地面時反射光的表現:

light-position-demo

加入燈泡表示點光源位置

我們就用小球來表示點光源的位置,可以重複使用現有的 objects.ball 物件,因此不需要修改 setup(),直接在 render() 多渲染一次 objects.ball,並利用 worldMatrix 使得物件縮小且平移至光源位置:

function render(app) {
  // ...
  { // light bulb
    gl.bindVertexArray(objects.ball.vao);

    const worldMatrix = matrix4.multiply(
      matrix4.translate(...state.lightPosition),
      matrix4.scale(0.1, 0.1, 0.1),
    );

    twgl.setUniforms(programInfo, {
      u_matrix: matrix4.multiply(viewMatrix, worldMatrix),
      u_worldMatrix: worldMatrix,
      u_normalMatrix: matrix4.transpose(matrix4.inverse(worldMatrix)),
      u_diffuse: [1, 1, 1],
      u_texture: textures.nil,
      u_specularExponent: 1000,
    });

    twgl.drawBufferInfo(gl, objects.ball.bufferInfo);
  }
  // ...
}

雖然筆者把燈泡的 u_diffuse 給上 [1, 1, 1],但是因為光源在球體內部,因此燈泡球體呈現黑色:

with-black-bulb

Emissive 自發光

為了讓燈泡球體有顏色,我們可以在 fragment shader 中加上一個 uniform,計算 gl_FragColor 時直接加上這個顏色,這個顏色即為自發光,變數名稱命名為 u_emissive:

 precision highp float;

 uniform sampler2D u_texture;
 uniform vec3 u_specular;
 uniform float u_specularExponent;
+uniform vec3 u_emissive;

 // ...

 void main() {
   // ...

   gl_FragColor = vec4(
     diffuse * diffuseBrightness +
-    u_specular * specularBrightness,
+    u_specular * specularBrightness +
+    u_emissive,
     1
   );
 }

接下來對各個物件指定自發光顏色,筆者讓原本的球體也有一點點的亮度 [0.15, 0.15, 0.15],而原本的燈泡就給黃色 [1, 1, 0]:

 function render(app) {
   // ...
   
   { // ball
     twgl.setUniforms(programInfo, {
       // ...
+      u_emissive: [0.15, 0.15, 0.15],
     });
   }
   
   { // light bulb
     twgl.setUniforms(programInfo, {
       // ...
+      u_emissive: [1, 1, 0],
     });
   }

   { // ground
     twgl.setUniforms(programInfo, {
       // ...
+      u_emissive: [0, 0, 0],
     });
   }
 }

今天的目標就完成啦:

yellow-bulb-point-lighting

事實上,物件『材質』對於光的反應、產生的顏色很可能遠不只這個系列文所提到的散射光、反射光、自發光,以這篇讀取 .obj/.mtl 3D 模型材質資料的文章來看,至少就還有環境光(ambient)等等

本篇完整的程式碼可以在這邊找到:


上一篇
反射光
下一篇
Normal Map
系列文
如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言