第六章:进入三维世界

121 阅读19分钟

衔接上一篇:第五章:颜色与纹理

之前我们的案例中都是围绕二维图形进行的,这一章我们将学习三维的世界。

收获

当你学习完下面内容后你会有如下收获:

  • 相机的概念、定义它的三要素
  • 视图矩阵的概念、作用、推导思路。
  • 可视范围的概念、正射投影透视投影的概念、特点与使用。
  • 如何通过隐藏面消除和多边形偏移来处理物体的前后关系。
  • 如何绘制出立方体以及给每个面指定颜色。

1.相机

首先让我们看看下面这张图,这张图是我从正面沿着负Z轴方向看去呈现在我视网膜上的景象,看上去是一个2D的平面。

4d66423646fdc1fdd5b4789b3a175cf.jpg

当我向右边移动,然后再看这个物体时呈现在我视网膜上的景象是下面这样的,实际上是一个3D的盒子。

67f39e7dfade36748608fd2f01accfe.jpg

这个盒子之所以从我们以为的2D变成真实的3D的过程,是因为我(观察者)的视角在不断的向右上角发生偏移。

想象一下,在一个虚拟的三维世界里,有许多物体,如立方体、球体和模型。如果没有一个观察者,这些物体只是存在于虚拟空间中,我们无法看到它们。

所以我们需要一个观察者(相机)在WebGL中充当了我们的眼睛。它决定了我们在场景中的位置、角度和视角。通过设置观察者的位置和方向,我们可以决定我们从哪个角度看场景,从而影响到渲染出来的图像。这让我们能够从不同的视角观察虚拟场景,就像在现实世界中改变我们的位置一样。

1.1 相机的定义

在计算机图形学中,相机是一个模拟现实世界中摄像机的概念,用于控制和模拟三维场景的观察和渲染。相机定义了观察者在场景中的位置、朝向以及如何将三维场景投影到二维屏幕上。通过调整相机的参数,可以控制渲染出来的图像的视角、透视效果和观察角度,从而实现各种视觉效果和交互体验。

相机的定义一般包括以下要素:

  1. 视点:观察者所在的位置。
  2. 视线:从视点出发沿着观察方向的射线。
  3. 上方向:最终绘制在屏幕上的影像中的向上的方向。

image.png

可能这时候你会好奇上方向是干嘛的?有视点和视线不就够了吗?之类的问题。这是因为你疏漏了观察者还可能以视线为轴旋转,如下图所示(头部发生偏移导致观察到的场景也偏移了)。所以,为了将观察者固定住,视点、视线、上方向缺一不可

image.png

2.视图矩阵

2.1 理解视图矩阵

WebGL中,我们可以用上述三个矢量创建一个视图矩阵,然后将该矩阵传给顶点着色器。视图矩阵可以表示观察者的状态,含有观察者的视点、观察目标点、上方向等信息。之所以被称为视图矩阵,是因为它最终影响了显示在屏幕上的视图,也就是观察者观察到的场景。

其实视图矩阵也分为平移、旋转、缩放这几种。假设现在WebGL场景中有一个几何体,现在我想看到这个几何体右侧30°的样子。按之前的惯例,我们会给几何体乘上一个顺时针旋转30°的旋转矩阵来完成。其实我们可以不转动目标而是把相机逆时针旋转30°也可以实现同样的效果。

IMG_20230815_235800.jpg

2.2 推导视图矩阵

视图矩阵会把原本世界中的场景放到相机里,因此,我们需要基于这一点推导出基于世界坐标系的相机坐标系。结合前面提到的相机的定义要素(视点、视线、上方向)我们可以依据这些相机定义信息推导出视图矩阵来。

想要计算视图矩阵,只要让其满足以下条件即可:

  • 将视点(至于)移至与世界坐标原点重合
  • 将视线旋转到-z轴上
  • 将上方向旋转到y轴上

2.2.1 平移相机

我们可以通过平移矩阵将视点移至与世界坐标原点重合,所以你需要先了解平移矩阵的推导过程。在此基础上我们先假设相机的坐标为(Tx,Ty,Tz),为了使其跟世界坐标系原点(0,0,0)重合。那么我们只需要将其跟自己坐标相减即可,也就是(-Tx,-Ty,-Tz)。将其代入即可得到如下矩阵。

IMG20230817145405.jpg

将相机平移到原点之后,视点就已经确定下来了。接着我们还需要将视点到目标点的视线变换为指向Z轴负半轴(0,0,-1)位置,以及将上方向变换为指向Y轴正半轴(0,1,0)的位置。为此我们还需要了解旋转矩阵的推导过程

2.2.2 旋转相机

旋转矩阵的推导非常复杂,如果你感兴趣的话可以看看这位大佬的这篇推导文章。非常详细的讲解了这个过程,下面这是最后推导出来的旋转矩阵:

IMG_20230817_172348.jpg

2.2.3 得出视图矩阵

有了上面的平移矩阵与旋转矩阵之后,只需要将平移矩阵和旋转矩阵相乘之后就能得到视图矩阵了。

IMG_20230817_193514.jpg

2.3 实现视图矩阵

:因为在代码实现的过程中还涉向量的叉积矩阵相乘等数学计算。为了我们能够将注意力集中在视图矩阵的实现步骤上,所以我们将采用Three.js的数学库来辅助运算。

我们可以编写一个函数computedViewMatrix()专门用来计算视图矩阵,而这个函数需要接收一些能够推导出基于世界坐标系的相机坐标系来的参数如下:

参数名类型说明
carmenVectorVector3视点(相机)矢量
targetVectorVector3目标矢量
upVectorVector3上方向矢量
/**
 * 计算视图矩阵
 * @param {Vector3} carmenVector 视点(相机)矢量
 * @param {Vector3} targetVector 目标矢量
 * @param {Vector3} upVector 上方向矢量
 * @returns {Matrix4} 视图矩阵
 */
const computedViewMatrix = (carmenVector, targetVector, upVector) => {
}

我们可以用目标矢量-视点矢量得到相机坐标系的Z轴

const z = new THREE.Vector3().subVectors(targetVector, carmenVector).normalize();

上面示例中subVectors()方法是用于两矢量相减,即目标矢量减视点矢量。最后将计算的结果归一化。

通过计算出来的Z轴与上方向矢量的叉积得到相机坐标系的X轴

const x = new THREE.Vector3().crossVectors(z, upVector).normalize();

上实例中crossVectors()方法用于计算两个矢量之间的叉积

最后又通过计算出来的X轴与Z轴的叉积得出相机坐标系的Y轴

const y = new THREE.Vector3().crossVectors(x, z).normalize();

结合之前推导的相机平移与旋转矩阵如下:

 // 旋转矩阵
  const rotateMatrix = new THREE.Matrix4().set(
    x.x, x.y, x.z, 0,
    y.x, y.y, y.z, 0,
    -z.x, -z.y, -z.z, 0,
    0, 0, 0, 1,
  )
  // 平移矩阵
  const translateMatrix = new THREE.Matrix4().set(
    1, 0, 0, -carmenVector.x,
    0, 1, 0, -carmenVector.y,
    0, 0, 1, -carmenVector.z,
    0, 0, 0, 1,
  )

最后将两个矩阵相乘即可得到视图矩阵。

// utils.js
/**
 * 计算视图矩阵
 * @param {Vector3} carmenVector 视点(相机)矢量
 * @param {Vector3} targetVector 目标矢量
 * @param {Vector3} upVector 上方向矢量
 * @returns {Matrix4} 视图矩阵
 */
const computedViewMatrix = (carmenVector, targetVector, upVector) => {
  const z = new THREE.Vector3().subVectors(targetVector, carmenVector).normalize();
  const x = new THREE.Vector3().crossVectors(z, upVector).normalize();
  const y = new THREE.Vector3().crossVectors(x, z).normalize();
  const rotateMatrix = new THREE.Matrix4().set(
    x.x, x.y, x.z, 0,
    y.x, y.y, y.z, 0,
    -z.x, -z.y, -z.z, 0,
    0, 0, 0, 1,
  )
  const translateMatrix = new THREE.Matrix4().set(
    1, 0, 0, -carmenVector.x,
    0, 1, 0, -carmenVector.y,
    0, 0, 1, -carmenVector.z,
    0, 0, 0, 1,
  )
  return rotateMatrix.multiply(translateMatrix).elements;
}

2.4 使用视图矩阵

绘制三个三角形

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script src="./utils.js"></script>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec4 a_Color;
      uniform mat4 u_ViewMatrix;
      varying vec4 v_Color;
      void main () {
        gl_Position = u_ViewMatrix * a_Position;
        v_Color = a_Color;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_Color;
      void main () {
        gl_FragColor = v_Color;
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;

      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');
      initShader(gl, vsSource, fsSource);

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
      const a_Color = gl.getAttribLocation(gl.program, 'a_Color');

      // prettier-ignore
      const verticeLib = [
        0.0, 0.5, -0.4, 0.4, 1.0, 0.4,// 绿色三角形在最后面-0.5,-0.5,-0.4,0.4 1.0,0.4,
        -0.5, -0.5, -0.4, 0.4, 1.0, 0.4,
        0.5, -0.5, -0.4, 1.0, 0.4, 0.4,

        0.5, 0.4, -0.2, 1.0, 0.4, 0.4,//黄色三角形在中间-0.5,0.4,-0.2,1.0,1.0,0.4,
        -0.5, 0.4, -0.2, 1.0, 1.0, 0.4,
        0.0, -0.6, -0.2, 1.0, 1.0, 0.4,

        0.0, 0.5, 0.0, 0.4, 0.4, 1.0, //蓝色三角形在最前面-0.5,-0.5,0.0,0.4 0.4 1.0,
        -0.5, -0.5, 0.0, 0.4, 0.4, 1.0,
        0.5, -0.5, 0.0, 1.0, 0.4, 0.4
      ]
      const vertices = new Float32Array(verticeLib);
      const FSIZE = vertices.BYTES_PER_ELEMENT;

      const vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
      gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_Color);

      const camera = new THREE.Vector3(0, 0, 0.1);
      const target = new THREE.Vector3(0, 0, 0);
      const up = new THREE.Vector3(0, 1, 0);

      const viewMatrix = computedViewMatrix(camera, target, up);
      gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);

      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(gl.TRIANGLES, 0, 9);
    </script>
  </body>
</html>

这里的utils.js第二章:WebGL入门这篇文章中有写到,暂时不需要我们理解里面的内容,用就好了。

image.png

然后修改相机位置,就能看到不同视角下的物体

const camera = new THREE.Vector3(0.2, 0, 0.1);
image.png
const camera = new THREE.Vector3(0, 1, 0.1);
image.png

3.使用键盘改变视点

到这里我们已经知道如何通过视图矩阵修改相机的视点、目标点和上方向了。所以接下来我们将实现通过键盘来控制视点的位置。

  • 右方向键按下:视点X轴坐标增大0.01
  • 左方向键按下:视点X轴坐标减小0.01
  • 上方向键按下:视点Y轴坐标增大0.01
  • 下方向键按下:视点Y轴坐标减小0.01

因为代码比较简单,所以就直接贴出来了。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script src="./utils.js"></script>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec4 a_Color;
      uniform mat4 u_ViewMatrix;
      varying vec4 v_Color;
      void main () {
        gl_Position = u_ViewMatrix * a_Position;
        v_Color = a_Color;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_Color;
      void main () {
        gl_FragColor = v_Color;
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;

      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');

      initShader(gl, vsSource, fsSource);

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
      const a_Color = gl.getAttribLocation(gl.program, 'a_Color');

      // prettier-ignore
      const verticeLib = [
        0.0, 0.5, -0.4, 0.4, 1.0, 0.4,// 绿色三角形在最后面-0.5,-0.5,-0.4,0.4 1.0,0.4,
        -0.5, -0.5, -0.4, 0.4, 1.0, 0.4,
        0.5, -0.5, -0.4, 1.0, 0.4, 0.4,

        0.5, 0.4, -0.2, 1.0, 0.4, 0.4,//黄色三角形在中间-0.5,0.4,-0.2,1.0,1.0,0.4,
        -0.5, 0.4, -0.2, 1.0, 1.0, 0.4,
        0.0, -0.6, -0.2, 1.0, 1.0, 0.4,

        0.0, 0.5, 0.0, 0.4, 0.4, 1.0, //蓝色三角形在最前面-0.5,-0.5,0.0,0.4 0.4 1.0,
        -0.5, -0.5, 0.0, 0.4, 0.4, 1.0,
        0.5, -0.5, 0.0, 1.0, 0.4, 0.4
      ]
      const vertices = new Float32Array(verticeLib);
      const FSIZE = vertices.BYTES_PER_ELEMENT;

      const vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
      gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_Color);

      const camera = new THREE.Vector3(0, 0, 0.1);
      const target = new THREE.Vector3(0, 0, 0);
      const up = new THREE.Vector3(0, 1, 0);

      let viewMatrix = computedViewMatrix(camera, target, up);

      window.addEventListener('keydown', (event) => {
        if (event.keyCode === 39 && camera.x <= 1) {
          // 向右
          camera.setX(camera.x + 0.01);
        } else if (event.keyCode === 37 && camera.x >= -1) {
          // 向左
          camera.setX(camera.x - 0.01);
        } else if (event.keyCode === 38 && camera.y <= 1) {
          // 向上
          camera.setY(camera.y + 0.01);
        } else if (event.keyCode === 40 && camera.y >= -1) {
          // 向下
          camera.setY(camera.y - 0.01);
        } else {
          return;
        }
        viewMatrix = computedViewMatrix(camera, target, up);
        draw(gl, 9, u_ViewMatrix, viewMatrix);
      });

      draw(gl, 9, u_ViewMatrix, viewMatrix);

      function draw(gl, n, u_ViewMatrix, viewMatrix) {
        gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);
        gl.clearColor(0, 0, 0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLES, 0, n);
      }
    </script>
  </body>
</html>

QQ录屏20230819215357.gif

4.可视范围

我们会发现当我们将视点移至接近极左或极右的位置时,三角形会有一部分缺失。如下图所示:

image.png

那为什么会出现这种情况呢?其实这是因为我们没有指定可视范围,即实际观察得到的区域边界。WebGL只显示可视范围内的区域,采用这种手段能够很好的降低程序开销。上面图片中我们能看到的部分就在可视范围内,而失去的部分到了可视范围外,所以没有绘制出来。

从某种程度上来说,这样做也模拟了人类观察物体的方式,如下图所示。我们人类也只能看到眼前的东西,水平视角大约200度左右。

image.png

除了水平和垂直范围内的限制,WebGL还限制观察者的可视深度,即“能够看多远”。所有这些限制,包括水平视角垂直视角可视深度,定义了可视空间。由于我们没有显式地指定可视空间,默认的可视深度又不够远,所以三角形的角看上去就消失了。

4.1 可视空间

有两类常用的可视空间:

  • 长方体可视空间,也称盒状空间,由正射投影产生。
  • 四棱锥/金字塔可视空间,由透视投影产生。

4.4.1 正射投影

正射投影的好处是用户可以方便地比较场景中物体的大小,因为物体看上去的大小与其所在的位置没有关系。

盒状可视空间的形状如下图所示。可视空间由前后两个矩形表面确定,分别称 近裁剪面(near clipping plane)和远裁剪面(far clipping plane)。

image.png

定义盒状可视空间

Three.js中提供了OrthographicCamera类,这个类中有个projectionMatrix属性可用来设置投影矩阵,定义盒装可视空间。

const orthographicCamera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
参数类型描述
left, rightnumber指定裁剪面的左右边界
top, bottomnumber指定近裁剪面的上下边界
near, farnumber指定近裁剪面和远裁剪面的位置

这里的projectionMatrix矩阵称为正射投影矩阵

为了证明WebGL只会绘制可视范围内中的东西,我们可以做一个测试。

  • 使用OrthographicCamera准备一个盒子可视空间,其中(near=0.0, far=0.5, left=-1.0, right=1.0, bottom=-1.0, top=1.0)。
  • 然后准备三个三角形,这三个三角形的Z轴分别是0.00.20.4
  • 通过键盘控制盒子可视空间的近裁剪面和远裁剪面,右方向键near增加0.1,左方向键near减少0.1。上方向键far增加0.1,下方向键far减少0.1。

如下图所示:

image.png

代码实现:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script src="./utils.js"></script>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec4 a_Color;
      uniform mat4 u_ProjectionMatrix;
      varying vec4 v_Color;
      void main () {
        gl_Position = u_ProjectionMatrix * a_Position;
        v_Color = a_Color;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_Color;
      void main () {
        gl_FragColor = v_Color;
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;

      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');

      initShader(gl, vsSource, fsSource);

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
      const u_ProjectionMatrix = gl.getUniformLocation(gl.program, 'u_ProjectionMatrix');

      const verticeLib = [
        0.0, 0.5, -0.4, 0.4, 1.0, 0.4,// 绿色三角形在最后面
        -0.5, -0.5, -0.4, 0.4, 1.0, 0.4,
        0.5, -0.5, -0.4, 1.0, 0.4, 0.4,

        0.5, 0.4, -0.2, 1.0, 0.4, 0.4,//黄色三角形在中间
        -0.5, 0.4, -0.2, 1.0, 1.0, 0.4,
        0.0, -0.6, -0.2, 1.0, 1.0, 0.4,

        0.0, 0.5, 0.0, 0.4, 0.4, 1.0, //蓝色三角形在最前面
        -0.5, -0.5, 0.0, 0.4, 0.4, 1.0,
        0.5, -0.5, 0.0, 1.0, 0.4, 0.4
      ]
      const vertices = new Float32Array(verticeLib);
      const FSIZE = vertices.BYTES_PER_ELEMENT;
      let near = 0.0, far = 0.5;
      const orthographicCamera = new THREE.OrthographicCamera(-1.0, 1.0, 1.0, -1.0, near, far);
      let projectionMatrix = orthographicCamera.projectionMatrix.elements;

      const vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
      gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_Color);

      window.addEventListener('keydown', (event) => {
        switch (event.keyCode) {
          case 39: orthographicCamera.near += 0.1; break;
          case 37: orthographicCamera.near -= 0.1; break;
          case 38: orthographicCamera.far += 0.1; break;
          case 40: orthographicCamera.far -= 0.1; break;
          default: return;
        }
        // 当我们修改OrthographicCamera属性时,需要调用updateProjectionMatrix来使得这些改变生效。
        orthographicCamera.updateProjectionMatrix();
        projectionMatrix = orthographicCamera.projectionMatrix.elements;
        draw(gl, 9, u_ProjectionMatrix, projectionMatrix);
      });

      draw(gl, 9, u_ProjectionMatrix, projectionMatrix);

      function draw(gl, n, location, matrix) {
        gl.uniformMatrix4fv(location, false, matrix);
        gl.clearColor(0, 0, 0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLES, 0, n);
      }
    </script>
  </body>
</html>

我们会发现当我们按一下右方向键后near0.0变到了0.1,随之处在Z0.0位置上的第一个三角就消失了。第三次按下向右方向键时near0.2变到了0.30000000000000004,随之第二个三角形也消失了。当第五次按下右方向键时near0.4变到了0.5,最后一个三角形也消失了。这是因为近横截面在不断的靠近远横截面,导致三角不处在可视范围之内了,WebGL自然也就不会进行绘制。

QQ录屏20230820155011.gif

4.4.2 透视投影

透视投影与正射投影不同的是,透视投影具有深度感。物体看上去的大小与位置有关,距离视点越远的看上去就越小。就如我们看道路两边成排的树木,越远的树看上去越矮。我们的眼睛就是这样观察世界的。

透视投影可视空间如下图所示。就像盒状可视空间那样,透视投影可视空间也有视点、视线、近裁剪面和远裁剪面,这样可视空间内的物体才会被显示,可视空间外的物体则不会显示。

image.png

定义透视投影可视空间

Three.js中提供了PerspectiveCamera类,这个类中有个projectionMatrix属性可用来设置投影矩阵,定义透视投影可视空间。

const perspectiveCamera = new THREE.PerspectiveCamera(fov, aspect, near, far);
参数类型描述
fovnumber可视空间顶面与底面间的夹角
aspectnumber指定裁截面的宽高比
near, farnumber指定近裁剪面和远裁剪面的位置

这里的projectionMatrix矩阵称为透视投影矩阵

接下来我们将创建一个fov30aspect1.0nearfar分别为1.0100.0的透视投影可视空间,并在可视空间中摆放3个大小一样的三角形。如下图所示:

image.png

代码实现:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script src="./utils.js"></script>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec4 a_Color;
      uniform mat4 u_ProjectionMatrix;
      uniform mat4 u_ViewMatrix;
      varying vec4 v_Color;
      void main () {
        gl_Position = u_ProjectionMatrix * u_ViewMatrix * a_Position;
        v_Color = a_Color;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_Color;
      void main () {
        gl_FragColor = v_Color;
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;

      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');

      initShader(gl, vsSource, fsSource);

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
      const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
      const u_ProjectionMatrix = gl.getUniformLocation(gl.program, 'u_ProjectionMatrix');

      const verticeLib = [
        0.75, 1.0, -4.0, 0.4, 1.0, 0.4,// 绿色
        0.25, -1.0, -4.0, 0.4, 1.0, 0.4,
        1.25, -1.0, -4.0, 1.0, 0.4, 0.4,

        0.75, 1.0, -2.0, 1.0, 1.0, 0.4,// 黄色
        0.25, -1.0, -2.0, 1.0, 1.0, 0.4,
        1.25, -1.0, -2.0, 1.0, 0.4, 0.4,

        0.75, 1.0, 0.0, 0.4, 0.4, 1.0,// 蓝色
        0.25, -1.0, 0.0, 0.4, 0.4, 1.0,
        1.25, -1.0, 0.0, 1.0, 0.4, 0.4
      ]
      const vertices = new Float32Array(verticeLib);
      const FSIZE = vertices.BYTES_PER_ELEMENT;
      const perspectiveCamera = new THREE.PerspectiveCamera(30, 1, 1, 100);
      const projectionMatrix = perspectiveCamera.projectionMatrix.elements;
      const camera = new THREE.Vector3(0, 0, 5);
      const target = new THREE.Vector3(0, 0, -100);
      const up = new THREE.Vector3(0, 1, 0);
      const viewMatrix = computedViewMatrix(camera, target, up);

      const vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
      gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_Color);

      draw(gl, 9, u_ProjectionMatrix, projectionMatrix);

      function draw(gl, n, u_ProjectionMatrix, projectionMatrix) {
        gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);
        gl.uniformMatrix4fv(u_ProjectionMatrix, false, projectionMatrix);

        gl.clearColor(0, 0, 0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLES, 0, n);
      }
    </script>
  </body>
</html>
image.png

我们会发现同样大小的三角形,在透视投影可视空间内距离视点远的看上去要比距离视点近的要小。

5.处理物体的前后关系

我们发现上面图中绿色三角形的一部分被黄色和蓝色三角形挡住了。如果你认为WebGL能够自动分析出三维对象的远近,并正确处理遮挡关系的话那你就错了。实际上在默认情况下,WebGL为了加速绘制操作,它会按照顶点在缓冲区中的顺序来处理它们的。

  const verticeLib = [
    0.75, 1.0, -4.0, 0.4, 1.0, 0.4,// 绿色
    0.25, -1.0, -4.0, 0.4, 1.0, 0.4,
    1.25, -1.0, -4.0, 1.0, 0.4, 0.4,

    0.75, 1.0, -2.0, 1.0, 1.0, 0.4,// 黄色
    0.25, -1.0, -2.0, 1.0, 1.0, 0.4,
    1.25, -1.0, -2.0, 1.0, 0.4, 0.4,

    0.75, 1.0, 0.0, 0.4, 0.4, 1.0,// 蓝色
    0.25, -1.0, 0.0, 0.4, 0.4, 1.0,
    1.25, -1.0, 0.0, 1.0, 0.4, 0.4
  ]

WebGL按照顶点缓冲区中的顺序(第1个是最远的绿色三角形,第2是中间的黄色三角形,第3个是最近的蓝色三角形)来进行绘制。后绘制的图形将覆盖已经绘制好的图形,所以恰好产生了近处的三角形挡住远处三角形的效果。

现在我们将缓冲区中三角形顶点数据的顺序调整一下,将近处的蓝色三角形定义在前面,然后是中间的黄色三角形,最后是远处的绿色三角形。如下所示:

  const verticeLib = [
    0.75, 1.0, 0.0, 0.4, 0.4, 1.0,// 蓝色
    0.25, -1.0, 0.0, 0.4, 0.4, 1.0,
    1.25, -1.0, 0.0, 1.0, 0.4, 0.4,

    0.75, 1.0, -2.0, 1.0, 1.0, 0.4,// 黄色
    0.25, -1.0, -2.0, 1.0, 1.0, 0.4,
    1.25, -1.0, -2.0, 1.0, 0.4, 0.4,

    0.75, 1.0, -4.0, 0.4, 1.0, 0.4,// 绿色
    0.25, -1.0, -4.0, 0.4, 1.0, 0.4,
    1.25, -1.0, -4.0, 1.0, 0.4, 0.4,
  ]

运行程序后如下图所示,你会发现本该出现在最远处的绿色三角形,却挡住了近处黄色和蓝色三角形。

image.png

这显然不是我们所期待的结果。

5.1 隐藏面消除

为了解决这个问题,WebGL提供了隐藏面消除功能。这个功能会帮助我们消除那些被遮挡的表面(隐藏面),使我们在绘制物体时不必顾虑其在缓冲区中的顺序。因为开启这个功能之后远处的物体会自动被近处的物体挡住,不会被绘制出来。

开启隐藏面消除功能,需要遵循以下两步:

  1. 开启隐藏面消除功能。
gl.enable(gl.DEPTH_TEST);

image.png

  1. 在绘制之前,清除深度缓冲区。
gl.clear(gl.DEPTH_BUFFER_BIT);

使用gl.clear()方法清除深度缓冲区。深度缓冲区是一个中间对象,其作用就是帮助WebGL进行隐藏面消除。WebGL在颜色缓冲区中绘制几何图形,绘制完成后将颜色缓冲区显示到<canvas>上。如果要将隐藏面消除,那就必须知道每个几何图形的深度信息,而深度缓冲区就是用来存储深度信息的。由于深度方向通常是Z轴方向,所以有时候我们也称它为Z缓冲区。

image.png

同时,我们还需要清除颜色缓冲区。可以使用按位或符号(|)连接gl.DEPTH_BUFFER_BITgl.COLOR_BUFFER_BIT,并作为参数传入gl.clear()

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
image.png

进行上面两个步骤之后,我们发现位于近处的三角形挡住了远处的三角形。得到了我们期待的结果。

5.2 深度冲突

隐藏面消除是WebGL的一项复杂而又强大的特性,在绝大多数情况下,它都能很好地完成任务。然而,当几何图形或物体的两个表面极为接近时,就会出现新的问题,那就是深度缓冲区有限的经度已经不能区分哪个在前,哪个在后了。这种现象被称为深度冲突

  const verticeLib = [
    0.75, 1.0, 0.0, 0.4, 0.4, 1.0,// 蓝色
    0.25, -1.0, 0.0, 0.4, 0.4, 1.0,
    1.25, -1.0, 0.0, 1.0, 0.4, 0.4,

    0.75, 1.0, 0.000001, 1.0, 1.0, 0.4,// 黄色
    0.25, -1.0,0.000001, 1.0, 1.0, 0.4,
    1.25, -1.0, 0.000001, 1.0, 0.4, 0.4,
  ]

按理说黄色的三角形的Z轴为要大于蓝色三角形的Z轴,所以黄色三角形应该在蓝色三角形前面。但是因为深度缓冲区有限的经度已经不能区分哪个在前,哪个在后了。所以在运行时就会出现蓝色三角形在黄色三角形之前的情况,如下图所示:

image.png

WebGL提供一种被称为多边形偏移的机制来解决这个问题。该机制将自动在Z值加上一个偏移量,偏移量的值由物体表面相对于观察者视线的角度来确定。启用该机制只需要两行代码:

  1. 启用多边形偏移
gl.enable(gl.POLYGON_OFFSET_FILL);
  1. 在绘制之前指定用来计算偏移量的参数
gl.polygonOffset(1.0, 1.0);

image.png

完整代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script src="./utils.js"></script>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec4 a_Color;
      uniform mat4 u_ProjectionMatrix;
      uniform mat4 u_ViewMatrix;
      varying vec4 v_Color;
      void main () {
        gl_Position = u_ProjectionMatrix * u_ViewMatrix * a_Position;
        v_Color = a_Color;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_Color;
      void main () {
        gl_FragColor = v_Color;
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;

      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');

      initShader(gl, vsSource, fsSource);

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
      const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
      const u_ProjectionMatrix = gl.getUniformLocation(gl.program, 'u_ProjectionMatrix');

      // prettier-ignore
      const verticeLib = [
        0.75, 1.0, 0.0, 0.4, 0.4, 1.0,// 蓝色
        0.25, -1.0, 0.0, 0.4, 0.4, 1.0,
        1.25, -1.0, 0.0, 1.0, 0.4, 0.4,

        0.75, 1.0, 0.000001, 1.0, 1.0, 0.4,// 黄色
        0.25, -1.0,0.000001, 1.0, 1.0, 0.4,
        1.25, -1.0, 0.000001, 1.0, 0.4, 0.4,
      ]
      const vertices = new Float32Array(verticeLib);
      const FSIZE = vertices.BYTES_PER_ELEMENT;
      // prettier-ignore
      const perspectiveCamera = new THREE.PerspectiveCamera(30, 1, 1, 100);
      const projectionMatrix = perspectiveCamera.projectionMatrix.elements;
      const camera = new THREE.Vector3(0, 0, 5);
      const target = new THREE.Vector3(0, 0, -100);
      const up = new THREE.Vector3(0, 1, 0);
      const viewMatrix = computedViewMatrix(camera, target, up);

      const vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
      gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_Color);

      draw(gl, 6, u_ProjectionMatrix, projectionMatrix);

      function draw(gl, n, u_ProjectionMatrix, projectionMatrix) {
        gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);
        gl.uniformMatrix4fv(u_ProjectionMatrix, false, projectionMatrix);

        gl.clearColor(0, 0, 0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
        // 启动多边形偏移
        gl.enable(gl.POLYGON_OFFSET_FILL);
        gl.drawArrays(gl.TRIANGLES, 0, n / 2); // 绘制绿色三角形
        gl.polygonOffset(1.0, 1.0); // 绘制多边形偏移
        gl.drawArrays(gl.TRIANGLES, n / 2, n / 2); // 绘制黄色三角形
      }
    </script>
  </body>
</html>
image.png

6.立方体

我们之前都是调用gl.drawArray()方法来进行绘制操作的。那么如何用该函数绘制出一个立方体呢?我们只能使用gl.TRIANGLESgl.TRIANGLE_STRIPg1.TRIANGLE_FAN模式来绘制三角形,那么最简单也最直接的方法就是,通过绘制两个三角形来拼成立方体的一个矩形表面,然后六个面拼成一个立方体。

image.png

像上面立方体的一个面由三角形(v0,v1,v2)和三角形(v0,v2,v3)组成,每个三角形有3个顶点,每个面需要用6个顶点。立方体一共6个面,所以一共需要6×6=36个顶点。将36个顶点的数据写入缓冲区,然后调用gl.drawArrays(gl.TRIANGLES, 0, 36)就可以绘制出正方体来。

image.png

但是我们发现立方体实际只有8个顶点,而我们却需要定义了36个顶点,这是因为每个顶点都会被多个三角形共用。那么我们是否能够只需要定义8个顶点就可以将立方体绘制出来呢?

没错,WebGL确实提供了完美的解决方案那就是使用gl.drawElements()函数代替gl.drawArrays()进行绘制,能够避免重复的定义顶点,保持顶点数量最小。但是为此我们需要知道每个三角形对应顶点在我们定义顶点列表中的索引。

我们可以将立方体拆分为顶点三角形。立方体被拆成6个面(前、后、左、右、上、下),每个面由两个三角形组成,与三角形列表中的两个三角形相关联。每个三角形都有3个顶点,与顶点列表中的3个顶点相关联,如图下图所示。三角形列表中的数字表示该三角形的3个顶点在顶点列表中的索引值。顶点列表中共有8个顶点,索引值为从0到7。

image.png

代码实现

至此我们了解了如何通过最少的顶点绘制物体的大概思路,接下来我们将根据以下的核心步骤来绘制出立方体。

1.准备顶点三角形索引列表

// 物体的8个顶点坐标与顶点颜色
const verticesColor = new Float32Array([
  1.0, 1.0, 1.0, 1.0, 1.0, 1.0, // v0
  -1.0, 1.0, 1.0, 1.0, 0.0, 1.0,// v1
  -1.0, -1.0, 1.0, 1.0, 0.0, 0.0,// v2
  1.0, -1.0, 1.0, 1.0, 1.0, 0.0,// v3
  1.0, -1.0, -1.0, 0.0, 1.0, 0.0,// v4
  1.0, 1.0, -1.0, 0.0, 1.0, 1.0,// v5
  -1.0, 1.0, -1.0, 0.0, 0.0, 1.0,// v6
  -1.0, -1.0, -1.0, 0.0, 0.0, 0.0,// v7
])
// 三角形索引构成的面 
const indices = new Uint8Array([
  0, 1, 2, 0, 2, 3, // 前
  0, 3, 4, 0, 4, 5, // 右
  0, 5, 6, 0, 6, 1, // 上
  1, 6, 7, 1, 7, 2, // 左
  7, 4, 3, 7, 3, 2, // 下
  4, 7, 6, 4, 6, 5, // 后
])

2.向缓冲区内写入坐标、颜色和索引

const vertexColorBuffer = gl.createBuffer();
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);// 顶点数据是与gl.ARRAY_BUFFER绑定
gl.bufferData(gl.ARRAY_BUFFER, verticesColor, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
gl.enableVertexAttribArray(a_Position);
gl.enableVertexAttribArray(a_Color);

const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);// 顶点索引与ELEMENT_ARRAY_BUFFER绑定
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
image.png image.png
  1. 绘制立方体
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
image.png

完整代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script src="./utils.js"></script>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec4 a_Color;
      uniform mat4 u_ProjectionMatrix;
      uniform mat4 u_ViewMatrix;
      varying vec4 v_Color;
      void main () {
        gl_Position = u_ProjectionMatrix * u_ViewMatrix * a_Position;
        v_Color = a_Color;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_Color;
      void main () {
        gl_FragColor = v_Color;
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;

      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');

      initShader(gl, vsSource, fsSource);

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
      const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
      const u_ProjectionMatrix = gl.getUniformLocation(gl.program, 'u_ProjectionMatrix');

      // prettier-ignore
      const verticesColor = new Float32Array([
        1.0, 1.0, 1.0, 1.0, 1.0, 1.0, // v0
        -1.0, 1.0, 1.0, 1.0, 0.0, 1.0,// v1
        -1.0, -1.0, 1.0, 1.0, 0.0, 0.0,// v2
        1.0, -1.0, 1.0, 1.0, 1.0, 0.0,// v3
        1.0, -1.0, -1.0, 0.0, 1.0, 0.0,// v4
        1.0, 1.0, -1.0, 0.0, 1.0, 1.0,// v5
        -1.0, 1.0, -1.0, 0.0, 0.0, 1.0,// v6
        -1.0, -1.0, -1.0, 0.0, 0.0, 0.0,// v7
      ])
      // prettier-ignore
      const indices = new Uint8Array([
        0, 1, 2, 0, 2, 3, // 前
        0, 3, 4, 0, 4, 5, // 右
        0, 5, 6, 0, 6, 1, // 上
        1, 6, 7, 1, 7, 2, // 左
        7, 4, 3, 7, 3, 2, // 下
        4, 7, 6, 4, 6, 5, // 后
      ])
      const FSIZE = verticesColor.BYTES_PER_ELEMENT;
      const perspectiveCamera = new THREE.PerspectiveCamera(30, canvas.width / canvas.height, 1, 100);
      const projectionMatrix = perspectiveCamera.projectionMatrix.elements;
      const camera = new THREE.Vector3(3, 4, 8);
      const target = new THREE.Vector3(0, 0, 0);
      const up = new THREE.Vector3(0, 1, 0);
      let viewMatrix = computedViewMatrix(camera, target, up);

      const vertexColorBuffer = gl.createBuffer();
      const indexBuffer = gl.createBuffer();

      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, verticesColor, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
      gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_Color);

      gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);
      gl.uniformMatrix4fv(u_ProjectionMatrix, false, projectionMatrix);

      gl.clearColor(0, 0, 0, 1);
      gl.enable(gl.DEPTH_TEST);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
      gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
    </script>
  </body>
</html>
image.png

6.1 为立方体的每个面指定颜色

我们发现上面立方体的颜色五彩斑斓,花里胡哨。之所以会绘制成彩色的立方体是因为我们仅使用了8个顶点来绘制立方体,而每个顶点会被多个面所共用。再加上WebGL的内插行为导致每个面都是彩色渐变的效果。

如果我们想要给立方体每个面指定颜色,那么就需要每个顶点就不能被其他面所共用。所以一个面就需要4个顶点,6个面我们就需要准备4×6=24个顶点。

那么我们该如何定义这24个顶点的坐标、颜色和索引呢?

其实非常简单我们可以先将立方体在我们的脑海中进行展开,像下面这样。

image.png

然后给每个面的四个顶点随意用序号进行标记,像下面这样。

绘图1.jpg

我们再根据自己标的序号将每个面划分为2个三角形,然后得出三角形列表索引。

注意:索引的顺序上没有要求,但是需要三三一组,合起来能够覆盖整个面。

前(v0, v1, v2, v2, v0, v3) √
前(v2, v1, v0, v0, v2, v3) √
前(v3, v2, v1, v1, v0, v3) √
...
前(v1, v0, v2, v1, v0, v3) ×
前(v2, v1, v3, v2, v3, v0) ×

因为上面的索引顺序无法覆盖整个面。

下面是我的三角形列表索引。

const indices = new Uint8Array([
  0, 1, 2, 0, 2, 3, // 前
  4, 5, 6, 4, 6, 7, // 后
  16, 17, 18, 16, 18, 19, // 左
  20, 21, 22, 20, 22, 23, //右
  12, 13, 14, 12, 14, 15, //上
  8, 9, 10, 8, 10, 11 //下
])

有了每个面的索引之后,我们还需要得到每个顶点坐标与颜色

虽然每个面的顶点不能共用,但顶点与顶点的坐标是存在一样的,如下所示:

v0 = v22 = v15 = (1.0, 1.0, 1.0)
v1 = v19 = v14 = (-1.0, 1.0, 1.0)
v2 = v11 = v18 = (-1.0, -1.0, 1.0)
v3 = v10 = v23 = (1.0, -1.0, 1.0)
v4 = v9 = v20 = (1.0, -1.0, -1.0)
v5 = v21 = v12 = (1.0, 1.0, -1.0)
v6 = v16 = v13 = (-1.0, 1.0, -1.0)
v7 = v8 = v17 = (-1.0, -1.0, -1.0)

下面是我的立方体的各顶点坐标及颜色。

const verticesColor = new Float32Array([
  // 前(红)
  1.0, 1.0, 1.0, 1.0, 0.0, 0.0,// v0
  -1.0, 1.0, 1.0, 1.0, 0.0, 0.0,// v1
  -1.0, -1.0, 1.0, 1.0, 0.0, 0.0,// v2
  1.0, -1.0, 1.0, 1.0, 0.0, 0.0,// v3

  // 后(绿)
  1.0, -1.0, -1.0, 0.0, 1.0, 0.0,// v4
  1.0, 1.0, -1.0, 0.0, 1.0, 0.0,// v5
  -1.0, 1.0, -1.0, 0.0, 1.0, 0.0,// v6
  -1.0, -1.0, -1.0, 0.0, 1.0, 0.0,// v7

  // 左(蓝)
  -1.0, 1.0, -1.0, 0.0, 0.0, 1.0,// v16
  -1.0, -1.0, -1.0, 0.0, 0.0, 1.0,// v17
  -1.0, -1.0, 1.0, 0.0, 0.0, 1.0,// v18
  -1.0, 1.0, 1.0, 0.0, 0.0, 1.0,// v19

  // 右(青)
  1.0, -1.0, -1.0, 0.0, 1.0, 1.0, // v20
  1.0, 1.0, -1.0, 0.0, 1.0, 1.0,// v21
  1.0, 1.0, 1.0, 0.0, 1.0, 1.0,// v22
  1.0, -1.0, 1.0, 0.0, 1.0, 1.0,// v23

  // 上(紫)
  1.0, 1.0, -1.0, 1.0, 0.0, 1.0, // v12
  -1.0, 1.0, -1.0, 1.0, 0.0, 1.0,// v13
  -1.0, 1.0, 1.0, 1.0, 0.0, 1.0,// v14
  1.0, 1.0, 1.0, 1.0, 0.0, 1.0,// v15

  // 下(白)
  -1.0, -1.0, -1.0, 1.0, 1.0, 1.0,// v8
  1.0, -1.0, -1.0, 1.0, 1.0, 1.0,// v9
  1.0, -1.0, 1.0, 1.0, 1.0, 1.0,// v10
  -1.0, -1.0, 1.0, 1.0, 1.0, 1.0,// v11
])

其他视图矩阵、透视投影矩阵等等代码都不需要改动。

完整代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script src="./utils.js"></script>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec4 a_Color;
      uniform mat4 u_ProjectionMatrix;
      uniform mat4 u_ViewMatrix;
      varying vec4 v_Color;
      void main () {
        gl_Position = u_ProjectionMatrix * u_ViewMatrix * a_Position;
        v_Color = a_Color;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_Color;
      void main () {
        gl_FragColor = v_Color;
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;

      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');

      initShader(gl, vsSource, fsSource);

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
      const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
      const u_ProjectionMatrix = gl.getUniformLocation(gl.program, 'u_ProjectionMatrix');

      // prettier-ignore
      const verticesColor = new Float32Array([
        // 前(红)
        1.0, 1.0, 1.0, 1.0, 0.0, 0.0,// v0
        -1.0, 1.0, 1.0, 1.0, 0.0, 0.0,// v1
        -1.0, -1.0, 1.0, 1.0, 0.0, 0.0,// v2
        1.0, -1.0, 1.0, 1.0, 0.0, 0.0,// v3

        // 后(绿)
        1.0, -1.0, -1.0, 0.0, 1.0, 0.0,// v4
        1.0, 1.0, -1.0, 0.0, 1.0, 0.0,// v5
        -1.0, 1.0, -1.0, 0.0, 1.0, 0.0,// v6
        -1.0, -1.0, -1.0, 0.0, 1.0, 0.0,// v7

        // 左(蓝)
        -1.0, 1.0, -1.0, 0.0, 0.0, 1.0,// v16
        -1.0, -1.0, -1.0, 0.0, 0.0, 1.0,// v17
        -1.0, -1.0, 1.0, 0.0, 0.0, 1.0,// v18
        -1.0, 1.0, 1.0, 0.0, 0.0, 1.0,// v19

        // 右(青)
        1.0, -1.0, -1.0, 0.0, 1.0, 1.0, // v20
        1.0, 1.0, -1.0, 0.0, 1.0, 1.0,// v21
        1.0, 1.0, 1.0, 0.0, 1.0, 1.0,// v22
        1.0, -1.0, 1.0, 0.0, 1.0, 1.0,// v23

        // 上(紫)
        1.0, 1.0, -1.0, 1.0, 0.0, 1.0, // v12
        -1.0, 1.0, -1.0, 1.0, 0.0, 1.0,// v13
        -1.0, 1.0, 1.0, 1.0, 0.0, 1.0,// v14
        1.0, 1.0, 1.0, 1.0, 0.0, 1.0,// v15

        // 下(白)
        -1.0, -1.0, -1.0, 1.0, 1.0, 1.0,// v8
        1.0, -1.0, -1.0, 1.0, 1.0, 1.0,// v9
        1.0, -1.0, 1.0, 1.0, 1.0, 1.0,// v10
        -1.0, -1.0, 1.0, 1.0, 1.0, 1.0,// v11
      ])
      // prettier-ignore
      const indices = new Uint8Array([
        0, 1, 2, 0, 2, 3, // 前
        4, 5, 6, 4, 6, 7, // 后
        16, 17, 18, 16, 18, 19, // 左
        20, 21, 22, 20, 22, 23, //右
        12, 13, 14, 12, 14, 15, //上
        8, 9, 10, 8, 10, 11 //下
      ])
      const FSIZE = verticesColor.BYTES_PER_ELEMENT;
      const perspectiveCamera = new THREE.PerspectiveCamera(30, canvas.width / canvas.height, 1, 100);
      const projectionMatrix = perspectiveCamera.projectionMatrix.elements;
      const camera = new THREE.Vector3(3, 4, 8);
      const target = new THREE.Vector3(0, 0, 0);
      const up = new THREE.Vector3(0, 1, 0);
      let viewMatrix = computedViewMatrix(camera, target, up);

      const vertexColorBuffer = gl.createBuffer();
      const indexBuffer = gl.createBuffer();

      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

      gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, verticesColor, gl.STATIC_DRAW);

      gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
      gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_Color);

      gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);
      gl.uniformMatrix4fv(u_ProjectionMatrix, false, projectionMatrix);

      gl.clearColor(0, 0, 0, 1);
      gl.enable(gl.DEPTH_TEST);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
      gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
    </script>
  </body>
</html>
image.png

参考

WebGL编程指南