大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 19 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。绘制出简易的 3D 场景後,本章节加入光照效果使得成像更加真实,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容
有了散射光的计算,物体的表面根据有没有被光照射到而显示;而本篇将介绍计算上较为复杂的 specular 反射光,笔者觉得加上这个效果之後,物体就可以呈现金属、或是光滑表面的质感,开始跳脱死板的颜色,接下来以此画面为目标:
在反射光的 wiki 中,反射光的意示图如下:
入射角与反射角的角度相同,也就是 θi
与 θr
相同,在本篇实做目标撷图中,其中球体上的白色反光区域,就是光线入射角与反射角角度很接近的地方;而在 fragment shader 内,与计算散射时一样,与其计算角度,不如利用单位向量的内积,先计算光线方向反向 surfaceToLightDirection
与表面到相机方向 surfaceToViewerDirection
的『中间向量』,也就是 surfaceToLightDirection
与 surfaceToViewerDirection
两个向量箭头顶点的中间位置延伸而成的单位向量 halfVector
,再拿 halfVector
与法向量做内积得到反射光的明暗度:
为了知道表面 O 点到相机方向,我们要在 shader 内计算出表面的位置,也就是只有经过 worldMatrix
做 transform 的位置,因此除了同时包含 worldMatrix
与 viewMatrix
的 u_matrix
之外,也得传 worldMatrix
,我们就叫这个 uniform u_worldMatrix
;另外也需要传送相机的位置进去 u_worldViewerPosition
:
function render(app) {
// ...
twgl.setUniforms(programInfo, {
+ u_worldViewerPosition: state.cameraPosition,
u_lightDir: state.lightDir,
});
// ...
{ // both ball and ground
// ...
const worldMatrix = matrix4.multiply(/* ... */);
twgl.setUniforms(programInfo, {
u_matrix: matrix4.multiply(viewMatrix, worldMatrix),
+ u_worldMatrix: worldMatrix,
u_normalMatrix: matrix4.transpose(matrix4.inverse(worldMatrix)),
u_diffuse: [0, 0, 0],
u_texture: textures.steel,
});
// ...
}
}
应该不难想像,面上某个点的『表面到相机方向』是三角形顶点到相机方向的中间值,符合 Day 5 的 varying 特性,也就是说我们可以让这个方向由顶点计算出来,用 varying 传送给 fragment shader,fragment shader 收到的表面到相机方向就会是平滑补间後的结果,笔者把这个方向叫做 v_surfaceToViewer
。有了 u_worldMatrix
以及 u_worldViewerPosition
,计算出 v_surfaceToViewer
:
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;
varying vec2 v_texcoord;
varying vec3 v_normal;
+varying vec3 v_surfaceToViewer;
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;
}
照着上方所说得来实做,并让结果 specular
直接加在颜色的所有 channel 上:
precision highp float;
uniform vec3 u_diffuse;
uniform sampler2D u_texture;
uniform vec3 u_lightDir;
varying vec2 v_texcoord;
varying vec3 v_normal;
+varying vec3 v_surfaceToViewer;
void main() {
vec3 diffuse = u_diffuse + texture2D(u_texture, v_texcoord).rgb;
vec3 normal = normalize(v_normal);
vec3 surfaceToLightDir = normalize(-u_lightDir);
float diffuseLight = clamp(dot(surfaceToLightDir, normal), 0.0, 1.0);
- gl_FragColor = vec4(diffuse * diffuseLight, 1);
+
+ vec3 surfaceToViewerDirection = normalize(v_surfaceToViewer);
+ vec3 halfVector = normalize(surfaceToLightDir + surfaceToViewerDirection);
+ float specularBrightness = dot(halfVector, normal);
+
+ gl_FragColor = vec4(
+ diffuse * diffuseLight + specularBrightness,
+ 1
+ );
}
先是把 v_surfaceToViewer
转成单位向量,而 surfaceToLightDir
在先前已经有算出来为单位向量,两者长度皆为 1
,加在一起除以二可以得到『中间向量』,但是之後也得转换成为单位向量,除以二的步骤就可以省略,因此平均向量 halfVector
这样算:normalize(surfaceToLightDir + surfaceToViewerDirection)
,最後与法向量做内积
如果存档去看渲染结果,看起来像是这样:
显然跟目标撷图不一样,为什麽呢?想想看,如果 halfVector
与法向量相差 60 度,那麽我们做完内积之後,可以获得 0.5 的 specular,这样的反射范围显然太大,我们希望内积之後的值非常接近 1 才能让 specular 有值,再套上 n 次方可以做到这件事,假设 n 为 40,那麽线图看起来像是这样,接近 0.9 时数值才开始明显大於 0:
本图撷取自此:https://www.desmos.com/calculator/yfa2jzzejm,读者可以来这边拉左方的 n 值感受一下
其实这个 n 的值可以根据不同物件材质而有所不同,因此加上 u_specularExponent
来控制,同时也加入控制反射光颜色的 uniform,称为 u_specular
,笔者在此顺便在 state 中加入对球体、地板不同的 specularExponent:
async function setup() {
// ...
return {
// ...
state: {
// ...
+ ballSpecularExponent: 40,
+ groundSpecularExponent: 100,
},
time: 0,
}
}
并且把 uniform 传送设定好,反射光设定成白光:
function render(app) {
// ...
twgl.setUniforms(programInfo, {
u_worldViewerPosition: state.cameraPosition,
u_lightDir: state.lightDir,
+ u_specular: [1, 1, 1],
});
// ...
{ // ball
// ...
const worldMatrix = matrix4.multiply(/* ... */);
twgl.setUniforms(programInfo, {
// ...
u_diffuse: [0, 0, 0],
u_texture: textures.steel,
+ u_specularExponent: state.ballSpecularExponent,
});
// ...
}
{ // ground
// ...
const worldMatrix = matrix4.multiply(/* ... */);
twgl.setUniforms(programInfo, {
// ...
u_diffuse: [0, 0, 0],
u_texture: textures.steel,
+ u_specularExponent: state.groundSpecularExponent,
});
// ...
}
最後实做到 fragment shader 内,pow()
GLSL 有内建,同时也加上 clamp()
避免数值跑到负的:
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;
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 surfaceToViewerDirection = normalize(v_surfaceToViewer);
vec3 halfVector = normalize(surfaceToLightDir + surfaceToViewerDirection);
- float specularBrightness = dot(halfVector, normal);
+ float specularBrightness = clamp(pow(dot(halfVector, normal), u_specularExponent), 0.0, 1.0);
gl_FragColor = vec4(
- diffuse * diffuseLight + specularBrightness,
+ diffuse * diffuseLight +
+ u_specular * specularBrightness,
1
);
如果把 HTML 对於 ballSpecularExponent
, groundSpecularExponent
的控制加上,便可以动态调整反射光的区域:
本篇的完整程序码可以在这边找到:
中间件 - Middleware 嗨大家好,因为在部署 Elastic Beanstalk 上遇到了...
比起JAVA档可以直接在命令列环境下进行练习和测试,JAVA WEB的专案就一定得在服务器(serv...
传说中的 REST (表现层状态转换) 出现了!一个不太好解释的名词,但面试我还真的碰到了…初心者...
在设置关键字的时候,有些 NG 行为是不能犯的,今天就想跟大家聊聊,当我们在揣测消费者的心思时,哪些...
铁人赛学习纪录来到了第28天,今天想来做点轻松的,决定用python做一只会飞的小鸟 首先大概设计一...