大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 24 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。本章节讲述的是如何透过 framebuffer 使 WebGL 预先计算资料到 texture,并透过这些预计算的资料制作镜面、阴影效果,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容
有了 framebuffer 的帮助,我们可以动用 GPU 的力量事先运算,在正式绘制画面时使用。继镜面完成之後,根据 Day 22 所说,另一个 framebuffer 的应用是阴影,接下来就来介绍如何制作出阴影效果
阴影的产生是因为物体表面到光源之间有其他物体而被遮住,为了得知有没有被遮住,我们可以从光源出发『拍摄』一次场景,从上篇改用 twgl 时有提到 framebuffer 也可以包含深度资讯,实际绘制画面时就可以利用深度资讯来得知是否在阴影下
但是目前的光源是平行光,这样要怎麽拍摄?首先,利用 Day 11 的 Orthogonal 3D 投影,如果光线是直直往 +y 的方向与地面垂直倒是蛮容易想像的,不过如果不是的时候,那麽感觉拍摄的范围就没办法很大(淡蓝色区域为投影区域,蓝色面为成像面):
笔者想到在矩阵运算中还有一个叫做 shear,可以把一个空间中的矩形转换成平行四边形,透过这个工具,可以使得投影区域为平行四边形:
如果去看 twgl.createFramebufferInfo()
预设建立的 framebuffer 与 textures 组合,可以看到一个存放颜色的 texture,但是另一个存放深度资讯却不是 texture,是一个叫做 WebGLRenderbuffer
的东西:
经过测试,WebGLRenderbuffer
无法当成 texture 使用,为了建立能放深度资讯的 texture,需要 WebGL extension WEBGL_depth_texture
,跟 Day 16 的 VAO 功能一样,不是 WebGL spec 的一部分,幸好 WEBGL_depth_texture
在主流浏览器中都有支援,只是需要写一点程序来启用:
async function setup() {
const gl = canvas.getContext('webgl');
// ...
const webglDepthTexExt = gl.getExtension('WEBGL_depth_texture');
if (!webglDepthTexExt) {
throw new Error('Your browser does not support WebGL ext: WEBGL_depth_texture')
}
// ...
}
启用後,建立 framebuffer-texture 时便可指定 texture 的格式为 gl.DEPTH_COMPONENT
存放深度资讯,笔者将此 framebuffer-texture 命名为 lightProjection
:
async function setup() {
// ...
framebuffers.lightProjection = twgl.createFramebufferInfo(gl, [{
attachmentPoint: gl.DEPTH_ATTACHMENT,
format: gl.DEPTH_COMPONENT,
}], 2048, 2048);
textures.lightProjection = framebuffers.lightProjection.attachments[0];
// ...
}
在拍摄深度时,颜色计算就变成多余的,同时为了预览深度照片的成像,因此建立了一个简单的 fragment shader,待会会与现有的 vertexShaderSource
连结:
precision highp float;
varying float v_depth;
void main() {
gl_FragColor = vec4(v_depth, v_depth, v_depth, 1);
}
可以看到这个 fragment shader 需要 varying v_depth
,因此在 vertex shader 中输出:
+varying float v_depth;
void main() {
// ...
+ v_depth = gl_Position.z / gl_Position.w * 0.5 + 0.5;
}
因为 gl_Position.z / gl_Position.w
clip space 中的范围是 -1 ~ +1,因此 * 0.5 + 0.5
使之介於 0 ~ +1 用於颜色输出,并且使用 twgl.createProgramInfo()
建立 depthProgramInfo
:
async function setup() {
// ...
+ const depthProgramInfo = twgl.createProgramInfo(gl,
+ [vertexShaderSource, depthFragmentShaderSource]
+ );
return {
gl,
- programInfo,
+ programInfo, depthProgramInfo,
// ...
}
}
现有的光线方向向量是由 state.lightRotationXY
所控制,根据产生程序:
const lightDirection = matrix4.transformVector(
matrix4.multiply(
matrix4.yRotate(state.lightRotationXY[1]),
matrix4.xRotate(state.lightRotationXY[0]),
),
[0, -1, 0, 1],
).slice(0, 3);
光线一开始向着 -y 方向,接着旋转 x 轴 state.lightRotationXY[0]
以及 y 轴 state.lightRotationXY[1]
,场景物件放置在 xz 平面上,因此 shear 时使用的角度为旋转 x 轴的 state.lightRotationXY[0]
,整个 transform 经过以下步骤:
matrix4.projection()
捕捉的正面看着 +z,需要先旋转使之看着 -y,接着旋转 y 轴 state.lightRotationXY[1]
,这两个转换就是 Day 13 的视角 transform,需要做反矩阵matrix4.projection()
捕捉的正面看着 +z,依据角度偏移 y 值:y' = y + z * tan(state.lightRotationXY[0])
matrix4.projection()
进行投影,捕捉场景中 xz 介於 0 ~ 20,y (深度)介於 0 ~ 10 的物件matrix4.projection()
会把原点偏移到左上,透过 matrix4.translate(1, -1, 0)
转换回来,最後捕捉场景中 xz 介於 -10 ~ +10,y 介於 -5 ~ +5 的物件把这些 transform 通通融合进 lightProjectionViewMatrix
:
function render(app) {
// ...
const lightProjectionViewMatrix = matrix4.multiply(
matrix4.translate(1, -1, 0),
matrix4.projection(20, 20, 10),
[ // shearing
1, 0, 0, 0,
0, 1, 0, 0,
0, Math.tan(state.lightRotationXY[0]), 1, 0,
0, 0, 0, 1,
],
matrix4.inverse(
matrix4.multiply(
matrix4.yRotate(state.lightRotationXY[1]),
matrix4.xRotate(degToRad(90)),
)
),
);
// ...
}
因为现在有多个 program,得在 renderBall()
以及 renderGround()
时指定使用的 program,因此加入 programInfo
参数到这两个 function
-function renderBall(app, viewMatrix) {
- const { gl, programInfo, textures, objects } = app;
+function renderBall(app, viewMatrix, programInfo) {
+ const { gl, textures, objects } = app;
// ...
}
-function renderGround(app, viewMatrix, mirrorViewMatrix) {
- const { gl, programInfo, textures, objects } = app;
+function renderGround(app, viewMatrix, mirrorViewMatrix, programInfo) {
+ const { gl, textures, objects } = app;
// ...
}
并且修改现有渲染到画面上的流程使用 depthProgramInfo
以及 lightProjectionViewMatrix
:
function render(app) {
const {
gl,
framebuffers,
- programInfo,
+ programInfo, depthProgramInfo,
state,
} = app;
twgl.bindFramebufferInfo(gl, framebuffers.mirror);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
- renderBall(app, mirrorViewMatrix);
+ renderBall(app, mirrorViewMatrix, programInfo);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// ...
- renderBall(app, viewMatrix);
- renderGround(app, viewMatrix, mirrorViewMatrix);
+ gl.useProgram(depthProgramInfo.program);
+
+ renderBall(app, lightProjectionViewMatrix, depthProgramInfo);
+ renderGround(app, lightProjectionViewMatrix, mirrorViewMatrix, depthProgramInfo);
}
我们就获得了灰阶的深度视觉化:
至於回到正式『画』时使用这些资讯绘制阴影的部份,将在下篇继续实做,本篇的完整程序码可以在这边找到:
>>: [Day24] iT邦帮忙502 Bad Gateway怎麽办? 教你自动侦测网页修复了没!
注:发文日和截图的日期不一定是同一天,所以价格计算上和当日不同,是很正常的。 声明:这一系列文章并无...
我们先来用insertion sort algorithm来解题。 虽然他的效率也不高,但这是很好理...
长条图显示土壤湿度 教学原文参考:长条图显示土壤湿度 这篇文章会介绍如何使用土让湿度感测器,搭配「点...
前言 我在《操作基础篇 04 — 做好笔记备份 ,使用 iCloud 和 Google Drive ...
新增删除操作 # 载入pandas import pandas as pd if __name__ ...