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 指定的统一纯色,在下个章节将加入光对於物体表面颜色的影响,使物体看起来更立体


<<:  Day1 用python写UI-前言

>>:  Day 14-制作购物车系统之安装及资料夹结构(三)

IOS Swift 什麽是Closure?不能只会func吗?

前言: 又到了发文的时间了,最近我的屁股一直长疔子真的好痛,有人知道要怎麽治标吗,一直跪着打程序也不...

Day 2:Golang 是什麽?

Golang 基本介绍: 是 Google 开发的静态编译程序语言,支援垃圾回收与并发,跟 C 的撰...

学习Python纪录Day13 - Web API、JSON

Web API Open data是一种Web API,使用HTTP请求来执行其他系统提供功能来存取...

Day24:今天我们来聊一下SQL Injection

SQL Injection是攻击者控制资料驱动的Web Application和Web最常见和最具破...

Chapter2 - Canvas动画(III)让我们跳过微积分 用轻松的方式画落叶吧

接下来终於要谈谈,让我们更轻松的物件了 其实网路上有很多相关的文章,都可以带你更深入JS时,但常常问...