Skip to content

上节带领大家学习了基本三角形图元的绘制方法,并讲解了如何使用缓冲区向着色器传递多种数据,本节我们开始学习如何使用三角形构建矩形。

目标

上节我们通过创建多个 buffer 实现渐变三角形的绘制,本节我们以矩形为例,掌握用三角形构建平面的方法。

本节示例较多,因此将演示地址源码地址放在相应段落中,此处暂不列举。

通过本节学习,你会掌握如下内容:

  • 通过基本三角形绘制矩形的思路。
  • 索引绘制的使用方法。
  • 使用三角带绘制矩形。
  • 使用三角扇绘制矩形。
  • 绘制圆形。
  • 绘制环形。
  • 顶点顺序的不同有什么影响。

基本三角形构建矩形

我们知道,一个矩形其实可以由两个共线的三角形组成,即 V0, V1, V2, V3,其中 V0 -> V1 -> V2 代表三角形A,V0 -> V2 -> V3代表三角形B。

请谨记,组成三角形的顶点要按照一定的顺序绘制。默认情况下,WebGL 会认为顶点顺序为逆时针时代表正面,反之则是背面,区分正面、背面的目的在于,如果开启了背面剔除功能的话,背面是不会被绘制的。当我们绘制 3D 形体的时候,这个设置很重要。关于背面剔除功能,我们在绘制立方体章节再进行讲解。

着色器

着色器部分和上节绘制三角形一样,没有变动。

  • 顶点着色器
    • a_Position
    • a_Color
    • a_Screen_Size
    • v_Color
  • 片元着色器
    • v_Color

JavaScript 部分

仍然从简单之处着手,绘制固定顶点的矩形。

首先准备组成矩形的三角形,每个三角形由三个顶点组成,两个矩形共需要六个顶点。

javascript
var positions = [
	30, 30, 255, 0, 0, 1,    //V0
	30, 300, 255, 0, 0, 1,   //V1
	300, 300, 255, 0, 0, 1,  //V2
	30, 30, 0, 255, 0, 1,    //V0
	300, 300, 0, 255, 0, 1,  //V2
	300, 30, 0, 255, 0, 1    //V3
]

我们给两个三角形设置不同颜色,其中,V0->V1->V2 三角形设置为红色, VO->V2->V3 三角形设置为绿色。

本节我们依然用单 buffer 来处理数据传递过程。

代码和上节基本一致,只是我们的顶点数组 positions 不再是动态更新的,而是固定的。

我们看下效果:

很简单,我们用两个基本三角形就实现了矩形的绘制。

索引方式绘制

不知道大家有没有发现,我们在绘制一个矩形的时候,实际上只需要 V0, V1, V2, V3 四个顶点即可,可是我们却存储了六个顶点,每个顶点占据 4 * 6 = 24 个字节,绘制一个简单的矩形我们就浪费了 24 * 2 = 48 字节的空间,那真正的 WebGL 应用都是由成百上千个,甚至几十万、上百万个顶点组成,这个时候,重复的顶点信息所造成的内存浪费就不容小觑了。

那有没有其他的方式改进一下呢?

答案当然是肯定的,WebGL 除了提供 gl.drawArrays 按顶点绘制的方式以外,还提供了一种按照顶点索引进行绘制的方法:gl.drawElements,使用这种方式,可以避免重复定义顶点,进而节省存储空间。我们看下 gl.drawElements 的使用方法,详细解释参见MDN

void gl.drawElements(mode, count, type, offset);

  • mode:指定绘制图元的类型,是画点,还是画线,或者是画三角形。
  • count:指定绘制图形的顶点个数。
  • type:指定索引缓冲区中的值的类型,常用的两个值:gl.UNSIGNED_BYTEgl.UNSIGNED_SHORT,前者为无符号8位整数值,后者为无符号16位整数。
  • offset:指定索引数组中开始绘制的位置,以字节为单位。

举例来说:

javascript
gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_BYTE, 0);

这段代码的意思是:采用三角形图元进行绘制,共绘制 3 个顶点,顶点索引类型是 gl.UNSIGNED_BYTE,从顶点索引数组的开始位置绘制。

使用 drawElements 绘制矩形

纸上得来终觉浅,绝知此事要躬行,我们改进下绘制矩形的例子,来学习 drawElements 的用法。

着色器

着色器部分依然不需要改动。

JavaScript部分

我们的 JavaScript 部分要有所改变了,采用索引绘制方式,我们除了准备存储顶点信息的数组,还要准备存储顶点索引的数组。

javascript
//存储顶点信息的数组
var positions = [
    30, 30, 255, 0, 0, 1,    //V0
    30, 300, 255, 0, 0, 1,   //V1
    300, 300, 255, 0, 0, 1,  //V2
    300, 30, 0, 255, 0, 1    //V3
];
//存储顶点索引的数组
var indices = [
    0, 1, 2, //第一个三角形
    0, 2, 3  //第二个三角形
];

除了多准备一个数组容器存储顶点索引以外,我们还需要将索引传递给 GPU,所以,仍然需要创建一个索引 buffer.

javascript
var indicesBuffer = gl.createBuffer();

按照惯例,创建完 buffer,我们需要绑定,这里要和 ARRAY_BUFFER 区分开来,索引 buffer 的绑定点是gl.ELEMENT_ARRAY_BUFFER

javascript
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);

接下来,我们就可以往 indicesBuffer 中传入顶点索引了:

javascript
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

之后执行绘制操作:

javascript
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

我们看下效果:

矩形能够绘制出来,但是颜色和我们之前的矩形有些不同,第二个三角形从红到绿渐变。

仔细回顾一下,我们用 drawArrays 进行绘制的时候,使用了六个顶点,每个三角形的顶点颜色一致,所以两个三角形的颜色都是单一的。 当采用 drawElements 方法进行绘制的时候,使用了四个顶点,第二个三角形的两个顶点 V0、V2 是红色的,第三个顶点 V3 是绿色的,所以造成了从 V0、V2 向 V3 的红绿渐变。

如果我们必须要实现两个不同颜色的单色三角形,还是应该用六个顶点来绘制,这时,使用 drawArrays 的方式更优一些。毕竟,不用创建索引数组和索引缓冲。

使用三角带构建矩形

我们学习了使用基本三角形绘制矩形的方法,接下来我们尝试一下使用三角带绘制矩形。

前面我们讲过,三角带的绘制特点是前后两个三角形是共线的,并且我们知道顶点数量与绘制的三角形的数量之间的关系是:

顶点数或者索引数 = 三角形数量 + 2

仍然以绘制矩形为目标,如果采用基本三角形进行绘制的话,需要准备六个顶点,即两个三角形。那如果采用三角带进行绘制的话,利用三角带的特性,我们实际需要的顶点数为 2 + 2 = 4,即矩形的四个顶点位置。

切记,顶点顺序不能乱哦。

画图看下组成矩形的三角形顶点的顺序

由上图可以看出,绘制三角带图元的时候,V0->V1->V2 组成第一个三角形,V2->V1->V3 组成第二个三角形。

三角带与基本三角形绘制在代码上的区别有两点:

  • 顶点数组的数据不同。
  • drawArrays 的第一个参数代表的图元类型不同。
    • 基本三角形:TRIANGLES。
    • 三角带:TRIANGLE_STRIP。
    • 三角扇:TRIANGLE_FAN。

关键代码

先看顶点数组:

javascript
var positions = [
    30, 300, 255, 0, 0, 1,   //V0  
    300, 300, 255, 0, 0, 1,  //V1
    30, 30, 255, 0, 0, 1,    //V2
    300, 30, 0, 255, 0, 1    //V3
]

再看绘制方法:

javascript
gl.drawArrays(gl.TRIANGLE_STRIP, 0, );

绘制方法改动不大,我们看下效果:

读者可能会问了,我能不能用 V1->V2->V0 绘制第一个三角形,V0->V2->V3绘制第二个三角形呢?

如果按照这个顺序绘制的话,按照三角带的绘制特点,V0->V2 这条线段是第二个三角形和第一个三角形的共线。

先不急着编码验证,我们画图看下效果:

如果你太确定,我们验证一下:

javascript
var positions = [
   300, 300, 255, 0, 0, 1,  //V1
   30, 30, 255, 0, 0, 1,    //V2
   30, 300, 255, 0, 0, 1,   //V0
   300, 30, 0, 255, 0, 1    //V3
]

我们把 V0 移到第三个位置,看下效果:

这和我们推演的效果相同,可以看出,使用三角带进行绘制时,一定要注意顶点的顺序,顶点顺序稍有差错,绘制出来的效果就与实际期待的大不相同。

三角扇绘制矩形

基本三角形和三角带绘制矩形的原理讲完了,还有一种图元方式:三角扇,它是否也能绘制矩形?

我们在绘制三角形章节讲到,三角扇是围绕着第一个顶点作为公共顶点绘制三角形的,并且使用三角扇绘制出来的三角形的数量和顶点数量之间的关系和三角带一样:

顶点数或者索引数 = 三角形数量 + 2

我们看下三角扇绘制矩形时的顶点分布以及顺序:

可以看出,使用三角扇需要绘制 4 个三角形,相应地顶点数量为 6 个:

三角形顶点组成
左边三角形V0 -> V1 -> V2
上边三角形V0 -> V2 -> V3
右边三角形V0 -> V3 -> V4
下边三角形V0 -> V4 -> V1

需要的顶点数组为

javascript
var positions = [
    165, 165, 255, 255, 0, 1, //V0
    30, 30, 255, 0, 0, 1,    //V1
    30, 300, 255, 0, 0, 1,   //V2
    300, 300, 255, 0, 0, 1,  //V3
    300, 30, 0, 255, 0, 1,   //V4
    30, 30, 255, 0, 0, 1,    //V1
]

绘制方式改为三角扇:

javascript
gl.drawArrays(gl.TRIANGLE_FAN, 0, positions.length / 6);

效果如下:

可以很明显的看出四个三角形都以中心点为顶点。

顶点顺序

其实不管是使用三角扇还是基本三角形,又或者是三角带绘制的时候,一定要保证顶点顺序是逆时针。如果三角形的顶点顺序不是逆时针,在开启背面剔除功能后,不是逆时针顺序的三角形是不会被绘制的。

我们不妨试一下,改变顶点顺序,将他们之间的关系从逆时针改为顺时针:

javascript
var positions = [
    165, 165, 255, 255, 0, 1, //V0
    30, 300, 255, 0, 0, 1,   //V2
    30, 30, 255, 0, 0, 1,    //V1
]

如果不开启背面提剔除功能,会发现三角形依然能够绘制,不受顺序的影响。

开启背面剔除功能:

javascript
gl.enable(gl.CULL_FACE);

开启后,可以发现页面空空如也,三角形没有被绘制。

当然,我们也可以更改面的显示方式,默认显示正面,我们可以通过如下方式,剔除正面,只显示背面:

javascript
gl.cullFace(gl.FRONT);

绘制圆形

矩形比较简单,两个三角形拼接起来就可以了。那常见的圆形该如何绘制呢?聪明的同学可能已经想到了:将圆形分割成以圆心为共同顶点的若干个三角形,三角形数越多,圆形越平滑。

如上图所示,我们将圆形划分成 12 个三角形,13 个顶点,我们需要计算每个顶点的坐标,我们定义一个生成圆顶点的函数:

  • x:圆心的 x 坐标
  • y:圆心的 y 坐标
  • radius:半径
  • n:三角形的数量
javascript
    var sin = Math.sin;
    var cos = Math.cos;
    function createCircleVertex(x, y, radius, n) {
      var positions = [x, y, 255, 0, 0, 1];
      for (let i = 0; i <= n; i++) {
        var angle = i * Math.PI * 2 / n;
        positions.push(x + radius * sin(angle), y + radius * cos(angle), 255, 0, 0, 1);
      }
      return positions;
    }
   var positions = createCircleVertex(100, 100, 50,  12);

将圆划分成 12 个三角形的效果:

有棱有角,不太自然,我们将圆切分成 50 个三角形试试:

javascript
var positions = createCircleVertex(100, 100, 50,  50);

效果如下:

可以看出, 50 个三角形组成的圆更加自然一些,三角形面数越多,画出的图形越自然越平滑,但是我们也不能无限划分,毕竟三角形的数量越多,顶点数量相应的变多,内存占用会变大。在绘制规则图形的时候,我们需要在图形显示效果与顶点数量之间做一个权衡。

绘制环形

再深入一下,我们看看环形如何绘制,动手画画图,应该能想到:

建立两个圆,一个内圆,一个外圆,划分n个近似于扇形的三角形,每个三角形的两条边都会和内圆和外圆相交,产生四个交点,这四个交点组成一个近似矩形,然后将近似矩形划分成两个三角形:

javascript
function createRingVertex(x, y, innerRadius, outerRadius, n) {
    var positions = [];
    var color = randomColor();
    for (var i = 0; i <= n; i++) {
        if (i % 2 == 0) {
            color = randomColor();
        }
        var angle = i * Math.PI * 2 / n;
        positions.push(x + innerRadius * sin(angle), y + innerRadius * cos(angle), color.r, color.g, color.b, color.a);
        positions.push(x + outerRadius * sin(angle), y + outerRadius * cos(angle), color.r, color.g, color.b, color.a);
    }
    var indices = [];
    for (var i = 0; i < n; i++) {
        var p0 = i * 2;
        var p1 = i * 2 + 1;
        var p2 = (i + 1) * 2 + 1;
        var p3 = (i + 1) * 2;
        if (i == n - 1) {
          p2 = 1;
          p3 = 0;
        }
        indices.push(p0, p1, p2, p2, p3, p0);
    }
    return { 
        positions: positions, 
        indices: indices 
    };
}

上面这个方法能够根据内圆半径和外圆半径以及三角形的数量返回顶点数组和索引数组,我们生成 100 个三角形的信息。

javascript
var geo = createRingVertex(100, 100, 20, 50, 100);

为了节省空间,我们采用索引绘制:

javascript
gl.drawElements(gl[currentType], indices.length, gl.UNSIGNED_SHORT, 0);

效果如下:

回顾

我们清楚了各种形状的平面其实都可以通过三角形图元组装而成,大家或许会说了,这样岂不是很累啊?确实很累,只不过实际应用中,我们往往都是通过 3D 建模软件为我们生成顶点、索引、颜色等信息。本节通过代码来生成模型顶点信息,是为了培养大家在绘制复杂图形时用三角形进行拆分的意识。

本节我们学习了使用基本三角形、三角带、三角扇绘制矩形、圆形、环形的方法,以及它们之间的使用区别,同时还学习了使用索引绘制的技巧,了解了顶点顺序的重要性。

下一节,我为大家介绍纹理贴图,学习如何将图片应用到平面上。