.obj 之绘制 & Skybox

大家好,我是西瓜,你现在看到的是 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 设定帆船所有子物件共用的 uniforms
  • objects.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);
 }

新的主角 -- 帆船就出现在画面中罗:

boat

笔者也把原本球体相关的程序移除,避免读取不必要的档案,程序码在此:

在上个章节中实做好的阴影、反射效果,配合海面的 normal map 以及 distortion,只要在 lightProjection 以及 reflection 执行船体的渲染,帆船的水面倒影、阴影就完成了,这麽一来整个场景已经可以看得出来是一艘帆船在海上罗;但是如果把视角调低,就可以立刻看到我们缺少的东西:天空

blank-sky

Skybox

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 法向量,类似於一颗球体表面上某个位置的法向量,取样的结果将是从立方体正中间往该向量方向出发,其延伸的线与面相交的点的颜色

cube-map

若将 Va 传入 textureCube(),会取样到 +x 图的正中央,Vb 的话会取样到 -y 图的正中央,Vc 的话会取样到 +y 图的中间偏左。这样一来这个天空就像是一个盒子一样,因此这样的效果叫做 skybox

读取图档并建立 texture cube map

笔者在 opengameart.org 找到 Sky Box - Sunny Day 作为接下来实做 skybox 的素材,把图贴在文章实在太占空间,读者可以点击这个连结来看:https://imgur.com/a/8EE6sl2

使用 WebGL API 建立、载入 texture 图片的方法与 Day 6 的 2D texture 差在两个地方:

  1. gl.bindTexture() 时目标为 gl.TEXTURE_CUBE_MAP 而非 gl.TEXTURE_2D
  2. 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;
  }
  // ...
}

这边的 loadImagelib/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 中看到六张图

images-loaded-devtools-network

Cube texture 是准备好了,但是如果要制作出 skybox 效果,还得要为 skybox 建立属於他的 shader, bufferInfo, VAO,同时还得依据视角产生正确的方向向量以进行 textureCube() 从立方体的面上进行取样,这些工作就留到下一篇继续讨论,到这边的进度也就只是上面几行:


<<:  DAY 15 Big Data 5Vs – Variety(速度) Glue(3) Glue Studio

>>:  Unity自主学习(十三):认识Unity介面(4)

Windows Server 安装 MySQL Community 免费社群版

在过去 MySQL 是一款免费开源的关联式资料库,在众多的中小型专案中做为资料库使用,在 2009 ...

[DAY29]vue dr-vue-echarts 图表套件(下)

我们这次来继续做图表,让网页更漂亮。 此篇参考: 点我 折线图 这是折线图的基本架构,元件名称叫li...

进击的软件工程师之路-软件战斗营 第七周

学习进度 JAVA常见错误(Error、Exception) 例外处理(throws、try cat...

[Day05] 团队无杂事,只有混乱的讯息流

在前公司时,有一天,同事愁眉苦脸的来找我: 「学长,PM 跑来问我 A 同事的东西什麽时候会搞定,Q...

如何在 SQL Server AOAG 设定环境之下, 套用修补程序 (patching)?

DBABootcamp 相信大家可以在网路上找到许多文章说明如何在 AlwaysOn 可用性群组 (...