2.5 Attribute vs Uniform 两条通道


文档摘要

2.5 Attribute vs Uniform 两条通道 本节摘要:送进着色器的数据不是一锅粥。Attribute 按顶点变化,从缓冲读取,是顶点着色器的输入;Uniform 在一次绘制调用内保持不变,由 CPU 在 draw 前写入,VS 和 FS 都能读。varying(或 2.0 的 in/out)则是 VS 写、光栅插值、FS 读的第三通道。把 MVP 矩阵当 Attribute 上传、或把每顶点颜色塞进 Uniform 再循环 draw,都会把通道用反,两端一起变慢。

2.5 Attribute vs Uniform 两条通道

本节摘要:送进着色器的数据不是一锅粥。Attribute 按顶点变化,从缓冲读取,是顶点着色器的输入;Uniform 在一次绘制调用内保持不变,由 CPU 在 draw 前写入,VS 和 FS 都能读。varying(或 2.0 的 in/out)则是 VS 写、光栅插值、FS 读的第三通道。把 MVP 矩阵当 Attribute 上传、或把每顶点颜色塞进 Uniform 再循环 draw,都会把通道用反,两端一起变慢。

本节导读

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

  1. 用“变化粒度”一句话区分 Attribute、Uniform、varying
  2. 说明 vertexAttribPointer 如何把缓冲字节解释成 Attribute
  3. 对照 uniformMatrix4fv 与顶点色 Attribute 各自该承载什么
  4. 解释实例化绘制如何引入第四种粒度:per-instance Attribute
  5. 指出 WebGL1 逐个 Uniform 与 WebGL2 UBO 在 CPU 提交上的差别

粒度决定通道,不是类型决定通道

颜色可以是 Attribute(每个顶点不同),也可以是 Uniform(整网格一个固有色)。矩阵几乎总是 Uniform,但实例化时模型矩阵可以变成 per-instance Attribute。类型(vec3、mat4)不决定通道,一次 draw 里它变不变、按什么单位变才决定。

剧场灯光可以对照:整场戏同一个追光颜色,是 Uniform;每个演员帽子上的 LED 颜色不同,是 Attribute;灯光打在纱幕上被空间混合后进观众眼睛,那层混合像 varying 插值。你不会给每个观众发一份剧场总灯光表的拷贝(那是把 Uniform 错当成 Attribute),也不会让全场帽子强制同色还用换场来模拟彩帽(那是把 Attribute 错当成 Uniform 加多次 draw)。

变化粒度从粗到细: Uniform 一次 draw 内全体顶点/片元相同 per-instance 同一实例内相同,实例之间不同 Attribute 每个顶点不同 varying 插值 片元之间不同,由顶点值插出来 纹理采样 由 UV 决定,粒度可以比几何更细

两条通道的 API 与合同

通道 谁写 谁读 典型载荷 设错时的症状
Attribute CPU 写入 VBO,指针解释 仅 VS 位置、法线、UV、顶点色 模型炸裂、UV 花、法线斑驳
Uniform CPU gl.uniform* VS 与 FS MVP、光方向、时间、固有色 整物体错位或整物体错色
varying VS 赋值 FS 读插值 传到片元的法线、UV、色 插值断裂、平面着色 vs 平滑
纹理 CPU 上传图像 FS 采样为主 反照率、法线贴图 黑图、重复、过滤糊

Attribute 必须 enable。只 vertexAttribPointer 不 enable,VS 读到的是 Attribute 的当前通用值(vertexAttrib* 设的那个常数)。这是一条隐蔽通道:你可以用它给“没有 VBO 的属性”提供常数,相当于伪 Uniform。偶尔有人靠这个画全红三角形。不要依赖它承载真正的 per-vertex 数据,也不要在 enable 后以为通用值还在——enable 之后走缓冲。

gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.vertexAttribPointer(locPos, 3, gl.FLOAT, false, 32, 0); gl.enableVertexAttribArray(locPos); gl.vertexAttribPointer(locUv, 2, gl.FLOAT, false, 32, 24); gl.enableVertexAttribArray(locUv); gl.uniformMatrix4fv(locMVP, false, mvp);

注意最后一行没有 bind 缓冲。Uniform 不走 ARRAY_BUFFER。这是通道隔离。把矩阵 bufferData 进 VBO 却用 uniformMatrix4fv 去设,或反过来,属于把两根水管接反。

WebGL1 的 Uniform 是一颗颗设的。摄像机矩阵要对每个 program 设一遍,物体多、材质多时,CPU 在 uniformMatrix4fv 上就能忙死。WebGL2 UBO 让一块缓冲被多个 program 共享读取,CPU 写一次相机。这是版本轴在通道上的具体红利。实例化则是 Attribute 通道的加宽:divisor 为 1 时,GPU 每换一个实例才取下一个 Attribute。草地、士兵、碎片适合它;用 Uniform 循环一万次 draw 是把粒度选粗之后用次数硬补。

怎么选通道:先写变化频率表

我在加一个新量之前先填表:每顶点不同?每实例不同?每物体不同?每帧全局不同?答案分别指向 Attribute、instance Attribute、Uniform(物体)、Uniform(全局,可 UBO)。纹理是“表面细节比几何密”时的逃逸口:不要为了墙污渍把顶点拆到像素级。

数据 推荐通道 反例
位置 Attribute 用 Uniform 画每个点再 draw POINTS
MVP Uniform 或 UBO 每个顶点存一份矩阵
顶点色渐变 Attribute 多 pass 换 Uniform 色
物体固有色 Uniform 为同色立方体加颜色 Attribute
时间 sin 波动 Uniform 时间 + VS 公式 每帧改所有顶点位置缓冲
一万棵树变换 instance Attribute 或实例化矩阵纹理 一万次 draw 设一万次 Uniform 矩阵

引擎把 Attribute 藏在几何对象,Uniform 藏在材质与灯光。出 bug 时仍要问粒度。材质颜色改了所有共用该材质的物体都变,那是 Uniform 共享;只有某网格的顶点色变,那是 Attribute。场景图泄漏时,这张粒度表比“在哪点了属性面板”更管用。

⚠️ 常见坑:uniform 矩阵忘记 useProgram 就上传。数据写进了上一个 program 的槽,当前物体用的是旧矩阵或零。通道对了,目标 program 不对,等于寄错仓库。

💡 关键直觉:通道是带宽与提交次数的开关。越细的粒度越吃顶点拉取;越粗的粒度越可能逼你增加 draw。选错不会报“通道错误”,只会又慢又丑。

varying 的插值让三角形内部的法线是三个顶点法线的混合,这才有平滑着色。若 CPU 组装时每个面拆出自己的顶点、法线取面法线,插值仍发生,但三个顶点法线相同,于是看起来是平坦着色。这不是 FS 写错,是 Attribute 里装的粒度已经是“每面”而不是“每平滑顶点”。对照第 2.2 节硬边拆点:那是 Attribute 粒度的几何后果。

问题:能不能把所有东西都做成纹理?

能,这是“超大 Uniform”的逃逸:把骨骼矩阵打进纹理,VS 里采样。WebGL1 在 VS 采样有限制,WebGL2 宽松些。这把通道换成了纹理,粒度变成“按整数 UV 取矩阵”。当 Uniform 数量触顶、或实例数据太大塞不进 Attribute 时才值得。默认别把通道表简化成“全部贴图”,采样延迟和精度是另一张账单。

第一个三角形用 Attribute 传位置和颜色,Uniform 传 MVP(哪怕是单位矩阵)。刻意不要把颜色当 Uniform。这样你能在调试器里看见两条通道都工作。再做一个整网格变色,把颜色改 Uniform,对比 draw 次数与代码行数。通道一旦建立手感,后面光照和蒙皮只是往表里加行。

实例化、UBO、纹理,三种“把粒度调粗或调细”的手段

当物体数涨到成千上万、几何相同、变换不同,Uniform 每物体提交变成 CPU 灾难。实例化把模型矩阵或颜色变成 per-instance Attribute,一次 draw 吃完。粒度从“每物体一次 draw”调到“每实例一行数据”。数据仍可能由 CPU 每帧更新实例仓,但 draw 税没了。实例仓的写入策略回到 2.3:若变换每帧全变,STREAM;若只有一部分活着,考虑分块 SubData。

UBO 把粒度往粗调:相机 VP、雾参数、光照全局项,多 program 共享。CPU 写一次。WebGL1 没有它,只能重复 uniform 调用。迁 2.0 却不建 UBO,等于把最肥的全局数据仍按 program 撒。优先把相机放进 UBO,收益立刻可见。物体固有色仍留独立 Uniform 或材质 UBO,看你们的绑定模型。

纹理把粒度调到比顶点更细。墙污渍不要拆顶点,要 UV。骨骼特别多时矩阵进纹理,是把 Uniform 数组溢出逃到采样。逃逸有代价:VS 采样限制、精度、缓存。先填频率表,表上没有“塞不下”再逃。乱逃会让通道表无法阅读,新人会把所有东西做成贴图,包括 MVP。

位置查询失败返回 -1。对 -1 调用 uniform 是静默空操作。拼错名字时画面用零矩阵,物体消失,日志没有。启动时断言 location 有效,是 CPU 合同检查,便宜且救命。引擎有时把未使用 Uniform 优化掉,location 也是 -1。这不是 bug,是链接器认为它没参与。调试时在着色器里真用一下该变量,或关掉优化,对照后再删。

默认值、禁用属性、以及程序切换时的脏 Uniform

切换 program 后,Uniform 不会从上一份程序带过来。每份程序有自己的槽。你必须给当前程序需要的槽赋值。漏赋则用驱动默认,常常是 0,物体在原点或黑色。CPU 侧按 program 缓存一份“上一帧已上传的 VP”,变了才写,没变可跳过。跳过是优化;漏写是 bug。先保证每 draw 前必要槽有效,再做脏标记。脏标记写错会比每次上传更难查。

禁用的 Attribute 走当前通用值。实例化时若某实例属性 disable,所有实例同值。这可以用来给“没有 per-instance 色”的网格提供白。混用 enable 状态时,VAO 会记下 enable。切 VAO 等于切 enable 集合。以为全局 enable 还在,其实 VAO 把它关了。调试属性问题先看 VAO 是否绑定正确,再看指针。通道问题多半是绑错对象,不是 GLSL 写错。

对照问答:-1 位置、实例化、脏标记

location 为 -1 为什么不报错?

规范允许对 -1 的 uniform 调用变成空操作。拼错名字、被链接器优化掉、用了错的 program,都会 -1。启动时断言必要槽不是 -1。调试版打印所有查询结果。生产版至少对 MVP 与采样器断言。物体消失先打 location,再怀疑矩阵数学。

实例化后颜色全错?

divisor 没设或 VAO 没记录实例缓冲。实例色被当成顶点色,前几个顶点吃掉实例表,后面读越界。先画两个实例、用显眼的红绿,确认每个实例整块同色,再扩数量。实例矩阵若按行主序上传,会再叠一层 3.1 的合同错误。一次只验证一个通道。

脏标记上传漏了相机怎么办?

所有物体用旧 VP,表现为整体滞后或第一帧错位。脏标记要在相机更新处置位,不要只在物体移动处置位。更稳的是每帧上传全局 UBO 一次,物体级才脏标记。全局数据量小,省不了几个调用,却能避免整场景错一帧。先稳后省。

工程备忘:布局表贴在 VAO 旁

每个 VAO 建立处贴一张表:属性名、location、size、type、stride、offset、divisor。表与 packed 数据生成器共用同一份常量。生成器改了 offset,表不改,就会出现法线吃位置的经典事故。Code review 只看这张表比看一串 magic number 快。Uniform 块同理:全局相机一份、物体一份、材质一份。命名前缀 uView、uModel、uMat,避免两份 program 同名不同含义。同名不同含义时,脏标记会把错误值当命中。前缀是通道卫生。

通道表应出现在每个新效果的设计文档第一页。没有表就不准写代码。表能阻止把时间塞进 Attribute、把顶点色塞进一万次 Uniform。实例化验收用红绿两实例,颜色错立刻可见。UBO 验收改一次相机,所有 program 跟着动,有一个没动就是没绑到同一块。脏标记先不做,等 profiler 证明 uniform 上传占主线程再做。过早脏标记的 bug 比上传更难查。location 断言做成 debug 构建默认开、release 关。release 仍应对 MVP 与采样器断言,这两类失败画面是空的,空比错更无信息。空的时候用户以为没加载,其实是通道寄错仓库。

通道验收不要一次上齐。先只有位置 Attribute 与 MVP Uniform 画出白色物体,再加 UV,再加实例色。每一步能指出新通道是否工作。齐上时错了要二分,浪费的正是这把尺。采样器也是 Uniform,整数槽位与纹理单元必须同号。同名不同号会导致串味,串味像贴图错,其实是通道寄错单元。启动打印 program 的槽表,能把这类问题从夜晚提前到加载条。

温故知新

  • 粒度决定通道:draw 内不变走 Uniform,每顶点变走 Attribute,片元变走插值或纹理。
  • API 隔离:指针与 enable 管 Attribute;gl.uniform* 管 Uniform,不绑 VBO。
  • enable 改变语义:开启后走缓冲,关闭时走通用顶点属性值。
  • UBO 与实例化:分别减 CPU 重复提交、减 draw 次数,是通道加宽不是新管线。
  • 引擎映射:材质≈Uniform 集,几何≈Attribute 布局;共享材质就是共享 Uniform。
  • 先填频率表再写代码:比事后用性能工具猜通道更便宜。

下一节对照纹理这条更细的通道:CPU 如何把图像变成像素仓库,GPU 如何按 UV 采样。


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