想象你是一家玩具工厂的车间主任,手下有一条全自动流水线(GPU),这一批要生产彩色小球(绘制一帧画面)。流水线上有两道关键工序:整形工位把毛坯打磨成标准形状(顶点着色器),上色工位给表面喷漆(片元着色器)。要让产线转起来,你需要同时送进三类信息:贴满车间的“本批次生产要求”、每件产品独有的原料卡、工序之间传的流转单。这三样东西,正是 GPU 着色器编程里的 Uniform、Buffer 和 Varying 变量。搞懂它们从哪来、存在哪里、怎么变化,你就拿到了读懂任何 shader 的数据地图。

岗位关联

对 AI 从业者:图形学是 AI 可视化、数字孪生、具身智能仿真的底层能力,而 shader 变量是 GPU 数据流的骨架。理清三兄弟的来龙去脉,写 WebGL/OpenGL 的底气和 debug 能力都会上一个台阶。

目录
  1. 可视化演示
  2. 变量总览——车间里的三条信息通道
  3. Uniform 常量——车间公告栏
  4. Buffer顶点缓冲——原料架与原料卡
  5. Varying插值——流水线上的流转单
  6. 数据流动管线——从车间主任到成品小球
  7. 对比与选型——三兄弟怎么选
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示GPU渲染管线中Uniform、Buffer、Varying三类变量的数据流动路径:从CPU到顶点着色器、经插值传递到片元着色器,对比其作用域与存储方式。

本节配有交互动效(进入视口后自动加载,约 3KB)。

三类着色器变量的管线数据流 Uniform:全局常量,CPU→VS/FS Buffer:顶点数据,CPU→VS Varying = interpolate(VS_output) Varying由顶点着色器输出,经光栅化插值后传递给片元着色器。

变量总览——车间里的三条信息通道

车间里贴公告、发原料卡、传流转单,是三种性质完全不同的沟通方式。GPU 世界也一样:一台着色器程序要并行处理成千上万个顶点、上百万个片元,数据必须走专用通道,才能高效送到每个工位手上。如果只靠一条路传数据,流水线早就堵死了。

在 GLSL 里,这三种通道恰好对应三类变量。Uniform 是 CPU 写入 GPU 的只读全局常量,像公告栏一样所有工位共享同一份;Buffer(顶点缓冲对象 VBO)是驻留在显存中的顶点数据仓库,着色器通过 attribute 接口按顶点编号逐条取用,像原料架上每件产品各自一张原料卡;Varying 则是顶点着色器写给片元着色器的“半成品说明单”,途中还会被光栅化阶段自动插值加工。三者共同织成 GPU 内外的数据流动网络。

所以先记住一句话:Uniform 全局共享,attribute 顶点私有,varying 阶段传递。后面每一节,都在给这句话补充细节。

CPU→ Uniform GPU;Buffer→ attribute VS;VS→ Varying FS{\displaystyle\mathrm{CPU} \xrightarrow{\ \mathrm{Uniform}\ } \mathrm{GPU};\qquad\mathrm{Buffer} \xrightarrow{\ \mathrm{attribute}\ } \mathrm{VS};\qquad\mathrm{VS} \xrightarrow{\ \mathrm{Varying}\ } \mathrm{FS}}
这条式子用三条箭头概括了三类变量的流向:CPU 的全局数据走 Uniform,显存里的顶点数组走 attribute 进顶点着色器,顶点着色器的输出走 Varying 去片元着色器。
  1. CPU→Uniform→GPUCPU 把“本批次生产要求”这类全局只读数据提交给 GPU,整条渲染流水线共享同一份拷贝,着色器里不能改它。
    U∈Rn,所有线程读到相同 U{\displaystyle U \in \mathbb{R}^{n},\quad \text{所有线程读到相同 } U}
  2. Buffer→attribute→VS显存中的顶点缓冲以 attribute 接口逐顶点喂给顶点着色器,每个顶点拿到自己那一行数据,不同顶点各不相同。
    attri=VBO[vtxi]{\displaystyle \mathrm{attr}_i = \mathrm{VBO}[\mathrm{vtx}_i]}
  3. VS→Varying→FS顶点着色器把计算结果写进 Varying,送往片元着色器;途中光栅化会自动对 Varying 做插值。
    VFS=lerp(VA,VB,t){\displaystyle V_{\mathrm{FS}} = \mathrm{lerp}(V_{A}, V_{B}, t)}

Uniform 常量——车间公告栏

车间主任每天开工前,会把本批次的生产要求写在公告栏上:产量 1000 个、表面颜色红色、硬度中等。所有工位抬头都能看到,但谁也不许涂改——这就是 Uniform 的本职。在 WebGL/OpenGL 里,CPU 使用 gl.uniform4fv、gl.uniformMatrix4fv 这类接口,把数据写进 GPU 的 uniform 存储区。着色器里所有并行线程读到的是同一份只读值。

实际项目里,Uniform 最常装的是这些“全场景通用”的小数据:模型视图投影矩阵、当前动画时间、光源方向与颜色、屏幕分辨率、雾浓度等等。它们的共同点是:量小、不随顶点变化、更新频率低(每帧甚至更久才动一次)。比如摄像机推近拉远,只需要每帧更新十几KB的矩阵数据,不值得惊动显存里的顶点大部队。

一句话回收比喻:公告栏上写什么,全车间就按什么干活;你只需要每帧改一次公告栏,不用对每个工人单独喊话。Uniform 就是这种“一次广播、全员执行”的高效通道。

gl_Position  =  M×V×P×p⃗{\displaystyle\mathrm{gl\_Position} \;=\; M \times V \times P \times \vec{p}}
这是顶点着色器里最常见的矩阵变换。M、V、P 三个矩阵都放在 uniform 中,所有顶点共用同一组矩阵,只是各自的 $\vec{p}$ 不同。
  1. p 向量顶点在模型局部坐标里的原始位置,来自 Buffer 的 attribute 输入。
    p⃗=(x, y, z, 1)T{\displaystyle \vec{p} = (x,\ y,\ z,\ 1)^{\mathsf{T}}}
  2. M 模型矩阵把物体从模型空间放到世界空间,CPU 每帧写入 uniform。
    M∈R4×4{\displaystyle M \in \mathbb{R}^{4 \times 4}}
  3. V 视图矩阵把世界坐标变换到相机视角,同样在 uniform 里。
    V∈R4×4{\displaystyle V \in \mathbb{R}^{4 \times 4}}
  4. P 投影矩阵做透视或正交投影,产生近大远小的效果,由 CPU 计算后放入 uniform。
    P∈R4×4{\displaystyle P \in \mathbb{R}^{4 \times 4}}

Buffer顶点缓冲——原料架与原料卡

光有公告栏还不够。1000 个球,每个球的半径、材质、花纹都不同,车间的“原料卡”必须一对一。GPU 里,这张原料卡存放在 Buffer / VBO 中:一块创建后常驻显存的数据区,里面紧密排列着顶点数组。每个顶点占一行,行内依次排好位置、颜色、UV、法线等字段,这些字段在着色器里就叫 attribute。

顶点着色器处理某个顶点时,GPU 会根据当前顶点编号,在缓冲里精准抓取那一行数据。CPU 要做三步准备工作:创建并填充 VBO;用 gl.vertexAttribPointer 告诉 GPU“这一列是位置、那一列是颜色、每列隔多少字节”;最后打开 attribute 开关。这套“描述排布”的机制就是 stride(跳步)和 offset(偏移)。

回看工厂:原料架上一摞摞卡片按产品编号排好,工位处理到几号产品就抽第几张卡。Buffer 的优势正在于此——数据常驻 GPU 显存,流水线多次开动(多次绘制)都不需要重复搬货。

attri  =  VBO ⁣[ vtx×stride+offseti ]{\displaystyle\mathrm{attr}_i \;=\; \mathrm{VBO}\!\left[\ \mathrm{vtx} \times \mathrm{stride} + \mathrm{offset}_i\ \right]}
这条公式说明 GPU 如何从一块扁平的字节缓冲区里拿出“第 vtx 个顶点的第 i 个属性”:先跳过前面整行,再往右偏移到目标列。
  1. vtx 顶点编号GPU 当前正在处理的顶点索引,每次调用顶点着色器递增。
    vtx∈{0,1,2,…,N−1}{\displaystyle \mathrm{vtx} \in \{0,1,2,\ldots,N-1\}}
  2. stride 步长一个顶点占用的总字节数,让 GPU 知道“下一行”往右跳多远。
    stride=∑jsize(fieldj){\displaystyle \mathrm{stride} = \sum_{j} \text{size}(\text{field}_j)}
  3. offset_i 偏移第 i 个属性在行内的起始位置,例如位置从 0 字节开始,颜色从第 12 字节开始。
    offseti=∑j<isize(fieldj){\displaystyle \mathrm{offset}_i = \sum_{j<i} \text{size}(\text{field}_j)}
  4. 整体取址把行号乘步长、加上列偏移,就能从 Buffer 里精确取出当前顶点第 i 个 attribute。
    addr=vtx×stride+offseti{\displaystyle \text{addr} = \mathrm{vtx}\times \mathrm{stride} + \mathrm{offset}_i}

Varying插值——流水线上的流转单

整形工位做完一个毛坯,会在流转单上写:当前温度 80℃、表面粗糙度 3。可传送带之后要处理的半成品数量多得多:一个三角形明明只有 3 个顶点,经光栅化扩散后却要生成上千个像素片元,每个片元都要一张流转单。GPU 的解法很巧妙:在每个像素的位置,按它离两个端点的远近,把端点上的流转单数值线性混合出一张新单子。

这就是 Varying:顶点着色器输出原始值,片元着色器得到的是插值后的值。前面公式里的 gl_Position 是内置输出,而用户自定义的 varying 同样遵循这条规则。颜色渐变、法线插值、UV 平滑过渡、雾效浓度变化,全靠它在两个着色器阶段之间“打圆场”。

想象传送带:两端放着一红一蓝两种颜料,中间经过的每一个半成品,都自动按距离混合出从红到蓝的平滑渐变——这就是为什么一个纯色三角形,只要给三个顶点不同的颜色,内部就能自动呈现渐变效果。

Varyingt  =  (1−t)⋅VaryingA+t⋅VaryingB,t∈[0, 1]{\displaystyle\mathrm{Varying}_{t} \;=\; (1-t)\cdot \mathrm{Varying}_{A} + t\cdot \mathrm{Varying}_{B},\qquad t\in[0,\,1]}
这是最经典的线性插值公式:t 越接近 0 越像 A 端点,越接近 1 越像 B 端点。GPU 的光栅化单元就是拿着它,为每个片元算出专属的 varying。
  1. Varying_A 与 Varying_B三角形边上两个端点的 varying 值,由顶点着色器写出。
    VA=fVS(attrA),VB=fVS(attrB){\displaystyle V_A = f_{\mathrm{VS}}(\mathrm{attr}_A),\quad V_B = f_{\mathrm{VS}}(\mathrm{attr}_B)}
  2. t 比例当前片元在两个端点之间的相对位置,由重心坐标或线性比例算出。
    t=dist(P,A)dist(B,A),t∈[0,1]{\displaystyle t = \frac{\text{dist}(P, A)}{\text{dist}(B, A)},\quad t\in[0,1]}
  3. 混合运算按 t 对每个分量分别做加权平均,得到该片元的 varying 值。
    Vt=(1−t) VA+t VB{\displaystyle V_t = (1-t)\,V_A + t\,V_B}

数据流动管线——从车间主任到成品小球

把三类变量放进一条完整流水线走一遍。CPU 每帧要做三件事:改公告栏(更新 uniform)→ 把原料卡搬上架(创建或更新 VBO)→ 按下开工按钮(发出 draw call)。GPU 随即启动三条流水段:顶点着色器一次处理一个顶点,用 attribute 读自己的原料卡、用 uniform 读公告栏,算出顶点位置并写 varying;光栅化单元在三角形内部生成海量片元,同时把 varying 插值好;片元着色器拿着插值后的 varying 与 uniform 完成最终上色;最后一块像素被写进颜色缓冲,一帧画面完成。

你会发现,三类变量在管线里各就各位、各司其职:Uniform 从头到尾都能被读取,是常驻的“环境变量”;Buffer 只服务顶点着色器,喂的是逐顶点原料;Varying 只存在于 VS 到 FS 之间,是阶段间的传话筒。

从车间主任按下按钮,到彩色小球滚下产线,中间所有数据搬运都由这三条通道完成。你只要在脑子里跑一遍这条流水线,遇到再复杂的 shader 也能瞬间定位“这里的数据是从哪条通道来的”。

最终像素  =  f FS( lerp( f VS(buf, uni)), uni ){\displaystyle\mathrm{最终像素} \;=\; f_{\,\mathrm{FS}}\Big(\ \mathrm{lerp}\big(\ f_{\,\mathrm{VS}}(\mathrm{buf},\ \mathrm{uni})\big),\ \mathrm{uni}\ \Big)}
这个嵌套式子浓缩了整条管线:buf 与 uni 先进顶点着色器,经过光栅化插值,再与 uni 一起进片元着色器,最终产出像素。
  1. buf 输入顶点缓冲数据,包含每个顶点的位置、颜色、法线等 attribute。
    buf={attr0,attr1,… }{\displaystyle \mathrm{buf} = \{\mathrm{attr}_0, \mathrm{attr}_1, \dots \}}
  2. f_VS 顶点着色器逐顶点执行,输出裁剪坐标与自定义 varying。
    Vout=fVS(attri, uni){\displaystyle V_{\mathrm{out}} = f_{\mathrm{VS}}(\mathrm{attr}_i,\ \mathrm{uni})}
  3. lerp 光栅化插值把顶点上的 varying 插值到每个片元。
    Vscan=lerp(Vout, t){\displaystyle V_{\mathrm{scan}} = \mathrm{lerp}(V_{\mathrm{out}},\ t)}
  4. f_FS 片元着色器逐片元执行,输出最终颜色。
    Color=fFS(Vscan, uni){\displaystyle \mathrm{Color} = f_{\mathrm{FS}}(V_{\mathrm{scan}},\ \mathrm{uni})}

对比与选型——三兄弟怎么选

回到车间:公告栏、原料卡、流转单,到底什么时候用哪个?诀窍是看三个问题:数据有几份、谁读谁写、存在哪里。Uniform 只有一份,CPU 写 GPU 只读,存在 GPU 的常量存储区;Buffer 有 N 份顶点数据,CPU 批量写、GPU 按顶点编号读,存在显存堆里;Varying 也有 N 份,但它是 VS 写、FS 读,数据在寄存器与插值单元之间流转,不经过内存。

选型经验可以浓缩成三句话:全局小量用 Uniform,逐顶点大数组用 Buffer,阶段间传递用 Varying。性能上还有几个潜规则:频繁切换 uniform 有开销但通常可接受;每帧全量重建大 VBO 是常见的渲染瓶颈,能复用就复用;varying 里别做昂贵计算,因为每个像素都会执行一遍片元着色器,代价会被放大几百倍。

| 维度 | Uniform | Buffer / attribute | Varying |

|---|---|---|---|

| 数据份数 | 1 份 | N 份(每顶点) | N 份(每片元) |

| 谁写 | CPU | CPU 批量 | 顶点着色器 |

| 谁读 | 所有阶段 | 顶点着色器 | 片元着色器 |

| 存哪里 | 常量存储区 | 显存堆 | 寄存器+插值单元 |

| 典型用途 | 矩阵/时间/光源 | 位置/法线/UV | 颜色渐变/纹理坐标 |

拿一张小卡片记下这组对比,你以后写 shader 时选变量就像老工人挑工具一样顺手。

valuei={U,uniform 型:所有线程同值buf[i],attribute 型:按顶点编号取lerp(VA, VB, ti),varying 型:片元间插值{\displaystyle\mathrm{value}_i =\begin{cases}U, & \text{uniform 型:所有线程同值}\\[4pt]\mathrm{buf}[i], & \text{attribute 型:按顶点编号取}\\[4pt]\mathrm{lerp}(V_A,\,V_B,\,t_i), & \text{varying 型:片元间插值}\end{cases}}
这张分段函数图总结了三种变量在 GPU 眼里“怎么取值”:uniform 看一眼公告栏就有,attribute 去显存按编号取,varying 则是两端点按 t 现算。
  1. U 常量值uniform 在 GPU 中只有一份物理存储,所有线程拿到相同数值。
    valuei=U,∀i{\displaystyle \mathrm{value}_i = U,\quad \forall i}
  2. buf[i] 索引取值attribute 型数据按顶点编号离散取用,每个顶点互不相同。
    valuei=buf[i],i=vtx{\displaystyle \mathrm{value}_i = \mathrm{buf}[i],\quad i = \mathrm{vtx}}
  3. lerp 函数varying 型在光栅化时为每个片元动态计算插值。
    valuei=(1−ti) VA+ti VB{\displaystyle \mathrm{value}_i = (1-t_i)\,V_A + t_i\,V_B}

AI 应用场景

实时 3D 场景中的光照与相机

阶段:数据组织与 GPU 渲染管线

解决的问题:传统动画循环里,CPU 每帧都要把相机位置、光源方向、模型矩阵传给 GPU。如果逐顶点拷贝这些数据,带宽立刻被挤爆。用 Uniform 做广播,一次提交全场景共享;模型顶点数据放入 Buffer 常驻显存,避免重复搬运。

具体案例:在一个三人称 3D 游戏里,玩家转动视角时,CPU 每帧更新 view/projection 矩阵到 uniform,场景中成千上万个静态模型的顶点坐标全在 VBO 里按兵不动,只有矩阵在变。光照计算需要的法线,通过 varying 从顶点着色器插值到每个片元,让模型表面呈现平滑明暗过渡。

数据可视化大屏的海量散点图

阶段:可视化渲染性能优化

解决的问题:百万级数据点如果每个点都走 CPU 绘制,帧率会跌到个位数。把点的坐标、数值、类别存进 Buffer,GPU 一次性批量处理;全局的缩放比例、旋转角度、颜色映射范围用 uniform 控制,交互时只改几个 uniform 就能瞬时重绘。

具体案例:一个城市交通可视化大屏展示 50 万个出行起点终点。用户拖拽旋转视角时,CPU 只更新 view 矩阵这个 uniform,顶点位置仍由 Buffer 里的原始经纬度数据支撑;散点颜色利用 varying 在顶点间插值,形成从蓝到红的密度渐变,一眼看清热点区域。

粒子系统的全局参数控制

阶段:GPU 粒子模拟与渲染

解决的问题:烟花、雨雪、星空等粒子特效,粒子数量动辄数万。每个粒子的位置、速度、颜色逐帧变化,适合存储在 Buffer 中由 CPU 更新上传;而时间、重力加速度、全局颜色淡出系数这些控制参数,用 uniform 每帧提交一次,效率远高于混进顶点数据。

具体案例:一个 AI 生成的 3D 粒子艺术装置中,50000 个粒子的坐标存入 VBO,CPU 每帧用物理方程更新粒子位置后整体上传;全局的“时间 t”以 uniform 形式传入片元着色器,控制粒子透明度与颜色随时间脉动,营造呼吸般的动态效果。

Python 演示

python55 行
TRIANGLE = [    (0.0, 0.0, 1.0, 0.0, 0.0),    (1.0, 0.0, 0.0, 1.0, 0.0),    (0.0, 1.0, 0.0, 0.0, 1.0),] class Uniforms:    def __init__(self, brightness=1.0, color_scale=1.0):        self.brightness = brightness        self.color_scale = color_scale def vertex_shader(attr, uniforms):    x, y, r, g, b = attr    varying = {        "pos": (x, y),        "color": (r * uniforms.color_scale,                  g * uniforms.color_scale,                  b * uniforms.color_scale),    }    return varying def pixel_shader(varying, uniforms):    r, g, b = varying["color"]    bri = uniforms.brightness    return (min(r * bri, 1.0), min(g * bri, 1.0), min(b * bri, 1.0)) def lerp(a, b, t):    return a + (b - a) * t def interpolate(v_a, v_b, t):    return {        "pos": (lerp(v_a["pos"][0], v_b["pos"][0], t),                lerp(v_a["pos"][1], v_b["pos"][1], t)),        "color": (lerp(v_a["color"][0], v_b["color"][0], t),                  lerp(v_a["color"][1], v_b["color"][1], t),                  lerp(v_a["color"][2], v_b["color"][2], t)),    } def render_edge(t=0.5, width=4, height=4):    uniforms = Uniforms(brightness=1.0)    vs = [vertex_shader(attr, uniforms) for attr in TRIANGLE]    grid = [[(0, 0, 0) for _ in range(width)] for _ in range(height)]    v = interpolate(vs[0], vs[1], t)    color = pixel_shader(v, uniforms)    px = int(v["pos"][0] * (width - 1))    py = int(v["pos"][1] * (height - 1))    grid[py][px] = color    return grid, vs[0]["color"], v["color"] if __name__ == "__main__":    grid, c0, c_mid = render_edge(t=0.5)    for row in grid:        print(" ".join("OK" if p != (0, 0, 0) else "--" for p in row))    print("varying 起点颜色:", c0)    print("插值中点颜色:", c_mid)
目的:用纯 Python 模拟一条微型 GPU 流水线——Buffer 顶点数据进顶点着色器生成 varying,光栅化线性插值后交给片元着色器,最终输出像素颜色。
  1. TRIANGLE = [这就是 Buffer 顶点缓冲,三行数据代表三个顶点,每行依次是坐标和 RGB 颜色。
  2. class Uniforms:模拟 Uniform 公告栏,brightness 和 color_scale 是全局共享参数。
  3. def vertex_shader(attr, uniforms):顶点着色器入口,参数里 attr 来自 Buffer,uniforms 来自公告栏。
  4. "color": (r * uniforms.color_scale,varying 输出时先把 uniform 的缩放系数作用到颜色上,体现“VS 读 uniform、写 varying”。
  5. def pixel_shader(varying, uniforms):片元着色器:只读 varying 和 uniform,输出最终颜色。
  6. def interpolate(v_a, v_b, t):光栅化插值,对 pos 和 color 每个分量做线性 lerp。
  7. vs = [vertex_shader(attr, uniforms) for attr in TRIANGLE]逐顶点跑一遍顶点着色器,得到每个顶点的 varying。
  8. v = interpolate(vs[0], vs[1], t)在边 AB 上取 t=0.5 处做插值,模拟片元接收到的 varying。
  9. grid[py][px] = color把片元颜色写进像素网格,对应渲染管线的最终输出阶段。

常见误区

误区我刚学的时候以为 uniform 就是普通全局变量,手滑在片元着色器里写 `uniform.x -= 0.1;`,心想反正所有地方都能访问,应该也能改。

正解Uniform 在 GPU 侧是严格只读的,只有 CPU 能通过 gl.uniform* 系列函数更新它。这很像车间公告栏:工人只能抬头看,不能拿笔涂改。如果着色器里需要可变中间值,应该用局部变量或 varying,而不是跟 uniform 较劲。

为何常见:因为 CPU 端的全局变量(比如 C 语言的 static)本来就允许读写,初学者很自然地把“全局可见”和“可写”绑定在一起,忽略了 GPU 并行执行时如果允许乱改共享值,结果将完全不可控。

误区看到 varying 出现在顶点着色器和片元着色器里,就以为它和 uniform 一样,是 CPU 塞给 GPU 的数据通道。

正解Varying 完全活在 GPU 内部,CPU 根本摸不到它。它的数据流方向是固定的:顶点着色器写、光栅化插值、片元着色器读。想从 CPU 进 GPU,只能走 uniform 或 buffer 这两条路。

为何常见:初学阶段很容易把“shader 里能看到的变量”都当成外部输入,忽略了着色器程序之间也有自己的内部通信协议,Varying 正是这个协议的信使。

误区写代码时图省事,每帧都重新 `createBuffer` + `bufferData`,觉得反正数据变了就要重新传,不然 GPU 会拿到旧数据。

正解VBO 一旦创建就常驻显存,可以跨帧、跨 draw call 反复使用。正确的做法是:静态几何体创建一次用到底;动态数据优先用 gl.bufferSubData 更新局部区域,而不是整块销毁重建。每帧全量重建缓冲区,就像每次做饭都把厨房拆了重盖,纯属浪费。

为何常见:新手写的第一版渲染循环往往是“初始化一次性 + 每帧绘制”,看到数据变化就本能地走“重新初始化”的路径,还没建立显存生命周期管理的心智模型。

误区面试时被问到“attribute 存在哪里”,我答“存在 attribute 里”,其实根本不知道 attribute 和 VBO 是一个物体的两面。

正解attribute 是着色器语言层面的“逻辑接口名”,描述顶点数据的语义(这是位置、这是颜色);VBO 是 GPU 显存里的“物理存储”(一堆字节)。两者靠 gl.bindBuffer + gl.vertexAttribPointer 绑定在一起,就像“身份证号”和“档案柜里的那份档案”的关系——前者是编号,后者是实物。

为何常见:教程里讲 shader 代码时频繁出现 attribute,讲 GL API 时又频繁出现 VBO,两套术语在不同章节出现,初学者很难自动把逻辑概念和物理存储缝合起来。

误区uniform 读取高效、所有线程共享,那我把几十万个粒子的坐标全塞进 uniform 数组不就行了?省得折腾 VBO。

正解Uniform 存储区容量很有限(通常只有几百个 vec4 的量级),而且它面向的是“一帧内不变”的数据。逐顶点、海量、高频变化的数据硬塞 uniform,轻则编译报错,重则拖垮性能。大数组的正确归宿是 Buffer 或纹理,由 GPU 按索引高效批量读取。

为何常见:Uniform 的 API 用起来简单(一个函数传参),而 Buffer 要绑定、要描述布局、要启用 attribute,步骤多显得麻烦。人总是倾向于用最省事的工具,哪怕它设计上并不适合这个场景。

面试题

Q1. 关于 Uniform 变量,下列说法正确的是?

单选题 · easy

  • A.它可以在片元着色器中随意修改
  • B.它保存每个顶点独有的位置数据
  • C.它由 CPU 写入、GPU 只读共享← 正确答案
  • D.它只在顶点着色器中可见

答案:C。Uniform 是 CPU 向 GPU 提交的全局只读数据,所有着色器阶段都能读到同一份值。A 错在着色器侧只读;B 是 attribute/Buffer 的职责;D 错在 Uniform 在 vertex 和 fragment 阶段都可见。

Q2. Varying 变量的主要作用是什么?

单选题 · easy

  • A.从 CPU 向 GPU 传递全局常量
  • B.在顶点和片元着色器之间传递插值数据← 正确答案
  • C.存储三角形顶点的索引顺序
  • D.在显存中保存顶点数组

答案:B。Varying 由顶点着色器输出、光栅化插值、片元着色器读取。A 说的是 Uniform,D 说的是 VBO/Buffer,C 与变量传递无关。

Q3. 顶点缓冲 VBO 中通常存放什么数据?

单选题 · easy

  • A.每帧更新的全局时间
  • B.只读的变换矩阵
  • C.每个顶点的位置、法线、UV← 正确答案
  • D.片元着色器的输出颜色

答案:C。VBO 存的是逐顶点的几何与属性数据,比如坐标、法线、纹理坐标。全局时间和矩阵属于 Uniform,输出颜色是片元着色器的产物。

Q4. 三角形三个顶点颜色经 varying 输出后,内部某像素得到的颜色是?

单选题 · mid

  • A.三个顶点颜色的加权混合值← 正确答案
  • B.三个顶点颜色的随机一个值
  • C.三个顶点颜色的算术平均值
  • D.由 uniform 单独决定的值

答案:A。光栅化阶段对 varying 做线性插值,片元离哪个顶点近就偏向那个颜色,是加权混合而非简单平均或随机。这个插值过程与 uniform 无关,所以 D 错。

Q5. 下列哪些数据适合用 Uniform 传递?

多选题 · mid

  • A.模型视图投影矩阵← 正确答案
  • B.当前动画时间← 正确答案
  • C.几十万粒子的位置数组
  • D.全局光源方向向量← 正确答案

答案:ABD。矩阵、时间、光源方向都是“一帧内全体共享、量小”的数据,适合 Uniform。几十万粒子的位置是逐顶点大数组,应该放 Buffer 中按索引读取,硬塞 uniform 会超过容量限制。

Q6. 关于 Buffer(VBO)与 attribute 的关系,正确的是?

多选题 · mid

  • A.attribute 是着色器里读取顶点数据的逻辑接口← 正确答案
  • B.VBO 存放在 GPU 显存中,可跨帧重复使用← 正确答案
  • C.每次绘制前都必须重建 VBO 才能保证数据正确
  • D.vertexAttribPointer 负责描述数据排布与取址← 正确答案

答案:ABD。attribute 是逻辑变量名,VBO 是物理存储,两者通过 vertexAttribPointer 绑定。VBO 常驻显存可复用,C 的“每次重建”是典型性能误区。

Q7. 光栅化阶段中,片元收到的 varying 数据是怎么来的?

单选题 · mid

  • A.每个片元自动生成一份插值后的 varying← 正确答案
  • B.只有三角形中心才有一份 varying
  • C.varying 的值只在顶点上有效
  • D.片元数量与插值完全不相关

答案:A。光栅化把三角形覆盖成大量像素,同时按像素位置对顶点 varying 做线性插值,因此每个片元都有自己的插值结果。B、C 都否定了插值的存在;D 则完全说反了。

Q8. 下面哪种做法最容易导致渲染性能问题?

单选题 · hard

  • A.每帧把视角矩阵写入 uniform
  • B.把逐顶点位置数组放在 VBO 中
  • C.每帧全量重新上传几十万顶点到 GPU← 正确答案
  • D.在顶点着色器里计算顶点位置

答案:C。每帧全量重建大 VBO 会产生海量 PCIe 带宽消耗,是经典性能瓶颈。A 的 uniform 更新开销很小,B 是正确用法,D 本来就是顶点着色器的本职。

Q9. 一个三角形只有 3 个顶点,片元着色器大概会执行多少次?

单选题 · hard

  • A.恰好 3 次
  • B.等于三角形覆盖的像素数量← 正确答案
  • C.与顶点数完全没有关联
  • D.只执行 1 次

答案:B。顶点着色器每个顶点执行一次,片元着色器每个片元(像素)执行一次。三角形覆盖几百个像素,片元着色器就跑几百次,这正是 varying 需要插值的原因。

Q10. 关于三类变量的对比,下列正确的有?

多选题 · extreme

  • A.Uniform 容量小但读取高效,适合少量全局数据← 正确答案
  • B.Buffer 适合大量逐顶点数据,可长期驻留显存← 正确答案
  • C.Varying 各字段在光栅化时自动线性插值← 正确答案
  • D.Uniform 可以为每个顶点保存不同的数值

答案:ABC。Uniform 是“一份共享”的只读常量,容量有限所以只装少量全局数据;Buffer 是逐顶点大数组的归属;Varying 由光栅化自动插值。D 说反了——Uniform 是所有顶点共享同一份值,逐顶点不同是 Buffer 的职责。