大家好,我是西瓜,你现在看到的是 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)等等
本篇完整的程序码可以在这边找到:
<<: [2021铁人赛 Day05] General Skills 02
GitHub Repo https://github.com/b2etw/Spring-Kotlin...
Canonical 是什麽 图片来源:https://www.samunderwood.co.uk/...
在python3中,想要制作日历有两种方式,先介绍第一种: -直接使用python中calendar...
Hello, 各位 iT邦帮忙 的粉丝们大家好~~~ 本篇是 Re: 从零开始用 Xamarin 技...
简介 vue原生元件,可达到cache目的。 使元件状态维持不变,不重走生命周期。 新增钩子 act...