大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 22 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容
在上个章节的最後我们不仅有散射光、反射光,还有使得物体表面有更多凹凸细节的 normal map,笔者从这个实做成果再进行修改,材质改用 Commission - Medieval 的 scale
,并且加上环境光 u_ambient
使物体有一个最低亮度,最後让相机操作更加完整:透过拖曳平移视角,使用滑鼠右键、滚轮或是多指手势可对视角进行缩放、转动。这个章节便从这个进度作为起始点
05-framebuffer-shadow.html
/ 05-framebuffer-shadow.js
完整程序码可以在这边找到:
可以发现点光源改回平行光,而且会随着时间改变方向,然後地板变成全黑的了,因为这是本章接下来要实做的
简单来说,这是一个 WebGL 渲染的目标,本系列文到这边渲染的目标皆为 <canvas />
元件,画给使用者看的,而 framebuffer 可以改变这件事,其中一个选项是使之渲染到 texture 上
为什麽要渲染到 texture 上呢?假设今天有一面镜子,镜子上所看到的图像,等同於从镜子中的相机看回原本世界,因此可以先从镜子内绘制一次场景到一个 texture 上,接着绘制镜子时就可以拿此 texture 来绘制;甚至感觉比较没有关联的阴影效果也需要透过 framebuffer 的功能,事先请 GPU 做一些运算,在正式『画』的时候使用
在实做镜面或是阴影之前,先来专注在 framebuffer 这个功能上,毕竟想想也知道镜子、阴影需要的不会只是 framebuffer,还需要一些能够让物件位置成像能对得起来的方法,因此本篇的目标是:渲染到 texture 上,接着渲染地板时使用该 texture,效果上来说像是把画面上的球体变到黑色地板中
首先在 setup()
中建立 framebuffer,并且把目标对准(bind)新建立的 framebuffer:
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
同时也建立好 texture 作为 framebuffer 渲染的目标,笔者先命名为 fb
,framebuffer 的缩写:
textures.fb = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, textures.fb);
const width = 2048;
const height = 2048;
gl.texImage2D(
gl.TEXTURE_2D,
0, // level
gl.RGBA, // internalFormat
width,
height,
0, // border
gl.RGBA, // format
gl.UNSIGNED_BYTE, // type
null, // data
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
可以看到建立了一个 2048x2048 大小的 texture,并且传 null
让资料留白,同时也得关闭 mipmap 功能,毕竟渲染到 texture 上之後,如果还要呼叫 gl.generateMipmap()
计算缩图就太浪费资源了,有需要的话可以回去参考 Day 7 的讲解
然後是建立『framebuffer 与 texture 的关联』:
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0, // attachment
gl.TEXTURE_2D,
textures.fb,
0, // level
);
呼叫 gl.framebufferTexture2D()
使得当下对准的 framebuffer 的一个 attachment 对准指定的 texture,因为我们现在关心的是颜色,gl.COLOR_ATTACHMENT0
使得渲染到 framebuffer 时,『颜色(gl_FragColor
)』部份会写入,最後 level
表示要写入 mipmap 的哪一层
建立完成後,在 app
下加入 framebuffers
物件来存放建立好的 framebuffer:
async function setup() {
// ...
+ const framebuffers = {}
{
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// ...
+ framebuffers.fb = {
+ framebuffer, width, height,
+ };
}
return {
gl,
programInfo,
- textures, objects,
+ textures, framebuffers, objects,
state: {
fieldOfView: degToRad(45),
cameraRotationXY: [degToRad(-45), 0],
cameraDistance: 15,
cameraViewing: [0, 0, 0],
cameraViewingVelocity: [0, 0, 0],
lightRotationXY: [0, 0],
},
time: 0,
};
}
如果接下来会需要先渲染到 framebuffer,再渲染到画面,那麽可以想见某些物体会需要绘制两次,为了避免重复程序码,笔者把标注 ball
, ground
的花括弧 {}
区域独立成两个 function:
function renderBall(app, viewMatrix)
function renderGround(app, viewMatrix)
准备完成後,在 render()
设定好全域 uniform 之後,呼叫 gl.bindFramebuffer()
切换到 framebuffer, 像这样渲染到 framebuffer 并写入 textures.fb
:
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers.fb.framebuffer);
gl.viewport(0, 0, framebuffers.fb.width, framebuffers.fb.height);
renderBall(app, viewMatrix);
要记得使用 gl.viewport()
设定渲染长宽跟 texture 一样大,接着就跟原本渲染到画面上一样,因此直接呼叫 renderBall()
渲染球体
那麽要怎麽让渲染目标切换回 <canvas />
呢?呼叫 gl.bindFramebuffer()
并传入 null
即可,不过一样要记得把渲染长宽设定好:
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);
renderGround(app, viewMatrix);
呼叫 renderGround()
渲染地板的同时,设定其 uniform 时在 u_diffuseMap
填上 textures.fb
使地板显示在 framebuffer 时渲染的样子:
function renderGround(app, viewMatrix) {
// ...
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_diffuseMap: textures.nil,
+ u_diffuseMap: textures.fb,
u_normalMap: textures.nilNormal,
u_specular: [1, 1, 1],
u_specularExponent: 200,
u_emissive: [0, 0, 0],
});
twgl.drawBufferInfo(gl, objects.ground.bufferInfo);
}
存档重整,转一下的确看得出来球体渲染在一幅画上的感觉,但是平移会看到残影:
有残影是因为上一次渲染到 texture 的东西不会被自动清除,因此透过 Day 1 的油漆工具清除 framebuffer-texture:
async function setup() {
// ...
gl.enable(gl.CULL_FACE);
gl.enable(gl.DEPTH_TEST);
+ gl.clearColor(1, 1, 1, 1);
return {
// ...
};
}
function render(app) {
// ...
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers.fb.framebuffer);
gl.viewport(0, 0, framebuffers.fb.width, framebuffers.fb.height);
+ gl.clear(gl.COLOR_BUFFER_BIT);
renderBall(app, viewMatrix);
}
虽然清除的颜色是白色,但是因为光线方向会移动导致散射(textures.fb
设定在 u_diffuseMap
上)亮度下降,除此之外球体成功透过 framebuffer 渲染到 texture,并绘制在平面上了,完整程序码可以在这边找到:
>>: Proxmox VE 安装虚拟机:Windows 10 (二)
今天大概会聊到的范围 rememberUpdateState 上一篇聊到,SideEffect 周...
前情提要: 看完记忆体储存差异,现在要来谈谈全域污染这件事。 基本scope概念 所谓的范畴Scop...
从入职到现在也帮助部门开发了许多的平台和应用服务,而原本虚拟主机的签约也快到期了,然而前阵子除了接触...
变数的世界 在C#世界中,基本上大家都希望有自己的归属感,所以他们都会被赋予一个的种族, 这也是所谓...
这个章节我们来介绍怎麽安装 Tailwind,和使用 Tailwind 我们一样使用 create-...