着色频率与材质系统
Shading Frequency & Material Systems
实现着色时要按"计算频率"划分工作:整个 draw call 不变的放到应用端作为 uniform,其余放在相应的可编程阶段。实践中大多数着色在像素级完成,顶点着色器负责变换和变形。材质系统把面向美术的材质与底层着色器组织起来,处理海量着色器变体的组合问题。
计算频率
| 频率 | 例子 | 位置 |
|---|---|---|
| "只算一次" | 常量子表达式、依赖硬件配置或安装选项的量 | 编译期、离线、安装或加载时 |
| 变化很慢 | 依赖游戏内时间的光照因子 | 可分摊到多帧 |
| 每帧 | 合并观察和投影矩阵 | 应用端 |
| 每模型 / 每 draw call | 与位置相关的光照参数、每个材质的参数 | 应用端,uniform |
| draw call 内变化 | 顶点(细分前)、面片(外壳)、细分后顶点(域)、图元(几何)、像素 | 可编程着色器 |
按更新频率给 uniform 分组既提高应用效率,也减少常量更新、提升 GPU 性能。
为什么逐像素
- 书中用三种顶点密度的模型对比:极密的龙模型两者差别很小;茶壶上逐顶点出现棱角状高光;两个三角形的平面上逐顶点明显错误。原因是高光等项在表面上非线性变化,而顶点着色器结果在三角形上是线性插值的。(旧文献分别称逐顶点、逐像素为 Gouraud 着色和 Phong 着色。)
- 只把高光放像素、其余放顶点理论上省一点,但实践中重复计算和额外 varying 的开销往往抵消收益。
- 越来越多着色计算用计算着色器实现(第 20 章)。
插值的细节
- 单位法线线性插值后长度小于 1,像素着色器里要重新归一化;顶点间法线长度差异大(如顶点混合的副作用)会让插值方向偏向长的一侧,所以通常插值前后都归一化。
- 指向特定位置的向量(视线、精确光源方向)通常不插值,而是插值表面位置后在像素着色器里相减得到。若必须插值这些向量,插值前不要归一化,否则方向错误。
- 光照所用坐标系(世界空间、相机空间,偶尔模型空间)由整个渲染系统统一决定:光源极多时用世界空间避免变换光源位置;相机空间便于优化与视线相关的运算并可能提高精度。
- 平面着色 (flat shading) 作为风格选择(如《Kentucky Route Zero》):把图元属性放在第一个顶点并关闭插值即可,通常在顶点着色器中实现。
实现示例要点
书中的 WebGL 2 示例:像素着色器循环遍历所有点光源(动态分支,简单场景可行,但不适合大量光源——见第 20 章);光源数组放在 uniform block 中并按 std140 布局(用 vec4 对齐);应用端在编译前把 MAXLIGHTS 替换为实际值; 和暖色在应用端算好作为 uniform 传入;clamp 到 [0,1](HLSL 的 saturate)在多数 GPU 上几乎免费。
材质系统
- 着色器是 GPU 可编程阶段的程序,是底层 API 资源;材质是面向美术的表面外观封装。两者不是一一对应:同一材质在不同渲染情况下可能用不同着色器,一个着色器也可被多个材质共享。
- 常见形式:材质模板(一类材质及其参数)+ 材质实例(模板 + 一组参数值);虚幻引擎支持多层级的模板继承。
- 参数可在运行时(uniform)或编译时(替换源代码)解析;常见的编译期参数是开关某个材质特性的布尔量,可由美术勾选,或由系统自动设置(例如远处物体关闭效果不明显的特性)。
- 材质参数可以经过复杂计算得到着色参数,例如地形材质根据高度和法线在高处的水平面上混入白色雪;随时间变化的霓虹灯。
需要组合的几类功能
- 表面着色与几何处理(刚体变换、顶点混合、变形、细分、实例化、裁剪);
- 表面着色与合成操作(discard、混合;在移动 GPU 上混合通常在像素着色器里完成);
- 着色参数的计算与着色模型本身;
- 可单独选择的材质特性;
- 着色模型与光源求值(延迟渲染会改变这种组合结构)。
GPU 着色器不支持编译后链接代码片段,所以这些组合只能在源代码层面完成,主要靠
#include、#if、#define等预处理和字符串操作。
着色器变体:运行时分支 vs 编译期变体
- 旧硬件上动态分支不可用或极慢,所有变体(包括各类光源数量的组合)都在编译期处理。
- 现代 GPU 对动态分支处理得很好,特别是在整个 draw call 内分支一致时;光源数量等现在多在运行时处理。但功能越多寄存器用量越大、占用率越低,所以编译期变体仍有价值。
- 书中例子:三类光源中复杂的通用聚光灯不到 5%,可以编两个变体——"有至少一个通用聚光灯"和"没有",常用的后者寄存器占用更低、更快。
- 规模:《Destiny: The Taken King》某些区域单帧使用超过 9000 个编译变体;Unity 有的着色器可能变体近 1000 亿,只编译实际用到的,并为此重新设计了编译系统。
材质系统的组织策略(通常混合使用)
| 策略 | 做法 |
|---|---|
| 代码复用 | 公共函数放共享文件,#include 引入 |
| 减法式 | 超级着色器 (übershader),用预处理条件和动态分支去掉不用的部分 |
| 加法式 | 功能定义为带输入输出的节点并组合,可用文本或可视化图编辑器(方便技术美术);虚幻的材质图只能影响着色模型输入的计算 |
| 模板式 | 定义接口,不同实现可插入;如把着色参数计算与着色模型分开。虚幻的"材质域"(Surface、Light Function)、Unity 的 surface shader;延迟着色中 G-buffer 也扮演接口 |
其他职责
- 跨平台:Destiny 用自定义着色语言方言加预处理层自动翻译到不同平台,虚幻和 Unity 有类似系统。
- 性能:Destiny 和虚幻会自动把整个 draw call 中不变的计算移出着色器;Destiny 按更新频率(每帧、每光源、每物体)划分常量作用域以降低 API 开销。
GEA 的视角:平面、Gouraud 与逐像素;着色器变体膨胀(卷 II 12.5.1、12.5.12)
- GEA 把着色频率讲成一段历史:最早的引擎每个三角形只做一次简化光照(忽略高光、通常只有一个光源),即平面着色,现在偶尔用来刻意营造"计算机渲染"的外观;之后在顶点上算光照、在三角形内插值顶点颜色,即 Gouraud 着色,网格密时效果尚可,三角形大时线性插值的缺陷非常明显;今天的引擎多在像素着色器里逐像素应用着色方程(卷 II 12.5.1,见 着色方程与经典光照模型)。这与本页「计算频率」「为什么逐像素」一致;本页 RTR4 的平面着色例子是作为风格选择的《Kentucky Route Zero》。
- 前向渲染处理复杂多光源场景时,会出现大量不同版本的顶点和像素着色器:处理不同光源数目、不同光源类型、不同蒙皮权重数目等。GEA 把避免这种着色器变体膨胀列为延迟渲染的好处之一(卷 II 12.5.12、12.5.12.1,见 延迟着色)。本页「着色器变体:运行时分支 vs 编译期变体」从材质系统角度讨论同一问题。
- 材质库:很多游戏只需少量标准材质(木、石、金属、塑料、布、皮肤……),网格以松耦合方式引用材质库中的材质,避免在每个网格里重复材质数据(卷 II 12.9.2,见 渲染开发与 GPU 调试工具)。
PBRT 的视角:纹理与材质分离的材质接口(PBRT 4e 10.5)
- 分工:纹理描述标量或光谱在表面上的空间变化;材质在交点处求值纹理,得到参数,构造对应的 BSDF。两者解耦,可以任意组合(与 Cook 1984 的 shade tree 相似;OSL 也是"着色器返回材质描述而非颜色"的模式)。
- 静态类型的
GetBxDF():每种材质声明自己的BxDF类型(如DiffuseMaterial返回DiffuseBxDF),通用代码可以在栈上为具体 BxDF 分配空间,并按类型分组在 GPU 上向量化执行(第 15 章)。代价:一种材质不能根据参数返回不同类型的 BxDF。Material::GetBSDF()通过TaggedPointer::Dispatch与泛型 lambda 把具体类型还原出来,用ScratchBuffer分配 BxDF 内存。 TextureEvaluator:材质用texEval(tex, ctx)而不是tex.Evaluate(ctx),让波前渲染器能把"纹理轻"与"纹理重"的材质分开处理;CPU 积分器用的UniversalTextureEvaluator被编译器优化为零开销。- MixMaterial:随机按
amount选一个子材质(材质版的随机 alpha 测试,见 透明渲染与 Alpha 混合),所以 50/50 混合不是两个 BSDF 的平均。低采样率下有噪声(书中图 10.20:1 spp 明显,128 spp 无碍,通常几十个样本足够),好处是采样和求值只针对一个 BSDF。它需要在SurfaceInteraction::GetBSDF()中特殊处理(循环解析嵌套的 Mix),作者承认这不是好的抽象设计。 - 空材质表示两种介质之间不散射的界面,返回未设置的 BSDF。
- 色散:
DielectricMaterial的折射率是光谱纹理;只要折射率不是ConstantSpectrum就调用TerminateSecondary(),只沿第一个波长追踪(反射时本可以保留全部波长,但此时还不知道会采样哪个分量)。 - 调试选项:可把所有材质替换为以 为反射率的漫反射,用于排查问题。
- BTF/SVBRDF 视角:BTF 是六维函数(空间 2 维 + 方向 4 维);pbrt 的"纹理 + 参数化 BSDF"相当于对 BTF 做了一种固定的因式分解。测量时 BTF 不预设因式分解,所以更适合采集;神经表示见 Rainer 等 2019/2020、NeuMIP(Kuznetsov 等 2021)。
分歧:粗糙度到 的映射
pbrt-v4 的 TrowbridgeReitzDistribution::RoughnessToAlpha() 是 (网页代码),材质的 remapRoughness 为真时使用;而 微表面理论 中 RTR4 记录的 Disney/Burley 约定是 ,被大多数实时引擎采用。同一个"粗糙度 0.5"在 pbrt 中对应 ,在 Disney 约定下是 ,跨渲染器迁移材质时必须换算()。书中只说该映射"给出相当直观的控制",没有讨论与 Disney 约定的差异。
原书问题(第 10 章参考文献)
- 第 10 章参考文献把 Rainer 等 2020 "Unified neural encoding of BTFs"(CGF 39(2))的第一作者写成 "Rainer, R."。同一作者在紧邻的 2019 条目中是 "Rainer, G."(Gilles Rainer),2020 论文的第一作者同样是 Gilles Rainer(Crossref,DOI 10.1111/cgf.13921,CGF 39(2), 167–178),应为 "Rainer, G."。
LearnOpenGL 的视角:Material / Light 结构体与光照贴图(Lighting / Materials · Lighting maps)
来源:Materials、Lighting maps;摘录 Lighting — Materials、Lighting — Lighting maps。
材质与灯光分量拆开
片元里用 GLSL struct Material { vec3 ambient, diffuse, specular; float shininess; } 与 struct Light { vec3 position, ambient, diffuse, specular; }。环境/漫反射材质色通常取物体色;镜面材质色控制高光色;灯光三分量可独立压暗环境、保持高光。Uniform 按 material.diffuse 这种「结构体名.成员」设置——结构体只是命名空间。
只把灯光当灰色强度时物体易过亮;把 light.ambient 降到约 0.2、diffuse 约 0.5、specular 保持 1 是教程默认手感。光色随时间 sin 变化可演示「能反射的颜色受入射光谱限制」(接 着色器中的颜色 的相乘模型)。
漫反射贴图与镜面贴图
整物体单一 vec3 材质不够:改用 sampler2D diffuse(及 specular)。环境项通常直接采样同一张漫反射贴图。sampler2D 是不透明类型,只能作为 uniform(不能当作普通函数参数随意实例化),構造含 sampler 的 struct 时尤需注意。
镜面贴图常用灰度:木纹区域近黑(无高光),金属边框近白。也可把彩色写进镜面贴图,但物理上高光色多由光源决定,观感往往不真实。练习中的 emission map 与 GEA「自发光纹理」相同:与光照条件无关的自发光(见 着色方程与经典光照模型)。
纹理单元:漫反射绑 GL_TEXTURE0、镜面绑 GL_TEXTURE1,并用 setInt("material.diffuse", 0) 等把 sampler 指到单元——细节见 着色器纹理采样、纹理映射管线。
UE5 的视角:图到 HLSL 与 Custom 的折叠代价
来源:Epic KB How the Unreal Engine Translates a Material Graph to HLSL;How the Unreal Engine Translates a Material Graph to HLSL、Custom Material Expressions。专页:材质图到 HLSL 的翻译、Custom 材质表达式、材质系统。
UE 材质图在 Apply 时由 FHLSLMaterialTranslator 填进 MaterialTemplate.ush,内置节点在翻译期做常量折叠(整次 draw call 恒定的运算收到 CPU / 均匀参数里)。这与本页「应用端 uniform vs 像素阶段」同一分层:能上提的别留给 GPU。Custom 节点把一段 HLSL 当黑盒插入,无法参与折叠,官方明确可能比等价节点图更贵——只在内置节点做不到时使用。静态参数与 Usage 标记则在编译期制造着色器排列,对应上文「变体膨胀」在 UE 中的具体旋钮。
出处
- realtime_rendering_4ed.pdf 第 5 章 Shading Basics
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 12 章 12.5.1、12.5.12、12.9.2 节
- PBRT 4e source excerpt: chapter 10 (Textures and Materials)
- Pharr, Jakob, Humphreys《Physically Based Rendering: From Theory to Implementation》第 4 版(MIT Press, 2023)第 10 章 10.1–10.5 节,pbr-book.org/4ed/Textures_and_Materials
- Lighting — Materials
- Lighting — Lighting maps
- learnopengl.com/Lighting/Materials
- learnopengl.com/Lighting/Lighting-maps
- How the Unreal Engine Translates a Material Graph to HLSL Epic KB
- Custom Material Expressions UE 5.8