3d游戲開發(fā)特性名詞_Atmospheric Scattering

Atmospheric Scattering 是計算機圖形學(xué)中用于模擬天空顏色、日落、體積光(God Rays)和空氣透視效果的核心技術(shù)。
Unity 中實現(xiàn)大氣散射,本質(zhì)上是在解決光線穿過介質(zhì)時的吸收(Absorption)和散射(Scattering)問題。
以下是底層物理原理的深度解析,以及兩種主流實現(xiàn)模式(Raymarching 和 LUT)在 Unity 中的具體落地指南。

第一部分:底層物理原理 (The Physics)

大氣散射的核心是輻射傳輸方程(Radiative Transfer Equation)的簡化版。當光線穿過大氣層時,會與微粒碰撞。

  1. 兩種關(guān)鍵散射類型
  • 瑞利散射 (Rayleigh Scattering):
    • 原理: 針對比光波長小的微粒(如空氣分子)。
    • 特性: 散射強度與波長的 4 次方成反比 (\frac{1}{\lambda^4})。這意味著藍光(波長短)比紅光(波長長)更容易被散射。
    • 視覺效果: 造成了白天的藍天和傍晚的紅夕陽(光程長,藍光散盡,只剩紅光)。
  • 米氏散射 (Mie Scattering):
    • 原理: 針對比光波長大的微粒(如氣溶膠、水滴、霧霾)。
    • 特性: 對波長不敏感,光線主要向前散射(Anisotropic)。
    • 視覺效果: 造成了太陽周圍的白色光暈和霧氣效果。
  1. 核心方程邏輯
    要計算眼睛看到某個像素的顏色,我們需要沿著視線(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ù)的場景。

  1. 原理
    這是一種暴力計算(Brute-force)方法。
  • 在 Pixel Shader 中,從相機向每個像素發(fā)射一條射線。
  • 將射線穿過大氣層的部分切分為 N 個步進點(Samples)。
  • 核心痛點(雙重循環(huán)): 在每一個步進點 P,你需要計算由于太陽照射產(chǎn)生了多少光(In-scattering)。這意味著你需要從點 P 再向太陽發(fā)射一條射線來計算透射率。
    • 外層循環(huán):沿視線累加亮度。
    • 內(nèi)層循環(huán):計算從太陽到當前點的光照衰減(光路深度)。
  1. 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 的方法。

  1. 原理
    由于大氣層通常假設(shè)為球體對稱,光照結(jié)果只取決于幾個參數(shù)(如高度、視線角度、太陽角度)。我們可以將那個昂貴的積分過程預(yù)先計算(Bake)到紋理中。
    運行時,Shader 不需要做循環(huán)積分,只需要根據(jù)當前的角度和高度采樣紋理。
  2. 常見的 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 上。
    • 用途: 直接采樣即可得到天空背景色,極快。
  1. 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" (程序化天空盒)。

  1. 最常用的內(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

      移動太陽
  1. 純靜態(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)完美的日出日落。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • """1.個性化消息: 將用戶的姓名存到一個變量中,并向該用戶顯示一條消息。顯示的消息應(yīng)非常簡單,如“Hello ...
    她即我命閱讀 5,028評論 0 6
  • 1、expected an indented block 冒號后面是要寫上一定的內(nèi)容的(新手容易遺忘這一點); 縮...
    庵下桃花仙閱讀 1,077評論 1 2
  • 一、工具箱(多種工具共用一個快捷鍵的可同時按【Shift】加此快捷鍵選取)矩形、橢圓選框工具 【M】移動工具 【V...
    墨雅丫閱讀 1,516評論 0 0
  • 跟隨樊老師和伙伴們一起學(xué)習心理知識提升自已,已經(jīng)有三個月有余了,這一段時間因為天氣的原因休課,順便整理一下之前學(xué)習...
    學(xué)習思考行動閱讀 986評論 0 2
  • 一臉憤怒的她躺在了床上,好幾次甩開了他抱過來的雙手,到最后還堅決的翻了個身,只留給他一個冷漠的背影。 多次嘗試抱她...
    海邊的藍兔子閱讀 980評論 1 4

友情鏈接更多精彩內(nèi)容