大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 27 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。在本系列文的最後章节将制作一个完整的场景作为完结作品:帆船与海,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容
.obj
取代球体Day 26 经过套件帮忙读取并准备好 app.objects.boat
,为绘制到画面上的部份建立一个 renderBoat
function:
function renderBoat(app, viewMatrix, programInfo) {
const { gl, textures, objects } = app;
const worldMatrix = matrix4.multiply(
matrix4.yRotate(degToRad(45)),
matrix4.translate(0, 0, 0),
matrix4.scale(1, 1, 1),
);
twgl.setUniforms(programInfo, {
u_matrix: matrix4.multiply(viewMatrix, worldMatrix),
u_worldMatrix: worldMatrix,
u_normalMatrix: matrix4.transpose(matrix4.inverse(worldMatrix)),
u_normalMap: textures.nilNormal,
});
objects.boat.forEach(({ bufferInfo, vao, uniforms }) => {
gl.bindVertexArray(vao);
twgl.setUniforms(programInfo, uniforms);
twgl.drawBufferInfo(gl, bufferInfo);
});
}
worldMatrix
与之前的物件差不多,用来决定整体物件位置的 transform,其中 matrix4.yRotate(degToRad(45))
让帆船可以稍微转一下不要屁股面对使用者twgl.setUniforms
设定帆船所有子物件共用的 uniformsobjects.boat.forEach()
把所有子物件绘制出来,在 loadBoatModel()
就为每个子物件整理好 bufferInfo
, vao
, uniforms
,只要把 VAO 工作区域切换好、设定个别子物件的 uniform(材质设定)便可以进行『画』的动作最後把 renderBall()
替换成 renderBoat()
:
function render(app) {
// ...
{ // lightProjection
gl.useProgram(depthProgramInfo.program);
twgl.bindFramebufferInfo(gl, framebuffers.lightProjection);
gl.clear(gl.DEPTH_BUFFER_BIT);
- renderBall(app, lightProjectionViewMatrix, depthProgramInfo);
+ renderBoat(app, lightProjectionViewMatrix, depthProgramInfo);
renderOcean(app, lightProjectionViewMatrix, reflectionMatrix, depthProgramInfo);
}
{ // reflection
twgl.bindFramebufferInfo(gl, framebuffers.reflection);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
- renderBall(app, reflectionMatrix, programInfo);
+ renderBoat(app, reflectionMatrix, programInfo);
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// ...
- renderBall(app, viewMatrix, programInfo);
+ renderBoat(app, viewMatrix, programInfo);
gl.useProgram(oceanProgramInfo.program);
twgl.setUniforms(oceanProgramInfo, globalUniforms);
renderOcean(app, viewMatrix, reflectionMatrix, oceanProgramInfo);
}
新的主角 -- 帆船就出现在画面中罗:
笔者也把原本球体相关的程序移除,避免读取不必要的档案,程序码在此:
在上个章节中实做好的阴影、反射效果,配合海面的 normal map 以及 distortion,只要在 lightProjection
以及 reflection
执行船体的渲染,帆船的水面倒影、阴影就完成了,这麽一来整个场景已经可以看得出来是一艘帆船在海上罗;但是如果把视角调低,就可以立刻看到我们缺少的东西:天空
3D 场景中的天空事实上只是一个背景,但是要符合视角方向,因此这个背景就成了一张 360 度的照片,类似於 Google 街景那样,在 WebGL 中要做出这样效果可以透过 gl.TEXTURE_CUBE_MAP
的 texture 来做到;我们常用的 texture 形式是 gl.TEXTURE_2D
,在 shader 中以 texture2D()
传入 2D 平面上的位置来取样,使用 gl.TEXTURE_CUBE_MAP
的 texture 时,要给他 6 张图,分别为 +x, -x, +y, -y, +z, -z,贴在下图立方体的 6 个面,在 shader 中使用 textureCube()
并传入三维向量(理论上也应该是单位向量),这个向量称为 normal 法向量,类似於一颗球体表面上某个位置的法向量,取样的结果将是从立方体正中间往该向量方向出发,其延伸的线与面相交的点的颜色
若将 Va
传入 textureCube()
,会取样到 +x 图的正中央,Vb
的话会取样到 -y 图的正中央,Vc
的话会取样到 +y 图的中间偏左。这样一来这个天空就像是一个盒子一样,因此这样的效果叫做 skybox
笔者在 opengameart.org 找到 Sky Box - Sunny Day 作为接下来实做 skybox 的素材,把图贴在文章实在太占空间,读者可以点击这个连结来看:https://imgur.com/a/8EE6sl2
使用 WebGL API 建立、载入 texture 图片的方法与 Day 6 的 2D texture 差在两个地方:
gl.bindTexture()
时目标为 gl.TEXTURE_CUBE_MAP
而非 gl.TEXTURE_2D
gl.texImage2D()
需要呼叫 6 次,把图片分别输入到 +x, -x, +y, -y, +z, -z,直接写下去程序码会很长:async function setup() {
// const textures = ...
{
const images = await Promise.all([
'https://i.imgur.com/vYEUTTe.png',
'https://i.imgur.com/CQYYFPo.png',
'https://i.imgur.com/Ol4h1f1.png',
'https://i.imgur.com/qYV0zv9.png',
'https://i.imgur.com/uapdS7d.png',
'https://i.imgur.com/MPL3hRV.png',
].map(loadImage));
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
gl.texImage2D(
gl.TEXTURE_CUBE_MAP_POSITIVE_X,
/* level: */ 0, /* internalFormat: */ gl.RGBA, /* format: */ gl.RGBA, /* type: */ gl.UNSIGNED_BYTE,
images[0],
);
gl.texImage2D(
gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
/* level: */ 0, /* internalFormat: */ gl.RGBA, /* format: */ gl.RGBA, /* type: */ gl.UNSIGNED_BYTE,
images[1],
);
gl.texImage2D(
gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
/* level: */ 0, /* internalFormat: */ gl.RGBA, /* format: */ gl.RGBA, /* type: */ gl.UNSIGNED_BYTE,
images[2],
);
gl.texImage2D(
gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
/* level: */ 0, /* internalFormat: */ gl.RGBA, /* format: */ gl.RGBA, /* type: */ gl.UNSIGNED_BYTE,
images[3],
);
gl.texImage2D(
gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
/* level: */ 0, /* internalFormat: */ gl.RGBA, /* format: */ gl.RGBA, /* type: */ gl.UNSIGNED_BYTE,
images[4],
);
gl.texImage2D(
gl.TEXTURE_CUBE_MAP_NEGATIVE_Z,
/* level: */ 0, /* internalFormat: */ gl.RGBA, /* format: */ gl.RGBA, /* type: */ gl.UNSIGNED_BYTE,
images[5],
);
gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
textures.skybox = texture;
}
// ...
}
这边的
loadImage
为lib/utils.js
的图片读取 async function
看一下 twgl.createTextures()
的文件,可以看到它也可以帮忙载入 texture cube map,既然现有实做中已经使用这个 function 载入 app.textures
,那麽只要加上这几行就相当於上面这 48 行程序码了:
async function setup() {
const textures = twgl.createTextures(gl, {
// ...
nil: { src: [0, 0, 0, 255] },
nilNormal: { src: [127, 127, 255, 255] },
+ skybox: {
+ target: gl.TEXTURE_CUBE_MAP,
+ src: [
+ 'https://i.imgur.com/vYEUTTe.png',
+ 'https://i.imgur.com/CQYYFPo.png',
+ 'https://i.imgur.com/Ol4h1f1.png',
+ 'https://i.imgur.com/qYV0zv9.png',
+ 'https://i.imgur.com/uapdS7d.png',
+ 'https://i.imgur.com/MPL3hRV.png',
+ ],
+ crossOrigin: true,
+ },
});
// ...
}
读取 6 张图并建立好 cube texture 後,可以在开发者工具的 Network tab 中看到六张图
Cube texture 是准备好了,但是如果要制作出 skybox 效果,还得要为 skybox 建立属於他的 shader, bufferInfo, VAO,同时还得依据视角产生正确的方向向量以进行 textureCube()
从立方体的面上进行取样,这些工作就留到下一篇继续讨论,到这边的进度也就只是上面几行:
<<: DAY 15 Big Data 5Vs – Variety(速度) Glue(3) Glue Studio
>>: Unity自主学习(十三):认识Unity介面(4)
在过去 MySQL 是一款免费开源的关联式资料库,在众多的中小型专案中做为资料库使用,在 2009 ...
我们这次来继续做图表,让网页更漂亮。 此篇参考: 点我 折线图 这是折线图的基本架构,元件名称叫li...
学习进度 JAVA常见错误(Error、Exception) 例外处理(throws、try cat...
在前公司时,有一天,同事愁眉苦脸的来找我: 「学长,PM 跑来问我 A 同事的东西什麽时候会搞定,Q...
DBABootcamp 相信大家可以在网路上找到许多文章说明如何在 AlwaysOn 可用性群组 (...