本文最后更新于2 天前,其中的信息可能已经过时,如有错误请留言
雾效分类与实现原理分析
- 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 勾选
- Project 面板中找到你的 URP Asset
- 在 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.hlsl和DepthAwareGaussianBlur.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
}



深入代码细节
- RT 是 Render 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
);





