URP 渲染实战 / 第 6 章 多光源与阴影 / 案例 6.2
案例 6.2自己计算衰减:为什么一定要有平滑窗口进阶
目标:不调用 GetAdditionalLight(),直接读取 URP 上传的光源数组,自己计算距离和角度衰减;对比平方反比、线性和 URP 三种衰减,看清“窗口”在剔除中的作用。
Assets/Cookbook/Ch06/FalloffCompare.shader(节选:punctual)
// 不用 GetAdditionalLight(),直接读 URP 上传的光源数组,自己算衰减。 // URP 14 在所有平台都走常量数组(USE_STRUCTURED_BUFFER_FOR_LIGHT_DATA 恒为 0) half3 PunctualLight(uint loopIndex, float3 positionWS, half3 normalWS) { #if USE_FORWARD_PLUS int i = int(loopIndex); // Forward+:循环变量就是全局光源下标 #else int i = GetPerObjectLightIndex(loopIndex); // Forward:先查“本物体的第几盏灯”对应的全局下标 #endif float4 lightPos = _AdditionalLightsPosition[i]; // w = 0 表示方向光 half3 color = _AdditionalLightsColor[i].rgb; half4 attenParam = _AdditionalLightsAttenuation[i]; // x = 1/range²,zw = 聚光角度参数 half4 spotDir = _AdditionalLightsSpotDir[i]; float3 lightVector = lightPos.xyz - positionWS * lightPos.w; float d2 = max(dot(lightVector, lightVector), HALF_MIN); half3 L = half3(lightVector * rsqrt(d2)); #if defined(_FALLOFF_INVERSESQUARE) float atten = rcp(d2); // 物理上的平方反比,没有窗口 #elif defined(_FALLOFF_LINEAR) float atten = saturate(1.0 - sqrt(d2 * attenParam.x)); // 1 - d/range #else float atten = DistanceAttenuation(d2, attenParam.xy); // URP:平方反比 × 平滑窗口 #endif atten *= AngleAttenuation(spotDir.xyz, L, attenParam.zw); // 聚光锥;点光的参数让它恒为 1 return color * atten * saturate(dot(normalWS, L)); }
// 不用 GetAdditionalLight(),直接读 URP 上传的光源数组,自己算衰减。 // (网页版的光源数组与 URP 14 同名:_AdditionalLightsPosition / Color / Attenuation / SpotDir) vec3 PunctualLight(int loopIndex, vec3 positionWS, vec3 normalWS) { // Forward+:循环变量就是全局光源下标;Forward:先查“本物体的第几盏灯”对应的全局下标。 // HLSL 版用 #if USE_FORWARD_PLUS 区分;网页版的 GetPerObjectLightIndex 在 Forward+ 下原样返回 int i = GetPerObjectLightIndex(loopIndex); vec4 lightPos = _AdditionalLightsPosition[i]; // w = 0 表示方向光 vec3 color = _AdditionalLightsColor[i].rgb; vec4 attenParam = _AdditionalLightsAttenuation[i]; // x = 1/range²,zw = 聚光角度参数 vec4 spotDir = _AdditionalLightsSpotDir[i]; vec3 lightVector = lightPos.xyz - positionWS * lightPos.w; float d2 = max(dot(lightVector, lightVector), HALF_MIN); vec3 L = lightVector * inversesqrt(d2); #if defined(_FALLOFF_INVERSESQUARE) float atten = 1.0 / d2; // 物理上的平方反比,没有窗口 #elif defined(_FALLOFF_LINEAR) float atten = saturate(1.0 - sqrt(d2 * attenParam.x)); // 1 - d/range #else float atten = DistanceAttenuation(d2, attenParam.xy); // URP:平方反比 × 平滑窗口 #endif atten *= AngleAttenuation(spotDir.xyz, L, attenParam.zw); // 聚光锥;点光的参数让它恒为 1 return color * atten * saturate(dot(normalWS, L)); }
- 下标换算。和
GetAdditionalLight内部一样:Forward 下循环变量是“本物体的第几盏灯”,要用GetPerObjectLightIndex查出全局下标;Forward+ 下循环变量本身就是全局下标。 - 方向光与点光统一处理。
_AdditionalLightsPosition的 w 分量对方向光为 0、对点光为 1,所以lightPos.xyz − positionWS × lightPos.w对方向光得到方向本身,对点光得到“从表面指向光源”的向量。这是 URP 的一个小技巧,省掉了分支。 - Range 不需要单独上传。
attenParam.x就是 1/Range²,线性衰减需要的 d/Range 用sqrt(d2 * attenParam.x)即可。

图 6-5 的前两格揭示了“窗口”的真正意义。剔除——无论是 Forward 的每物体列表,还是 Forward+ 的 tile/Z-Bin 掩码——都是保守的、粗粒度的:它只保证“Range 范围内的像素一定会算这盏灯”,却不保证“Range 范围外的像素一定不算”。如果衰减在 Range 处不为 0,那么剔除粒度就会直接暴露在画面上:Forward+ 下是 tile 形状的硬边,Forward 下则是物体一进一出范围时整块的亮度跳变。只有让衰减在 Range 处平滑归零,剔除才是“隐形”的。

调参建议灯光的 Range 不要设得“刚好够”,而要让平方反比在 Range 附近已经足够暗,这样窗口只做最后的收尾,衰减看起来最自然;Range 也不要无谓地大,它直接决定了这盏灯参与多少像素的计算。Intensity 与 Range 是两个独立旋钮:前者决定亮度,后者决定影响范围和性能。