3d游戏开发特性名词_Atmospheric Scattering

Atmospheric Scattering 是计算机图形学中用于模拟天空颜色、日落、体积光(God Rays)和空气透视效果的核心技术。
Unity 中实现大气散射,本质上是在解决光线穿过介质时的吸收(Absorption)和散射(Scattering)问题。
以下是底层物理原理的深度解析,以及两种主流实现模式(Raymarching 和 LUT)在 Unity 中的具体落地指南。

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

大气散射的核心是辐射传输方程(Radiative Transfer Equation)的简化版。当光线穿过大气层时,会与微粒碰撞。

  1. 两种关键散射类型
  • 瑞利散射 (Rayleigh Scattering):
    • 原理: 针对比光波长小的微粒(如空气分子)。
    • 特性: 散射强度与波长的 4 次方成反比 (\frac{1}{\lambda^4})。这意味着蓝光(波长短)比红光(波长长)更容易被散射。
    • 视觉效果: 造成了白天的蓝天和傍晚的红夕阳(光程长,蓝光散尽,只剩红光)。
  • 米氏散射 (Mie Scattering):
    • 原理: 针对比光波长大的微粒(如气溶胶、水滴、雾霾)。
    • 特性: 对波长不敏感,光线主要向前散射(Anisotropic)。
    • 视觉效果: 造成了太阳周围的白色光晕和雾气效果。
  1. 核心方程逻辑
    要计算眼睛看到某个像素的颜色,我们需要沿着视线(View Ray)积分。
    其中包含三个关键项:
  • Transmittance (透射率 T): 根据比尔-朗伯定律(Beer-Lambert Law),光线在传播过程中会呈指数衰减。T = e^{-\sum \text{密度} \times \text{距离}}。
  • Phase Function (相函数 S): 决定光线撞击粒子后向哪个方向散射(瑞利各个方向较均匀,米氏主要向前)。
  • Density (密度 \rho): 大气密度随高度呈指数下降(越高越稀薄)。
第二部分:实现模式一 —— Raymarching (光线步进)

这是最直观、动态性最强,但性能开销最大的方法。常用于高质量的体积云或即时调整大气参数的场景。

  1. 原理
    这是一种暴力计算(Brute-force)方法。
  • 在 Pixel Shader 中,从相机向每个像素发射一条射线。
  • 将射线穿过大气层的部分切分为 N 个步进点(Samples)。
  • 核心痛点(双重循环): 在每一个步进点 P,你需要计算由于太阳照射产生了多少光(In-scattering)。这意味着你需要从点 P 再向太阳发射一条射线来计算透射率。
    • 外层循环:沿视线累加亮度。
    • 内层循环:计算从太阳到当前点的光照衰减(光路深度)。
  1. Unity 实现步骤
  • 几何体: 通常使用一个巨大的反面球体(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 (射向太阳) - 内层循环
        // 计算从采样点到太阳的透射率
        float sunRayOpticalDepth = GetOpticalDepthToSun(samplePoint, sunDir);

        float3 transmittance = exp(-(opticalDepthView + sunRayOpticalDepth) * ScatteringCoefficients);

        totalLight += transmittance * density * stepSize;
    }
    return totalLight * PhaseFunction(dot(rayDir, sunDir)) * SunIntensity;
}
  • 优化: * 内层循环很慢,通常会限制步数。
    • 或者将“点到太阳的光学深度”通过数学近似(如 Chapman Function)来替代内层循环。
第三部分:实现模式二 —— Lookup Texture (LUT / 预计算)

这是工业界(如 3A 游戏、虚幻引擎、Unity HDRP)的主流做法。著名的实现包括 Sean O'Neil 和 Eric Bruneton 的方法。

  1. 原理
    由于大气层通常假设为球体对称,光照结果只取决于几个参数(如高度、视线角度、太阳角度)。我们可以将那个昂贵的积分过程预先计算(Bake)到纹理中。
    运行时,Shader 不需要做循环积分,只需要根据当前的角度和高度采样纹理。
  2. 常见的 LUT 组合
    你需要生成以下几张纹理(通常是 HDR 格式):
  • Transmittance LUT (2D):
    • 含义: 从大气中任意高度 h 向任意天顶角 \theta 看去,光线能通过多少。
    • 坐标: X轴 = 视线角度 (0 \to 90^\circ),Y轴 = 高度 (Ground \to Top)。
    • 用途: 直接替换 Raymarching 中的内层循环(计算点到太阳的透射率)。
  • Multi-Scattering LUT (3D 或 4D):
    • 含义: 计算光线经过多次散射后的结果(让阴影处的天空不至于死黑)。
    • 用途: 叠加到最终颜色上。
  • Sky View LUT (2D - 现代主流):
    • 含义: 预计算最终视线的 In-scattering 结果。
    • 坐标: 将视线方向参数化映射到 UV 上。
    • 用途: 直接采样即可得到天空背景色,极快。
  1. Unity 实现步骤
  • Compute Shader (预计算阶段):
    • 编写 Compute Shader,离线或在游戏启动时运行一次。
    • 执行类似 Raymarching 的积分逻辑,但将结果写入 RenderTexture。
  • Runtime Shader (渲染阶段):
    • 在天空盒 Shader 或后处理 Shader 中。
    • 根据相机位置和视线方向计算 UV 坐标。
    • float3 skyColor = tex2D(SkyViewLUT, uv).rgb;
    • 应用色调映射(Tonemapping)。

大气散射

在unity urp 内置的LUT实现太阳东升西落

不需要那种写实级别的、带体积云的、基于 LUT 的高级大气散射,Unity URP 自带了一个非常经典的解决方案,叫做 "Procedural Skybox" (程序化天空盒)。

  1. 最常用的内置方案:Procedural Skybox (程序化天空盒)
    这是 Unity 默认用来模拟大气散射的方法。它不是基于物理的体积渲染,而是一个数学近似模型。
  • 特点:
    • 轻量级: 性能消耗极低,移动端随便跑。
    • 半动态: 当你旋转平行光(太阳)时,它会自动计算日出、日落、夜晚的颜色变化。
    • 局限: 看起来有点“数码味”(不真实),没有云,地平线过渡比较生硬。
  • 如何开启:
    • 在 Project 窗口右键 -> Create -> Material,命名为 Mat_Skybox。
    • 选中材质,在 Inspector 顶部的 Shader 下拉菜单中选择:
      Skybox -> Procedural。
    • 打开 Window -> Rendering -> Lighting。
    • 在 Environment 选项卡中,把 Mat_Skybox 拖到 Skybox Material 槽位里。
    • 确保 Sun Source 选的是你场景里的 Directional Light。
  • 关键参数 (这就是简化的散射控制):
    • Sun Size: 太阳圆盘的大小。
    • Sun Size Convergence: 太阳周围光晕的大小(模拟米氏散射)。
    • Atmosphere Thickness: (核心) 大气厚度。
      • 调大 = 空气更厚,散射更强,白天偏黄,日落更红(像火星或雾霾天)。
      • 调小 = 空气稀薄,天空更蓝/黑(像高原或太空)。
    • Sky Tint / Ground: 可以在物理计算的基础上叠加颜色。


      sun_mat

      environment

      移动太阳
  1. 纯静态方案:Cubemap / HDRI (全景图) (略)

在unity urp 用LUT实现太阳东升西落

在 Unity URP (Universal Render Pipeline) 中实现这套“动态 LUT 大气散射”方案,核心在于将计算逻辑(Compute Shader)与渲染逻辑(Shader Graph / HLSL)解耦。
这套方案不需要修改 URP 的管线源码,非常干净。我们可以把它拆解为三个步骤:

  • 数据层 (Compute Shader): 负责每帧更新那张“小图” (Sky View LUT)。
  • 控制层 (C# 脚本): 负责传递太阳方向,调度 Compute Shader,并把结果存入全局变量。
  • 表现层 (Shader Graph): 读取那张图,贴在天空盒上。
第一步:编写 Compute Shader (生成器)

首先,我们需要一个能在 GPU 上跑的脚本,用来生成“天空颜色图”。
创建一个 .compute 文件,比如 SkyLUTGenerator.compute。
(这里为了演示,简化了物理积分公式,重点展示结构)

#pragma kernel CSMain

// 输出的目标纹理 (那张“小图”)
RWTexture2D<float4> Result;

// 外部传入的变量
float3 _SunDir;
float _AtmosphereHeight;
float _PlanetRadius;

// [核心] 简单的单次散射积分模拟 (Rayleigh + Mie)
// 实际项目中这里要替换成严谨的 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); // 高光指数
    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# 控制器 (驱动器)

我们需要一个 MonoBehaviour 挂在场景里,负责每帧告诉 Compute Shader 太阳在哪,并运行它。
创建一个 C# 脚本 AtmosphereManager.cs:

using UnityEngine;
using UnityEngine.Rendering;

[ExecuteAlways] // 编辑器模式下也能运行,方便预览
public class AtmosphereManager : MonoBehaviour
{
    public ComputeShader skyComputeShader;
    public Light sunLight; // 拖入你的平行光
    
    // 生成的动态 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 转换有时候需要小心,通常用 -transform.forward
        skyComputeShader.SetVector("_SunDir", -sunLight.transform.forward);

        // 2. 绑定纹理容器
        skyComputeShader.SetTexture(_kernelHandle, "Result", _skyLUT);

        // 3. 运行 Compute Shader
        // 线程组数量 = 分辨率 / 线程组大小(8)
        skyComputeShader.Dispatch(_kernelHandle, resolution.x / 8, resolution.y / 8, 1);

        // 4. 【关键一步】设置全局纹理
        // 这样所有的 Shader Graph 只要叫 "_DynamicSkyLUT" 就能自动读到这张图
        Shader.SetGlobalTexture("_DynamicSkyLUT", _skyLUT);
    }
}
第三步:URP Shader Graph (接收者)

最后,我们需要一个材质球贴在 Skybox 上,读取上面生成的 _DynamicSkyLUT。

  • 创建 Shader Graph:
    • 右键 -> Create -> Shader Graph -> URP -> Unlit Shader Graph。
    • 命名为 AtmosphereSkybox。
  • 设置属性:
    • 在 Blackboard 中创建一个 Texture2D 属性,命名为 MainTex (或者留空,我们主要靠全局变量)。
    • 重要: 我们不需要在属性面板里暴露 LUT,因为我们是用 C# SetGlobalTexture 传进来的。
    • 在 Shader Graph 内,添加一个 Property 节点,名称必须严格改成 _DynamicSkyLUT (不需设为 Exposed)。这样它就会自动抓取 C# 传过来的那张图。
  • 构建连线逻辑:
    • Input: View Direction (World Space, Normalized)。
    • Math: 我们需要把 3D 向量转回 UV。
      • Arccosine(ViewDir.y) -> 得到天顶角。
      • Arctangent2(ViewDir.z, ViewDir.x) -> 得到方位角。
      • 将它们 Remap 到 0 ~ 1 的范围。
    • Sample Texture 2D:
      • Texture: 连上 _DynamicSkyLUT 属性节点。
      • UV: 连上刚才计算出的 UV。
    • Output: 连接到 Base Color。
  • 应用:
    • 创建一个材质 Mat_Sky,使用这个 Shader。
    • 打开 Lighting 面板 -> Environment -> Skybox Material,换成 Mat_Sky。
第四步:进阶技巧 (如何处理雾效 Fog)

如果你只做了 Skybox,场景里的物体还是黑的。为了让物体也受到大气影响(即雾效),你需要让场景里的物体也能读到这张 LUT。
在 URP 中有两种做法:

  • 简单做法 (Shader Graph 修改):
    • 在所有场景物体的 Shader (比如 Lit Shader Graph) 中,添加一个 Emission 节点。
    • 计算物体位置到相机的距离 -> 算出雾的强度。
    • 用同样的方法(View Direction -> UV -> Sample _DynamicSkyLUT)采样大气颜色。
    • 将大气颜色叠加到 Emission 或 Albedo 上。
  • 高级做法 (Render Feature - 后处理):
    • 写一个 URP ScriptableRendererFeature。
    • 在 RenderPass 中执行一次全屏 Blit。
    • 利用深度图 (Depth Texture) 重建每个像素的世界坐标。
    • 计算该像素到相机的距离。
    • 采样 _DynamicSkyLUT 并混合颜色。
    • 这相当于自己写了一个简单的 Volumetric Fog。
总结

在 Unity URP 中实现的核心流程:

  • C#: Update() -> Dispatch Compute Shader -> Shader.SetGlobalTexture("_SkyLUT", rt).
  • Shader: ViewDir -> UV -> Sample(_SkyLUT).
    这样,无论你怎么旋转 Directional Light,C# 都会驱动 Compute Shader 瞬间重画 LUT,Shader Graph 里的天空颜色就会实时平滑过渡,实现完美的日出日落。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • """1.个性化消息: 将用户的姓名存到一个变量中,并向该用户显示一条消息。显示的消息应非常简单,如“Hello ...
    她即我命阅读 9,166评论 0 6
  • 1、expected an indented block 冒号后面是要写上一定的内容的(新手容易遗忘这一点); 缩...
    庵下桃花仙阅读 3,856评论 0 2
  • 一、工具箱(多种工具共用一个快捷键的可同时按【Shift】加此快捷键选取)矩形、椭圆选框工具 【M】移动工具 【V...
    墨雅丫阅读 4,357评论 0 0
  • 跟随樊老师和伙伴们一起学习心理知识提升自已,已经有三个月有余了,这一段时间因为天气的原因休课,顺便整理一下之前学习...
    学习思考行动阅读 3,715评论 0 2
  • 一脸愤怒的她躺在了床上,好几次甩开了他抱过来的双手,到最后还坚决的翻了个身,只留给他一个冷漠的背影。 多次尝试抱她...
    海边的蓝兔子阅读 3,349评论 0 4

友情链接更多精彩内容