乘风破浪的WebGL系列-使用 WebGL 构建基础 2d 几何形体

avatar
Web前端 @CVTE_希沃

希沃ENOW大前端

公司官网:CVTE(广州视源股份)

团队:CVTE旗下未来教育希沃软件平台中心enow团队

本文作者:

前言

WebGL 没有提供一些基础的 API 来帮助你构建矩形、圆形或则立方体和球体等基础几何形体,如果你想用原生的 WebGL 来绘制这些形体的话,就需要了解 WebGL 支持直接绘制的基础图元,以及如果通过这些基础图元去构建更加复杂的几何形体。

基础图元

图元(primitive) 是指绘制元素的最基础图形元素,在 WebGL 中绘制各种复杂的形状,最终都是由这些基础图元所组成。在图形学界,对 API 支持哪些图元,存在两种声音,一种是系统需要提供少数几种所用硬件都能支持且功能互不重叠的图元,同时硬件需要能够高效地生成这些图元;一种是系统需要支持常用的各种图元,包括文本、曲线、矩形、圆和曲面等,从而使用户能够更轻松地构建复杂应用。在 WebGL 中,没有提供矩形和圆形等常用图形而是提供了 7 种图元,进行归类,可分成三种类型,即点、线(线、条带线、循环线)和三角形(三角形、三角带、三角扇面),通过这几种图元,由开发者自行组合出其它复杂的形状。接下来我们来具体讲讲这几种图元。

WebGL 中默认点(POINTS)的大小为 1.0,因此使用默认值时,每个点实际上就是屏幕上的一个像素,你也可以通过设置 gl_pointsize  来控制点的大小,如下图所示,绘制不同大小的点:

线

WebGL 中的线指的是线段(LINES),一条独立的线段,由两个点来表示,多个线段可以相互连接形成一系列线段,这些线段连接在一起且最终是开放的,则叫作 条带线(LINE_STRIP),如果最终最终首尾相连则叫做 循环线 (LINE_LOOP),如下图所示:

需要注意的是,使用线段图元绘制出的线段默认宽度是 1px,且 WebGL 没有提供可以设置线宽的 API,因此如果要实现带宽度的线段,还需要使用其它复杂的方法,这里就不多做介绍了。

三角形

三角形(TRIANGLES)由不共线的三点两两连接,所组成的一个闭合的平面图形,在外观上,三角形和上面提到的循环线,通过闭合折线生成的三角形外观上相同,但是前者可以通过填充模式绘制三角形,而后者不可以。 多个三角形通过共享顶点和边形成了三角带(TRIANGLE_STRIP)和三角扇面(TRIANGLE_FAN),如下图所示

三角形是以每三个顶点绘制一个三角形,例如第一个三角形使用顶点(v0,v1,v2),则第二个使用(v3,v4,v5),以此类推。如果顶点个数小于 3 则,则三角形不会被绘制,如果顶点的个数 n 不是 3 的倍数,那么最后的一个或者两个顶点会被忽略。如下利用(v0,v1,v2)绘制三角形:


// 定义了三角形的三个顶点坐标(v0, v1, v2),以中心点为坐标原点,z 轴为 0
var data = [
  -0.5, 0.5, 0.0,
  -0.5, -0.5, 0.0,
  0.5, 0.5, 0.0,
];
sendDataToSharder(gl, data);
gl.drawArrays(gl.TRIANGLE, 0, 3);

三角带利用从第一个点开始的前三个点构成第一个三角形,从第二个点开始的三个点构成第二个三角形,因此绘制有两个三角形组成的三角带,总共只需要用到 4 个点,相应的代码片段和效果如下:

// 定义了三角带用到的四个顶点坐标(v0, v1, v2,v3)
var data = [
  -0.5, 0.5, 0.0,
  -0.5, -0.5, 0.0,
  0.5, 0.5, 0.0,
  0.5, -0.5, 0.0
];
sendDataToSharder(gl, data);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

三角扇面利用从第一个点开始的前三个点构成第一个三角形,接下来的一个点(第四个点)和前一个三角形的最后一条边组成第二个三角形,比如给出四个顶点(v0,v1,v2,v3),第一个三角形是(v0,v1,v2),最后一条边是(v2,v0)即终点和起点的连线,则第三个三角形是(v2,v0,v3),相应的代码片段和效果如下:

var data = [
  -0.5, 0.5, 0.0,
  -0.5, -0.5, 0.0,
  0.5, 0.5, 0.0,
  0.5, -0.5, 0.0
];
sendDataToSharder(gl, data);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

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

基础图元绘制方法

WebGL 提供了两个方法用于绘制基础图元,既 drawArrays 和 drawElements 

drawArray

前面基础图元部分的代码示例,我们就已经使用到了 drawArrays ,其用于根据指定的顶点数组绘制相应的图元,其函数定义和参数说明如下: void gl.drawArrays(mode, first, count); 

参数描述
mode指定绘制的方式,可接收以下常量符号∶ gl.POINTS、gl.LINES,g1.LINE_STRIP、g1.LINELOOP、g1.TRIANGLES、gl.TRIANGLE STRIP、gl.TRIANGLE_FAN
first指定从第几个顶点开始绘制
count指定绘制需要用到多少个顶点

使用 drawArrays 方法,利用 TRIANGLES 图元绘制一个矩形,代码如下:

// 传入六个顶点数据(v0, v1, v2, v0, v2, v3)
var data = [
  -0.5, 0.5, 0.0, // v0
  -0.5, -0.5, 0.0, // v1
  0.5, 0.5, 0.0, // v2
  -0.5, 0.5, 0.0, // v0
  0.5, 0.5, 0.0, // v2
  0.5, -0.5, 0.0, // v3
];
      
sendDataToSharder(gl, data);
gl.drawArrays(gl.TRIANGLES, 0, data.length / 3);

drawElements

仔细观察上面 drawArrays 的代码不难发现,一个矩形只有四个顶点,即 (v0, v1, v2, v3),但实际我们传入到缓冲区的却要用到六个顶点数据(v0, v1, v2, v0, v2, v3),冗余了 2 个顶点信息,即浪费了 2 * (4 * 3) 共 24 个字节,随着总顶点数的增多,内存浪费就更严重。 drawElements 提供了按顶点索引进行绘制的功能,使用这个方法,就可以避免重复地定义顶点,其函数定义和参数说明如下: void gl.drawElements(mode, count, type, offset);

参数描述
mode指定绘制的方式,可接收以下常量符号∶ gl.POINTS、gl.LINES,g1.LINE_STRIP、g1.LINELOOP、g1.TRIANGLES、gl.TRIANGLE STRIP、gl.TRIANGLE_FAN
count指定绘制需要用到多少个顶点
type指定元素数组缓冲区中的值的类型。可能的值是: gl.UNSIGNED_BYTE、gl.UNSIGNED_SHORT
offset指定元素数组缓冲区中的偏移量。必须是给定类型大小的有效倍数

还是以上面绘制矩形为例,使用 drawElements 改进后的代码如下:

// 传入四个顶点数据(v0, v1, v2, v3)
var data = [
  -0.5, 0.5, 0.0, // v0
  -0.5, -0.5, 0.0, // v1
  0.5, 0.5, 0.0, // v2
  0.5, -0.5, 0.0, // v3
];
// 生成索引数据
var indices = [
  0, 1, 2, //第一个三角形
  0, 2, 3, //第二个三角形
];
// 创建新的缓冲区写入索引数据
function bindIndices(gl, indices) {
  var indicesBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
}
sendDataToSharder(gl, data);
bindIndices(gl, indices);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

构建几何形体

使用线段绘制多边形

利用线段图元我们可以构建出各种多边形的线框 image.png image.png image.png

const center = [0, 0] // 圆心
const radius = 0.5 // 半径
const count = 12 // 多边形边数
const positions = []

for (let i = 0; i <= count; i++) {
  const angle = (Math.PI * 2 * i) / count
  const pointX = radius * Math.cos(angle) + center[0]
  const pointY = radius * Math.sin(angle) + center[1]
  positions.push(pointX, pointY, 0)
}

 gl.drawArrays(gl.LINE_LOOP, 0, positions.length / 3)

使用三角剖分填充多边形

对一般的多边形进行填充会可能会出现问题,因为多边形的顶点可能不在同一个平面,但如果多边形是三角形的话就不会存在问题,因为三角形的定义是 三角形是由同一平面内不在同一直线上的三条线段'首尾'顺次连接所组成的封闭图形 。因此,三角形是 WebGL 能识别并可填充的唯一几何实体。因此,在 WebGL 中填充多边形的第一步,就是将多边形分割成多个三角形。这种将多边形分割成若干个三角形的操作,在图形学中叫做 三角剖分  (Triangulation)。 image(3).jpg 对简单多边形进行三角剖分比较容易,而复杂多边形由于有边的相交和面积重叠区域,所以相对困难许多,因此通常我们可以借助一些第三方库,如 earcut 来进行三角剖。比如我们要对如下一个多边行进行三角剖分:

首先我们要列出他的顶点数据,然后借助 earcut 生成三角剖分后的相应的索引数据,利用前面学的 drawElement 索引绘制的方法进行绘制即可,示例代码如下:

const vertices = [
  -0.7, 0.5, 0,
  -0.4, 0.3, 0,
  -0.25, 0.71, 0,
  -0.1, 0.56, 0,
  -0.1, 0.13, 0,
  0.4, 0.21, 0,
  0, -0.6, 0,
  -0.3, -0.3, 0,
  -0.6, -0.3, 0,
  -0.45, 0.0, 0,
];
var indices = earcut(vertices, null, 3);
sendDataToSharder(gl, program, vertices);
bindIndices(gl, indices);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

image.png

参考资料

  1. 交互式计算机图形学——基于WEBGL的自顶向下方法(第7版)
  2. WebGL 编程指南
  3. 如何利用三角剖分和向量操作描述并处理多边形 time.geekbang.org/column/arti…
  4. 掘金小册《webGL 入门与实践》- 用基本图形构建平面 juejin.cn/book/684473…