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 矩阵的逆变换,我们可以将屏幕空间的深度值转换为真实的三维坐标,进而进行体积采样和光散射计算。

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

相关推荐
RPGMZ1 天前
RPGMakerMZ 地图存档点制作 标题继续游戏直接读取存档
开发语言·javascript·游戏·游戏引擎·rpgmz·rpgmakermz
郝学胜-神的一滴1 天前
[简化版 GAMES 101] 计算机图形学 07:图形学投影完全推导
c++·unity·图形渲染·three.js·unreal engine
晴夏。1 天前
UE垃圾回收的全方面讲解(通俗易懂)【底层实现、触发方式、引用保持、优化、工具】
ue5·游戏引擎·ue·垃圾回收
相信神话20212 天前
3.2《酒魂》规则设计文档
游戏引擎·godot·2d游戏编程·godot4·2d游戏开发
Avalon7122 天前
Unity3D响应式渲染UI框架UniVue
游戏·ui·unity·c#·游戏引擎
风酥糖2 天前
Godot游戏练习01-第33节-新增会爆炸的敌人
游戏·游戏引擎·godot
ellis19702 天前
Unity UI性能优化一之插件【Unity UI Optimization Tool】
unity·性能优化
Zik----2 天前
Unity基础学习笔记(B站视频课整理)
unity·vr
郑寿昌3 天前
UE5与UE6在Lumen和Nanite的差异解析
游戏引擎·图形渲染·着色器
郝学胜-神的一滴3 天前
罗德里格斯旋转公式(Rodrigues‘ Rotation Formula)完整推导
c++·unity·godot·图形渲染·three.js·unreal