2.2 顶点图元:CPU 组装 vs GPU 消费


文档摘要

2.2 顶点图元:CPU 组装 vs GPU 消费 本节摘要:GPU 不认识“立方体”或“人物”,它只认识绘制模式提交的图元:点、线、三角形。顶点位置、法线、UV 由 CPU 填进数组,拓扑由 、 等模式决定,索引则让多个三角形共用同一顶点。CPU 负责组装这份几何合同,GPU 在顶点着色之后按合同消费。选错图元模式或把索引当位置,画面会碎成无法辨认的折纸。 核心问题 阅读完本节,你应当能够: 列出 WebGL 常用图元模式,并说明各自顶点数量与连接规则 对照 顺序消费与 按索引复用 解释为什么三角形是 3D 网格的通货,而四边形不是一等公民 计算一份网格在“重复顶点”与“索引复用”两种布局下的大致内存差 指出 CPU 侧生成球体网格时,哪些工作不该每帧重做 GPU 只吃图元,不吃模型格式

2.2 顶点图元:CPU 组装 vs GPU 消费

本节摘要:GPU 不认识“立方体”或“人物”,它只认识绘制模式提交的图元:点、线、三角形。顶点位置、法线、UV 由 CPU 填进数组,拓扑由 TRIANGLESTRIANGLE_STRIP 等模式决定,索引则让多个三角形共用同一顶点。CPU 负责组装这份几何合同,GPU 在顶点着色之后按合同消费。选错图元模式或把索引当位置,画面会碎成无法辨认的折纸。

核心问题

阅读完本节,你应当能够:

  1. 列出 WebGL 常用图元模式,并说明各自顶点数量与连接规则
  2. 对照 drawArrays 顺序消费与 drawElements 按索引复用
  3. 解释为什么三角形是 3D 网格的通货,而四边形不是一等公民
  4. 计算一份网格在“重复顶点”与“索引复用”两种布局下的大致内存差
  5. 指出 CPU 侧生成球体网格时,哪些工作不该每帧重做

GPU 只吃图元,不吃模型格式

建模软件里的物体有面、有平滑组、有细分曲面。进 WebGL 之后,这些名词全部消失。剩下的是一串数字:每三个浮点一个位置,也许再附法线与 UV,再加一份索引说哪三个编号构成三角形。CPU 是翻译:把 glTF、OBJ 或程序化形状译成这串数字。GPU 是食客:按当前绘制模式一次取 1、2 或 3 个已着色顶点,围成图元。

这解释了为什么“我加载了模型却是空的”常常不是着色器问题,而是翻译合同破了:索引类型是 16 位却有超过 65535 个顶点,或者绘制模式是 TRIANGLE_STRIP 却按独立三角形的数据去喂。GPU 不会报“这不像一只茶壶”,它会认真画出一堆乱三角形。

点、线、三角形覆盖了几乎全部 2D/3D 需求。点适合粒子与调试法线;线适合线框与矢量叠加;三角形适合一切有面积的表面。LINE_LOOPTRIANGLE_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 做一次,还是假装 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,只会让所有档位都跑最贵路径。

索引宽度、重启图元、以及 CPU 生成器的可复用性

WebGL2 支持 primitive restart,让条带中插入重启索引来断开。用得好能少些退化三角。我仍默认独立三角,因为生成器简单、引擎导出简单、重启在 1.0 不是核心。生成器要写成纯函数:输入细分等级,输出位置法线 UV 索引。纯函数便于在 Worker 预热,也便于测试绕序。带闭包乱改全局数组的生成器,会在第二处调用时污染第一份网格。CPU 组装的卫生与 GPU 无关,但错误会显示成 GPU 炸裂。

大世界拼接:地块之间共享边缘顶点要焊缝。焊缝是资产或生成器合同,运行时着色器补不了裂缝。裂缝会在光照下变成黑线,像法线错误。先用线框看缝,再看法线。拓扑问题优先于公式问题,这是本章的反复句。

对照问答:绕序、裂缝、LOD

为什么镜像后的角色缺一面?

负缩放翻转绕序,背面剔除把翻转后的正面当背面丢掉。资产侧不要用负缩放表达镜像,改用镜像网格或临时改 frontFace。运行时检测矩阵行列式符号再切 frontFace 可以救,但容易泄漏到下一物体。pass 内改了必须改回。缺面优先查绕序,而不是加双面着色器把片元税翻倍。

地块拼缝在运动时闪黑线?

边缘顶点位置若在两块地里各算一次,浮点差会在光栅时闪。生成器应保证缝上顶点比特级相同,或共享同一份边缘缓冲。着色器里做缝融合是事后补丁,带宽更贵。线框模式能看见缝,比调雾更直接。

LOD 该换网格还是改 FS 里的 if?

换网格。FS 里的 if 让所有档位都跑最复杂公式,还可能破坏早深度。距离判定在 CPU,换 VAO 在 CPU,GPU 只看到更少三角。两档网格的法线与 UV 要各自正确,不要指望低模用法线贴图冒充高模拓扑。法线贴图补的是细节,补不了轮廓。

工程备忘:生成器测试与导出清单

程序化网格必须有黄金数据。把细分等级 8 的球体顶点数、索引数、包围盒写进断言,生成器一改就能发现绕序或 UV 接缝。接缝在极点最常见,UV 挤在一点,法线贴图会花。极点用特例 UV 或接受花。导出清单:索引类型、是否切线、是否硬边拆点、单位是米还是厘米。单位错了,3.1 的 near/far 全无意义。厘米当米会把模型放到极远,深度精度立刻烂。清单是 CPU 资产合同,比着色器里补缩放更干净。教学网格用手写三角没问题;产品网格禁止在运行时用字符串拼 OBJ。解析属构建期。运行时只吃缓冲。

重点提炼

  • GPU 只认图元:点、线、三角形;模型格式必须在 CPU 译成数组加拓扑。
  • 三角形是通货:四边形、N 边形都要拆;引擎接口再甜,上传仍是三角。
  • 索引换重复drawElements 让独特顶点进仓库,编号进 IBO;硬边法线仍会拆点。
  • 16 位上限:WebGL1 默认索引宽度限制单网格规模,大模型要拆或升 32 位。
  • 拓扑不变则上传一次:每帧重建球体是 CPU 自虐;变形优先放 VS。
  • 绕序是组装合同:镜像变换会翻转正面,表现为“缺面”,根因不在着色器公式。

下一节对照缓冲区:这些顶点数组如何从 CPU 内存搬进 GPU 常驻,以及写入频率如何选提示。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U