前端路上的小学生

【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()这个函数:

这个函数的目的是求出:

  1. L : radiance
  2. OpticalDepth : Optical Thickness
  3. Transmittance

具体的执行过程为:

  1. Ray 分别和 ground, atmosphere 进行求交 性行为,通过此行为得出接下来 Ray Marching 需要March的距离 tMax

  2. 准备Ray Marching的参数

    1. tMax / SampleCount 就得到了March过程中每一步的步长 dt

    2. Light Direction记为 wi,World Direction记为 wo (之前提到过,World Direction是相机在大气坐标系中的Direction),它们之间的角度为 cosTheta

    3. 计算mie scattering和rayleigh scattering的Phase function

    float MiePhaseValueLight0 = HgPhase(Atmosphere.MiePhaseG, -cosTheta);
    float RayleighPhaseValueLight0 = RayleighPhase(cosTheta);
    
  3. Ray march the atmosphere to integrate optical depth

    1. march过程中,每前进一步,就计算这一个segment的 Optical Depth。然后把所有segment的 Optical Depth 累加起来
      在视频的入门篇里面我提到过Homogeneous mediumTransmmittance的计算方式:Tr(xy)=eσt||xy||
      还有Optical Thickness的计算方式:σt||xy||

    对应到代码,是这样的:

    //计算这一个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;
    
    1. 计算 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;
    

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 累乘起来?我们先定义空间中距离的度量为 d

Transmittance的式子是这样的 Tr(xy)=eσt||xy||=eσtdx,y
Transmittance累乘起来就成了这样 T1T2T3=eσt(d1+d2+d3)=eσtd

所以 Transmittance 的累积需要通过累乘来实现

接着,我们用公式来表示代码:

S=LiLsσs

Sint=SS×Tstepσt

L=L+TSint

T=TTstep

我猜哥哥姐姐萌看到上面的公式人傻了,其实这是 SIGGRAPH 2015 - Advances in Real-time Rendering course 中提到的:沿着视线方向积分 froxel 的 scattering和extinction,以解出 LiTr (froxel : frustum voxel 的缩写)

具体的公式是这样的:

0dstepeσtx×Sdx=SS×eσtdstepσt

它的含义就是:沿着ray marching的对一个step上 (single scattered lighttransmittance) 的 product integral

为什么要做这个积分:

至于Single Scattering的方程,我在教程里面是提到过的:

Ls(xω)=Ap(x,ω,ω)Lr(xx)V(x,x)H(xx)dx

Lr(xx)=Tr(x,x)L(xx)

在教程里面我也提到过,简化之后我们使用 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

还记得吗?我在教程里面提到过的公式:

Ψms=L2ndorderFms

其中 Fms 是这样计算的,代码和公式都很直观(这里先留个坑,后面会介绍 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);

然后是 L2ndorder 的计算

float3 L = InScatteredLuminance * SumOfAllMultiScatteringEventsContribution;

最后把 Ψms 记录到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;
posted @   Heskey0  阅读(1563)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性

本站勉强运行 1184 天 10 小时 55 分 50 秒

喜欢请打赏

扫描二维码打赏

支付宝打赏

点击右上角即可分享
微信分享提示