WebGL+Three.js—第五章 WebGL三维世界

2,460 阅读20分钟

5.1 3D 基础

5.1.1 视点、目标点、上方向

    1、视点:可以简易的理解为眼睛,也叫观察点

        视线的起点,也就是眼睛所在三维空间中的位置(eyeX,eyeY,eyeZ)。

    2、目标点:可以理解为我们要看的物体

        被观察目标所在的点,当确立目标点和视点时,视线方向也随着确立。目标点的坐标用(atX,atY,atZ)表示。

    3、上方向:正方向

        最终绘制在屏幕上的影像中的向上的方向,即正方向。当视线方向确定时,观察者还能够以视线为轴旋转的。当指定上方向时,整个坐标就彻底固定住。可以联想一下,一个人观察一个物体只确定眼睛和观察的物体时,他的头可以向上向下向左向右偏转,为了固定观察者我们还需要指定观察者的上方向。上方向是具有3个分量的矢量(upX,upY,upZ)

        注意:这个上方向不一定要垂直于z轴(视线),它的目的仅仅是为了固定观察者,因为我们最终的目的是在观察平面创建一个新的坐标系,而新的坐标系的x轴是同时垂直于上方向和z轴,因此上方向与z轴不垂直也无所谓。

image.png

image.png

    在webgl的默认规则中,视点位于坐标系统原点(0,0,0),视线为Z轴负方向(屏幕内),上方向为Y轴负方向(向下)。

5.1.2 观察平面

    如果在webgl里需要把物体显示出来,显示为我们需要看到的模样,它的实际意义就是通过观察平面创建一个新的坐标系,并且把物体所有的坐标映射到观察平面上,而且我们最终要获取的就是这个映射关系

image.png

5.1.3 辅助函数

    如果想创建新的坐标系,需要通过以下辅助函数来实现。

    1、归一化函数:归一化到0-1的区间内

// 归一化函数
function normalized(arr) {
  let sum = 0;
  for (let i=0; i<arr.length; i++) {
    sum += arr[i] * arr[i];
  }

  const middle = Math.sqrt(sum);

  for (let i=0; i<arr.length; i++) {
    arr[i] = arr[i] / middle;
  }
}

    2、叉积:求两个平面的法向量

image.png

     几何意义:两个向量叉乘结果是一个新向量,新向量的方向垂直于原来两个向量所在的平面,z轴指向通过右手定则来判定。

     公式:A·B = |A| × |B| × sin(θ)

     说明:|A|、|B|表示向量的模(长度),θ表示向量A向向量B旋转的夹角(或者B往A旋转的夹角)。注意:夹角虽然相同,但有正负之分,可参考下图。

     公式推导:设θ1是A与x轴的夹角,设θ2是B与x轴的夹角,向量A旋转至向量B的夹角为θ2-θ1

            |A||B|sin(θ)

         = |A||B|sin(θ2-θ1)

         = |A||B|(sinθ2*cosθ1 - sinθ1*cosθ2)

         = (|A|*cosθ1)*(|B|*sinθ2) - (|A|*sinθ1)*(|B|*cosθ2)

         = AxBy - AyBx

     推导结果:二维的叉积公式:AxBy - AyBx = a[0] * b[1] - a[1] * b[0]。

// 叉积函数 获取法向量
function cross(a, b) {
  return new Float32Array([    a[1] * b[2] - a[2] * b[1],
    a[2] * b[0] - a[0] * b[2],
    a[0] * b[1] - a[1] * b[0]
  ]);
}

    3、点积:求某点在x,y,z轴上的投影长度

image.png

     几何意义:A在B上的投影长度乘以B的模长。

     公式:A·B = |A| × |B| × cos(θ)

     说明:|A|、|B|表示向量的模(长度),θ表示向量A和B之间的夹角

     公式推导:设θ1是A与x轴的夹角,设θ2是B与x轴的夹角,向量A与向量B的夹角为θ1-θ2

            |A||B|cos(θ)

         = |A||B|cos(θ1-θ2)

         = |A||B|(cosθ1*cosθ2 + sinθ1*sinθ2)

         = (|A|*cosθ1)*(|B|*cosθ2) + (|A|*sinθ1)*(|B|*sinθ2)

         = AxBx + AyBy

     推导结果:二维的点积公式:A·B = AxBx + AyBy;三维的点积公式:A·B = AxBx + AyBy + AzBz

/**
 * 点积函数 获取投影长度
 * @param a: 第一个点坐标[x, y, z]
 * @param b: 第二个点坐标[x, y, z]
 */
function dot(a, b) {
  return a[0] * b[0] + a[1] * b[1] + a[2] * a[2]
}

    4、向量差:获取视点到目标点之间的向量

image.png

     几何意义:向量的减法:如果a、b是互为相反的向量,那么a=-b,b=-a,a+b=0。0的反向量为0OA-OB=BA.即“共同起点,指向被减”,例如:a=(x1,y1),b=(x2,y2),则a-b=(x1-x2,y1-y2)。

// 向量差
function minus(a, b) {
  return new Float32Array([    a[0] - b[0],
    a[1] - b[1],
    a[2] - b[2]
  ]);
}

5.1.4 获取视图矩阵

    1、视点、目标点、上方向

// 视图矩阵获取
function getViewMatrix(eyex, eyey, eyez, lookAtx, lookAty, lookAtz, upx, upy, upz) {
  // 视点
  const eye = new Float32Array([eyex, eyey, eyez]);
  // 目标点
  const lookAt = new Float32Array([lookAtx, lookAty, lookAtz]);
  // 上方向
  const up = new Float32Array([upx, upy, upz]);
}  

    2、确定z轴向量

        z轴向量是最容易确定的,因为现在有了视点和目标点,只需要通过它们两个坐标的向量差就可以得到z轴向量。

// 向量差
function minus(a, b) {
  return new Float32Array([
    a[0] - b[0],
    a[1] - b[1],
    a[2] - b[2]
  ]);
}
// 视图矩阵获取
function getViewMatrix(eyex, eyey, eyez, lookAtx, lookAty, lookAtz, upx, upy, upz) {
  // 视点
  const eye = new Float32Array([eyex, eyey, eyez]);
  // 目标点
  const lookAt = new Float32Array([lookAtx, lookAty, lookAtz]);
  // 上方向
  const up = new Float32Array([upx, upy, upz]);

  // 确定z轴
  const z = minus(eye, lookAt);
}  

    3、确定x轴向量

        当前z轴和上方向已经确定,上方向是一定垂直于z轴的。根据这两个相互垂直的向量,就可以确定x轴,因为x轴是垂直于上方向和z轴的,可以通过它们的叉积获得。

/**
 * 叉积函数 获取法向量
 * @param a: 第一个点坐标[x, y, z]
 * @param b: 第二个点坐标[x, y, z]
 */
function cross(a, b) {
  return new Float32Array([
    a[1] * b[2] - a[2] * b[1],
    a[2] * b[0] - a[0] * b[2],
    a[0] * b[1] - a[1] * b[0]
  ]);
}
// 视图矩阵获取
function getViewMatrix(eyex, eyey, eyez, lookAtx, lookAty, lookAtz, upx, upy, upz) {
  // 视点
  const eye = new Float32Array([eyex, eyey, eyez]);
  // 目标点
  const lookAt = new Float32Array([lookAtx, lookAty, lookAtz]);
  // 上方向
  const up = new Float32Array([upx, upy, upz]);

  // 确定z轴
  const z = minus(eye, lookAt);

  normalized(z);
  normalized(up);

  // 确定x轴
  const x = cross(z, up);
}  

    4、确定y轴向量

        y轴垂直于x轴和z轴,因此可以通过它们的叉积获得y轴向量。

// 视图矩阵获取
function getViewMatrix(eyex, eyey, eyez, lookAtx, lookAty, lookAtz, upx, upy, upz) {
  // 视点
  const eye = new Float32Array([eyex, eyey, eyez]);
  // 目标点
  const lookAt = new Float32Array([lookAtx, lookAty, lookAtz]);
  // 上方向
  const up = new Float32Array([upx, upy, upz]);

  // 确定z轴
  const z = minus(eye, lookAt);

  normalized(z);
  normalized(up);

  // 确定x轴
  const x = cross(z, up);

  normalized(x);
  // 确定y轴
  const y = cross(x, z);
}

    5、获得视图矩阵

        矩阵的构成是由x、y、z3个轴的向量以及它们的模长所确定,每个向量都是一个数组,里面包含着[x,y,z]。模长由点积函数可以获取。

// 视图矩阵获取
function getViewMatrix(eyex, eyey, eyez, lookAtx, lookAty, lookAtz, upx, upy, upz) {
  // 视点
  const eye = new Float32Array([eyex, eyey, eyez]);
  // 目标点
  const lookAt = new Float32Array([lookAtx, lookAty, lookAtz]);
  // 上方向
  const up = new Float32Array([upx, upy, upz]);

  // 确定z轴
  const z = minus(eye, lookAt);

  normalized(z);
  normalized(up);

  // 确定x轴
  const x = cross(z, up);

  normalized(x);
  // 确定y轴
  const y = cross(x, z);

  return new Float32Array([
    x[0],         y[0],         z[0],         0,
    x[1],         y[1],         z[1],         0,
    x[2],         y[2],         z[2],         0,
    -dot(x, eye), -dot(y, eye), -dot(z, eye), 1
  ]);
}

5.1.5 使用视图矩阵

    这里假设物体就在坐标原点(0.0, 0.0, 0.0),视点在(0.0, 0.1, 0.2)位置,上方向为(0.0, 0.6, 0.0),也就是在物体的y方向正上方。

let eyey = -0.1;
function amination() {
  eyey += 0.01;
  if (eyey > 1) {
    eyey = -0.1;
  }
  const vm = getViewMatrix(0.0, 0.1, 0.2, 0.0, 0.0, 0.0, 0.0, 0.6, 0.0);
  gl.uniformMatrix4fv(mat, false, vm);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(amination);
}
amination();

image.png

5.1.6 代码示例

5.2 正射投影

5.2.1 正射投影概念

image.png

    正射投影也叫平行投影,它最大的特点就是不管物体距离视点有多远,投影之后物体的大小和尺寸都不变。

image.png

    正射投影它在做坐标映射的时候,它的所有投影线都是垂直于绘图平面,这个时候它的大小和比例都不会改变的。

image.png

    正射投影矩阵就是把当前空间内的坐标转换到x[-1,1]、y[-1,1]、z[-1,1]的区间内。

5.2.2 推导过程

image.png

    假设以点A做坐标转换:

    1、x轴坐标转换

image.png

    2、y轴坐标转换

image.png

    3、z轴坐标转换

image.png

    4、伪矩阵的转换关系

        关于伪矩阵的介绍,可以参考矩阵简介

image.png

    5、正射投影矩阵

image.png

5.2.3 代码实现过程

    1、创建正射投影矩阵

/**
 * 获取正射投影矩阵
 * @param l: 左
 * @param r: 右
 * @param t: 上
 * @param b: 下
 * @param n: 近
 * @param f: 远
 * */
function getOrtho(l, r, t, b, n, f){
  return new Float32Array([
    2 / (r - l), 0,           0,           0,
    0,           2 / (t - b), 0,           0,
    0,           0,           -2 /(f - n), 0,
    -(r+l)/(r-l),-(t+b)/(t-b),-(f+n)/(f-n),1
  ]);
}

    2、获取正射投影矩阵

        矩阵需要传入6个参数,分别是左右上下远近。这里以x轴(左右)为例子说明,由于三角形的x坐标最小是-0.5,最大是0.5,如果我们的左右参数分别设置为-1和1,那么它的正射投影就正好在绘图平面的中间,长度为平面的一半。

// 创建三角形系列数据
const points = new Float32Array([
  -0.5, -0.5,
  0.5, -0.5,
  0.0, 0.5
]);

function amination() {
  const ortho = getOrtho(-1, 1, 1, -1, 0, 20);
  gl.uniformMatrix4fv(mat, false, ortho);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(amination);
}
amination();

image.png

        如果此时左右两个参数传入的是-2和2,那么三角形的x轴长度就只有绘图长度的1/4

function amination() {
  const ortho = getOrtho(-2, 2, 1, -1, 0, 20);
  gl.uniformMatrix4fv(mat, false, ortho);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(amination);
}

image.png

5.2.4 代码示例

5.3 透视投影

5.3.1 透视投影概念

image.png

    透视投影属于中心投影,它是从某个投射中心将物体投射到单一投影面上所得到的图形。透视图与人们观看物体时所产生的视觉效果非常接近,很好的反映出来物体的空间形象。

5.3.2 计算透视投影矩阵

    1、把透视投影的棱台映射为长方体

image.png

    2、坐标转换

image.png

    3、求x、y坐标

 

        参数介绍:np:近截面  fp:远截面   p':投影后的顶点  p:物体顶点  N:近截面到观察点的距离   F: eye:观察点   (取近截面为投影面)

        根据p与p'的相似三角形性质可得:

        同理,在z轴和y轴上可得:

        从而得到投影后得坐标:

        到此,已把顶点投影到视平面,此时所有点的z值均为-N。若以此为最终坐标,则无法在后续中进行深度测试等操作,所以需要保留原本顶点在空间中的z值。

        由此可把坐标最终定为:

        此坐标的x与y是相对于视平面的,而z是相对于原先的空间坐标。可以想象若用此坐标来构成原物体,则已经产生形变。

        由于z的坐标未知,根据z=0*x+0*y+az+b的公式,目前的矩阵为:

image.png

    4、求z坐标

        由于要把p'转换为齐次坐标,而齐次坐标在变换为普通坐标时,需要同除w,x'和y'中均为x和y同除以了-z。所以也需要把p'中的z变换为带有-1/z的形式,从而更方便齐次坐标到普通坐标的转换。

        因此,原来的公式z = az + b带有-1/z的形式,就变成z = -(az + b) / z,最终得出-z*z = az + b。将近平面的-N和远平面的-F代入到公式:

        -N*N = -a*N + b

        -F*F = -a*F + b

        公式推导:

        (1)-N*N = -a*N + b ——> b = a*N - N*N

        (2)-F*F = -a*F + b ——> -F*F = -a*F + a*N - N*N ——> a(F - N) = F*F - N*N ——> a = F + N

        (3)b = a*N - N*N ——> b = (F + N)*N - N*N = FN

        经过推导得出a等于F + N,b等于FN,因此目前矩阵为:

image.png

    5、正射投影矩阵与透视投影矩阵结合

        它们通过矩阵相乘的方式结合起来,得到以下的矩阵:

image.png

        推演过程:

        (1)矩阵相乘公式

            n×n阶矩阵乘法公式可以表述为:两个矩阵A和B相乘,用A的第1行各个数与B的第1列各个数对应相乘后加起来,就是乘法结果中第1行第1列;再用A的第1行各个数与B的第2列各个数对应相乘后加起来,得到第1行第2列,以此类推。

5158345adf313c657d77739b800d43c.png

        (2)简化过程

21857ee4ee078a3da525242e846572b.jpg              06dd6a4b22034e665eef206e2a6ed8d.jpg

    6、求上下左右边界

image.png

        根据视角α和宽高比(aspect)求出top、bottom、left、right四个边界方向:

        (1)top:上边界其实就是y坐标,t = n * tan(α / 2)

        (2)bottom:下边界与上边界相反,b = -t

        (3)aspect:宽高比

            由于上边界为t,那么平面的高度就是2t,宽度两个左边界或者右边界的距离,假设为2r,因此aspect = 2r / 2t = r / t。

        (4)right:右边界

            因为aspect = r / t,所以r = aspect * t = n * aspect * tan(α / 2)

        (5)left:左边界与右边界相反,l = -r

    7、得到最终的透视投影矩阵

        根据上述的上下左右边界,可以得到以下公式:

        (1)r - l = 2 * n * aspect * tan(α / 2)

        (2)t - b = 2 * n * tan(α / 2)

        (3)r + l = 0

        (4)t + b = 0

image.png

5.3.3 代码实现过程

    1、获取透视投影矩阵

        这里除了将参数传入矩阵函数,还需要先将角度转换成弧度值。

/**
 * 获取透视投影矩阵
 * @param fov: 角度
 * @param aspect: 宽高比
 * @param far: 远距离
 * @param near: 近距离
 * */
function getPerspective(fov, aspect, far, near) {
  fov = fov * Math.PI / 180;
  return new Float32Array([
    1/(aspect*Math.tan(fov / 2)), 0, 0, 0,
    0, 1/(Math.tan(fov/2)),0,0,
    0,0,-(far+near)/(far-near),-(2*far*near)/(far-near),
    0,0,-1,0,
  ])
}

    2、创建6个三角形的点数据

const points = new Float32Array([
  0.75,1.0,-0.6, 1.0,0.0,0.0,
  0.25,-1.0,-0.6, 1.0,0.0,0.0,
  1.0, -1.0,-0.6, 1.0,0.0,0.0,

  0.75,1.0,-0.5, 0.0,1.0,0.0,
  0.25,-1.0,-0.5, 0.0,1.0,0.0,
  1.0, -1.0,-0.5, 0.0,1.0,0.0,

  0.75,1.0,-0.4, 0.0,0.0,1.0,
  0.25,-1.0,-0.4, 0.0,0.0,1.0,
  1.0, -1.0,-0.4, 0.0,0.0,1.0,

  -0.75,1.0,-0.6, 1.0,0.0,0.0,
  -0.25,-1.0,-0.6, 1.0,0.0,0.0,
  -1.0, -1.0,-0.6, 1.0,0.0,0.0,

  -0.75,1.0,-0.5, 0.0,1.0,0.0,
  -0.25,-1.0,-0.5, 0.0,1.0,0.0,
  -1.0, -1.0,-0.5, 0.0,1.0,0.0,

  -0.75,1.0,-0.4, 0.0,0.0,1.0,
  -0.25,-1.0,-0.4, 0.0,0.0,1.0,
  -1.0, -1.0,-0.4, 0.0,0.0,1.0,
])

    3、调用透视矩阵生成画布

        首先创建视点坐标,然后传入视图矩阵的函数里。然后调用透视矩阵:角度设置为150°;宽高比为画布的宽与高比:cxt.width/cxt.height;远近距离分别设置为100和1。最后将视图矩阵和透视矩阵同时作用在坐标上,将它们两个矩阵做一个混合。

let eyex = 0.0;
let eyey = -0.1;
let eyez = 0.2;
function draw() {
  const vm = getViewMatrix(eyex,eyey,eyez,0.0,0.0,0.0,0.0,0.6,0.0);
  const perspective = getPerspective(150, ctx.width / ctx.height, 100, 1);
  gl.uniformMatrix4fv(mat, false, mixMatrix(vm, perspective));
  gl.drawArrays(gl.TRIANGLES, 0, 3 * 6);
}
draw();

image.png

    4、添加键盘事件

        为了方便观察这6个三角形,可以添加键盘的左右上下键的事件控制视点的坐标。

document.onkeydown = function (e) {
  switch (e.keyCode) {
    case 37: eyex += 0.01; break;         // 左
    case 38: eyey += 0.01; break;         // 上
    case 39: eyex -= 0.01; break;         // 右
    case 40: eyey -= 0.01; break;         // 下
  }
  draw();
}

    5、解决遮挡问题

        我们发现它在改变视点的时候,会出现左边第一个三角形会被右边的后排三角形遮挡了。

image.png

        要解决这个问题,可以通过开启深度测试去解决:gl.enable(gl.DEPTH_TEST)。启用了之后,OpenGL在绘制的时候就会检查,当前像素前面是否有别的像素,如果别的像素挡道了它,那它就不会绘制,也就是说,OpenGL就只绘制最前面的一层。

function draw() {
  const vm = getViewMatrix(eyex,eyey,eyez,0.0,0.0,0.0,0.0,0.6,0.0);
  const perspective = getPerspective(150, ctx.width / ctx.height, 100, 1);
  gl.enable(gl.DEPTH_TEST);
  gl.uniformMatrix4fv(mat, false, mixMatrix(vm, perspective));
  gl.drawArrays(gl.TRIANGLES, 0, 3 * 6);
}

image.png

5.3.4 代码示例

5.4 立方体绘制(顶点法)

5.4.1 基本概念

    顶点法是最常用的一种方法,这里是通过顶点法来绘制立方体。

image.png

    绘制一个立方体需要有8个顶点,前面有4个,后面有4个,8个顶点共同组成了6个面形成一个立方体。

5.4.2 绘制立方体

    1、创建8个顶点数据

// 顶点
const v0 = [1,1,1];
const v1 = [-1,1,1];
const v2 = [-1,-1,1];
const v3 = [1,-1,1];
const v4 = [1,-1,-1];
const v5 = [1,1,-1];
const v6 = [-1,1,-1];
const v7 = [-1,-1,-1];

    2、将顶点数据组合成立方体

        点、线、三角形这些属于基本图形,立方体由6个面组成,每个面是一个正方形,而每个正方形实际上是由两个三角形拼接而成的。

        例如正面有V0、V1、V2、V3这4个顶点,我们可以将它拆分为两个三角形分别是△V0V1V2和△V0V2V3。以此类推,最终可以拆分成12个三角形。

const points = new Float32Array([
  ...v0,...v1,...v2, ...v0,...v2, ...v3, // 前
  ...v0,...v3,...v4, ...v0,...v4, ...v5, // 右
  ...v0,...v5,...v6, ...v0,...v6, ...v1, // 上
  ...v1,...v6,...v7, ...v1,...v7, ...v2, // 左
  ...v7,...v4,...v3, ...v7,...v3, ...v2, // 底
  ...v4,...v7,...v6, ...v4,...v6, ...v5, // 后
]);

image.png

5.4.3 美化立方体

    1、调整视点

        我们画的是一个由6个面组成的立方体,但实际上看到的是一个矩形,这是受了视角的影响,只需要调整一下视点的坐标(eyex, eyey, eyez)即可观察到立方体的形状:

let eyex = 3;
let eyey = 3;
let eyez = 5;
function draw() {
  const vm = getViewMatrix(eyex,eyey,eyez,0.0,0.0,0.0,0.0,0.6,0.0);
  const perspective = getPerspective(150, ctx.width / ctx.height, 100, 1);
  gl.enable(gl.DEPTH_TEST);
  gl.uniformMatrix4fv(mat, false, mixMatrix(perspective, vm));
  gl.drawArrays(gl.TRIANGLES, 0, points.length / 3);
}
draw();

image.png

    2、调整角度

        虽然现在能看到立方体,但图形太小,这是因为传入投射矩阵的角度过大导致,这里需要把观察角度调小即可:把getPerspective的第一个参数改成30度(getPerspective方法在上述的5.3透视投影有介绍)

let eyex = 3;
let eyey = 3;
let eyez = 5;
function draw() {
  const vm = getViewMatrix(eyex,eyey,eyez,0.0,0.0,0.0,0.0,0.6,0.0);
  const perspective = getPerspective(30, ctx.width / ctx.height, 100, 1);
  gl.enable(gl.DEPTH_TEST);
  gl.uniformMatrix4fv(mat, false, mixMatrix(perspective, vm));
  gl.drawArrays(gl.TRIANGLES, 0, points.length / 3);
}
draw();

image.png

    3、添加旋转效果

        为了证明图形是一个立方体,我们可以添加一个旋转效果:在正射投影和透视投影的结合矩阵基础上,再混合旋转矩阵:

let eyex = 3;
let eyey = 3;
let eyez = 5;
let deg = 0;

function draw() {
  deg += 0.01;
  const rotate = getRotateMatrix(deg);
  const vm = getViewMatrix(eyex,eyey,eyez,0.0,0.0,0.0,0.0,0.6,0.0);
  const perspective = getPerspective(30, ctx.width / ctx.height, 100, 1);
  gl.enable(gl.DEPTH_TEST);
  gl.uniformMatrix4fv(mat, false, mixMatrix(mixMatrix(perspective, vm), rotate));
  gl.drawArrays(gl.TRIANGLES, 0, points.length / 3);
  requestAnimationFrame(draw)
}
draw();

e3feb780-0bd4-4371-9f16-a48ceb120a2c.gif

    4、添加立方体颜色

        目前立方体的棱跟立方体融合在一起,特别是在旋转的过程中无法看清,可以给立方体添加颜色。最简单的方式是将顶点坐标作为颜色传进片元着色器里(vColor = aPosition)。

// 顶点着色器
const VERTEX_SHADER_SOURCE = `
  attribute vec4 aPosition;
  attribute vec4 aColor;
  varying vec4 vColor;

  uniform mat4 mat;
  void main() {
    gl_Position = mat * aPosition;
    vColor = aPosition;
  }
`;

// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
  precision lowp float;
  varying vec4 vColor;

  void main() {
    gl_FragColor = vColor;
  }
`;

6ff0b156-694b-42cc-b12f-1cbebf0bc574.gif

    5、给立方体每个面设置纯色

        我们给立方体每个面设置一种颜色,注意每个面是由2个三角形形成的,因此每个面要设6个颜色值。

const VERTEX_SHADER_SOURCE = `
  attribute vec4 aPosition;
  attribute vec4 aColor;
  varying vec4 vColor;

  uniform mat4 mat;
  void main() {
    gl_Position = mat * aPosition;
    vColor = aColor;
  }
`;

const colorData = new Float32Array([
  1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,        // 红色
  0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,        // 绿色
  0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,        // 蓝色
  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,        // 白色
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,        // 黑色
  0,1,1,0,1,1,0,1,1,0,1,1,0,1,1,0,1,1,        // 浅蓝色
])
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.STATIC_DRAW);
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aColor);

634df8f9-5eda-41e0-94fb-07d779121d26.gif

5.4.4 代码示例

5.5 立方体绘制(索引法)

5.5.1 基本概念

    顶点法是先创建8个顶点,然后通过解构的方式把所有的点坐标按照每个面的2个三角形的点,按顺序放进数组里=来绘制立方体。

    绘制立方体还有另一种方式就是索引法,它可以不通过解构,直接使用它的下标来表示,例如...v0,...v1,...v2改成012。

5.5.2 绘制立方体

    1、使用二维数组存储8个顶点数据

// 顶点
const vertices = new Float32Array([
  1,1,1,
  -1,1,1,
  -1,-1,1,
  1,-1,1,
  1,-1,-1,
  1,1,-1,
  -1,1,-1,
  -1,-1,-1
]);

    2、创建索引数据

        因为索引数据所接收的是整型,因此使用Uint8Array,然后参照下图每个面的2个三角形的点索引进行存储。

image.png

const index = 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,6,7,4,6,5
])

    3、创建索引缓冲区对象

        注意:一般我们绑定缓冲区对象的时候,使用的类型是gl.ARRAY_BUFFER,但是在绑定索引数据的时候,要使用gl.ELEMENT_ARRAY_BUFFER类型。

// 创建缓冲区对象
const indexBuffer = gl.createBuffer();
// webgl关联缓冲区对象
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// 顶点数据写入缓冲区对象
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indeces, gl.STATIC_DRAW);

    4、drawElements方法

        同样的,在绘制的时候,也不使用我们常用的drawArrays方法,而是换成drawElements方法。

image.png

let eyex = 3;
let eyey = 3;
let eyez = 5;
let deg = 0;

function draw() {
  deg += 0.01;
  const rotate = getRotateMatrix(deg);
  const vm = getViewMatrix(eyex,eyey,eyez,0.0,0.0,0.0,0.0,0.6,0.0);
  const perspective = getPerspective(30, ctx.width / ctx.height, 100, 1);
  gl.enable(gl.DEPTH_TEST);
  gl.uniformMatrix4fv(mat, false, mixMatrix(mixMatrix(perspective, vm), rotate));
  gl.drawElements(gl.TRIANGLES, indeces.length, gl.UNSIGNED_BYTE, 0);
  requestAnimationFrame(draw)
}
draw();

a2c35c5f-974f-4030-a1ab-fe953ecf562f.gif

5.5.3 设置立方体颜色

    1、调整顶点数据

        第一步要先按照每个面的4个顶点组合在一起,根据下图调整顶点数组:

image.png

// 创建三角形系列数据
const vertices = new Float32Array([
  // 0123  前面
  1, 1, 1,
  -1, 1, 1,
  -1,-1, 1,
  1,-1, 1,
  // 0345  右面
  1, 1, 1,
  1,-1, 1,
  1,-1,-1,
  1, 1,-1,
  // 0156  上面
  1, 1, 1,
  1, 1, -1,
  -1, 1,-1,
  -1, 1,1,
  // 1267  左面
  -1, 1, 1,
  -1,1, -1,
  -1, -1,-1,
  -1,-1,1,
  // 2347  下面
  -1,-1, 1,
  1,-1, 1,
  1,-1,-1,
  -1,-1,-1,
  // 4567  后面
  1,-1,-1,
  1, 1,-1,
  -1, 1,-1,
  -1,-1,-1,
]);

    2、设置颜色数据

        颜色数据由rgb组成,也就是每3个数据表示一个颜色,由于一个面有4个顶点,因此每一行设置4个相同的颜色。

const colors = new Float32Array([
  0.4,0.4,1.0,0.4,0.4,1.0,0.4,0.4,1.0,0.4,0.4,1.0,
  0.4,1.0,0.4,0.4,1.0,0.4,0.4,1.0,0.4,0.4,1.0,0.4,
  1.0,0.4,0.4,1.0,0.4,0.4,1.0,0.4,0.4,1.0,0.4,0.4,
  1.0,1.0,0.4,1.0,1.0,0.4,1.0,1.0,0.4,1.0,1.0,0.4,
  1.0,0.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,
  0.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,
])

    3、创建颜色缓冲区

const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aColor)

    4、修改索引数组

        索引得根据顶点数据进行调整,因为目前顶点的每个面的顺序有所调整,因此索引值也随之调整即可。

// 创建三角形系列数据
const vertices = new Float32Array([
  // 0123  前面
  1, 1, 1,
  -1, 1, 1,
  -1,-1, 1,
  1,-1, 1,
  // 0345  右面
  1, 1, 1,
  1,-1, 1,
  1,-1,-1,
  1, 1,-1,
  // 0156  上面
  1, 1, 1,
  1, 1, -1,
  -1, 1,-1,
  -1, 1,1,
  // 1267  左面
  -1, 1, 1,
  -1,1, -1,
  -1, -1,-1,
  -1,-1,1,
  // 2347  下面
  -1,-1, 1,
  1,-1, 1,
  1,-1,-1,
  -1,-1,-1,
  // 4567  后面
  1,-1,-1,
  1, 1,-1,
  -1, 1,-1,
  -1,-1,-1,
]);
const indeces = new Uint8Array([
  0,1,2,0,2,3,
  4,5,6,4,6,7,
  8,9,10,8,10,11,
  12,13,14,12,14,15,
  16,17,18,16,18,19,
  20,21,22,20,22,23,
])

        注意:这里的索引并不是顶点的下标,而是现在顶点数组里的索引。例如012023指的是顶点数组里"前面"的第0个到第3个下标的顶点,而456467指的是顶点数组里"右面"的第4个到第7个下标的顶点,以此类推。

54bd0b73-85fd-4560-8383-52a3c417d44f.gif

5.5.4 代码示例