大家好,我是西瓜,你現在看到的是 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 軸位置,觀察接近地面時反射光的表現:
我們就用小球來表示點光源的位置,可以重複使用現有的 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]
,但是因為光源在球體內部,因此燈泡球體呈現黑色:
為了讓燈泡球體有顏色,我們可以在 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],
});
}
}
今天的目標就完成啦:
事實上,物件『材質』對於光的反應、產生的顏色很可能遠不只這個系列文所提到的散射光、反射光、自發光,以這篇讀取 .obj/.mtl 3D 模型材質資料的文章來看,至少就還有環境光(ambient)等等
本篇完整的程式碼可以在這邊找到: