UE5.3 对体积雾的实现和原理是基于 寒霜引擎 Siggraph 2015Siggraph 2014 Wronski关于大气散射的演讲

UE 渲染管线对体积雾的实现是基于体素网格

散射原理浅析

Wronski Siggraph 2014 scattering

散射基本概念

从能量角度看,入射光被分解成了三个部分,透射光,散出光,被吸收

$$
L_{incoming} = L_{transmitted}+L_{absorbed}+L_{scattered}
$$

图片

在存在散射的场景中,光线在传播的过程中不断的散出,散入,被吸收

如下图,黑色是光线路径,绿色是被光线被散出,灰色是不断有其他散射光加入当前光路图片

Beer-Lambert law

物理公式,定义了散射下 出入射光线比 T

$$
T(A\rightarrow B) = e^{-\int_{A}^{B}\beta e(x)dx}
$$

Phase functions

用于描述各个角度的散射强度,如下图,描述了光线入射云后往各个方向的散射情况

具有能量守恒的性质,因此所有方向的积分必须等于 1图片
计算体积雾实际使用的是 Henyey Greenstein phase function

用于模拟类似米氏散射的各向异性散射

更复杂的 phase function 可以由多个 Henyey-Greenstein 函数加权或级联构造图片

实现方案汇总

目前基本是四套方案

  1. 基于深度、高度的解析雾,效果好,但无法支持变化不均匀的雾效

  2. 基于 billboard 的2D雾,依赖美术人员,不够动态,广告牌旋转可能有旋转问题

  3. 基于后处理和径向模糊,容易模拟类似丁达尔效应的效果,但当看不到光源时,雾效完全消失

  4. 基于ray-marching,效果最好,但是开销太过大,不支持前向渲染

  5. 基于体素纹理的实现是对 ray-marching的模拟,充分利用了GPU的并行性,但是距离仍然有限

目前实际运用基本是混合方案,

解析雾用于远处场景,3D纹理的体积雾用于模拟近距离的雾效,2D雾用于某些特殊角度的美术需求

寒霜引擎的实现

寒霜引擎 Siggraph 2015

建模

图片
如上图所示,前半是模拟物体的反射光透过散射体后剩余的辐射度,后半计算散射体积的效果

每个采样点(绿色点)只计算一次散射,就能得到相当好的效果

灰框:Beer-Lambert law 的公式,计算光线穿越体积后的剩余辐射度,模拟散出(Out-scattering)

橙框:根据物体表面BRDF计算出来往摄像机的辐射度

蓝框:散入的光线,是各个方向上散入的一个简单求和(In-scattering)

紫框:可视性,处于阴影区域的体积不计算散射

红圈:phase function

数据流

图片
首先将各种参与散射的体积体素化,将其各种参数(反射率,密度等)存入纹理

然后利用场景的光源和阴影信息,并行的在每个体素cell中计算散射光数据

最后,执行积分过程,获取最后的3D纹理

前置内容

视锥体froxelization

[froxel Physically Based Rendering]

第一步将摄像机视锥体进行 “froxelization”,结合了屏幕 tile 和视锥体 Z 轴分割的一种体素化方式

每一个小单元(froxel)可以使用 computer shader 计算离散位置的光散射

其结果存储在 3D 纹理的相应纹素中,从而可支持前向和延迟渲染的使用

深度上的切片 ZSlice 和视空间的 Depth 指数型对应,即离相机越近3D纹理的采样越密,越远则越少

1
2
// ue5
float ZSlice = log2(SceneDepth * View.VolumetricFogGridZParams.x + View.VolumetricFogGridZParams.y) * View.VolumetricFogGridZParams.z * View.VolumetricFogInvGridSize.z;

图片
如图,绿色高亮的视锥体 froxelization 可视化, 5×3 tiles, 8 depth slices

图片

UE 渲染流程

概述

渲染时,先进行 VolumetricFog 的相关计算,将体积雾信息存储在一个3D纹理中

然后在 ExponentialHeightFog 中计算高度雾并且采样体积纹理,两者混合得到最终的雾效

图片

图片

InitializVolumeAttributes 清出一张 Texture3D,VolumetricFog.VBufferA
VoxelizeVolumePrimitives 各种局部体积雾在这一步被体素化,吸收系数、散射参数等被写入VolumetricFog.VBufferA
LightScattering LightScatteringCS计算雾对各种灯光的散射等
FinalIntegration FinalIntegrationCS 执行散射消光等效果的累计,最终得到 VolumetricFog.IntegratedLightScattering 体积纹理
ExponentialHeightFog 计算高度雾,CombineVolumetricFog()采样体积纹理,混合两种雾效

3D纹理

渲染时使用一张3D纹理,将场景中的需要渲染的体积雾信息和阴影信息储存到一张和相机视锥体对齐的3D纹理中,当绘制物体的时候利用物体的世界空间坐标采样这张3D纹理,直接在 PixelShader 中计算雾效之后的颜色。

图片
本质是在纹理中逐层步进来模拟 raymarching,从近向远更新每一层纹素

UE5.3 默认的体积雾纹理大小为 (分辨率宽 / 16) x (分辨率高 / 16) x 64,纹理的 depth 默认为 64

如图,截帧查看纹理第49层和64层的数据

图片
图片
图片

源码分析

基于 UE5.3

FinalIntegration

执行一个积分的过程,从前到后遍历每层 layer 对散射和透射率进行累计

每个线程组负责累加由近到远的一系列 froxel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// -------------------------------------------
// VolumetricFog.usf
// -------------------------------------------

[numthreads(THREADGROUP_SIZE, THREADGROUP_SIZE, 1)] // computer shader调用GPU上的线程组, 可知一次处理z方向上的一组
void FinalIntegrationCS(
uint3 GroupId : SV_GroupID,
uint3 DispatchThreadId : SV_DispatchThreadID,
uint3 GroupThreadId : SV_GroupThreadID)
{
uint3 GridCoordinate = DispatchThreadId;

float3 AccumulatedLighting = 0;
float AccumulatedTransmittance = 1.0f;
float3 PreviousSliceTranslatedWorldPosition = ComputeCellTranslatedWorldPosition(uint3(GridCoordinate.xy, 0), float3(0.5f, 0.5f, 0.0f));
float AccumulatedDepth = 0.0;
for (int LayerIndex = 0; LayerIndex < VolumetricFog.ViewGridSizeInt.z; LayerIndex++) // 逐层更新
{
uint3 LayerCoordinate = uint3(GridCoordinate.xy, LayerIndex); // 获取体素纹理坐标
float4 PreExposedScatteringAndExtinction = LightScattering[LayerCoordinate]; //获取预曝光的散射和消光

float3 LayerTranslatedWorldPosition = ComputeCellTranslatedWorldPosition(LayerCoordinate, .5f); //计算当前cell的世界位置
float StepLength = length(LayerTranslatedWorldPosition - PreviousSliceTranslatedWorldPosition); //计算前后两个cell的步长
PreviousSliceTranslatedWorldPosition = LayerTranslatedWorldPosition;

float Transmittance = exp(-PreExposedScatteringAndExtinction.w * StepLength); //计算透射率

AccumulatedDepth += StepLength; //累积深度

// Fade in as a function of depth
float FadeInLerpValue = saturate(AccumulatedDepth * VolumetricFogNearFadeInDistanceInv); //计算淡入插值

// See "Physically Based and Unified Volumetric Rendering in Frostbite"
#define ENERGY_CONSERVING_INTEGRATION 1
#if ENERGY_CONSERVING_INTEGRATION // 能量守恒下
float3 ScatteringIntegratedOverSlice = FadeInLerpValue * (PreExposedScatteringAndExtinction.rgb - PreExposedScatteringAndExtinction.rgb * Transmittance) / max(PreExposedScatteringAndExtinction.w, .00001f); //根据淡入插值计算散射
AccumulatedLighting += ScatteringIntegratedOverSlice * AccumulatedTransmittance; //累积光照
#else // 能量不守恒下
AccumulatedLighting += FadeInLerpValue * PreExposedScatteringAndExtinction.rgb * AccumulatedTransmittance * StepLength;
#endif

AccumulatedTransmittance *= lerp(1.0f, Transmittance, FadeInLerpValue); //更新累积透射率

RWIntegratedLightScattering[LayerCoordinate] = float4(AccumulatedLighting, AccumulatedTransmittance); //将累积的光照和透射率写回体积纹理中
}
}

ExponentialHeightFog

常用参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 高度雾参数
// FogStruct.ExponentialFogParameters:
// x: FogDensity * exp2(-FogHeightFalloff * (CameraWorldPosition.z - FogHeight)),
// y: FogHeightFalloff,
// z: MaxWorldObserverHeight,
// w: StartDistance.

// FogStruct.ExponentialFogParameters2:
// x: FogDensitySecond * exp2(-FogHeightFalloffSecond * (CameraWorldPosition.z - FogHeightSecond)),
// y: FogHeightFalloffSecond,
// z: FogDensitySecond,
// w: FogHeightSecond

// FogStruct.ExponentialFogParameters3:
// x: FogDensity,
// y: FogHeight,
// z: whether to use cubemap fog color,
// w: FogCutoffDistance.

// FogStruct.FogInscatteringTextureParameters:
// x: mip distance scale,
// y: bias,
// z: num mips

PixelShader

计算采样体积雾的uvw,高度雾,混合体积雾和高度雾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// --------------------------------------------
// HeightFogPixelShader.usf
// --------------------------------------------
void ExponentialPixelMain(
float2 TexCoord : TEXCOORD0,
float3 ScreenVector : TEXCOORD1,
float4 SVPos : SV_POSITION,
out float4 OutColor : SV_Target0
)
{
float SceneDepth;
float DeviceZ;

DeviceZ = Texture2DSampleLevel(SceneTexturesStruct.SceneDepthTexture, SceneTexturesStruct.PointClampSampler, TexCoord, 0).r;
SceneDepth = ConvertFromDeviceZ(DeviceZ);

float3 WorldPositionRelativeToCamera = SvPositionToTranslatedWorld(float4(SVPos.xy, max(1.5e-10, DeviceZ), 1.0));

// 计算采样体积雾的VolumeUV
// 引入cell offset消除一部分噪声
{
float ZSlice = log2(SceneDepth * View.VolumetricFogGridZParams.x + View.VolumetricFogGridZParams.y) * View.VolumetricFogGridZParams.z * View.VolumetricFogInvGridSize.z;

uint3 Rand16Bits = Rand3DPCG16(int3(SVPos.xy, View.StateFrameIndexMod8));
float3 Rand3D = (float3(Rand16Bits) / float(uint(0xffff))) * 2.0f - 1.0f;
float3 CellOffset = UpsampleJitterMultiplier * Rand3D;

float3 VolumeUV = float3(((SVPos.xy - View.ViewRectMin.xy) + CellOffset.xy) * View.VolumetricFogSVPosToVolumeUV, ZSlice);
VolumeUV.xy = min(VolumeUV.xy, View.VolumetricFogUVMax);
}

float4 HeightFogInscatteringAndOpacity = CalculateHeightFog(WorldPositionRelativeToCamera); // 计算高度雾,只计算 VolumetricFogMaxDistance 之外的区域

float4 FogInscatteringAndOpacity = CombineVolumetricFog(HeightFogInscatteringAndOpacity, VolumeUV, 0, SceneDepth); // 采样3D纹理,混合高度雾和体积雾
float2 OcclusionTexCoord = clamp(TexCoord, OcclusionTextureMinMaxUV.xy, OcclusionTextureMinMaxUV.zw);
    float LightShaftMask = Texture2DSample(OcclusionTexture, OcclusionSampler, OcclusionTexCoord).x;
    FogInscatteringAndOpacity.rgb *= LightShaftMask;

FogInscatteringAndOpacity.rgb *= View.PreExposure; // 全局雾效的预曝光强度

if (bOnlyOnRenderedOpaque > 0.0)
{
FogInscatteringAndOpacity.rgb = 0;
FogInscatteringAndOpacity.a = 1;
}

OutColor = ( float4(FogInscatteringAndOpacity.rgb, FogInscatteringAndOpacity.w) ); // 最终雾效
}

Key Functions

CalculateHeightFog() 如果启用了体积雾,则从ExcludeDistance这个范围内排除高度雾

ExcludeDistance的结果是视锥体中的一个截面,即之前视锥体 froxel 的 far plane,这个距离外用高度雾,距离内应用体积雾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
half4 CalculateHeightFog(float3 WorldPositionRelativeToCamera)
{
float ExcludeDistance = 0;

#if PERMUTATION_SUPPORT_VOLUMETRIC_FOG
// Volumetric fog covers up to MaxDistance along ViewZ, exclude analytical fog from this range
float CosAngle = dot(normalize(WorldPositionRelativeToCamera), View.ViewForward);
float InvCosAngle = (CosAngle > FLT_EPSILON) ? rcp(CosAngle) : 0;
ExcludeDistance = View.VolumetricFogMaxDistance * InvCosAngle;
#endif
// 计算高度雾,实际起始距离 = max(ExcludeDistance, FogStruct.ExponentialFogParameters.w)
half4 FogInscatteringAndOpacity = GetExponentialHeightFog(WorldPositionRelativeToCamera, ExcludeDistance);
return FogInscatteringAndOpacity; // 最终高度雾效
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// @param WorldPositionRelativeToCamera = WorldPosition - InCameraPosition
half4 GetExponentialHeightFog(float3 WorldPositionRelativeToCamera, float ExcludeDistance)
{
const half MinFogOpacity = FogStruct.ExponentialFogColorParameter.w; // 最小透明度
const float MaxWorldObserverHeight = FogStruct.ExponentialFogParameters.z; // cos终结角度

const float3 WorldObserverOrigin = float3(LWCHackToFloat(PrimaryView.WorldCameraOrigin).xy, min(LWCHackToFloat(PrimaryView.WorldCameraOrigin).z, MaxWorldObserverHeight)); // Clamp z to max height

// 场景物体到成像平面的参数
float3 CameraToReceiver = WorldPositionRelativeToCamera;  // 场景物体到相机的向量
CameraToReceiver.z += LWCHackToFloat(PrimaryView.WorldCameraOrigin).z - WorldObserverOrigin.z; // Compensate this vector for clamping the observer height 
float CameraToReceiverLengthSqr = dot(CameraToReceiver, CameraToReceiver); // 距离的平方
float CameraToReceiverLengthInv = rsqrt(max(CameraToReceiverLengthSqr, 0.00000001f)); // 距离的倒数
float CameraToReceiverLength = CameraToReceiverLengthSqr * CameraToReceiverLengthInv;// 距离的值
half3 CameraToReceiverNormalized = CameraToReceiver * CameraToReceiverLengthInv;// 距离的单位向量

float RayOriginTerms = FogStruct.ExponentialFogParameters.x; // x : FogDensity * exp2(-FogHeightFalloff * (CameraWorldPosition.y - FogHeight)),可以理解成光线起始点的密度
float RayOriginTermsSecond = FogStruct.ExponentialFogParameters2.x; // FogDensitySecond * exp2(-FogHeightFalloffSecond * (CameraWorldPosition.y - FogHeightSecond))
float RayLength = CameraToReceiverLength; // 光线长度
float RayDirectionZ = CameraToReceiver.z;

// 启用体积雾时,一些关于开始距离的参数
ExcludeDistance = max(ExcludeDistance, FogStruct.ExponentialFogParameters.w); // 距离在ExcludeDistance内时,不使用指数高度雾
if (ExcludeDistance > 0)
{
float ExcludeIntersectionTime = ExcludeDistance * CameraToReceiverLengthInv; // 到截面的相对时间
float CameraToExclusionIntersectionZ = ExcludeIntersectionTime * CameraToReceiver.z; // 相机到相交点的 垂直 偏移
float ExclusionIntersectionZ = WorldObserverOrigin.z + CameraToExclusionIntersectionZ; // 相交点的 垂直 坐标
float ExclusionIntersectionToReceiverZ = CameraToReceiver.z - CameraToExclusionIntersectionZ; // 相交点到着色点的 垂直 偏移

// 从排除距离开始计算高度雾,而不是从相机开始
RayLength = (1.0f - ExcludeIntersectionTime) * CameraToReceiverLength; // 从实际开始点到物体的距离,高度雾内光线的实际长度
RayDirectionZ = ExclusionIntersectionToReceiverZ; // 相交点到着色点的 垂直 偏移

// 计算exp的指数,主要把相机的CameraWorldPosition.y替换成新开始点的位置
float Exponent = max(-127.0f, FogStruct.ExponentialFogParameters.y * (ExclusionIntersectionZ - FogStruct.ExponentialFogParameters3.y));
RayOriginTerms = FogStruct.ExponentialFogParameters3.x * exp2(-Exponent); // FogDensity * exp2(-FogHeightFalloff * (ExclusionIntersectionZ - FogHeight))

float ExponentSecond = max(-127.0f, FogStruct.ExponentialFogParameters2.y * (ExclusionIntersectionZ - FogStruct.ExponentialFogParameters2.w));  
RayOriginTermsSecond = FogStruct.ExponentialFogParameters2.z * exp2(-ExponentSecond);
}

// Calculate the "shared" line integral by adding the two line integrals together (from two different height falloffs and densities)
// the ray from the camera to the receiver position的线性积分,利用雾密度函数
float ExponentialHeightLineIntegralShared = CalculateLineIntegralShared(FogStruct.ExponentialFogParameters.y, RayDirectionZ, RayOriginTerms);
ExponentialHeightLineIntegralShared+= CalculateLineIntegralShared(FogStruct.ExponentialFogParameters2.y, RayDirectionZ, RayOriginTermsSecond);

float ExponentialHeightLineIntegral = ExponentialHeightLineIntegralShared * RayLength;

//雾色
half3 InscatteringColor = ComputeInscatteringColor(CameraToReceiver, CameraToReceiverLength);

// 最终的系数
half ExpFogFactor = max(saturate(exp2(-ExponentialHeightLineIntegral)), MinFogOpacity);

half3 FogColor = (InscatteringColor) * (1 - ExpFogFactor); // 雾色 * 衰减 + 直接光散射

return half4(FogColor, ExpFogFactor);
}


采样体积纹理,高度雾和体积雾的混合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
float4 CombineVolumetricFog(float4 GlobalFog, float3 VolumeUV, uint EyeIndex, float SceneDepth)
{
float4 VolumetricFogLookup = float4(0, 0, 0, 1);

float VolFogStartDistance = 0.0f;
if (FogStruct.ApplyVolumetricFog > 0)
{
VolFogStartDistance = FogStruct.VolumetricFogStartDistance; // 体积雾开始距离
VolumetricFogLookup = Texture3DSampleLevel(FogStruct.IntegratedLightScattering, View.SharedBilinearClampedSampler, VolumeUV, 0); // uvw 采样体积纹理 IntegratedLightScattering
VolumetricFogLookup.rgb *= View.OneOverPreExposure; // 消除预曝光,后续会重新乘全局曝光
}
VolumetricFogLookup = lerp(float4(0, 0, 0, 1), VolumetricFogLookup, saturate((SceneDepth - VolFogStartDistance) * 100000000.0f));

// 混合体积雾和解析雾
return float4(VolumetricFogLookup.rgb + GlobalFog.rgb * VolumetricFogLookup.a, VolumetricFogLookup.a * GlobalFog.a);
}

常用控制台参数

r.VolumetricFog 1 是否启用体积雾

r.VolumetricFog.GridPixelSize 16 tile 的大小,默认 16px * 16px

r.VolumetricFog.GridSizeZ 64 3D纹理的Z轴上切片的数量

r.VolumetricFog.DepthDistributionScale 32 缩放切片的深度分布

r.VolumetricFog.TemporalReprojection 1 是否使用时间重投影

r.VolumetricFog.Jitter 1 时域超采样

r.VolumetricFog.HistoryWeight .9 每帧应对 history 值进行多少加权

r.VolumetricFog.Emissive 1 自发光组件

r.VolumetricFog.LightFunction 1 渲染体积雾时是否生成光照函数进行采样

r.VolumetricFog.InjectRaytracedLights 0 是否将具有光线追踪阴影的灯光注入体积雾

参考

[1] 寒霜引擎 Siggraph 2015

[2] Wronski关于大气散射的演讲 Siggraph 2014

[3] froxelization、ZSlice映射, Physically Based Rendering

[4] Volumetric Fog Rendering with Froxels

[5] Beer–Lambert law - Wikipedia

[6] Ubpa/ExponentialHeightFog (github.com)

[7] 虚幻引擎中的体积雾 | 虚幻引擎 5.4 文档 | Epic Developer Community