Atmospheric Scattering 是計算機圖形學(xué)中用于模擬天空顏色、日落、體積光(God Rays)和空氣透視效果的核心技術(shù)。
Unity 中實現(xiàn)大氣散射,本質(zhì)上是在解決光線穿過介質(zhì)時的吸收(Absorption)和散射(Scattering)問題。
以下是底層物理原理的深度解析,以及兩種主流實現(xiàn)模式(Raymarching 和 LUT)在 Unity 中的具體落地指南。
第一部分:底層物理原理 (The Physics)
大氣散射的核心是輻射傳輸方程(Radiative Transfer Equation)的簡化版。當光線穿過大氣層時,會與微粒碰撞。
- 兩種關(guān)鍵散射類型
- 瑞利散射 (Rayleigh Scattering):
- 原理: 針對比光波長小的微粒(如空氣分子)。
- 特性: 散射強度與波長的 4 次方成反比 (\frac{1}{\lambda^4})。這意味著藍光(波長短)比紅光(波長長)更容易被散射。
- 視覺效果: 造成了白天的藍天和傍晚的紅夕陽(光程長,藍光散盡,只剩紅光)。
- 米氏散射 (Mie Scattering):
- 原理: 針對比光波長大的微粒(如氣溶膠、水滴、霧霾)。
- 特性: 對波長不敏感,光線主要向前散射(Anisotropic)。
- 視覺效果: 造成了太陽周圍的白色光暈和霧氣效果。
- 核心方程邏輯
要計算眼睛看到某個像素的顏色,我們需要沿著視線(View Ray)積分。
其中包含三個關(guān)鍵項:
- Transmittance (透射率 T): 根據(jù)比爾-朗伯定律(Beer-Lambert Law),光線在傳播過程中會呈指數(shù)衰減。T = e^{-\sum \text{密度} \times \text{距離}}。
- Phase Function (相函數(shù) S): 決定光線撞擊粒子后向哪個方向散射(瑞利各個方向較均勻,米氏主要向前)。
- Density (密度 \rho): 大氣密度隨高度呈指數(shù)下降(越高越稀?。?。
第二部分:實現(xiàn)模式一 —— Raymarching (光線步進)
這是最直觀、動態(tài)性最強,但性能開銷最大的方法。常用于高質(zhì)量的體積云或即時調(diào)整大氣參數(shù)的場景。
- 原理
這是一種暴力計算(Brute-force)方法。
- 在 Pixel Shader 中,從相機向每個像素發(fā)射一條射線。
- 將射線穿過大氣層的部分切分為 N 個步進點(Samples)。
- 核心痛點(雙重循環(huán)): 在每一個步進點 P,你需要計算由于太陽照射產(chǎn)生了多少光(In-scattering)。這意味著你需要從點 P 再向太陽發(fā)射一條射線來計算透射率。
- 外層循環(huán):沿視線累加亮度。
- 內(nèi)層循環(huán):計算從太陽到當前點的光照衰減(光路深度)。
- Unity 實現(xiàn)步驟
- 幾何體: 通常使用一個巨大的反面球體(Inverted Sphere)包裹場景,或者使用全屏后處理(Post-processing)。
- Shader 邏輯:
// 偽代碼邏輯
float3 CalculateScattering(float3 rayOrigin, float3 rayDir) {
float3 totalLight = 0;
float opticalDepthView = 0;
// 1. Raymarching Loop (視線方向)
for (int i = 0; i < STEP_COUNT; i++) {
float3 samplePoint = rayOrigin + rayDir * currentDist;
float height = length(samplePoint) - PlanetRadius;
float density = exp(-height / ScaleHeight);
opticalDepthView += density * stepSize;
// 2. Secondary Ray (射向太陽) - 內(nèi)層循環(huán)
// 計算從采樣點到太陽的透射率
float sunRayOpticalDepth = GetOpticalDepthToSun(samplePoint, sunDir);
float3 transmittance = exp(-(opticalDepthView + sunRayOpticalDepth) * ScatteringCoefficients);
totalLight += transmittance * density * stepSize;
}
return totalLight * PhaseFunction(dot(rayDir, sunDir)) * SunIntensity;
}
- 優(yōu)化: * 內(nèi)層循環(huán)很慢,通常會限制步數(shù)。
- 或者將“點到太陽的光學(xué)深度”通過數(shù)學(xué)近似(如 Chapman Function)來替代內(nèi)層循環(huán)。
第三部分:實現(xiàn)模式二 —— Lookup Texture (LUT / 預(yù)計算)
這是工業(yè)界(如 3A 游戲、虛幻引擎、Unity HDRP)的主流做法。著名的實現(xiàn)包括 Sean O'Neil 和 Eric Bruneton 的方法。
- 原理
由于大氣層通常假設(shè)為球體對稱,光照結(jié)果只取決于幾個參數(shù)(如高度、視線角度、太陽角度)。我們可以將那個昂貴的積分過程預(yù)先計算(Bake)到紋理中。
運行時,Shader 不需要做循環(huán)積分,只需要根據(jù)當前的角度和高度采樣紋理。 - 常見的 LUT 組合
你需要生成以下幾張紋理(通常是 HDR 格式):
- Transmittance LUT (2D):
- 含義: 從大氣中任意高度 h 向任意天頂角 \theta 看去,光線能通過多少。
- 坐標: X軸 = 視線角度 (0 \to 90^\circ),Y軸 = 高度 (Ground \to Top)。
- 用途: 直接替換 Raymarching 中的內(nèi)層循環(huán)(計算點到太陽的透射率)。
- Multi-Scattering LUT (3D 或 4D):
- 含義: 計算光線經(jīng)過多次散射后的結(jié)果(讓陰影處的天空不至于死黑)。
- 用途: 疊加到最終顏色上。
- Sky View LUT (2D - 現(xiàn)代主流):
- 含義: 預(yù)計算最終視線的 In-scattering 結(jié)果。
- 坐標: 將視線方向參數(shù)化映射到 UV 上。
- 用途: 直接采樣即可得到天空背景色,極快。
- Unity 實現(xiàn)步驟
- Compute Shader (預(yù)計算階段):
- 編寫 Compute Shader,離線或在游戲啟動時運行一次。
- 執(zhí)行類似 Raymarching 的積分邏輯,但將結(jié)果寫入 RenderTexture。
- Runtime Shader (渲染階段):
- 在天空盒 Shader 或后處理 Shader 中。
- 根據(jù)相機位置和視線方向計算 UV 坐標。
- float3 skyColor = tex2D(SkyViewLUT, uv).rgb;
- 應(yīng)用色調(diào)映射(Tonemapping)。

在unity urp 內(nèi)置的LUT實現(xiàn)太陽東升西落
不需要那種寫實級別的、帶體積云的、基于 LUT 的高級大氣散射,Unity URP 自帶了一個非常經(jīng)典的解決方案,叫做 "Procedural Skybox" (程序化天空盒)。
- 最常用的內(nèi)置方案:Procedural Skybox (程序化天空盒)
這是 Unity 默認用來模擬大氣散射的方法。它不是基于物理的體積渲染,而是一個數(shù)學(xué)近似模型。
- 特點:
- 輕量級: 性能消耗極低,移動端隨便跑。
- 半動態(tài): 當你旋轉(zhuǎn)平行光(太陽)時,它會自動計算日出、日落、夜晚的顏色變化。
- 局限: 看起來有點“數(shù)碼味”(不真實),沒有云,地平線過渡比較生硬。
- 如何開啟:
- 在 Project 窗口右鍵 -> Create -> Material,命名為 Mat_Skybox。
- 選中材質(zhì),在 Inspector 頂部的 Shader 下拉菜單中選擇:
Skybox -> Procedural。 - 打開 Window -> Rendering -> Lighting。
- 在 Environment 選項卡中,把 Mat_Skybox 拖到 Skybox Material 槽位里。
- 確保 Sun Source 選的是你場景里的 Directional Light。
- 關(guān)鍵參數(shù) (這就是簡化的散射控制):
- Sun Size: 太陽圓盤的大小。
- Sun Size Convergence: 太陽周圍光暈的大?。M米氏散射)。
- Atmosphere Thickness: (核心) 大氣厚度。
- 調(diào)大 = 空氣更厚,散射更強,白天偏黃,日落更紅(像火星或霧霾天)。
- 調(diào)小 = 空氣稀薄,天空更藍/黑(像高原或太空)。
-
Sky Tint / Ground: 可以在物理計算的基礎(chǔ)上疊加顏色。
sun_mat
environment
移動太陽
- 純靜態(tài)方案:Cubemap / HDRI (全景圖) (略)
在unity urp 用LUT實現(xiàn)太陽東升西落
在 Unity URP (Universal Render Pipeline) 中實現(xiàn)這套“動態(tài) LUT 大氣散射”方案,核心在于將計算邏輯(Compute Shader)與渲染邏輯(Shader Graph / HLSL)解耦。
這套方案不需要修改 URP 的管線源碼,非常干凈。我們可以把它拆解為三個步驟:
- 數(shù)據(jù)層 (Compute Shader): 負責每幀更新那張“小圖” (Sky View LUT)。
- 控制層 (C# 腳本): 負責傳遞太陽方向,調(diào)度 Compute Shader,并把結(jié)果存入全局變量。
- 表現(xiàn)層 (Shader Graph): 讀取那張圖,貼在天空盒上。
第一步:編寫 Compute Shader (生成器)
首先,我們需要一個能在 GPU 上跑的腳本,用來生成“天空顏色圖”。
創(chuàng)建一個 .compute 文件,比如 SkyLUTGenerator.compute。
(這里為了演示,簡化了物理積分公式,重點展示結(jié)構(gòu))
#pragma kernel CSMain
// 輸出的目標紋理 (那張“小圖”)
RWTexture2D<float4> Result;
// 外部傳入的變量
float3 _SunDir;
float _AtmosphereHeight;
float _PlanetRadius;
// [核心] 簡單的單次散射積分模擬 (Rayleigh + Mie)
// 實際項目中這里要替換成嚴謹?shù)?Bruneton 或 O'Neil 積分公式
float3 IntegrateScattering(float3 rayDir, float3 sunDir) {
// ... 簡化的偽代碼 ...
// 計算瑞利散射 (藍色)
float rayleigh = max(0, dot(rayDir, sunDir));
float3 blue = float3(0.2, 0.5, 1.0) * (1.0 + rayleigh * rayleigh);
// 計算米氏散射 (太陽光暈)
float mie = pow(max(0, dot(rayDir, sunDir)), 200.0); // 高光指數(shù)
float3 white = float3(1, 1, 1) * mie;
return blue + white;
}
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
// 1. 獲取當前像素的 UV (0~1)
float w, h;
Result.GetDimensions(w, h);
float2 uv = id.xy / float2(w, h);
// 2. 將 UV 映射回 3D 視線方向 (View Direction)
// 這是一個逆向過程:我們要把 2D 紋理展開成半球
// 簡單映射:x -> 方位角(Azimuth), y -> 天頂角(Zenith)
float theta = uv.y * 3.1415926 * 0.5; // 0 到 90度
float phi = uv.x * 3.1415926 * 2.0; // 0 到 360度
float3 viewDir;
viewDir.y = sin(theta);
float xz = cos(theta);
viewDir.x = xz * cos(phi);
viewDir.z = xz * sin(phi);
// 3. 計算顏色
float3 color = IntegrateScattering(viewDir, _SunDir);
// 4. 寫入紋理
Result[id.xy] = float4(color, 1.0);
}
第二步:編寫 C# 控制器 (驅(qū)動器)
我們需要一個 MonoBehaviour 掛在場景里,負責每幀告訴 Compute Shader 太陽在哪,并運行它。
創(chuàng)建一個 C# 腳本 AtmosphereManager.cs:
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteAlways] // 編輯器模式下也能運行,方便預(yù)覽
public class AtmosphereManager : MonoBehaviour
{
public ComputeShader skyComputeShader;
public Light sunLight; // 拖入你的平行光
// 生成的動態(tài) LUT 分辨率 (如 256x128 足夠了)
public Vector2Int resolution = new Vector2Int(256, 128);
private RenderTexture _skyLUT;
private int _kernelHandle;
void OnEnable()
{
InitRT();
}
void InitRT()
{
if (_skyLUT == null || _skyLUT.width != resolution.x)
{
// 必須是 ARGBFloat 或 ARGBHalf (HDR 精度)
_skyLUT = new RenderTexture(resolution.x, resolution.y, 0, RenderTextureFormat.ARGBHalf);
_skyLUT.enableRandomWrite = true; // 允許 Compute Shader 寫入
_skyLUT.Create();
}
_kernelHandle = skyComputeShader.FindKernel("CSMain");
}
void Update()
{
if (skyComputeShader == null || sunLight == null) return;
if (_skyLUT == null) InitRT();
// 1. 傳遞太陽方向 (取反是因為光線從太陽射過來,而我們計算散射通常看光線反方向)
// 注意:URP 的 Space 轉(zhuǎn)換有時候需要小心,通常用 -transform.forward
skyComputeShader.SetVector("_SunDir", -sunLight.transform.forward);
// 2. 綁定紋理容器
skyComputeShader.SetTexture(_kernelHandle, "Result", _skyLUT);
// 3. 運行 Compute Shader
// 線程組數(shù)量 = 分辨率 / 線程組大小(8)
skyComputeShader.Dispatch(_kernelHandle, resolution.x / 8, resolution.y / 8, 1);
// 4. 【關(guān)鍵一步】設(shè)置全局紋理
// 這樣所有的 Shader Graph 只要叫 "_DynamicSkyLUT" 就能自動讀到這張圖
Shader.SetGlobalTexture("_DynamicSkyLUT", _skyLUT);
}
}
第三步:URP Shader Graph (接收者)
最后,我們需要一個材質(zhì)球貼在 Skybox 上,讀取上面生成的 _DynamicSkyLUT。
- 創(chuàng)建 Shader Graph:
- 右鍵 -> Create -> Shader Graph -> URP -> Unlit Shader Graph。
- 命名為 AtmosphereSkybox。
- 設(shè)置屬性:
- 在 Blackboard 中創(chuàng)建一個 Texture2D 屬性,命名為 MainTex (或者留空,我們主要靠全局變量)。
- 重要: 我們不需要在屬性面板里暴露 LUT,因為我們是用 C# SetGlobalTexture 傳進來的。
- 在 Shader Graph 內(nèi),添加一個 Property 節(jié)點,名稱必須嚴格改成 _DynamicSkyLUT (不需設(shè)為 Exposed)。這樣它就會自動抓取 C# 傳過來的那張圖。
- 構(gòu)建連線邏輯:
- Input: View Direction (World Space, Normalized)。
- Math: 我們需要把 3D 向量轉(zhuǎn)回 UV。
- Arccosine(ViewDir.y) -> 得到天頂角。
- Arctangent2(ViewDir.z, ViewDir.x) -> 得到方位角。
- 將它們 Remap 到 0 ~ 1 的范圍。
- Sample Texture 2D:
- Texture: 連上 _DynamicSkyLUT 屬性節(jié)點。
- UV: 連上剛才計算出的 UV。
- Output: 連接到 Base Color。
- 應(yīng)用:
- 創(chuàng)建一個材質(zhì) Mat_Sky,使用這個 Shader。
- 打開 Lighting 面板 -> Environment -> Skybox Material,換成 Mat_Sky。
第四步:進階技巧 (如何處理霧效 Fog)
如果你只做了 Skybox,場景里的物體還是黑的。為了讓物體也受到大氣影響(即霧效),你需要讓場景里的物體也能讀到這張 LUT。
在 URP 中有兩種做法:
- 簡單做法 (Shader Graph 修改):
- 在所有場景物體的 Shader (比如 Lit Shader Graph) 中,添加一個 Emission 節(jié)點。
- 計算物體位置到相機的距離 -> 算出霧的強度。
- 用同樣的方法(View Direction -> UV -> Sample _DynamicSkyLUT)采樣大氣顏色。
- 將大氣顏色疊加到 Emission 或 Albedo 上。
- 高級做法 (Render Feature - 后處理):
- 寫一個 URP ScriptableRendererFeature。
- 在 RenderPass 中執(zhí)行一次全屏 Blit。
- 利用深度圖 (Depth Texture) 重建每個像素的世界坐標。
- 計算該像素到相機的距離。
- 采樣 _DynamicSkyLUT 并混合顏色。
- 這相當于自己寫了一個簡單的 Volumetric Fog。
總結(jié)
在 Unity URP 中實現(xiàn)的核心流程:
- C#: Update() -> Dispatch Compute Shader -> Shader.SetGlobalTexture("_SkyLUT", rt).
- Shader: ViewDir -> UV -> Sample(_SkyLUT).
這樣,無論你怎么旋轉(zhuǎn) Directional Light,C# 都會驅(qū)動 Compute Shader 瞬間重畫 LUT,Shader Graph 里的天空顏色就會實時平滑過渡,實現(xiàn)完美的日出日落。


