大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 25 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。本章节讲述的是如何透过 framebuffer 使 WebGL 预先计算资料到 texture,并透过这些预计算的资料制作镜面、阴影效果,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容
在 Day 24 渲染好深度并绘制到画面上,可以看到中间一颗球的轮廓,并且在其顶部的地方颜色深度更深,表示更接近深度投影的投影面,接下来让这个拍摄深度的目标移动到 framebuffer/texture 去,并且在渲染给使用者时使用
现在开始除了镜面的 framebuffer 渲染之外又要多了光源投影,为了让渲染到不同 framebuffer 之程序能够在程序码中比较好分辨,笔者建立一个 {}
区域来表示这个区域在做光源投影:
function render(app) {
// ...
{ // lightProjection
gl.useProgram(depthProgramInfo.program);
twgl.bindFramebufferInfo(gl, framebuffers.lightProjection);
gl.clear(gl.DEPTH_BUFFER_BIT);
renderBall(app, lightProjectionViewMatrix, depthProgramInfo);
renderGround(app, lightProjectionViewMatrix, mirrorViewMatrix, depthProgramInfo);
}
// ...
}
把这个区域放置在镜面 framebuffer 渲染前,毕竟在镜面世界可以看到阴影。因为渲染到镜面世界时与正式渲染都使用主要的 programInfo
,把 gl.useProgram()
移动下来与设定全域 uniform 到此 programInfo
的 twgl.setUniforms()
放在一起,同时也把镜面世界的渲染用 {}
包起来:
function render(app) {
const {
gl,
- framebuffers,
+ framebuffers, textures,
programInfo, depthProgramInfo,
state,
} = app;
- gl.useProgram(programInfo.program);
-
const lightProjectionViewMatrix = matrix4.multiply( /* ... */)
// ...
{ // lightProjection
// ...
}
+ gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {
u_worldViewerPosition: cameraMatrix.slice(12, 15),
u_lightDirection: lightDirection,
u_ambient: [0.4, 0.4, 0.4],
});
- twgl.bindFramebufferInfo(gl, framebuffers.mirror);
- gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+ { // mirror
+ twgl.bindFramebufferInfo(gl, framebuffers.mirror);
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
- renderBall(app, mirrorViewMatrix, programInfo);
+ renderBall(app, mirrorViewMatrix, programInfo);
+ }
// ...
}
最後是让正式『画』的程序回复使用 viewMatrix
, programInfo
:
function render(app) {
// ...
{ // mirror
// ...
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.canvas.width = gl.canvas.clientWidth;
gl.canvas.height = gl.canvas.clientHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
- gl.useProgram(depthProgramInfo.program);
-
- renderBall(app, lightProjectionViewMatrix, depthProgramInfo);
- renderGround(app, lightProjectionViewMatrix, mirrorViewMatrix, depthProgramInfo);
+ renderBall(app, viewMatrix, programInfo);
+ renderGround(app, viewMatrix, mirrorViewMatrix, programInfo);
}
这麽一来深度资讯就会存在 textures.lightProjection
中,接下来请参考这张图:
经过光源投影之後,B 点上的深度来自 A 点,如果从 C 进行光源投影同样会到达 B 点的位置,但是深度将会比较深,我们可以利用这一点来检查是否在阴影下,把 C 点投影到 B 点的原理其实跟 Day 23 镜面计算 texture 位置一样,将在 fragment shader 中得到的表面位置进行 framebuffer 的 view matrix 转换,也就是 lightProjectionViewMatrix
把光源投影的 view 矩阵用名为 u_lightProjectionMatrix
的 uniform 传入,并且在 vertex shader 中 transform 成 v_lightProjection
投影後的位置:
uniform mat4 u_mirrorMatrix;
+uniform mat4 u_lightProjectionMatrix;
// ...
varying float v_depth;
+varying vec4 v_lightProjection;
void main() {
v_depth = gl_Position.z / gl_Position.w * 0.5 + 0.5;
+ v_lightProjection = u_lightProjectionMatrix * worldPosition;
}
在 fragment shader 方面,接收 u_lightProjectionMatrix
以及 v_lightProjection
,并且跟 v_mirrorTexcoord
一样要除以 .w
使之与 clip space 中的位置相同,接着需要两个深度:
v_lightProjection.z / v_lightProjection.w
计算而来的 lightToSurfaceDepth
: 表示该点(可能为 A 或是 C 点)投影下去的深度u_lightProjectionMap
查询到的值:光源投影时该点的深度,也就是 B 点上的值// ...
uniform sampler2D u_lightProjectionMap;
varying vec4 v_lightProjection;
void main() {
// ...
vec2 lightProjectionCoord =
v_lightProjection.xy / v_lightProjection.w * 0.5 + 0.5;
float lightToSurfaceDepth =
v_lightProjection.z / v_lightProjection.w * 0.5 + 0.5;
float lightProjectedDepth = texture2D(
u_lightProjectionMap,
lightProjectionCoord
).r;
}
除了 lightProjectionCoord
要 * 0.5 + 0.5
以符合 texture 上的座标范围外,v_lightProjection.z / v_lightProjection.w
在 clip space 为 -1 ~ +1,也要传换成 0 ~ +1,以符合深度 texture 『颜色』的 channel 值域。资料准备就绪,进行深度比较:
float occulusion = lightToSurfaceDepth > lightProjectedDepth ? 0.5 : 0.0;
diffuseBrightness *= 1.0 - occulusion;
specularBrightness *= 1.0 - occulusion * 2.0;
笔者使用 occulusion
表示『有多少成的光源被遮住』,并设定成在阴影下时减少 50% 的散射光亮度以及全部反射光,结果长这样:
真的该有阴影的地方是有阴影了:
不过显然阴影区域太大,而且球体上光照的区域也有一点一点的阴影,为什麽会这样呢?尽管像是上方示意图中的 A 点,光源投影下来的深度与後来重算的深度可能因为 GPU 计算过程中浮点数的微小差异而导致 lightToSurfaceDepth > lightProjectedDepth
成立,为了避免这个问体我们让 lightToSurfaceDepth
必须比 lightProjectedDepth
还要大出一定的数值才判定为有阴影,笔者让这个值为 0.01
:
void main() {
// ..
- float occulusion = lightToSurfaceDepth > lightProjectedDepth ? 0.5 : 0.0;
+ float occulusion = lightToSurfaceDepth > 0.01 + lightProjectedDepth ? 0.5 : 0.0;
}
阴影功能就完成罗:
完整的程序码可以在这边找到:
好了,花了这麽多篇介绍光线相关的效果,从散射光、反射光到镜面与阴影,这些效果加在一起可以制作出颇生动的画面,不觉得上面的画面蛮漂亮的吗?在此同时本系列技术文章也将进入尾声,下个章节将制作一个完整的场景作为完结作品:帆船与海
前言 写完一半了,洒花 你的使用的Sheet Application安全吗? 正文 概念 CSV是一...
今天来讲讲本地推播 1.先导入推播 import UserNotifications 2.先获取权限...
NPM (全称 Node Package Manager,即「node包管理器」),它是一个线上套件...
Q_Q 没学完啦 Redux-saga 范例 import { createStore, appl...
今天就用昨天讲到的CRUD搭配RESTful API做出一个留言板,首先先来定义一下留言板会需要有的...