体积雾
本文最后更新于2 天前,其中的信息可能已经过时,如有错误请留言

源码:CristianQiu/Unity-URP-Volumetric-Light: Unity package for versions 2022.3 and Unity 6 and above. Adds support to render volumetric lighting for both the main and additional lights in URP. Compatible with URP render graph in Unity 6 and above.

雾效分类与实现原理分析

  • Attenuation Mode:根据雾的衰减方式
类型含义视觉效果
Linear线性衰减,距离越远雾越均匀增加变化比较平均,容易控制,但真实感较弱
Exponential指数衰减,近处变化慢,远处雾感增长更明显比线性自然,常用于普通距离雾
Exponential Squared指数平方衰减,远处雾增长更快远景会更快被雾吞掉,雾感更厚、更强烈
  • Distance Mode:雾依据什么来计算
类型含义基本原理常见用途
Depth-based Fog基于深度的雾读取相机深度图,离相机越远雾越重远景雾、空气透视、场景纵深
Height-based Fog基于高度的雾根据世界空间高度判断雾浓度,通常低处更浓地面雾、山谷雾、水面雾、晨雾
  • 实现方式
类型含义原理特点
Screen-space Volumetric Fog屏幕空间体积雾根据相机画面的深度图,对每个屏幕像素沿视线计算雾实现相对轻,适合 URP 后处理式插件
World-space / Froxel Volumetric Fog世界空间 / 体素体积雾在相机视锥或世界中建立三维雾网格,记录雾密度和光照更真实,更复杂,常见于 HDRP 或高级引擎方案
  • 一种是全屏后处理雾
    这种通常用 Full Screen Pass Renderer Feature 或自定义 Renderer Feature。它读取 Camera Depth,根据距离、高度、噪声算雾,再覆盖到屏幕上
  • 另一种是真体积雾 / Fog Volume
    这种会在场景中放一个 box/volume,通过 raymarch 沿着视线一步步采样体积密度。它更真实,也能做局部雾块、光束、噪声流动,但性能更贵

使用步骤:

  • 打开 Depth Texture
    • Project 面板中找到你的 URP Asset
      → Inspector
      → Rendering
      → Depth Texture 勾选
  • 在 Hierarchy 里:创建空物体,添加 Volume 组件,并勾选:Is Global
  • 在 Volume 组件里新建或指定一个:Profile
  • 添加 Volumetric Fog Override在 Volume Profile 里:Add Override -
    Volumetric Fog
  • 如果你想让点光源、聚光灯影响雾,在对应 Light 物体上加:Volumetric Additional Light

原理

这个体积雾不是在场景里真正生成一团 3D 雾模型,而是通过 URP 自定义渲染管线扩展 实现的

。首先使用 ScriptableRendererFeature 把一个自定义的 RenderPass 插入到 URP 渲染流程中;然后在渲染时读取相机的 Depth Texture,知道每个像素对应的场景深度,也就是从相机到物体之间有多远;接着 Shader 会沿着较低分辨率上视线方向进行近似采样(raymarch),估算这段空间中有多少雾,并根据光源方向、雾密度、散射强度、噪声扰动等参数计算雾的亮度和颜色;如果开启阴影或遮挡,体积雾还会参考阴影信息,让被物体挡住的区域变暗,形成类似“光束被建筑、树木切开”的效果;最后再把这张半分辨率雾图做深度感知模糊和上采样,把这层体积雾结果按 cameraColor.rgb * volumetricFog.a + volumetricFog.rgb 的方式混合到当前画面上

深度为什么要下采样:

  • 体积雾很耗性能。因为它不是简单采样一次颜色,而是每个像素要沿着视线方向走很多步
  • 如果全分辨率每个像素都这样做,开销会很大。所以你的实现先把深度降到半分辨率:
  • 然后在半分辨率上生成雾图,再放大回全分辨率。这样性能会好很多。
  • 但这也带来一个问题:半分辨率的一个像素对应原图的 2×2 像素。这个 2×2 里可能同时有:
    • 前景石头后景地面天空树干边缘
  • 所以 DownsampleDepth.shader 必须谨慎决定保留哪个深度
    如果保留了后景深度,雾就会穿过前景物体。
    如果保留了前景最近深度,遮挡会更稳定,但边缘可能稍微硬一点。
    所以后来建议取最近深度,本质是让体积雾更保守地判断:

    只要这一小块区域里有前景物体,就优先认为雾应该被前景物体挡住

    这能减少穿透问题。

代码文件:

  • VolumetricFogRenderPass.cs:体积雾真正的执行流程,Execute() 里现在核心流程是这样的:
//把当前相机真正的深度 RT
//交给 DownsampleDepth Shader
//输出到半分辨率的 downsampledCameraDepthRTHandle
RTHandle cameraDepthRT =
    renderingData.cameraData.renderer.cameraDepthTargetHandle;

Blitter.BlitCameraTexture(
    cmd,
    cameraDepthRT,
    downsampledCameraDepthRTHandle,
    downsampleDepthMaterial,
    downsampleDepthPassIndex
);

//然后再用这张下采样深度图生成体积雾:
//用半分辨率深度图
//生成半分辨率体积雾图

Blitter.BlitCameraTexture(
    cmd,
    downsampledCameraDepthRTHandle,
    volumetricFogRenderRTHandle,
    volumetricFogMaterial,
    volumetricFogRenderPassIndex
);
//后面再模糊:
Blitter.BlitCameraTexture(
    cmd,
    volumetricFogRenderRTHandle,
    volumetricFogBlurRTHandle,
    volumetricFogMaterial,
    volumetricFogHorizontalBlurPassIndex
);

Blitter.BlitCameraTexture(
    cmd,
    volumetricFogBlurRTHandle,
    volumetricFogRenderRTHandle,
    volumetricFogMaterial,
    volumetricFogVerticalBlurPassIndex
);
最后合成回画面:
Blitter.BlitCameraTexture(
    cmd,
    cameraColorRt,
    volumetricFogUpsampleCompositionRTHandle,
    volumetricFogMaterial,
    volumetricFogUpsampleCompositionPassIndex
);

Blitter.BlitCameraTexture(
    cmd,
    volumetricFogUpsampleCompositionRTHandle,
    cameraColorRt
);
  • 所以 VolumetricFogRenderPass.cs 的核心作用可以概括为:
相机深度
→ 下采样深度
→ 生成体积雾
→ 模糊
→ 放大并合成回最终画面

你之前的问题,关键就出在第一步和 DownsampleDepth.shader 的配合上。

  • Hidden/VolumetricFog.shader 生成雾图,一张中间纹理
  • DepthAwareUpsample.hlslDepthAwareGaussianBlur.hlsl(t:Shader VolumetricFog找到文件)
    • 因为体积雾是半分辨率生成的,所以边缘会粗糙。为了让它看起来柔和,需要模糊和上采样
  • VolumetricFogRendererFeature.cs:把体积雾插进 URP

修复问题

  • 在 Unity 2022.3 / URP 14 版本测试中,当场景中使用自定义 Shader 材质并开启自定义体积雾时,如果自定义 Shader 没有正确参与 URP 深度流程,或体积雾下采样阶段读取的不是当前渲染传入的相机深度 RT,而是默认的 _CameraDepthTexture,可能会导致雾效无法正确根据前景物体深度截断,从而出现类似穿模、透视或遮挡关系错误的问题

改动1:VolumetricFogRenderPass.cs

using (new ProfilingScope(cmd, downsampleDepthProfilingSampler))
{
Blitter.BlitCameraTexture(cmd, downsampledCameraDepthRTHandle, downsampledCameraDepthRTHandle, downsampleDepthMaterial, downsampleDepthPassIndex);
volumetricFogMaterial.SetTexture(DownsampledCameraDepthTextureId, downsampledCameraDepthRTHandle);
}

//改成:
using (new ProfilingScope(cmd, downsampleDepthProfilingSampler))
{
    RTHandle cameraDepthRT = renderingData.cameraData.renderer.cameraDepthTargetHandle;

    Blitter.BlitCameraTexture(
        cmd,
        cameraDepthRT,
        downsampledCameraDepthRTHandle,
        downsampleDepthMaterial,
        downsampleDepthPassIndex
    );

    volumetricFogMaterial.SetTexture(DownsampledCameraDepthTextureId, downsampledCameraDepthRTHandle);
}

改动2: DownsampleDepth.shader

Shader "Hidden/DownsampleDepth"
{
    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
        }

        Pass
        {
            Name "DownsampleDepth"

            ZTest Always
            ZWrite Off
            Cull Off
            Blend Off
            ColorMask R

            HLSLPROGRAM

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
            
            #pragma target 4.5
            #pragma editor_sync_compilation

            #pragma vertex Vert
            #pragma fragment Frag

            float Frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

                float4 depths;

                // 当前半分辨率像素,对应原始深度图中的 2x2 区域
                uint2 fullResTopLeftCorner = uint2(input.positionCS.xy * 2.0);

                // 读取 Blitter 实际传进来的 source,也就是相机深度图
                // 不再读取全局 _CameraDepthTexture / LoadSceneDepth
                depths.x = LOAD_TEXTURE2D_X(_BlitTexture, fullResTopLeftCorner + uint2(0, 1)).r;
                depths.y = LOAD_TEXTURE2D_X(_BlitTexture, fullResTopLeftCorner + uint2(1, 1)).r;
                depths.z = LOAD_TEXTURE2D_X(_BlitTexture, fullResTopLeftCorner + uint2(1, 0)).r;
                depths.w = LOAD_TEXTURE2D_X(_BlitTexture, fullResTopLeftCorner + uint2(0, 0)).r;

                float minDepth = Min3(depths.x, depths.y, min(depths.z, depths.w));
                float maxDepth = Max3(depths.x, depths.y, max(depths.z, depths.w));

                // 原版是 minDepth / maxDepth 交错返回:
                // return (uint(input.positionCS.x + input.positionCS.y) & 1) > 0 ? minDepth : maxDepth;
                //
                // 这会产生棋盘/条纹状的深度变化。
                // 改成固定取最近深度,避免半分辨率体积雾上采样时出现条纹。
                #if UNITY_REVERSED_Z
                    return maxDepth;
                #else
                    return minDepth;
                #endif
            }

            ENDHLSL
        }
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
        }

        Pass
        {
            Name "DownsampleDepth"

            ZTest Always
            ZWrite Off
            Cull Off
            Blend Off
            ColorMask R

            HLSLPROGRAM

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
            
            #pragma editor_sync_compilation

            #pragma vertex Vert
            #pragma fragment Frag

            float Frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

                float4 depths;

                // 当前半分辨率像素,对应原始深度图中的 2x2 区域
                uint2 fullResTopLeftCorner = uint2(input.positionCS.xy * 2.0);

                // 读取 Blitter 传入的相机深度图
                depths.x = LOAD_TEXTURE2D_X(_BlitTexture, fullResTopLeftCorner + uint2(0, 1)).r;
                depths.y = LOAD_TEXTURE2D_X(_BlitTexture, fullResTopLeftCorner + uint2(1, 1)).r;
                depths.z = LOAD_TEXTURE2D_X(_BlitTexture, fullResTopLeftCorner + uint2(1, 0)).r;
                depths.w = LOAD_TEXTURE2D_X(_BlitTexture, fullResTopLeftCorner + uint2(0, 0)).r;

                float minDepth = Min3(depths.x, depths.y, min(depths.z, depths.w));
                float maxDepth = Max3(depths.x, depths.y, max(depths.z, depths.w));

                // 固定取最近深度,避免 min/max 交错造成可见条纹
                #if UNITY_REVERSED_Z
                    return maxDepth;
                #else
                    return minDepth;
                #endif
            }

            ENDHLSL
        }
    }

    Fallback Off
}

深入代码细节

  • RTRender Texture / Render Target,意思是 GPU 渲染过程中用来存放画面结果的一张“渲染纹理”,比如深度图、体积雾图、模糊后的雾图、相机颜色图都可以是 RT;
  • Handle 直译是“句柄”,意思是你不直接操作底层那张真正的 GPU 纹理,而是拿到一个用来访问和管理它的引用。合起来看,RTHandle 就是 Unity URP/HDRP 用来管理 Render Texture 的句柄,它可以帮你处理分辨率变化、动态分辨率、XR、多相机等情况。简单说:RT 是那张渲染出来的图,Handle 是 Unity 给这张图套的一层管理引用
  • CommandBuffer:你先把一堆渲染指令写到一个“命令列表”里,然后交给 Unity / GPU 去执行。
  • Blitter 是 Unity URP/SRP 提供的工具类,Blit 在渲染里通常表示:把一张图像拷贝 / 处理 / 转移到另一张图像
Blitter.BlitCameraTexture(
    cmd,
    source,
    target,
    material,
    passIndex
);
  • cmd = CommandBuffer,记录这条 GPU 命令
    source = 输入 RT
    target = 输出 RT
    material = 要使用的材质
    passIndex = 材质 Shader 里的第几个 Pass
Blitter.BlitCameraTexture(
    cmd,
    source,
    target
);
学习笔记如有侵权,请提醒我,我会马上删除
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇