URP 渲染实战 / 第 9 章 PBR 理论 / 案例 9.9
案例 9.9在 Gamma 空间算光照会怎样入门
目标:两盏彩色方向光照一个白球,对比线性空间与 Gamma 空间的光照结果。
做法:管线是线性的,我们“模拟”老式 Gamma 工作流:先把材质颜色和灯光颜色编码回 sRGB 数值,在编码后的数值上做 Lambert 乘加,最后再解码一次,以抵消管线最终写入 sRGB 目标时的编码,让屏幕显示的正是“在 Gamma 数值上算出来的结果”。
Assets/Cookbook/Ch09/GammaVsLinear.shader(节选:frag)
half4 frag(Varyings i) : SV_Target { float3 N = normalize(i.normalWS); Light l1 = GetMainLight(); float3 L2 = normalize(_Light2Dir.xyz); // 材质颜色、灯光颜色在 Shader 里拿到的都已经是线性值 float3 albedo = _BaseColor.rgb; float3 c1 = l1.color, c2 = _Light2Color.rgb; if (_GammaSpace > 0.5) { // 错误示范:把它们变回 sRGB 编码值再去做乘法和加法 albedo = LinearToSRGB(albedo); c1 = LinearToSRGB(c1); c2 = LinearToSRGB(c2); } float3 lit = albedo * (c1 * saturate(dot(N, l1.direction)) + c2 * saturate(dot(N, L2))); if (_GammaSpace > 0.5) lit = SRGBToLinear(saturate(lit)); // 抵消管线最后的 sRGB 编码 return half4(lit, 1); }
void main() { vec3 N = normalize(normalWS); Light l1 = GetMainLight(); vec3 L2 = normalize(_Light2Dir.xyz); // 材质颜色、灯光颜色在 Shader 里拿到的都已经是线性值 vec3 albedo = _BaseColor.rgb; vec3 c1 = l1.color, c2 = _Light2Color.rgb; if (_GammaSpace > 0.5) { // 错误示范:把它们变回 sRGB 编码值再去做乘法和加法 albedo = LinearToSRGB(albedo); c1 = LinearToSRGB(c1); c2 = LinearToSRGB(c2); } vec3 lit = albedo * (c1 * saturate(dot(N, l1.direction)) + c2 * saturate(dot(N, L2))); if (_GammaSpace > 0.5) lit = SRGBToLinear(saturate(lit)); // 抵消管线最后的 sRGB 编码 fragColor = vec4(lit, 1.0); }

线性空间的结果之所以“对”,是因为光的叠加在物理上就是能量相加:50% 的橙光加 50% 的蓝光,总能量就是两者之和。Gamma 空间的数值不与能量成正比,加法失去了意义。这也是为什么“同一套美术资源,在 Gamma 项目里看起来对,搬到线性项目就偏亮偏灰”——当初在 Gamma 里调出来的参数本身就补偿了错误。新项目请一律使用线性空间。
移动端注意部分很老的 OpenGL ES 2.0 设备不支持 sRGB 读写,只能用 Gamma 空间。URP 14 仍保留了 Gamma 空间的支持(源码里可以看到
UNITY_COLORSPACE_GAMMA 分支),但 PBR 的结果在 Gamma 空间下只能算“近似可用”。今天的目标设备基本都支持线性空间。HDR 与色调映射
辐射率没有上限。正午太阳下白墙的亮度,可能是室内暗角的数千倍;9.6 节算过,光滑表面的 GGX 峰值可以上千。如果帧缓冲只能存 0~1,这些信息在光照计算后就被截断了:高光变成一片纯白,Bloom 也无从提取“到底有多亮”。
所以 PBR 管线需要 HDR 渲染目标(URP 14 默认使用 32 位的 B10G11R11 浮点格式,可在 URP Asset 的 HDR Precision 里改为 64 位的 R16G16B16A16),让光照、半透明混合、后处理都在高动态范围下进行,最后由色调映射(Tonemapping)把它压缩到显示器能显示的 0~1,再做 sRGB 编码: