【Heskey带你玩渲染】EGSR2020 UE/寒霜引擎 大气系统底层剖析
UE/寒霜引擎 大气系统底层剖析
首先,老规矩:
未经允许禁止转载(防止某些人乱转,转着转着就到蛮牛之类的地方去了)
B站:Heskey0
唠叨几句
最近挺忙的,早上上网课,白天上班,下班还得写作业。但还是在一周之内参考数十篇论文,把PPT做了出来,然后录了教程,这不给个三连?。
马上就要离开腾讯了,回归正常的校园生活,写作业,考试(眼泪掉下来)。
Pre. 体渲染基础
https://www.bilibili.com/video/BV1EL4y1u7Aq
这里我默认哥哥姐姐萌已经观看了百人计划的体渲染课程(我课讲的很垃,所以如果没心思看视频的可以看看PPT),这篇博客是对引擎篇的一些补充内容,哥哥姐姐萌可以根据需要有选择性地观看本博客。
1.Transmittance LUT
LUT是要实时更新的,所以要可读可写:
UAV:Unordered Access View 在性能方面费用稍高,但支持同时读/写纹理等功能
RWTexture2D:可读可写的Texture2D
RWTexture2D<float3> TransmittanceLutUAV;
[numthreads(THREADGROUP_SIZE, THREADGROUP_SIZE, 1)]
void RenderTransmittanceLutCS(uint3 ThreadId : SV_DispatchThreadID)
{
//return;
float2 PixPos = float2(ThreadId.xy) + 0.5f;
// Compute camera position from LUT coords
float2 UV = (PixPos) * SkyAtmosphere.TransmittanceLutSizeAndInvSize.zw;
float ViewHeight;
float ViewZenithCosAngle;
UvToLutTransmittanceParams(ViewHeight, ViewZenithCosAngle, UV);
// A few extra needed constants
float3 WorldPos = float3(0.0f, 0.0f, ViewHeight);
float3 WorldDir = float3(0.0f, sqrt(1.0f - ViewZenithCosAngle * ViewZenithCosAngle), ViewZenithCosAngle);
SamplingSetup Sampling = (SamplingSetup)0;
{
Sampling.VariableSampleCount = false;
Sampling.SampleCountIni = SkyAtmosphere.TransmittanceSampleCount;
}
const bool Ground = false;
const float DeviceZ = FarDepthValue;
const bool MieRayPhase = false;
const float3 NullLightDirection = float3(0.0f, 0.0f, 1.0f);
const float3 NullLightIlluminance = float3(0.0f, 0.0f, 0.0f);
const float AerialPespectiveViewDistanceScale = 1.0f;
SingleScatteringResult ss = IntegrateSingleScatteredLuminance(
float4(PixPos,0.0f,1.0f), WorldPos, WorldDir,
Ground, Sampling, DeviceZ, MieRayPhase,
NullLightDirection, NullLightDirection, NullLightIlluminance, NullLightIlluminance,
AerialPespectiveViewDistanceScale);
float3 transmittance = exp(-ss.OpticalDepth);
TransmittanceLutUAV[int2(PixPos)] = transmittance;
}
熟悉CUDA和CS的同学,应该对ThreadId这个东西不陌生,然后看到CS这个后缀,就能确定了,这个东西是Compute Shader,UE使用CS来计算LUT。对于不懂Compute Shader的同学,请移步这里:
https://zhuanlan.zhihu.com/p/468861191
当然这节课是脱UE的裤子,不详细介绍CS,所以下面直接进入正题。
1.1. 参数准备
教程里提到,Transmittance LUT 的 (u,v) 对应的是 (altitude, zenith angle),在虚幻的代码里面,是这两个变量:
float ViewHeight;
float ViewZenithCosAngle;
LUT的size可以通过Unreal的Console调整,Pixel Position除以LUT的size得到UV:
//SkyAtmosphere.TransmittanceLutSizeAndInvSize.xy代表LUT的 size
//SkyAtmosphere.TransmittanceLutSizeAndInvSize.zw代表LUT的 1/size
float2 UV = (PixPos) * SkyAtmosphere.TransmittanceLutSizeAndInvSize.zw;
然后把UV扔进下面的函数就能完成对这两个变量的赋值:
UvToLutTransmittanceParams(ViewHeight, ViewZenithCosAngle, UV);
进而求得相机的World Position, World Direction,这里的World Position不是Actor的Position,而是大气坐标系中的Position,大气坐标系的原点是地球的中心:
float3 WorldPos = float3(0.0f, 0.0f, ViewHeight);
float3 WorldDir = float3(0.0f, sqrt(1.0f - ViewZenithCosAngle * ViewZenithCosAngle), ViewZenithCosAngle);
1.2. Core
准备完参数之后,这段代码的核心就是IntegrateSingleScatteredLuminance()
这个函数,通过这个函数计算出 Optical Depth(也就是我在教程中提到的 Optical Thickness,这俩是一个东西),然后用 Optical Depth 计算出 transmittance,然后记录到 UAV 中。
那么,接下来就来拆解IntegrateSingleScatteredLuminance()
这个函数:
这个函数的目的是求出:
L
: radianceOpticalDepth
: Optical ThicknessTransmittance
具体的执行过程为:
-
Ray 分别和 ground, atmosphere 进行求交 性行为,通过此行为得出接下来 Ray Marching 需要March的距离
tMax
-
准备Ray Marching的参数
-
tMax / SampleCount
就得到了March过程中每一步的步长dt
-
Light Direction记为
wi
,World Direction记为wo
(之前提到过,World Direction是相机在大气坐标系中的Direction),它们之间的角度为cosTheta
-
计算mie scattering和rayleigh scattering的Phase function
float MiePhaseValueLight0 = HgPhase(Atmosphere.MiePhaseG, -cosTheta); float RayleighPhaseValueLight0 = RayleighPhase(cosTheta);
-
-
Ray march the atmosphere to integrate optical depth
- march过程中,每前进一步,就计算这一个segment的 Optical Depth。然后把所有segment的 Optical Depth 累加起来
在视频的入门篇里面我提到过Homogeneous medium中Transmmittance的计算方式:
还有Optical Thickness的计算方式:
对应到代码,是这样的:
//计算这一个segment的Optical Depth //extinction coefficient * distance * scale const float3 SampleOpticalDepth = Medium.Extinction * dt * AerialPespectiveViewDistanceScale; //累加 Optical Depth OpticalDepth += SampleOpticalDepth; //顺便算一下Transmittance const float3 SampleTransmittance = exp(-SampleOpticalDepth);
其中AerialPespectiveViewDistanceScale的作用大家应该一看代码就明白了,AerialPespectiveViewDistanceScale是SkyAtmosphereComponent.cpp里面的一个参数
AerialPespectiveViewDistanceScale = 1.0f;
- 计算 radiance 和 Throughput,这里先贴代码,后面再详细介绍。因为到这里,Transmittance LUT的计算过程已经结束了,我们得到了Optical Depth,然后得到Transmittance,并将其存入UAV中
float3 S = ExposedLight0Illuminance * (PlanetShadow0 * TransmittanceToLight0 * PhaseTimesScattering0 + MultiScatteredLuminance0 * Medium.Scattering); float3 Sint = (S - S * SampleTransmittance) / Medium.Extinction; L += Throughput * Sint; Throughput *= SampleTransmittance;
- march过程中,每前进一步,就计算这一个segment的 Optical Depth。然后把所有segment的 Optical Depth 累加起来
2.Sky-View LUT
RWTexture2D<float3> SkyViewLutUAV;
[numthreads(THREADGROUP_SIZE, THREADGROUP_SIZE, 1)]
void RenderSkyViewLutCS(uint3 ThreadId : SV_DispatchThreadID)
{
//return;
float2 PixPos = float2(ThreadId.xy) + 0.5f;
float2 UV = PixPos * SkyAtmosphere.SkyViewLutSizeAndInvSize.zw;
float3 WorldPos = GetCameraPlanetPos();
// For the sky view lut to work, and not be distorted, we need to transform the view and light directions
// into a referential with UP being perpendicular to the ground. And with origin at the planet center.
// This is the local referencial
float3x3 LocalReferencial = GetSkyViewLutReferential(View.SkyViewLutReferential);
// This is the LUT camera height and position in the local referential
float ViewHeight = length(WorldPos);
WorldPos = float3(0.0, 0.0, ViewHeight);
// Get the view direction in this local referential
float3 WorldDir;
UvToSkyViewLutParams(WorldDir, ViewHeight, UV);
// And also both light source direction
float3 AtmosphereLightDirection0 = View.AtmosphereLightDirection[0].xyz;
AtmosphereLightDirection0 = mul(LocalReferencial, AtmosphereLightDirection0);
float3 AtmosphereLightDirection1 = View.AtmosphereLightDirection[1].xyz;
AtmosphereLightDirection1 = mul(LocalReferencial, AtmosphereLightDirection1);
// Move to top atmospehre
if (!MoveToTopAtmosphere(WorldPos, WorldDir, Atmosphere.TopRadiusKm))
{
// Ray is not intersecting the atmosphere
SkyViewLutUAV[int2(PixPos)] = 0.0f;
return;
}
SamplingSetup Sampling = (SamplingSetup)0;
{
Sampling.VariableSampleCount = true;
Sampling.MinSampleCount = SkyAtmosphere.FastSkySampleCountMin;
Sampling.MaxSampleCount = SkyAtmosphere.FastSkySampleCountMax;
Sampling.DistanceToSampleCountMaxInv = SkyAtmosphere.FastSkyDistanceToSampleCountMaxInv;
}
const bool Ground = false;
const float DeviceZ = FarDepthValue;
const bool MieRayPhase = true;
const float AerialPespectiveViewDistanceScale = 1.0f;
SingleScatteringResult ss = IntegrateSingleScatteredLuminance(
float4(PixPos, 0.0f, 1.0f), WorldPos, WorldDir,
Ground, Sampling, DeviceZ, MieRayPhase,
AtmosphereLightDirection0, AtmosphereLightDirection1, View.AtmosphereLightColor[0].rgb, View.AtmosphereLightColor[1].rgb,
AerialPespectiveViewDistanceScale);
SkyViewLutUAV[int2(PixPos)] = ss.L;
}
2.1. 参数准备
跟 Transmittance LUT 的参数准备类似
把UV扔进下面的函数就能完成对 WorldDir
, ViewHeight
这两个变量的赋值:
UvToSkyViewLutParams(WorldDir, ViewHeight, UV);
2.2. Core
还是那个函数 IntegrateSingleScatteredLuminance()
,这次是取函数返回结果中的 L
,真是曰了dog了,为啥要把不同LUT的计算写在一个函数里面
那么我们接着分析这个函数:
float3 S = ExposedLight0Illuminance * (PlanetShadow0 * TransmittanceToLight0 * PhaseTimesScattering0 + MultiScatteredLuminance0 * Medium.Scattering);
float3 Sint = (S - S * SampleTransmittance) / Medium.Extinction;
L += Throughput * Sint;
Throughput *= SampleTransmittance;
先分析下第四行,为啥要把 SampleTransmittance
累乘起来?我们先定义空间中距离的度量为
Transmittance的式子是这样的
Transmittance累乘起来就成了这样
所以 Transmittance
的累积需要通过累乘来实现
接着,我们用公式来表示代码:
我猜哥哥姐姐萌看到上面的公式人傻了,其实这是 SIGGRAPH 2015 - Advances in Real-time Rendering course 中提到的:沿着视线方向积分 froxel 的 scattering和extinction,以解出 和 (froxel : frustum voxel 的缩写)
具体的公式是这样的:
它的含义就是:沿着ray marching的对一个step上 (single scattered light
和 transmittance)
的 product integral
为什么要做这个积分:
至于Single Scattering的方程,我在教程里面是提到过的:
在教程里面我也提到过,简化之后我们使用 isotropic phase function 即可。所以,把常量都提出来,积分里面就只剩下了single scattered light 和 transmittane 的乘积。
到这里,IntegrateSingleScatteredLuminance这个函数名字的由来,大家应该就清楚了
到这里,Sky-View LUT的计算过程也就结束了,之后把 L 存入UAV即可
3. Area Perspective LUT
SingleScatteringResult ss = IntegrateSingleScatteredLuminance(
float4(PixPos, 0.0f, 1.0f), RayStartWorldPos, WorldDir,
Ground, Sampling, DeviceZ, MieRayPhase,
View.AtmosphereLightDirection[0].xyz, View.AtmosphereLightDirection[1].xyz, View.AtmosphereLightColor[0].rgb, View.AtmosphereLightColor[1].rgb,
AerialPespectiveViewDistanceScale,
tMaxMax);
const float Transmittance = dot(ss.Transmittance, float3(1.0f / 3.0f, 1.0f / 3.0f, 1.0f / 3.0f));
CameraAerialPerspectiveVolumeUAV[ThreadId] = float4(ss.L, Transmittance);
- 代码里面很明显的一点:
CameraAerialPerspectiveVolumeUAV[ThreadId]
,这里使用的索引为一个向量。 - Area Perspective LUT是两张Volume Texture。计算的具体步骤:先使用之前提到的
IntegrateSingleScatteredLuminance
计算出radiance和transmittance,然后用一个float4
存储了 radiance 和 transmittance。
// +0.5 to always have a distance to integrate over
float Slice = ((float(ThreadId.z) + 0.5f) * SkyAtmosphere.CameraAerialPerspectiveVolumeDepthResolutionInv);
Slice *= Slice; // squared distribution
Slice *= SkyAtmosphere.CameraAerialPerspectiveVolumeDepthResolution;
- 回忆一下教程中的内容,Area Perspective LUT 的生成是类似于 CSM 一样把 frustum 分成很多很多 slices
- 从 Slice 的计算方式可以看出,使用 ThreadId.z 作为Volume Texture的深度。
4. Multiple Scattering LUT
还记得吗?我在教程里面提到过的公式:
其中 是这样计算的,代码和公式都很直观(这里先留个坑,后面会介绍 MultiScatAs1
以及 InScatteredLuminance
的由来)
// For a serie, sum_{n=0}^{n=+inf} = 1 + r + r^2 + r^3 + ... + r^n = 1 / (1.0 - r)
const float3 R = MultiScatAs1;
const float3 SumOfAllMultiScatteringEventsContribution = 1.0f / (1.0f - R);
然后是 的计算
float3 L = InScatteredLuminance * SumOfAllMultiScatteringEventsContribution;
最后把 记录到LUT中
MultiScatteredLuminanceLutUAV[int2(PixPos)] = L * Atmosphere.MultiScatteringFactor;
Core
SingleScatteringResult r0 = IntegrateSingleScatteredLuminance(float4(PixPos, 0.0f, 1.0f), WorldPos, WorldDir, Ground, Sampling, DeviceZ, MieRayPhase,
LightDir, NullLightDirection, OneIlluminance, NullLightIlluminance, AerialPespectiveViewDistanceScale);
SingleScatteringResult r1 = IntegrateSingleScatteredLuminance(float4(PixPos, 0.0f, 1.0f), WorldPos, -WorldDir, Ground, Sampling, DeviceZ, MieRayPhase,
LightDir, NullLightDirection, OneIlluminance, NullLightIlluminance, AerialPespectiveViewDistanceScale);
float3 IntegratedIlluminance = (SphereSolidAngle / 2.0f) * (r0.L + r1.L);
float3 MultiScatAs1 = (1.0f / 2.0f)*(r0.MultiScatAs1 + r1.MultiScatAs1);
float3 InScatteredLuminance = IntegratedIlluminance * IsotropicPhase;
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性