Unity URP 体积光与雾效 基于深度重建世界空间位置,实现体积雾与体积光

在现代游戏渲染中,体积光(Volumetric Lighting)和体积雾(Volumetric Fog)是营造氛围、提升沉浸感的关键技术。它们能够让光线可见化,让雾气充满体积感,使整个场景更加真实可信。

本文将深入探讨在 Unity URP(Universal Render Pipeline) 环境下,如何通过深度图重建世界空间位置,并基于此实现高质量的体积光与体积雾效果。

一、核心原理概述

1.1 体积渲染的基本思想

体积渲染的核心思想是:将三维空间中的介质(如雾气、烟尘、光线中的粒子)视为无数微小的体积元素,通过累积这些元素对光线的吸收和散射,最终得到视觉效果。

体积渲染关键步骤
  • 从摄像机发射射线,穿过每个像素
  • 沿射线方向进行等间距采样
  • 在每个采样点重建世界空间位置
  • 计算该点的介质密度和光照强度
  • 沿路径累积光的散射和吸收

二、深度重建世界空间位置

2.1 为什么需要深度重建?

在 URP 中,深度图(Depth Texture)只存储了每个像素的深度值(0-1范围),我们无法直接获取该像素对应的三维空间位置。要实现体积渲染,必须将这个深度值转换为真正的世界空间坐标。

2.2 Shader 实现

以下是完整的深度重建 Shader 代码,逐行解析:

cs 复制代码
// 获取深度纹理(URP 内置 _CameraDepthTexture)
sampler2D _CameraDepthTexture;
// 重建世界空间位置的核心函数
float3 ReconstructWorldPosition(float2 uv, float depth)
{
    // 构建该像素的裁剪空间坐标 (NDC)
    float4 clipPos = float4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
    // 获取当前渲染相机的 VP 矩阵
    float4x4 invVP = unity_CameraInvProjection;
    float4 worldPos = mul(invVP, clipPos);
    
    // 透视除法,转换到世界空间
    worldPos.xyz /= worldPos.w;
    
    // 考虑相机世界位置偏移
    worldPos.xyz += _WorldSpaceCameraPos;
    
    return worldPos.xyz;
}

2.3 深度重建流程图

三、体积雾实现

3.1 体积雾的数学模型

体积雾基于 Beer-Lambert 定律描述光线穿过介质时的衰减:

沿视线累积的光学深度公式:

3.2 体积雾 Shader

cs 复制代码
// 体积雾属性
uniform float _FogDensity = 0.1;      // 雾的浓度
uniform float _FogHeight = 10.0;     // 雾的高度范围
uniform float3 _FogColor;              // 雾的颜色
// 计算某点的雾密度
float SampleFogDensity(float3 worldPos)
{
    // 基于高度的雾密度衰减
    float heightFactor = exp(-worldPos.y * 0.1);
    
    // 基础雾密度 × 高度衰减
    return _FogDensity * heightFactor;
}
// 体积雾累积函数
float4 VolumetricFog(float4 color, float2 uv, float depth)
{
    // 重建世界空间位置
    float3 worldPos = ReconstructWorldPosition(uv, depth);
    float3 cameraPos = _WorldSpaceCameraPos;
    
    // 视线方向
    float3 rayDir = normalize(worldPos - cameraPos);
    float rayLength = length(worldPos - cameraPos);
    
    // 采样步长和次数
    float stepSize = rayLength / 32.0;
    float accumulatedFog = 0.0;
    
    // 沿视线采样累积
    for (int i = 0; i < 32; i++)
    {
        float3 samplePos = cameraPos + rayDir * stepSize * i;
        float density = SampleFogDensity(samplePos);
        accumulatedFog += density * stepSize;
    }
    
    // Beer-Lambert 衰减
    float fogFactor = 1.0 - exp(-accumulatedFog);
    
    // 混合雾颜色
    return lerp(color, float4(_FogColor, 1.0), fogFactor);
}

3.3 体积雾效果示意

四、体积光实现

4.1 体积光的基本原理

体积光(God Rays)模拟光线穿过介质(如雾气、尘埃)时的散射效果。当光线经过体积介质时,部分光线被散射到观察者眼中,形成可见的光柱效果。

4.2 体积光 Shader

cs 复制代码
// 体积光属性
uniform float4 _LightPos;          // 光源世界位置
uniform float3 _LightColor;       // 光源颜色
uniform float _Scattering = 1.0;  // 散射系数
uniform float _Density = 1.0;      // 介质密度
// Ray Marching 体积光累积
float3 VolumetricLight(float2 uv, float depth)
{
    float3 worldPos = ReconstructWorldPosition(uv, depth);
    float3 cameraPos = _WorldSpaceCameraPos;
    
    // 计算到表面的距离
    float distToSurface = length(worldPos - cameraPos);
    float3 rayDir = normalize(worldPos - cameraPos);
    
    // 光线到采样点的方向
    float3 lightDir = normalize(_LightPos.xyz - cameraPos);
    
    // 采样参数
    int numSteps = 64;
    float stepSize = distToSurface / numSteps;
    float3 accumulatedLight = float3(0.0);
    
    for (int i = 0; i < numSteps; i++)
    {
        // 当前采样点
        float3 samplePos = cameraPos + rayDir * stepSize * i;
        
        // 计算到光源的距离
        float distToLight = length(_LightPos.xyz - samplePos);
        
        // Mie 散射相位函数
        float cosAngle = dot(-rayDir, lightDir);
        float phase = MieScattering(cosAngle);
        
        // 光源衰减 + 散射相位
        float attenuation = 1.0 / (1.0 + distToLight * distToLight);
        float3 lightContrib = _LightColor * attenuation * phase;
        
        // 累积光线贡献
        accumulatedLight += lightContrib * _Density * stepSize;
    }
    
    // 应用全局散射系数
    return accumulatedLight * _Scattering;
}
// Mie 散射相位函数(近似)
float MieScattering(float cosAngle)
{
    const float g = 0.8;  // 各向异性参数
    return (1.0 - g * g) / 
           (4.0 * 3.14159 * pow(1.0 + g * g - 2.0 * g * cosAngle, 1.5));
}

五、性能优化策略

5.1 采样数量控制

体积渲染的主要性能瓶颈在于沿视线方向的采样次数。以下是不同采样级别的权衡:

采样级别 采样次数 视觉效果 性能消耗
16-24 次 噪点明显,颗粒感强 ★★★★★
32-48 次 基本可用,需降噪 ★★★☆☆
64-128 次 细腻平滑,效果优秀 ★★☆☆☆

5.2 降噪技术

为了在有限的采样次数下获得平滑效果,通常需要配合降噪技术:

常用降噪方案
  • 双边滤波(Bilateral Filter):保留边缘,平滑噪点
  • 时间积累(Temporal Accumulation):利用历史帧信息
  • 深度感知降噪:根据深度变化调整滤波强度
  • 噪点抖动(Jitter):使用蓝噪声或哈希函数实现采样位置随机化

5.3 分辨率缩放

另一个常见优化是以较低分辨率渲染体积效果,然后上采样到全分辨率:

cs 复制代码
// 以 1/2 或 1/4 分辨率渲染体积光
uniform float _ResolutionScale = 0.5;
void Main(float2 uv, out float4 color)
{
    // 缩放 UV 到低分辨率
    float2 lowResUV = uv * _ResolutionScale;
    float lowResDepth = tex2D(_CameraDepthTexture, lowResUV).r;
    
    // 在低分辨率下计算体积效果
    color = VolumetricLight(lowResUV, lowResDepth);
    
    // 后续通过上采样(双线性/双三次)恢复分辨率
    // 配合深度感知插值避免边缘模糊
}

六、在 URP 中集成

6.1 配置 Depth Texture

首先需要在 URP 渲染器配置中启用深度纹理:

URP Renderer AssetDepth Texture✓Opaque Texture✓HDR必须启用 Depth Texture 才能在自定义 Pass 中访问深度信息

6.2 创建体积雾 Pass

完整的 URP 自定义渲染 Pass 示例:

cs 复制代码
六、在 URP 中集成
6.1 配置 Depth Texture
首先需要在 URP 渲染器配置中启用深度纹理:

URP Renderer Asset
Depth Texture
✓
Opaque Texture
✓
HDR
必须启用 Depth Texture 才能在自定义 Pass 中访问深度信息
6.2 创建体积雾 Pass
完整的 URP 自定义渲染 Pass 示例:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class VolumetricPass : ScriptableRenderPass
{
    private Material _volumetricMaterial;
    
    public VolumetricPass(Material mat)
    {
        _volumetricMaterial = mat;
        renderPassEvent = RenderPassEvent.AfterRenderingSkybox;
    }
    
    public override void Execute(
        ScriptableRenderContext context, 
        ref RenderingData renderingData)
    {
        // 获取相机渲染数据
        Camera camera = renderingData.cameraData.camera;
        if (camera.cameraType != CameraType.Game) return;
        
        // 设置 Shader 属性
        _volumetricMaterial.SetMatrix("_InvVP", 
            camera.projectionMatrix * camera.worldToCameraMatrix);
        
        // 启用深度纹理采样
        Shader.SetGlobalTexture("_CameraDepthTexture",
            renderingData.cameraData.depthTexture);
        
        // 提交渲染命令
        CommandBuffer cmd = CommandBuffer.Pool.Get();
        cmd.Blit(null, RenderTargetHandle.CameraTarget.Identifier(), 
            _volumetricMaterial);
        context.ExecuteCommandBuffer(cmd);
        CommandBuffer.Pool.Release(cmd);
    }
}

总结

基于深度重建世界空间位置是实现高质量体积光与雾效的基础。通过理解 VP 矩阵的逆变换,我们可以将屏幕空间的深度值转换为真实的三维坐标,进而进行体积采样和光散射计算。

关键要点:采样数量与性能的权衡、使用降噪技术提升视觉质量、以及通过分辨率缩放等优化手段实现实时渲染。

相关推荐
张老师带你学3 小时前
unity 树资源 有樱花树 buildin
科技·游戏·unity·游戏引擎·模型
魔士于安3 小时前
unity 植物 不常见 花 触手植物
游戏·unity·游戏引擎·贴图·模型
魔士于安4 小时前
unity=>传送门特效 带自由视角旋转放大 鼠标操作
前端·游戏·unity·游戏引擎·贴图·模型
南無忘码至尊5 小时前
Unity学习90天 - 第4天 - 认识物理系统基础并实现物体碰撞反弹
学习·unity·游戏引擎
南無忘码至尊5 小时前
Unity学习90天 - 第4天 - 学习预制体 Prefab + 实例化并实现按鼠标生成子弹
学习·unity·游戏引擎
魔士于安18 小时前
Unity资源Toon City Pack 发电厂 工地 公园 地铁站口 银行 车 直升飞机 可动 URP
游戏·unity·游戏引擎·贴图·模型
SmartRadio21 小时前
NRF52833 + MPU6050 室内定位跟随无人机
游戏引擎·无人机·cocos2d
心前阳光21 小时前
Unity之运行时标准材质半透明无效果
unity·游戏引擎·材质
张老师带你学1 天前
Unity buildin 石头围墙 树木 树墩子 卡通风格 栅栏 小桥 低多边形
科技·游戏·unity·游戏引擎·模型