UE5.3 对体积雾的实现和原理是基于 寒霜引擎 Siggraph 2015 和 Siggraph 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 函数加权或级联构造
实现方案汇总 目前基本是四套方案
基于深度、高度的解析雾,效果好,但无法支持变化不均匀的雾效
基于 billboard 的2D雾,依赖美术人员,不够动态,广告牌旋转可能有旋转问题
基于后处理和径向模糊,容易模拟类似丁达尔效应的效果,但当看不到光源时,雾效完全消失
基于ray-marching,效果最好,但是开销太过大,不支持前向渲染
基于体素纹理的实现是对 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 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 [numthreads (THREADGROUP_SIZE, THREADGROUP_SIZE, 1 )] 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, .5 f); float StepLength = length (LayerTranslatedWorldPosition - PreviousSliceTranslatedWorldPosition); PreviousSliceTranslatedWorldPosition = LayerTranslatedWorldPosition; float Transmittance = exp (-PreExposedScatteringAndExtinction.w * StepLength); AccumulatedDepth += StepLength; float FadeInLerpValue = saturate (AccumulatedDepth * VolumetricFogNearFadeInDistanceInv); #define ENERGY_CONSERVING_INTEGRATION 1 #if ENERGY_CONSERVING_INTEGRATION float3 ScatteringIntegratedOverSlice = FadeInLerpValue * (PreExposedScatteringAndExtinction.rgb - PreExposedScatteringAndExtinction.rgb * Transmittance) / max (PreExposedScatteringAndExtinction.w, .00001 f); 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
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 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 )); { 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); float4 FogInscatteringAndOpacity = CombineVolumetricFog (HeightFogInscatteringAndOpacity, VolumeUV, 0 , SceneDepth); 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 float CosAngle = dot (normalize (WorldPositionRelativeToCamera), View.ViewForward); float InvCosAngle = (CosAngle > FLT_EPSILON) ? rcp (CosAngle) : 0 ; ExcludeDistance = View.VolumetricFogMaxDistance * InvCosAngle; #endif 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 half4 GetExponentialHeightFog (float3 WorldPositionRelativeToCamera, float ExcludeDistance) { const half MinFogOpacity = FogStruct.ExponentialFogColorParameter.w; const float MaxWorldObserverHeight = FogStruct.ExponentialFogParameters.z; const float3 WorldObserverOrigin = float3 (LWCHackToFloat (PrimaryView.WorldCameraOrigin).xy, min (LWCHackToFloat (PrimaryView.WorldCameraOrigin).z, MaxWorldObserverHeight)); float3 CameraToReceiver = WorldPositionRelativeToCamera; CameraToReceiver.z += LWCHackToFloat (PrimaryView.WorldCameraOrigin).z - WorldObserverOrigin.z; 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; float RayOriginTermsSecond = FogStruct.ExponentialFogParameters2.x; float RayLength = CameraToReceiverLength; float RayDirectionZ = CameraToReceiver.z; ExcludeDistance = max (ExcludeDistance, FogStruct.ExponentialFogParameters.w); 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; float Exponent = max (-127.0f , FogStruct.ExponentialFogParameters.y * (ExclusionIntersectionZ - FogStruct.ExponentialFogParameters3.y)); RayOriginTerms = FogStruct.ExponentialFogParameters3.x * exp2 (-Exponent); float ExponentSecond = max (-127.0f , FogStruct.ExponentialFogParameters2.y * (ExclusionIntersectionZ - FogStruct.ExponentialFogParameters2.w)); RayOriginTermsSecond = FogStruct.ExponentialFogParameters2.z * exp2 (-ExponentSecond); } 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 ); 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