3、绘制基础图形:点、线、三角形、矩形、多边形

好,咱们今天来点真家伙。上一章我们把三角形画出来了,但说实话,那只是开胃菜。真正干活的时候,你得面对各种形状——点、线、三角形、矩形,甚至多边形。这一章,我就带你把这些基础图形挨个撸一遍。

我个人习惯把WebGL的绘制方式分成两类:gl.drawArraysgl.drawElements。前者简单粗暴,后者灵活高效。咱们先讲前者,再讲后者,最后对比一下什么时候该用谁。

3.1 从点开始:最简单的图形

点是最基本的图元。在WebGL里,一个点就是一个顶点。你可能会想:「点有什么好画的?」嗯,我在项目里还真遇到过——有一次做粒子系统,几千个点同时渲染,性能压力不小。那时候我才意识到,点虽然简单,但用好了能玩出花来。

画点的代码其实就一行:

gl.drawArrays(gl.POINTS, 0, 1);

这里的 gl.POINTS 告诉WebGL我们要画点,0 是从第0个顶点开始,1 是画1个点。如果你想画10个点,就把第三个参数改成10。

小技巧: 点的大小可以在顶点着色器里用 gl_PointSize 控制。我一般设成10.0左右,太小了看不清,太大了又像坨屎。

3.2 画线:连接两个点

线比点稍微复杂一点。WebGL提供了三种画线模式:

模式 说明 示例
gl.LINES 每两个顶点画一条独立线段 顶点0-1画一条,2-3画一条
gl.LINE_STRIP 连接所有顶点形成一条折线 0-1-2-3 连起来
gl.LINE_LOOP 类似LINE_STRIP,但首尾相连 0-1-2-3-0 闭合

举个例子,画一条从(0,0)到(0.5,0.5)的线:

const vertices = new Float32Array([
  0.0, 0.0,
  0.5, 0.5
]);
// ... 绑定缓冲区、传数据 ...
gl.drawArrays(gl.LINES, 0, 2);

我曾经在做一个地图标注项目时,用 LINE_STRIP 画路径轨迹。当时踩了个坑——线宽在WebGL里默认是1像素,想改宽?对不起,大部分浏览器不支持 gl.lineWidth()。后来我改用三角形条带模拟粗线,才解决了这个问题。

注意: gl.lineWidth() 在大多数WebGL实现中只支持1.0。别指望它能像桌面OpenGL那样随意调粗细。如果你需要粗线,考虑用三角形代替。

3.3 三角形:WebGL的基石

三角形是WebGL里最重要的图元。为什么?因为GPU就是为三角形优化的。任何复杂的3D模型,最终都会被拆成三角形来渲染。

画一个三角形很简单:

const vertices = new Float32Array([
  -0.5, -0.5,
   0.5, -0.5,
   0.0,  0.5
]);
// ... 绑定缓冲区 ...
gl.drawArrays(gl.TRIANGLES, 0, 3);

WebGL提供了三种三角形模式:

  • gl.TRIANGLES:每三个顶点画一个独立三角形。6个顶点画2个三角形。
  • gl.TRIANGLE_STRIP:条带模式,每新增一个顶点就和前两个顶点组成新三角形。画4个顶点能得到2个三角形。
  • gl.TRIANGLE_FAN:扇形模式,所有三角形共享第一个顶点。适合画圆形、扇形。

我个人最喜欢用 TRIANGLE_STRIP,因为它省顶点。比如画一个矩形,用 TRIANGLES 需要6个顶点,用 TRIANGLE_STRIP 只需要4个。省顶点就是省内存,省带宽,省性能。

3.4 矩形:两个三角形拼出来的

WebGL没有直接的「矩形」图元。你得用两个三角形拼。说白了,矩形就是两个三角形共享一条边。

TRIANGLES 画矩形:

const vertices = new Float32Array([
  // 第一个三角形
  -0.5, -0.5,
   0.5, -0.5,
  -0.5,  0.5,
  // 第二个三角形
   0.5, -0.5,
   0.5,  0.5,
  -0.5,  0.5
]);
gl.drawArrays(gl.TRIANGLES, 0, 6);

你看,顶点 (-0.5, -0.5)(0.5, -0.5) 重复出现了。这就是浪费。用 TRIANGLE_STRIP 可以优化:

const vertices = new Float32Array([
  -0.5, -0.5,
   0.5, -0.5,
  -0.5,  0.5,
   0.5,  0.5
]);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

4个顶点搞定。但这里有个坑——顶点顺序很重要。 TRIANGLE_STRIP 的顶点顺序是:0-1-2组成第一个三角形,1-2-3组成第二个。如果你顺序搞反了,出来的图形会扭曲。

3.5 多边形:用三角剖分搞定

多边形就更有意思了。WebGL原生不支持五边形、六边形这些。你得自己把多边形拆成三角形。这个过程叫「三角剖分」。

举个简单的例子,画一个五边形:

// 五边形的5个顶点(中心在原点,半径0.5)
const vertices = new Float32Array([
  0.0,  0.5,    // 顶点0
  0.475, 0.154, // 顶点1
  0.293, -0.404,// 顶点2
  -0.293, -0.404,// 顶点3
  -0.475, 0.154  // 顶点4
]);

// 用TRIANGLE_FAN,以顶点0为中心
gl.drawArrays(gl.TRIANGLE_FAN, 0, 5);

这里用 TRIANGLE_FAN 最合适。第一个顶点作为扇形的中心,后续每个顶点和中心、前一个顶点组成三角形。5个顶点就能画出五边形。

如果是凹多边形,事情就复杂了。我建议你用现成的三角剖分库,比如 earcut。我在做GIS项目时,处理过几千个顶点的复杂多边形,手动剖分太痛苦了,用库省心很多。

3.6 gl.drawElements:索引绘制的威力

好了,前面讲的都是 gl.drawArrays。现在聊聊 gl.drawElements。这玩意儿才是真正的高效方案。

它的核心思想是:顶点数据存一份,用索引来引用。还是拿矩形举例:

// 顶点数据(不重复)
const vertices = new Float32Array([
  -0.5, -0.5,  // 顶点0
   0.5, -0.5,  // 顶点1
   0.5,  0.5,  // 顶点2
  -0.5,  0.5   // 顶点3
]);

// 索引数据(告诉WebGL怎么连)
const indices = new Uint16Array([
  0, 1, 2,  // 第一个三角形
  0, 2, 3   // 第二个三角形
]);

// 绑定索引缓冲区
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

// 绘制
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

你看,顶点只存了4个,但通过索引引用了6次。如果是一个复杂的3D模型,顶点共享率能达到70%以上。省下来的内存可不是一星半点。

核心区别:
  • gl.drawArrays:直接按顶点顺序绘制。简单,但顶点可能重复。
  • gl.drawElements:通过索引引用顶点。复杂,但高效,省内存。

我建议你养成习惯:只要顶点有重复,就用 gl.drawElements。我在做第一个3D项目时,傻乎乎地用 drawArrays 画了一个立方体,24个顶点(每个面4个,6个面)。后来改用索引,只需要8个顶点。性能提升肉眼可见。

3.7 实战对比:什么时候用哪个?

场景 推荐方式 原因
粒子系统(几千个点) gl.drawArrays 每个点独立,没有共享顶点
简单三角形/矩形 两者皆可 顶点少,差别不大
复杂3D模型 gl.drawElements 顶点共享率高,省内存
动态更新顶点 gl.drawArrays 索引缓冲区更新麻烦

嗯,这里要注意一点:gl.drawElements 的索引类型建议用 UNSIGNED_SHORT(16位),最多支持65536个顶点。如果你的模型顶点超过这个数,就得用 UNSIGNED_INT(32位),但要注意浏览器兼容性。

好了,这一章的内容就到这里。总结一下:

  • 点、线、三角形、矩形、多边形,WebGL都能画,但各有各的套路。
  • gl.drawArrays 简单直接,适合顶点不重复的场景。
  • gl.drawElements 高效省内存,适合复杂模型。
  • 三角形是万能的,任何图形都能拆成三角形。

下一章,我们会聊颜色和纹理。到时候你会发现,光有形状还不够,得给它们穿上衣服才好看。