URP 渲染实战 / 第 5 章 经典光照模型 / 案例 5.8
案例 5.8逐顶点与逐像素光照入门
目标:把同一套 Blinn-Phong 分别放在顶点着色器和片元着色器里计算,直观看到 Gouraud 着色的问题,并知道它在今天还有什么用。
两个容易混淆的名字
“Phong”这个名字在图形学里指两样东西:一是案例 5.4 的 Phong 反射模型(怎么算一个点的亮度),二是 Phong 着色(Phong Shading,在三角形内部插值法线、逐像素计算光照)。与之对应的是 Gouraud 着色:在顶点上算好光照颜色,再在三角形内部插值颜色。两者可以与任何反射模型组合,比如“用 Gouraud 着色计算 Blinn-Phong 反射”。
代码
Assets/Cookbook/Ch05/Gouraud.shader(节选:shade)
half3 ShadeBlinnPhong(float3 positionWS, float3 N) { float3 V = GetWorldSpaceNormalizeViewDir(positionWS); Light mainLight = GetMainLight(); float3 L = mainLight.direction; half3 radiance = mainLight.color * mainLight.distanceAttenuation; float NdotL = dot(N, L); float spec = pow(saturate(dot(N, SafeNormalize(L + V))), _Shininess) * step(0.0, NdotL); half3 color = (_BaseColor.rgb * saturate(NdotL) + _SpecColor.rgb * spec) * radiance; return color + _BaseColor.rgb * SampleSH(N) * _AmbientStrength; }
vec3 ShadeBlinnPhong(vec3 positionWS, vec3 N) { vec3 V = GetWorldSpaceNormalizeViewDir(positionWS); Light mainLight = GetMainLight(); vec3 L = mainLight.direction; vec3 radiance = mainLight.color * mainLight.distanceAttenuation; float NdotL = dot(N, L); float spec = pow(saturate(dot(N, SafeNormalize(L + V))), _Shininess) * step(0.0, NdotL); vec3 color = (_BaseColor.rgb * saturate(NdotL) + _SpecColor.rgb * spec) * radiance; return color + _BaseColor.rgb * SampleSH(N) * _AmbientStrength; }
Assets/Cookbook/Ch05/Gouraud.shader(节选:vertfrag)
Varyings vert(Attributes input) { Varyings output; VertexPositionInputs pos = GetVertexPositionInputs(input.positionOS.xyz); float3 normalWS = TransformObjectToWorldNormal(input.normalOS); output.positionCS = pos.positionCS; #if defined(_PER_VERTEX) output.color = ShadeBlinnPhong(pos.positionWS, normalWS); #else output.positionWS = pos.positionWS; output.normalWS = normalWS; #endif return output; } half4 frag(Varyings input) : SV_Target { #if defined(_PER_VERTEX) return half4(input.color, 1); #else return half4(ShadeBlinnPhong(input.positionWS, normalize(input.normalWS)), 1); #endif }
#ifdef VERTEX void main() { VertexPositionInputs pos = GetVertexPositionInputs(positionOS.xyz); vec3 nWS = TransformObjectToWorldNormal(normalOS); gl_Position = pos.positionCS; #if defined(_PER_VERTEX) vertexColor = ShadeBlinnPhong(pos.positionWS, nWS); #else positionWS = pos.positionWS; normalWS = nWS; #endif } #endif #ifdef FRAGMENT void main() { #if defined(_PER_VERTEX) fragColor = vec4(vertexColor, 1.0); #else fragColor = vec4(ShadeBlinnPhong(positionWS, normalize(normalWS)), 1.0); #endif } #endif
光照函数 ShadeBlinnPhong 只写一份,用 _PER_VERTEX 关键字决定在哪个阶段调用。注意 Varyings 结构体也随之变化:逐顶点时只需要传一个颜色,逐像素时要传世界坐标和法线。这里用的是 shader_feature_local(不带 _fragment 后缀),因为顶点和片元两个阶段都要看到这个关键字。

逐顶点光照的计算量与顶点数成正比,逐像素与像素数成正比。在早期硬件和今天的低端移动设备上,屏幕像素往往远多于顶点,逐顶点能省下大量计算。它的问题也很明确:任何在一个三角形内部变化剧烈的光照项都会出错,高光首当其冲,聚光灯的光斑边缘、点光源近处的衰减也一样。漫反射和环境光变化平缓,逐顶点计算的误差通常可以接受。
URP 里有两处正是这样用的:
- URP Asset 的 可以选 Per Vertex,此时 Shader 会被打开
_ADDITIONAL_LIGHTS_VERTEX关键字,附加光只在顶点里算 Lambert(URP 的VertexLighting()函数),用于低端设备,第 6 章会处理这个关键字。 - 球谐环境光可以选择逐顶点求值(
EVALUATE_SH_VERTEX)或混合求值(EVALUATE_SH_MIXED),因为环境光本来就很平滑。