恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Unity URP高效雾效方案:从原理到工程实践
首页
资讯中心
/
Unity URP高效雾效方案:从原理到工程实践
Unity URP高效雾效方案:从原理到工程实践
发布时间:2026/8/7 1:22:26
1. 项目概述为什么我们需要一个高效的URP雾效方案在实时渲染的世界里雾效Fog远不止是营造氛围的“气氛组”。它承担着至关重要的视觉和性能职责一是提供深度线索帮助玩家理解场景中物体的远近关系避免视觉上的“扁平感”二是作为性能优化手段通过远处物体的雾化淡出可以优雅地隐藏LOD细节层次切换的接缝甚至直接作为远裁剪面之外的“软边界”。在Unity的通用渲染管线URP中虽然内置了基础的雾效支持但很多开发者尤其是对画面表现有更高要求的中重度项目常常感到“不够用”或“性能吃紧”。这就是“URPFog”这类自定义、优化型雾效方案诞生的背景。URPFog的核心目标直指“快速”和“高效”。它不是一个从零开始发明轮子的过程而是对URP现有雾效系统的一次深度定制和性能攻坚。想象一下在一个开放世界或大场景中你需要一个既能覆盖远山、又能渗透进森林峡谷同时还能与动态光照如昼夜交替、手电筒正确交互的体积雾。URP内置的指数高度雾可能显得力不从心而直接启用HDRP级别的体积雾又杀鸡用牛刀对移动端或性能敏感的平台极不友好。URPFog就是在URP的框架下寻找那个性能与表现力的黄金平衡点。它适合那些已经使用URP但觉得默认雾效效果单一、性能有优化空间或者需要实现特定艺术风格如风格化雾、场景过渡雾的开发者。接下来我将拆解实现这样一个高效雾效方案的核心思路、技术细节以及避坑指南。2. 核心设计思路在URP框架下重新定义雾实现一个自定义的URP雾效首先要彻底理解URP的渲染流程和扩展点。URP采用可编程渲染管线SRP其核心在于一系列可配置的RenderPass。我们的雾效计算理想的位置是在所有不透明物体渲染完成之后天空盒渲染之前或之后作为一个全屏后处理效果来应用。但更高效、更物理正确的方式是将其集成到光照计算和物体着色中。2.1 方案选型屏幕空间 vs 体积纹理 vs 顶点/像素着色屏幕空间后处理雾这是最直观的方式。在RenderPass中获取相机深度纹理_CameraDepthTexture和法线纹理根据每个像素的深度值即距离计算雾的强度因子然后与屏幕颜色进行混合。优点是实现简单独立于场景物体。缺点是它是“2.5D”的无法表现光线在雾中穿行的体积感即物体边缘和内部没有被雾“包裹”的感觉且对透明物体处理复杂。体积纹理3D Texture雾这是向HDRP体积雾看齐的方案。预先计算或实时生成一个3D纹理其中每个纹素存储了世界空间中某一点的雾密度、颜色等数据。在着色器中通过射线步进Ray Marching对这个3D纹理进行采样累积雾的贡献。效果最真实能实现光线散射、上帝光等复杂效果。但性能开销巨大尤其是步进次数多时在移动端基本不可行。顶点/像素着色器集成雾这是URPFog追求“高效”的关键。我们不在后处理中做而是在每个物体的着色器Shader中根据其世界空间位置实时计算雾因子。URP内置的UniversalFragmentShaderInput结构体通常已经包含了世界空间位置positionWS。我们可以在URP的Lighting.hlsl或自定义的着色器函数中插入雾的计算。这种方式性能佳计算分散到每个物体且与物体本身的复杂度绑定无需全屏后处理开销。效果合理每个顶点/像素独立计算能体现物体形状与雾的交互尽管不是真正的体积计算。兼容性好能与URP的光照模型、阴影自然融合。URPFog的选择为了兼顾效率与足够好的表现力我们采用方案三着色器集成作为主干并对其进行增强。同时我们可以提供一个可选的、轻量级的屏幕空间雾作为后备或补充用于处理那些没有使用我们定制着色器的物体如第三方资产或者实现全局的雾效覆盖。2.2 雾效模型选择从简单到复杂确定了计算位置接下来要决定用什么数学模型来描述雾。线性雾Linear雾密度从起始距离到结束距离线性增长。计算最简单但效果生硬不自然。float fogFactor clamp((distance - fogStart) / (fogEnd - fogStart), 0.0, 1.0);指数雾Exponential雾密度随距离呈指数增长。这是URP内置的常用模型效果比线性自然。// 指数雾 float fogFactor 1.0 - exp(-fogDensity * distance); // 指数平方雾衰减更快 float fogFactor 1.0 - exp(-(fogDensity * distance) * (fogDensity * distance));高度雾Height Fog雾密度不仅随距离也随高度Y轴变化。这是实现“地面雾”、“云层”效果的关键。通常结合指数模型使用。float heightFactor clamp((worldPos.y - fogBaseHeight) / fogHeight, 0.0, 1.0); heightFactor exp(-fogHeightFalloff * heightFactor); // 高度衰减因子 float distanceFactor exp(-fogDensity * distance); // 距离衰减因子 float fogFactor 1.0 - (distanceFactor * heightFactor);体积雾近似Anisotropic/Scattering Approximation为了实现更逼真的光线散射效果如丁达尔效应我们需要在着色器中模拟光线在介质中的散射。这通常使用Henyey-Greenstein相位函数等模型计算光源方向与视线方向的点积来模拟前向或后向散射。虽然这不是真正的体积计算但能在物体着色上添加一种“光晕”感。URPFog的设计我们将实现一个可配置的混合模型。核心是指数高度雾因为它能覆盖大部分自然场景需求远处和低洼处雾浓。同时我们将可选地集成一个简化的散射近似计算用于主要方向光如太阳以低成本模拟上帝光效果。通过Shader关键字#pragma shader_feature或材质属性开关来控制这些功能的启用确保不需要时零开销。3. 核心实现细节与Shader编码理论清晰后我们进入实战环节。我们将创建一个名为URPFog的渲染特性ScriptableRendererFeature和对应的通道ScriptableRenderPass但核心逻辑封装在HLSL文件中供着色器包含。3.1 创建雾效计算的核心HLSL库首先创建一个URPFog.hlsl文件。这个文件定义雾的参数结构体、计算函数并负责与Unity的Shader属性_FogColor,_FogDensity等对接。// URPFog.hlsl #ifndef URP_FOG_INCLUDED #define URP_FOG_INCLUDED // 雾效参数对应C#脚本中设置的Shader全局属性 float4 _FogColor; float _FogDensity; float _FogStart; float _FogEnd; float _FogHeight; float _FogBaseHeight; float _FogHeightFalloff; float _FogScatteringIntensity; float _FogScatteringDirection; // 类似Anisotropy控制散射方向 // 雾效模式定义 #define FOG_MODE_NONE 0 #define FOG_MODE_EXPONENTIAL 1 #define FOG_MODE_EXPONENTIAL_SQUARED 2 #define FOG_MODE_HEIGHT 3 int _FogMode; // 核心雾因子计算函数 float ComputeFogFactor(float3 worldPos, float distanceToCam) { float fogFactor 0.0; if (_FogMode FOG_MODE_EXPONENTIAL) { fogFactor 1.0 - exp(-_FogDensity * distanceToCam); } else if (_FogMode FOG_MODE_EXPONENTIAL_SQUARED) { fogFactor 1.0 - exp(-_FogDensity * _FogDensity * distanceToCam * distanceToCam); } else if (_FogMode FOG_MODE_HEIGHT) { // 计算高度衰减 float height worldPos.y - _FogBaseHeight; float heightFactor exp(-_FogHeightFalloff * max(height, 0.0)); // 计算距离衰减 float distanceFactor exp(-_FogDensity * distanceToCam); // 合并 fogFactor 1.0 - (distanceFactor * heightFactor); // 确保在地面以下也有雾可选 if (height 0.0) { fogFactor max(fogFactor, 1.0 - exp(-_FogHeightFalloff * abs(height))); } } // 线性雾作为备选这里省略... return saturate(fogFactor); // 确保在[0,1]范围 } // 应用雾效到颜色 float4 ApplyFog(float4 inputColor, float fogFactor, float3 worldPos, float3 viewDir, float3 lightDir) { float4 fogColor _FogColor; // 可选简单的散射近似需要光源方向 #if defined(_FOG_SCATTERING_ON) if (_FogScatteringIntensity 0) { // 使用简化的HG相位函数近似 float cosTheta dot(normalize(viewDir), normalize(-lightDir)); // 视线与光线的点积 float scattering 1.0 _FogScatteringDirection * cosTheta; scattering max(scattering, 0.1); // 防止过暗 // 散射强度随雾因子增强 fogColor.rgb * lerp(1.0, scattering, _FogScatteringIntensity * fogFactor); } #endif // 标准线性混合 return lerp(inputColor, fogColor, fogFactor); } #endif3.2 集成到URP Lit Shader中接下来我们需要修改URP的Lit着色器使其在计算最终颜色时调用我们的雾效函数。我们不直接修改Unity的Shader文件而是通过自定义的RendererFeature在渲染时插入我们的代码或者更常见的是复制一份URP的Lit Shader Graph节点或Shader源码进行修改。这里以修改Shader源码为例在片元着色器Fragment Shader中在计算完所有光照和表面颜色之后应用雾效// 在片元着色器函数中假设已经计算出了表面颜色 surfaceColor float3 worldPos input.positionWS; float3 viewDirWS GetWorldSpaceNormalizeViewDir(worldPos); float distanceToCam length(viewDirWS); // 注意viewDir是从相机到点的向量其长度就是距离 float fogFactor ComputeFogFactor(worldPos, distanceToCam); // 获取主光源方向用于散射计算 Light mainLight GetMainLight(); float3 mainLightDir mainLight.direction; float4 finalColor ApplyFog(float4(surfaceColor, 1.0), fogFactor, worldPos, viewDirWS, mainLightDir); return finalColor;关键技巧为了最大化兼容性和性能我们应该通过#pragma shader_feature_local _FOG_SCATTERING_ON来让散射计算成为可选的变体。这样在材质中不启用散射的物体就不会编译相关代码节省Shader变体和GPU指令。3.3 创建C#脚本控制雾效参数我们需要一个MonoBehaviour脚本或者更好的一个VolumeComponent继承自VolumeComponent和IPostProcessComponent来在运行时动态控制雾效参数并将其传递给Shader。使用Volume系统可以与URP的场景后期处理设置无缝集成是更专业的选择。// URPFogVolume.cs using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; [System.Serializable, VolumeComponentMenu(Custom/URP Fog)] public class URPFogVolume : VolumeComponent, IPostProcessComponent { public FogModeParameter mode new FogModeParameter(FogMode.Exponential); public ColorParameter color new ColorParameter(Color.gray, true, true, true); public MinFloatParameter density new MinFloatParameter(0.01f, 0.0f); public MinFloatParameter start new MinFloatParameter(0.0f, 0.0f); public MinFloatParameter end new MinFloatParameter(100.0f, 0.0f); public MinFloatParameter height new MinFloatParameter(10.0f, 0.0f); public FloatParameter baseHeight new FloatParameter(0.0f); public MinFloatParameter heightFalloff new MinFloatParameter(0.2f, 0.0f); public ClampedFloatParameter scatteringIntensity new ClampedFloatParameter(0.0f, 0.0f, 1.0f); public ClampedFloatParameter scatteringDirection new ClampedFloatParameter(0.0f, -0.9f, 0.9f); // 类似各向异性 public bool IsActive() density.value 0.0f; public bool IsTileCompatible() false; // 通常雾效不是瓦片兼容的 [System.Serializable] public sealed class FogModeParameter : VolumeParameterFogMode { public FogModeParameter(FogMode value, bool overrideState false) : base(value, overrideState) { } } public enum FogMode { None, Exponential, ExponentialSquared, Height } }然后我们需要一个URPFogRendererFeature来在渲染过程中将Volume中的参数设置到Shader的全局属性Shader.SetGlobal*中。这个RendererFeature可以插入到RenderPassEvent.BeforeRenderingPostProcessing或AfterRenderingSkybox等位置确保在物体渲染前参数已就绪。// URPFogRenderFeature.cs public class URPFogRenderFeature : ScriptableRendererFeature { class URPFogRenderPass : ScriptableRenderPass { private Material fogMaterial; // 如果使用屏幕空间后处理雾才需要材质 private URPFogVolume fogVolume; private RenderTargetIdentifier source; private RenderTargetHandle tempTexture; public void Setup(RenderTargetIdentifier source) { this.source source; var stack VolumeManager.instance.stack; fogVolume stack.GetComponentURPFogVolume(); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (fogVolume null || !fogVolume.IsActive()) { // 如果雾效未激活只设置一个默认值或清除全局属性 Shader.SetGlobalFloat(_FogMode, 0); return; } // 将Volume参数传递给Shader全局属性 Shader.SetGlobalInt(_FogMode, (int)fogVolume.mode.value); Shader.SetGlobalColor(_FogColor, fogVolume.color.value); Shader.SetGlobalFloat(_FogDensity, fogVolume.density.value); Shader.SetGlobalFloat(_FogStart, fogVolume.start.value); Shader.SetGlobalFloat(_FogEnd, fogVolume.end.value); Shader.SetGlobalFloat(_FogHeight, fogVolume.height.value); Shader.SetGlobalFloat(_FogBaseHeight, fogVolume.baseHeight.value); Shader.SetGlobalFloat(_FogHeightFalloff, fogVolume.heightFalloff.value); Shader.SetGlobalFloat(_FogScatteringIntensity, fogVolume.scatteringIntensity.value); Shader.SetGlobalFloat(_FogScatteringDirection, fogVolume.scatteringDirection.value); // 如果需要屏幕空间后处理雾在这里进行Blit操作 // if (fogMaterial ! null) { ... } } } private URPFogRenderPass fogPass; public override void Create() { fogPass new URPFogRenderPass(); fogPass.renderPassEvent RenderPassEvent.BeforeRenderingPostProcessing; // 在后期处理前设置参数 } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { fogPass.Setup(renderer.cameraColorTarget); renderer.EnqueuePass(fogPass); } }4. 性能优化与高级技巧实现功能只是第一步让它在各种设备上流畅运行才是URPFog“高效”二字的体现。4.1 性能优化关键点计算精度取舍在片元着色器中worldPos和distance的计算是性能敏感点。对于移动平台可以考虑在顶点着色器中计算雾因子然后通过插值传递给片元着色器interpolated fog。虽然精度稍差但对于平滑变化的雾效视觉差异很小却能显著节省性能。// 顶点着色器中 output.fogFactor ComputeFogFactor(positionWS, distanceToCam); // 片元着色器中直接使用input.fogFactorShader变体管理我们通过shader_feature定义了_FOG_SCATTERING_ON。务必在项目设置Graphics - Shader Stripping中或者通过自定义的ShaderVariantCollection来管理这些变体避免生成不必要的Shader变体导致包体膨胀和运行时加载卡顿。距离计算优化length(viewDirWS)需要开方运算。对于指数雾我们可以使用距离的平方来近似避免sqrt操作。因为exp(-density * distance)和exp(-density * sqrt(distanceSqr))在视觉上差异不大时可以用前者替换。float distanceSqr dot(viewDirWS, viewDirWS); // 使用近似fogFactor 1.0 - exp(-_FogDensity * _FogDensity * distanceSqr);分平台配置在URPFogVolume中可以为不同平台如UNITY_ANDROID或UNITY_IOS设置不同的默认参数或限制参数范围如最大密度、禁用散射确保低端机也能运行。4.2 效果增强与艺术控制噪声扰动让雾的密度产生动态变化可以增加真实感。在计算雾密度因子时采样一张3D噪声纹理或程序化噪声根据世界坐标和时间进行偏移。float3 noiseUVW worldPos * _FogNoiseScale _Time.y * _FogNoiseSpeed; float noise tex3D(_FogNoiseTex, noiseUVW).r; float finalDensity _FogDensity * (1.0 noise * _FogNoiseStrength);基于深度的边缘柔化在雾的边缘特别是线性雾的结束处与天空盒或其他清晰物体的交界可能很生硬。可以在雾因子计算的最后用一个smoothstep函数进行柔化。fogFactor smoothstep(0.0, 1.0, fogFactor); // 简单的平滑 // 或者基于距离的平滑 float smoothRange 10.0; fogFactor smoothstep(_FogEnd - smoothRange, _FogEnd, distanceToCam);雾效与天空盒融合确保远处雾的颜色与天空盒颜色能平滑过渡。我们的ApplyFog函数最终是用雾颜色与物体颜色混合。对于天空盒我们需要确保在远裁剪面处雾因子接近1这样天空盒就会完全被雾色覆盖。这通常需要根据相机的远平面距离来调整雾的结束距离。4.3 常见问题与排查实录在实际集成和使用URPFog的过程中你几乎一定会遇到下面这些问题雾效在透明物体上不正确透明物体如粒子、半透明材质的渲染顺序在Opaque之后。如果雾效计算依赖于深度纹理屏幕空间方案透明物体的深度可能没有被正确写入或者其颜色混合方式特殊。解决方案对于着色器集成方案确保你的透明Shader也包含了雾效计算代码。对于屏幕空间方案可能需要单独为透明物体编写一个后处理Pass或者使用双Pass渲染。雾效与后期处理如Color Grading冲突如果雾效在后处理之前应用那么后续的色调映射、颜色分级会改变雾的颜色可能不符合预期。解决方案将雾效的渲染Pass顺序renderPassEvent调整到所有后期处理之后AfterRenderingPostProcessing但这意味着你需要将结果Blit到最终屏幕上。更常见的做法是在着色器集成方案中雾是光照计算的一部分它发生在后期处理之前这就需要美术在调整后期时将雾的颜色考虑进去。移动设备上性能骤降即使采用了顶点雾在植被茂密顶点数多的场景也可能有压力。排查步骤使用Unity Profiler的GPU模块查看是哪个Shader或Pass耗时高。检查是否意外为所有材质都启用了散射计算_FOG_SCATTERING_ON。通过Frame Debugger查看材质使用的Shader变体。确认是否在片元着色器中进行了昂贵的噪声纹理采样或复杂的数学运算。考虑降级方案如在低画质下关闭噪声和散射。雾效在VR中双眼不一致或产生立体感错误在VR中每只眼睛的渲染视图略有不同。如果雾效计算完全基于屏幕空间或单个相机的位置会导致立体感破坏。解决方案在VR模式下确保雾效计算基于每只眼睛各自的视图矩阵和世界空间位置。在Shader中使用unity_StereoEyeIndex和对应的视图/投影矩阵数组来进行正确的计算。与第三方Shader或Asset不兼容这是着色器集成方案的最大挑战。你不能强迫所有第三方资源使用你的Shader。后备方案保留并优化一个屏幕空间雾效的后处理Pass作为全局覆盖。在URPFogRenderPass的Execute方法中如果检测到当前渲染的物体是使用标准URP Lit Shader或我们标记的自定义Shader就使用着色器雾否则在渲染完所有不透明物体后用这个屏幕空间Pass对整个画面施加一层雾。这虽然牺牲了一点性能但保证了效果的统一性。我的实操心得在项目中期接入URPFog时最头疼的就是处理已有的上百种材质。我的策略是先开发出屏幕空间版本作为“安全网”确保场景整体有雾效。然后写一个编辑器脚本批量扫描项目中的材质将使用标准URP Lit的材质替换为我们的定制版本复制一份并修改Shader引用。对于复杂的自定义Shader则逐个评估手动将雾效计算函数#include进去。这个过程很繁琐但一劳永逸。对于性能一定要在目标最低配置的设备上比如一台旧的安卓手机做真机测试参数调校要以低端机的表现为准高端机自然没问题。