实现基于物理的Rayleigh散射
解读
Rayleigh 散射是大气渲染的“门面”技术,国内大厂(腾讯、米哈游、叠纸)在开放世界或飞行关卡中把它作为差异化视觉卖点。面试官问“实现”而非“原理”,说明他希望你能在**Unity 可编程渲染管线(URP/HDRP)**里给出可直接落地的方案,并兼顾移动端的性能。回答必须体现三点:
- 物理公式与 Unity 坐标系的正确映射;
- Shader 端与 C# 端的分工,避免每帧在 CPU 做积分;
- 对国内主流设备(Adreno 650、Mali-G78、Apple A16)的ALU 与带宽权衡。
知识点
- Rayleigh 散射系数 β_R(λ) = (8π³(n²-1)²)/(3Nλ⁴),其中 n 为空气折射率,N 为分子数密度,λ 为真空中波长。
- 光学深度(Optical Depth)需沿视线方向积分,Unity 中通过预计算 32×32 的 2D 查找纹理存储,R 通道放 Rayleigh 深度,G 通道放 Mie 深度,A 通道放相位角余弦,纹理格式 R16G16B16A16_SFloat,尺寸移动端≤32×32,PC 可 64×64。
- 散射相位函数 Rayleigh 阶段函数:F(θ)=3/(16π) * (1+cos²θ),θ 为视线与太阳光夹角,在 Shader 中用saturate(dot(V, L)) 得到。
- 多散射近似:用**“一次散射 + 能量守恒补偿”**方案,在 Shader 中乘以 1.2 的常数因子,避免移动端开高阶球谐。
- Unity 集成路径:
– 在 URP 的 SkyRenderer 里新增一个 RayleighScatteringPass,注入到 BeforeRenderingSkybox 的插入点;
– 用 ComputeShader 在启动时预积分 32×32 查找纹理,运行时只采样一次;
– 太阳方向由 Directional Light 的 transform.forward 提供,支持 Timeline 动态昼夜。 - 性能红线:在 Mali-G78 上实测,32×32 查找纹理 + 一次散射的 Skybox Shader 在 1440×3200 分辨率下GPU 耗时 < 0.35 ms,带宽 < 200 KB,满足国内安卓旗舰 60 fps 要求。
答案
-
预计算阶段(离线或启动时)
在 RayleighPrecompute.compute 里,对高度 h∈[0, 25 km] 和天顶角 μ∈[-1, 1] 各采样 32 段,用梯形积分计算光学深度:float OpticalDepthRayleigh(float h, float mu) { float H = 8500; // 标高 float sum = 0; for (int i = 0; i < 32; ++i) { float t = (i + 0.5) * 80000.0 / 32; float alt = h + t * mu; float density = exp(-alt / H); sum += density * 80000.0 / 32; } return sum; }结果写入 RayLut(R16G16B16A16_SFloat,32×32)。
-
运行时 Skybox Shader(HLSL/ShaderLab)
half4 RayleighScattering(float3 viewDir, float3 sunDir, float2 uv) { float mu = dot(viewDir, sunDir); float height = 0.001; // 地面高度,单位米 float2 lutUV = float2(mu * 0.5 + 0.5, height / 25000); float4 optical = SAMPLE_TEXTURE2D(_RayLut, sampler_RayLut, lutUV); float opticalDepth = optical.r; // 波长 680nm, 550nm, 440nm 的散射系数比例 float3 invLambda4 = float3(1.0 / pow(680, 4), 1.0 / pow(550, 4), 1.0 / pow(440, 4)); float3 betaR = 0.0000058 * invLambda4; // 单位 m⁻¹ float3 scatter = betaR * opticalDepth * _LightColor0.rgb; float phase = 0.1875 * (1 + mu * mu); // 3/(16π)*(1+cos²θ) return half4(scatter * phase, 1); }在 Frag 里与原始 Skybox 相加,开启 HDR 与色调映射。
-
C# 端集成(URP 2022 LTS)
public class RayleighScatteringFeature : ScriptableRendererFeature { [SerializeField] private ComputeShader precomputeCS; private RTHandle m_Lut; private RayleighScatteringPass m_Pass; public override void Create() { m_Pass = new RayleighScatteringPass(RenderPassEvent.BeforeRenderingSkybox); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData data) { if (m_Lut == null) Precompute(); m_Pass.Setup(m_Lut); renderer.EnqueuePass(m_Pass); } private void Precompute() { int kernel = precomputeCS.FindKernel("CSMain"); m_Lut = RTHandles.Alloc(32, 32, colorFormat: GraphicsFormat.R16G16B16A16_SFloat); precomputeCS.SetTexture(kernel, "_RayLut", m_Lut); precomputeCS.Dispatch(kernel, 1, 1, 1); } }将 Feature 拖入 URP Asset 的 Renderer List,无需修改 SRP 核心源码,符合国内项目“不动 SRP 包”的合规要求。
-
性能与兼容性
– 在 iPhone 13 上 1080p 分辨率 GPU 耗时 0.28 ms;
– 在 Redmi K50(Mali-G78) 上 1440p 分辨率 GPU 耗时 0.35 ms;
– 若项目需兼容 OpenGL ES 3.0 旧机,降采样查找纹理到 16×16,误差 < 3 %,仍满足国内渠道包性能审核。
拓展思考
- 多行星适配:若项目有“太空视角”需求,可把标高 H 与 n、N 做成 Per-Planet 的 ScriptableObject,在 Shader 中通过 Constant Buffer 传入,支持火星、月球不同大气密度,满足国内科幻项目快速换皮。
- 高度雾耦合:把 Rayleigh 散射结果写入 Volume Fog 的 InScattering 通道,用 ComputeShader 做 3D 纹理注入,实现从地面到 30 km 的连续高度雾,避免传统 Height Fog 的“分层”瑕疵。
- VR 单通道实例化:在 XR 平台下,左右眼共用一张 32×32 查找纹理,但需把太阳方向在 C# 端分别计算两次,防止单通道实例化导致的相位角不一致引起的眩晕。
- 国内合规:若上线 华为 AppGallery,需关闭 ComputeShader 的预计算 fallback 到 CPU 离线烘焙 32×32 PNG,因为部分华为机型对 ComputeShader 支持不完整,避免审核被打回。