2.2 顶点图元:CPU 组装 vs GPU 消费 本节摘要:GPU 不认识“立方体”或“人物”,它只认识绘制模式提交的图元:点、线、三角形。顶点位置、法线、UV 由 CPU 填进数组,拓扑由 、 等模式决定,索引则让多个三角形共用同一顶点。CPU 负责组装这份几何合同,GPU 在顶点着色之后按合同消费。选错图元模式或把索引当位置,画面会碎成无法辨认的折纸。 核心问题 阅读完本节,你应当能够: 列出 WebGL 常用图元模式,并说明各自顶点数量与连接规则 对照 顺序消费与 按索引复用 解释为什么三角形是 3D 网格的通货,而四边形不是一等公民 计算一份网格在“重复顶点”与“索引复用”两种布局下的大致内存差 指出 CPU 侧生成球体网格时,哪些工作不该每帧重做 GPU 只吃图元,不吃模型格式
本节摘要:GPU 不认识“立方体”或“人物”,它只认识绘制模式提交的图元:点、线、三角形。顶点位置、法线、UV 由 CPU 填进数组,拓扑由
TRIANGLES、TRIANGLE_STRIP等模式决定,索引则让多个三角形共用同一顶点。CPU 负责组装这份几何合同,GPU 在顶点着色之后按合同消费。选错图元模式或把索引当位置,画面会碎成无法辨认的折纸。
阅读完本节,你应当能够:
drawArrays 顺序消费与 drawElements 按索引复用建模软件里的物体有面、有平滑组、有细分曲面。进 WebGL 之后,这些名词全部消失。剩下的是一串数字:每三个浮点一个位置,也许再附法线与 UV,再加一份索引说哪三个编号构成三角形。CPU 是翻译:把 glTF、OBJ 或程序化形状译成这串数字。GPU 是食客:按当前绘制模式一次取 1、2 或 3 个已着色顶点,围成图元。
这解释了为什么“我加载了模型却是空的”常常不是着色器问题,而是翻译合同破了:索引类型是 16 位却有超过 65535 个顶点,或者绘制模式是 TRIANGLE_STRIP 却按独立三角形的数据去喂。GPU 不会报“这不像一只茶壶”,它会认真画出一堆乱三角形。
点、线、三角形覆盖了几乎全部 2D/3D 需求。点适合粒子与调试法线;线适合线框与矢量叠加;三角形适合一切有面积的表面。LINE_LOOP 和 TRIANGLE_FAN 是省顶点的语法糖,遇到索引网格时我更愿意把它们展开成独立三角形,换可预测性。
独立三角形: v0 v1 v2 | v3 v4 v5 | ... 三角形带: v0 v1 v2 -> 下一三角共用边 三角形扇: 中心点 + 一圈外围
| 模式 | 每图元顶点数 | CPU 组装负担 | 典型用途 | 风险 |
|---|---|---|---|---|
| POINTS | 1 | 低 | 粒子、调试 | 点大小在各实现不一致 |
| LINES | 2 | 中 | 独立线段 | 相邻线段不自动相连 |
| LINE_STRIP | 连续 | 低 | 轨迹、描边 | 一处错位后面全歪 |
| TRIANGLES | 3 | 高但清晰 | 通用网格 | 顶点重复多,需索引补 |
| TRIANGLE_STRIP | 复用边 | 低 | 地形条带、UI 矩形 | 绕序翻转要插退化三角形 |
| TRIANGLE_FAN | 复用中心 | 低 | 圆盘 | 凹多边形会自交 |
drawArrays 按数组顺序切图元,简单,适合教学和一次性形状。drawElements 多一张索引表:顶点仓库只存独特顶点,三角形用三个编号去仓库取。立方体若按独立三角形写,8 个角会被写成 36 个顶点(12 个三角形 × 3);用索引则 8 个位置加 36 个索引。法线若要硬边,角上位置相同但法线不同,又不能共用,仓库会膨胀——这是 CPU 组装时的真实权衡:平滑着色省顶点,硬边要拆顶点。
// 概念:同一三角形,两种提交 // 数组路径:顶点按 0,1,2, 0,2,3 重复存储 gl.drawArrays(gl.TRIANGLES, 0, 6); // 索引路径:顶点 4 个,索引 6 个 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
WebGL1 索引常用 UNSIGNED_SHORT,单网格顶点上限约 6.5 万。更大网格要拆,或上 OES_element_index_uint / WebGL2 的 UNSIGNED_INT。这是 CPU 组装阶段就必须做的切割,不是运行时着色器能补的。引擎加载大模型时自动拆 mesh,本质是在履行这份索引合同。
绕序(winding)决定三角形朝前还是朝后。CPU 按逆时针写的面,在 GPU 默认约定里是正面。镜像缩放会把绕序翻过来,于是背面剔除把模型“吃掉一块”。这不是剔除单元的 bug,是组装合同与变换合同冲突。第 3.3 节会对照剔除与深度;这里先强调:绕序是 CPU 写顶点时就签下的,GPU 只执行。
球体、圆柱、格子地面,常常由循环生成。生成是纯 CPU:嵌套循环算出位置与 UV,推进数组,再 bufferData 一次。把它放进 requestAnimationFrame 等于每帧分配大数组、每帧上传,GPU 还没忙,主线程已经在垃圾回收。正确对照是:拓扑不变则 CPU 只组装一次;要变形,优先 VS 里扭(波浪、呼吸),或 WebGL2 变换反馈;只有拓扑本身在变(破坏、布尔)才重切网格。
| 变化种类 | 放哪端 | 理由 |
|---|---|---|
| 整网旋转平移 | Uniform 矩阵 | 拓扑不变,VS 乘一下 |
| 水面高度场 | VS 用公式或高度图 | 顶点还在,位置按程序改 |
| 角色骨骼 | 见第 4.4 节,优先 GPU 蒙皮 | 顶点仓库不变,权重在 Attribute |
| 爆炸成碎片 | CPU 重切或预切 | 拓扑变了,GPU 不能凭空长出新三角形 |
| 粒子新生死亡 | CPU 维护池,或 GPU 用点精灵加反馈 | 点图元让“网格”退化成仓库 |
教学里喜欢用 TRIANGLE_STRIP 画一个矩形两个三角形,省两个顶点。生产网格几乎全是独立 TRIANGLES 加索引,因为建模导出就是这样,合批也简单。我倾向默认独立三角形:调试时用线模式画同一份索引就能出线框,不必维护第二份拓扑。条带留给明确的程序化条状物,例如一条丝带。
⚠️ 常见坑:索引缓冲绑到
ARRAY_BUFFER上,或绘制时类型写UNSIGNED_BYTE却按 16 位填数据。GPU 会按错误步长取编号,画面呈现“随机连线”。绑定目标与类型是合同的一部分,不是装饰。💡 关键直觉:顶点仓库回答“有哪些点”,图元模式和索引回答“怎么连”。着色器永远看不到立方体,它只看到当前这个顶点。
早期固定管线有 QUADS。WebGL 没有。四边形要拆成两个三角形。拆的对角线怎么选,影响变形时是否出现接缝。CPU 组装必须做这个决定。引擎的 PlaneGeometry 看起来像四边形接口,上传时仍是三角形。不要在着色器里假设“第四个顶点还在”,它不在图元里。
第一个彩色三角形值得用 drawArrays 手写三个顶点,把位置与颜色交错或分开存在 CPU 数组里,确认 GPU 按 TRIANGLES 吃三个点。等网格变大,再上索引。不要第一步就上条带和扇,那会把拓扑错误藏进“看起来差不多的三角形”里。
面数与顶点属性布局会一起决定带宽。位置 3 个 float、法线 3 个、UV 2 个,每个顶点 32 字节。一百万顶点就是 32 MB 只读仓库,还不计索引。CPU 组装时就该问:这张网格是否必须这个密度?简化是 CPU 资产管线的事,不是片元着色器能补救的。把高模直接丢进浏览器,两端一起受罚:上传时间在 CPU,变换与带宽在 GPU。
建模软件默认三角化。导出 glTF 时,独立三角形加索引是常态。运行时再把索引展开成 drawArrays 只会涨内存。反过来,为了“省索引”去生成条带,要对艺术家网格做条带化,算法复杂、收益有限,还破坏硬边拆点。生产默认独立三角形,是在用可预测性换一点点顶点重复。重复用索引压,不压拓扑花样。
调试拓扑:把绘制模式临时改成 LINES,用同一份索引。线框能立刻暴露绕序错误、裂缝、重复面。比在 FS 里输出世界位置当颜色更直接。若线框正常、填充缺面,查剔除;若线框就乱,查索引类型与偏移。分清“连错”和“扔错”,对应 2.2 与 3.3。
点图元的大小在 WebGL 里受实现限制,移动端 gl_PointSize 上限可能很小,不能当通用粒子方案。粒子更常见的是相机朝向的小三角或四边形(两个三角)。那是 CPU 组装两张三角,或 VS 里用实例化展开。把 POINTS 当粒子引擎,会在真机上变成几乎看不见的闪点。图元选择是效果路线的第一步,不是渲染器内部细节。
网格简化是 CPU 资产问题。运行时根据距离换 LOD 网格,本质是换 VAO,拓扑仍是三角。不要幻想 FS 能“少画一些三角”——FS 只决定已光栅片元的颜色。少三角要靠换几何或剔除。视锥剔除在 CPU 丢物体;背面剔除在 GPU 丢三角;两者都不是着色器公式。把 LOD 写进材质里当 if,只会让所有档位都跑最贵路径。
WebGL2 支持 primitive restart,让条带中插入重启索引来断开。用得好能少些退化三角。我仍默认独立三角,因为生成器简单、引擎导出简单、重启在 1.0 不是核心。生成器要写成纯函数:输入细分等级,输出位置法线 UV 索引。纯函数便于在 Worker 预热,也便于测试绕序。带闭包乱改全局数组的生成器,会在第二处调用时污染第一份网格。CPU 组装的卫生与 GPU 无关,但错误会显示成 GPU 炸裂。
大世界拼接:地块之间共享边缘顶点要焊缝。焊缝是资产或生成器合同,运行时着色器补不了裂缝。裂缝会在光照下变成黑线,像法线错误。先用线框看缝,再看法线。拓扑问题优先于公式问题,这是本章的反复句。
负缩放翻转绕序,背面剔除把翻转后的正面当背面丢掉。资产侧不要用负缩放表达镜像,改用镜像网格或临时改 frontFace。运行时检测矩阵行列式符号再切 frontFace 可以救,但容易泄漏到下一物体。pass 内改了必须改回。缺面优先查绕序,而不是加双面着色器把片元税翻倍。
边缘顶点位置若在两块地里各算一次,浮点差会在光栅时闪。生成器应保证缝上顶点比特级相同,或共享同一份边缘缓冲。着色器里做缝融合是事后补丁,带宽更贵。线框模式能看见缝,比调雾更直接。
换网格。FS 里的 if 让所有档位都跑最复杂公式,还可能破坏早深度。距离判定在 CPU,换 VAO 在 CPU,GPU 只看到更少三角。两档网格的法线与 UV 要各自正确,不要指望低模用法线贴图冒充高模拓扑。法线贴图补的是细节,补不了轮廓。
程序化网格必须有黄金数据。把细分等级 8 的球体顶点数、索引数、包围盒写进断言,生成器一改就能发现绕序或 UV 接缝。接缝在极点最常见,UV 挤在一点,法线贴图会花。极点用特例 UV 或接受花。导出清单:索引类型、是否切线、是否硬边拆点、单位是米还是厘米。单位错了,3.1 的 near/far 全无意义。厘米当米会把模型放到极远,深度精度立刻烂。清单是 CPU 资产合同,比着色器里补缩放更干净。教学网格用手写三角没问题;产品网格禁止在运行时用字符串拼 OBJ。解析属构建期。运行时只吃缓冲。
drawElements 让独特顶点进仓库,编号进 IBO;硬边法线仍会拆点。下一节对照缓冲区:这些顶点数组如何从 CPU 内存搬进 GPU 常驻,以及写入频率如何选提示。