3、绘制基础图形:点、线、三角形、矩形、多边形
好,咱们今天来点真家伙。上一章我们把三角形画出来了,但说实话,那只是开胃菜。真正干活的时候,你得面对各种形状——点、线、三角形、矩形,甚至多边形。这一章,我就带你把这些基础图形挨个撸一遍。
我个人习惯把WebGL的绘制方式分成两类:gl.drawArrays 和 gl.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高效省内存,适合复杂模型。- 三角形是万能的,任何图形都能拆成三角形。
下一章,我们会聊颜色和纹理。到时候你会发现,光有形状还不够,得给它们穿上衣服才好看。