iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Modern Web

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

Multiple objects (下)

大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 16 篇文章。本系列文章從 WebGL 基本運作機制以及使用的原理開始介紹,最後建構出繪製 3D、光影效果之網頁。介紹完 WebGL 運作方式與 2D transform 後,本章節講述的是建構、transform 並渲染多個 3D 物件,如果在閱讀本文時覺得有什麼未知的東西被當成已知的,可能可以在前面的文章中找到相關的內容

在上一篇我們輸入了兩組 3D 物件的資料,但是最後因為沒有改變 vertex attribute 使用的 buffer 導致繪製了不符合預期的結果,要能讓 vertex attribute 與 buffer 的關聯快速做切換,我們需要 OES_vertex_array_object 這個 WebGL extension

Vertex Attribute Object (VAO)

回想一下 Day 3 把 buffer 與 vertex attribute 建立關係的部份,也就是下圖紅色框起來的區域

vertex-attrib-pointer

經過 gl.bindBuffer() 指定對準的 buffer,接著呼叫 gl.vertexAttribPointer() 來指定 vertex 的 attribute 使用對準好的 buffer,也就是說在不使用 vertex attribute object (接下來簡稱 VAO)的情況下,我們其實也可以在每次 gl.drawArrays() 之前更換 vertex attribute 使用的 buffer,但是對於每一個 attribute 就要執行一次 gl.bindBuffer() 以及 gl.vertexAttribPointer(),如果我們有兩個或甚至更多 attribute 的時候,除了程式碼更複雜之外,多餘的 GPU call 也會讓性能下降

因此,VAO 就來拯救我們了,透過建立一個 VAO,我們會獲得一個『工作空間』,在這個工作空間建立好 vertex attribute 與 buffer 的關聯,接著切換到其他 VAO/工作空間 指定 attribute-buffer 時,不會影響到原本 VAO/工作空間 attribute-buffer 的關聯,要執行繪製時再切換回原本的 VAO 進行繪製;假設我們有兩個物件分別叫做 Obj 1, Obj 2,有兩個 attribute AB,那麼 VAO 使用下來會像是這樣:

vertex-attrib-object

啟用 VAO 功能

這個功能屬於 WebGL extension,不過不是指要從 Chrome web store 或是 Firefox Add-ons 下載的瀏覽器擴充套件,比較像是 WebGL spec 上沒有指定要支援,但是各家瀏覽器可以自行加入的功能,所以得看各家瀏覽器的臉色來決定特定功能能不能用,幸好 OES_vertex_array_object 相容性相當不錯,為了啟用此 WebGL extension,在 canvas.getContext('webgl'); 之後放上這些程式:

// after canvas.getContext('webgl');
const oesVaoExt = gl.getExtension('OES_vertex_array_object');
if (oesVaoExt) {
  gl.createVertexArray = (...args) => oesVaoExt.createVertexArrayOES(...args);
  gl.deleteVertexArray = (...args) => oesVaoExt.deleteVertexArrayOES(...args);
  gl.isVertexArray = (...args) => oesVaoExt.isVertexArrayOES(...args);
  gl.bindVertexArray = (...args) => oesVaoExt.bindVertexArrayOES(...args);
} else {
  throw new Error('Your browser does not support WebGL ext: OES_vertex_array_object')
}

gl.getExtension() 取得 WebGL extension,經過 if 檢查沒問題有東西的話,在 gl 物件上直接建立對應的 function 方便之後操作

事實上,這是模仿 WebGL2 的 API,在 WebGL2 中,vertex attribute object 的功能變成 spec 的一部分

建立並使用『工作空間』

setup() 時,要分別為 P 物件以及球體建立並切換到各自的『工作空間』,再進行 buffer 與 attribute 的綁定,同時也把建立的 vao 放入該物件的 js object 中:

async function setup() {
  // ...
  { // pModel
    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);

    // gl.bindBuffer, gl.vertexAttribPointer ...

    objects.pModel = {
      attribs, numElements,
      vao, buffers,
    }
  }
  // ...
  { // ball
    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);

    // gl.bindBuffer, gl.vertexAttribPointer ...

    objects.ball = {
      attribs, numElements,
      vao, buffers,
    };
  }
  // ...
}

render() 繪製物件之前先切到對應的 VAO,那麼之前設定好的 attribute-buffer 關聯就跟著回來了:

function render(app) {
  // ...
  { // pModel
    gl.bindVertexArray(objects.pModel.vao);

    // gl.drawArrays()...
  }
  // ...
  { // ball
    gl.bindVertexArray(objects.ball.vao);

    // gl.drawArrays()...
  }
}

存檔重整,P 物件與球體都正常的畫出來囉:

p-and-sphere-both-rendered

補上地板

地板只是一個 plane,也就是 2 個三角形、6 個頂點即可做出來,手刻 a_position 的 buffer 資料並不是難事,不過筆者這邊透過上篇 import 進來的 twgl 幫忙,使用 twgl.primitives.createPlaneVertices 建立 xz 平面,長寬都給 1,大小再透過 transform 調整,並且記得加上 VAO 的建立與切換,剩下的程式碼就跟球體那邊差不多依樣畫葫蘆:

async function setup() {
  // ...
  { // ground
    const attribs = twgl.primitives.deindexVertices(
      twgl.primitives.createPlaneVertices(1, 1)
    );
    const numElements = attribs.position.length / attribs.position.numComponents;
    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);

    const buffers = {};

    // a_position
    buffers.position = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);

    gl.enableVertexAttribArray(attributes.position);
    gl.vertexAttribPointer(
      attributes.position,
      3, // size
      gl.FLOAT, // type
      false, // normalize
      0, // stride
      0, // offset
    );

    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array(attribs.position),
      gl.STATIC_DRAW,
    );

    objects.ground = {
      attribs, numElements,
      vao, buffers,
    };
  }
  // ...
}

渲染部份也是:

function render(app) {
  // ...
  { // ground
    gl.bindVertexArray(objects.ground.vao);

    const worldMatrix = matrix4.multiply(
      matrix4.translate(250, -100, -50),
      matrix4.scale(500, 1, 500),
    );

    gl.uniformMatrix4fv(
      uniforms.matrix,
      false,
      matrix4.multiply(viewMatrix, worldMatrix),
    );

    gl.uniform3f(uniforms.color, 0.5, 0.5, 0.5);

    gl.drawArrays(gl.TRIANGLES, 0, objects.ground.numElements);
  }
  // ...
}

存檔重整,就得到 P 物件、球體加上地板,開始有場景的感覺了:

finished-objects

本篇的完整程式碼可以在這邊找到:

3D & 多個物件(Objects)就到這邊,讀者可以嘗試移動視角感受一下這個 3D 場景,不知道有沒有覺得球體感覺很不立體,因為我們在這個物件渲染時不論從哪邊看,每個面都是 uniform 指定的統一純色,在下個章節將加入光對於物體表面顏色的影響,使物體看起來更立體


上一篇
Multiple objects (上)
下一篇
Normals & Lighting
系列文
如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起30

尚未有邦友留言

立即登入留言