Multiple objects (上)

大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 15 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。介绍完 WebGL 运作方式与 2D transform 後,本章节讲述的是建构、transform 并渲染多个 3D 物件,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容

到目前的范例,画面上都只有一个物件,既然已经介绍完 3D 物件的产生、在空间中的 transform、相机控制以及 perspective 投影到画布上,接下来来让所谓『场景』比较有场景的感觉,加入一颗球体以及地板:

target-look

重构程序码使得加入多物件变得容易

之前在准备 P 字母 3D 模型资料准备的时候,分别对 a_position, a_color 制作了 attributes 资料, vertexAttribArray 以及 buffer,这些都属於 P 字母这个『物件』的内容,接下来要加入其他物件,因此建立一个 objects 来存这些物件,在 objects 下每个物件自己再有一个 Javascript object 来存放 attributes, vertexAttribArray 以及 buffer 等资讯:

async function setup() {
  // ...
  const objects = {};
  // ...
}

把原本 modelBufferArrays.attribs, modelBufferArrays.numElements, buffers 放置到 objects.pModel 内:

async function setup() {
  // ...
  { // pModel
    const { attribs, numElements } = createModelBufferArrays();

    const buffers = {};

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

    gl.enableVertexAttribArray(attributes.position);
    gl.vertexAttribPointer(attributes.position, /* ... */);

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

    // a_color
    buffers.color = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);

    gl.enableVertexAttribArray(attributes.color);
    gl.vertexAttribPointer(attributes.color, /* ... */);

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

    objects.pModel = {
      attribs, numElements,
      buffers,
    };
  }
  // ...
}

setup() / render() 改传 objects:

 async function setup() {
   // ...
   return {
     gl,
     program, attributes, uniforms,
-    buffers, modelBufferArrays,
+    objects,
     state: {/* ... */},
     time: 0,
   };
 }
 
 function render(app) {
   const {
     gl,
     program, uniforms,
-    modelBufferArrays,
+    objects,
     state,
   } = app;
 
 }

最後修改让原本使用 modelBufferArrays 的程序改从 objects 取用,并把 P 物件本身的 transform (worldMatrix) 放在专属的程序码区域:

function render(app) {
  // ...
  { // pModel
    const worldMatrix = matrix4.multiply(
      matrix4.translate(...state.translate),
      matrix4.xRotate(state.rotate[0]),
      matrix4.yRotate(state.rotate[1]),
      matrix4.zRotate(state.rotate[2]),
      matrix4.scale(...state.scale),
    );

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

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

加入纯色物件绘制功能

因为待会要加入的物件都是纯色,因此不需要传送 a_color 进去指定每个 vertex / 三角形的颜色,我们可以让 fragment shader 接收 uniform u_color 来指定整个物件的颜色:

precision highp float;

uniform vec3 u_color;

varying vec3 v_color;

void main() {
  gl_FragColor = vec4(v_color + u_color, 1);
}

这边直接让两种来源相加:v_color + u_color,因为在 a_color 没输入的时候 RGB 三个 channel 都会是 0,因此 v_color 就会是 [0, 0, 0],对於 a_color 有值的 P 物件来说,我们要做的就是在绘制 P 物件时把 u_color 设定成 [0, 0, 0];同时要记得取得 uniform 的位置:

 async function setup() {
   // ...
   const uniforms = {
     matrix: gl.getUniformLocation(program, 'u_matrix'),
+    color: gl.getUniformLocation(program, 'u_color'),
   };
   // ...
 }

最後在 render() 时给 P 物件的 u_color 设定成 [0, 0, 0]:

{ // pModel
  // ...
  gl.uniform3f(uniforms.color, 0, 0, 0);
  // ... gl.drawArrays(...)
}

TWGL: A Tiny WebGL helper Library

我们接下来要产生球体以及地板所需的 a_position,也就是每个三角形各个顶点的位置,难道我们又要写一长串程序来产生这些资料了吗?幸好网路上有大大已经帮我们写好了 -- TWGL: A Tiny WebGL helper Library

这个套件里面不仅可以产生球体、平面等物件所需的资料,同时他也是一层对 WebGL 的薄薄包装,读者们应该也有感受到 WebGL API 的冗长,像是从 Day 6 开始我们自己包装的 createShader / createProgram,到 vertex attribute, buffer 等操作都有,使得程序码可以减少不少,在套件首页上就有不少使用 WebGL API 以及 TWGL 的比较;不过本篇就先只用到 twgl.primitives 来产生球体、平面物件的资料

引入这个套件有很多方法,笔者使用 unpkg 所提供的 CDN 服务,在 ES module 中,直接引用:

import * as twgl from 'https://unpkg.com/[email protected]/dist/4.x/twgl-full.module.js';

建立球体、地板物件

首先是球体,使用 twgl.primitives.createSphereVertices(radius, subdivisionsAxis, subdivisionsHeight) 产生球体资料,第一个参数表示半径、第二三个参数表示要分成多少个区段产生顶点,分越多这个球体就越精致,我们先用这样的设定印出来看看:

console.log(
  twgl.primitives.createSphereVertices(10, 32, 32)
)

twgl.primitives.createSphereVertices

看起来 position 会是我们需要的资料,同时还有 texcoord 取用 texture 的对应位置、normal 法向量,那麽 indices 是什麽?在 WebGL 中,还有一种绘制模式 gl.drawElements() 使得绘制时透过 indices 扮演类似指标的角色去取得其指向之 vertex attribute 的值,可以避免重复的 vertex 资料,不过这个模式之後再来深入介绍,本篇不使用,如果要取得以三角形 vertex 为单位的 attribute 资料,我们需要 twgl.primitives.deindexVertices() 跟着 indices 指标取得直接资料,并且透过 position 资料长度除以 3 得到 numElements 要绘制的顶点数量,接下来 createBuffer, enableVertexAttribArray 等与之前类似:

async function setup() {
  // ...
  { // ball
    const attribs = twgl.primitives.deindexVertices(
      twgl.primitives.createSphereVertices(10, 32, 32)
    );
    const numElements = attribs.position.length / attribs.position.numComponents;

    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.ball = {
      attribs, numElements,
    };
  }
  // ...
}

准备好资料後,在 render() 加上球体的绘制:

function render(app) {
  // ...
  { // ball
    const worldMatrix = matrix4.multiply(
      matrix4.translate(300, -80, 0),
      matrix4.scale(3, 3, 3),
    );

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

    gl.uniform3f(uniforms.color, 67/255, 123/255, 208/255);

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

存档重整後,球体有出现了,但是原本的 P 物体消失,画面中还有一条不知道是什麽的东西:

only-sphere-is-drawn

为什麽呢?在 render() 分别绘制 P 物件以及球体时,只有切换了 uniform,而在 setup() 设定好的 vertex attribute 与 buffer 之间的关系显然在 render() 这边没有进行切换,事实上,在 setup() 中第二次呼叫的 gl.vertexAttribPointer() 就把 position attribute 改成与球体的 position buffer 绑定,因此最後绘制时两次 gl.drawArrays() 都是绘制球体,只是第一次绘制时候 objects.pModel.numElements 比球体顶点数少很多所以只有一小条出现

要解决这个问题,我们需要 Vertex Attribute Object 这个功能,将在下一篇继续介绍,本篇的完整程序码:


<<:  1 先设计游戏吧

>>:  IT 铁人赛 k8s 入门30天 -- day1 什麽是k8s? k8s能做到什麽?

Day 10 : PHP - 常用的阵列函数有哪些?

上篇介绍了PHP的阵列宣告、印出方式,这篇想和大家介绍PHP常用的阵列函数有哪些 1.in_arra...

[自学笔记] URL Encoding

URL Encoding(URL编码) URL 编码将字符转换为可以通过 Internet 传输的格...

Excel VBA 巨集设计问题 不同表格中VLOOKUP找资料

现在做了一个表格 内容如图下分了商品编号, 仓库号和仓库名 希望能输入商品编号後能自动带出仓库号和仓...

【C++】Data Type Size Of

这次我们要来学习资料型态在程序中的大小,亦即调查其所占的空间。 我列出一些常用的data type~...

初学者跪着学JavaScript Day17: 物件:new Set()

一日客语:中文:不好意思 客语:paiˇse! ㄆㄞˇ 厶ㄟ 1.set 是一个集合 2.集合没有索...