GPU Gems 1 · Chapter 33. Converting Production RenderMan Shaders to Real-Time
以 Stuart Little 毛发 shader 为例,把 RenderMan 的隐式光源与表面计算映射到 Cg 的 vertex/fragment 程序,再用频率分工、纹理查表、向量化和误差证据获得实时近似。
学习目标
- 能把 RenderMan 的光源遍历、表面变量和 shading rate 映射成应用、vertex 与 fragment 三层职责
- 能解释为什么实时 Cg 需要显式管理光源、为什么多 pass 会随光源数量增加而变贵
- 能使用 texture lookup、向量化和计算上移等优化,并用画面差异、采样率与指令数检查近似质量
- 能在质量、硬件 profile、fragment 工作量和 shader 可维护性之间做出可复现的转换取舍
GPU Gems 1 第 33 章以《Stuart Little》的 RenderMan 毛发 shader 为案例,讨论生产离线 shader 如何转成硬件 shader。重点不是把 RenderMan 语法机械替换为 Cg,而是重新理解 GPU 的执行频率:RenderMan 的 illuminance 可以隐式遍历光源,GPU 却需要应用显式提供参数;RenderMan 的一个 surface shader 也常常要拆成 vertex program 与 fragment program 才能在实时预算内运行。
↡描述表面如何响应光照、包含材质参数和光源交互的生产级 RenderMan shader,在转换中被拆分为硬件可执行阶段 是转换的语义基线。原章的 fur shader 里包含毛发切线、表面法线、根部到尖部参数、漫反射和高光等数据;转换前先问“这个值由谁、多久变化一次”,再决定它落在哪个 GPU 阶段。
1. 从生产 shader 到硬件 profile 的转换边界
离线渲染语言可以接受每帧数小时的计算时间,以换取复杂光照和材质表达;实时预览的价值则是尽快看到增量变化。Cg 是原章使用的硬件 shading language,编译器可以按不同 hardware profile 生成指令。这里的概念也适用于其他 shader 语言,但具体寄存器、指令数和分支行为必须以目标 profile 为准。
转换时至少要保留三份证据:原始 RenderMan 图像作为视觉基线,Cg 初始转换作为语义基线,优化后图像和差异图作为取舍记录。这样才能区分“代码改错了”和“为了实时预算主动接受了近似”。
2. Lights:把隐式 illuminance 变成应用协议
↡在 RenderMan surface shader 中遍历满足空间和角度条件的场景光源,并让表面计算读取当前光源贡献的语句 会在 shading point 处迭代符合条件的光源,表面 shader 可以读取光色、方向,甚至通过消息传递取得额外信息。Cg 没有直接等价的语句,因此不能只把 illuminance(P, N, width) 改成一个函数调用就结束。
最直接的 Cg 映射是固定长度参数,例如 lightDir[10];它可以在循环中累加,但超过十盏灯就要改 shader。另一条路径是 multipass:每个 pass 只绑定当前光源,把结果加到 framebuffer。它把光源数量管理移到应用,却会重复运行 vertex 与 fragment 计算;光源越多、shader 越复杂,成本越明显。
复杂 light source shader 还需要一套稳定参数协议。原章通过预处理宏把 uber light 的参数声明和调用列表集中起来,让入口签名可读,并把当前光源参数显式传入。RenderMan 中的 message passing 在这个模型里由当前 pass 的参数绑定替代:不要依赖隐式消息,要让应用知道每个值来自哪里。
3. Vertex program 与 fragment program:同一语义的不同频率
↡在 GPU 上按输入顶点执行、负责变换并准备可插值 varying 数据的程序阶段 的工作单位是顶点。它可以计算依赖顶点位置的 light direction、eye direction,也可以把 hair tangent、hair parameter、surface normal 等数据直接传给下一个阶段。
↡在 GPU 上按生成的 fragment 执行、负责纹理操作和最终颜色计算的程序阶段 的工作单位接近最终像素,通常会执行更多毛发高光和纹理运算,而且会覆盖可见与不可见的 generated fragments。RenderMan 中由 ShadingRate 表达的采样概念,不能简单等同为 GPU 的 vertex/fragment 频率;硬件会在 rasterization 阶段对 varying 做线性插值,输入几何采样率因此直接影响画质。
可以用下面的标注方式开始拆分:
application: hairNorm 不随 view 或 light 改变
vertex: L、V 随 vertex position 改变,但沿一段曲线变化较慢
fragment: 最终颜色、纹理采样和真正随 fragment 改变的高光如果把 dot product 从 fragment 移到 vertex,就必须接受 rasterizer 的 ↡vertex 输出经过 rasterizer 线性插值后,与真实 fragment 计算之间产生的近似误差。曲率高或采样稀疏的区域更容易出现误差,因此“上移”不是无条件优化,而是带着采样率约束的近似。
4. 优化一:把稳定计算移到 application 或 vertex
原始 fur fragment program 中,有一段由法线和切线构造 hairNorm 的代码不依赖 view 或 light。它可以在 application level 预先计算,再作为新 attribute 传给 vertex program。另一组 dot(T, L) 和 dot(T, V) 依赖光源和视点,不能放到 application,但如果一段曲线内变化缓慢,可以放到 vertex,再交给 rasterizer 插值。
// application or vertex stage
float3 S = cross(nSN, T);
float3 N_hair = cross(T, S);
float l = saturate(dot(nSN, T));
float3 hairNorm = normalize((l * nSN) + ((1.0 - l) * N_hair));
// fragment stage keeps only values that truly vary per fragment
float T_Dot_nL = saturate(dot(T, L));
float T_Dot_nV = saturate(dot(T, V));移动前后要同时核对数据接口:vertex 输出寄存器必须提供 fragment 需要的 varying,属性的坐标空间要一致,曲线采样率要足以支撑插值。少一个输入或错误地混用 world/object space,都会把性能优化变成画面错误。
5. 优化二:用 texture lookup 近似复杂函数
↡用 1D、2D 或 3D 纹理保存复杂函数的采样结果,在 shader 中以输入变量索引近似原始数学计算的优化方法 适合重复计算、输入维度明确且可接受近似的函数。Kajiya-Kay 毛发模型中的项可以由两个 dot product 作为坐标,预先生成二维浮点纹理;fragment program 用 lookup 替代 acos、sin 等完整计算。
查表不是免费魔法:要决定纹理空间是否归一化、尺寸是否足够、采样是否带来 banding,并用一个简单的差异程序把“完整函数”和“纹理结果”显示成颜色。LUT 分辨率应由误差实验确定,不应只凭纹理看起来够大。
6. 优化三:利用向量化和 profile 约束
GPU 常以向量宽度执行算术。原章把两个类似的 power 结果组织成向量,再与 spec 分量并行相乘,帮助编译器识别可向量化的工作。纹理查表也要考虑目标 profile 的纹理类型、坐标方式和寄存器限制;Cg 的硬件无关源代码最终仍会被 profile 约束。
// scalar-like organization
float3 Cspec = (spec.x * lightColor * pow(Kajiya, rough.x))
+ (spec.y * lightColor * pow(Kajiya, rough.y));
// vector-friendly organization
float2 powVal = spec * pow(Kajiya, rough);
float3 Cspec = lightColor * powVal.x + lightColor * powVal.y;分支也要谨慎:原章指出目标硬件可能同时计算条件两侧,条件语句不一定减少成本。优化目标通常是更短的 fragment program,但必须以 profile 输出和实际 frame time 为证据,而不是把“源码更漂亮”当作性能结论。
RenderMan → real-time lab
用频率、查表和证据压缩 fragment 工作
这是把原章优化顺序做成的关系实验:把稳定计算移出 fragment,必要时用纹理近似复杂函数,再用采样率和差异指标检查质量。数值为示意,不代替目标硬件 profiling。
误差可接受,继续用差异图复核;先记录画面差异,再接受指令数和 frame time 的改善。
7. 质量—性能取舍:近似要有可回归的边界
原章记录了一个清晰的经验:初始 fragment shader 虽然容易转换,却因为没有考虑 GPU 架构而运行缓慢;通过把计算移出 fragment、查表和向量化,示例指令规模从 104 降到 42,画面差异在实践中很小。但生产级角色可能有数百万根毛发,即使优化后的 fragment shader 仍然太慢,最后需要更激进的 vertex-program fur shader,并接受更明显的近似。
因此发布级转换应保存以下回归表:
- 语义:高光、颜色变化和重要轮廓是否出现在正确位置
- 质量:差异图、几何采样率、LUT 分辨率与高曲率区域误差
- 成本:vertex/fragment 指令、纹理读取、光源 pass 数和实际 frame time
- 可维护性:RenderMan 原逻辑改变后,优化后的硬件 shader 是否仍能定位对应语义
最终实现可能已经不像原始 RenderMan 文件,但它仍应能回答“哪一段生产逻辑被移动、近似或删除”。否则下一次艺术修改会无法安全回流。
小结
- RenderMan 的
illuminance、消息和 shading rate 在 GPU 上没有逐项等价物,转换需要应用显式管理光源和数据。 - vertex program 按顶点准备可插值数据,fragment program 按 generated fragment 计算最终颜色,稳定计算还可以上移到 application。
- vertex 到 fragment 的线性插值会引入与采样率、曲率相关的误差;频率优化必须同时检查画质。
- texture lookup 能近似复杂函数,向量化能帮助 GPU 利用宽算术,但二者都要受目标 hardware profile 约束。
- 指令数、差异图、光源数量、LUT 误差和 frame time 应共同组成优化证据;实时版本是可解释、可回归的近似,而不是盲目追求更短代码。
练习
问题 1|光源映射 RenderMan 的 illuminance 可以遍历场景光源,而 Cg 没有直接等价语句。请比较固定数组和 multipass 两种转换方案,并说明各自的代价。
问题 2|频率分工 下列三类计算应如何放置:不随 view/light 改变的 hairNorm、随光源和顶点位置变化的 dot(T, L)、每个 fragment 的最终高光纹理组合?请说明把第二类移到 vertex 的风险。
问题 3|查表优化 一个二维毛发函数由 T_Dot_nL 和 T_Dot_e 决定。如何判断使用 2D texture lookup 是否值得?至少列出两个性能指标和两个质量指标。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- RenderMan surface shader
- 描述表面响应光照的生产级 shader,转换时需要拆分到硬件执行阶段。
- illuminance
- RenderMan 中遍历满足条件的场景光源并读取其贡献的语句。
- vertex program
- 按顶点执行、准备变换结果和可插值 varying 数据的 GPU 程序阶段。
- fragment program
- 按 generated fragment 执行并计算最终颜色或深度的 GPU 程序阶段。
- texture lookup
- 用纹理采样结果近似原始复杂函数的 shader 优化方法。
- interpolation error
- vertex 输出经过 rasterizer 线性插值后,与真实 fragment 计算之间产生的近似误差。