iT邦幫忙

2021 iThome 鐵人賽

DAY 27
1
Modern Web

如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起系列 第 27

.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() 從立方體的面上進行取樣,這些工作就留到下一篇繼續討論,到這邊的進度也就只是上面幾行:


上一篇
3D 物件檔案 — .obj
下一篇
繪製 Skybox
系列文
如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言