Subsurface Scattering in Raster Pipeline of UE4

寻找着色模型入口

我们首先找到UE4的Subsurface Scattering是在哪里实现的,打开RenderDoc,发现是在StandardDeferredLighting这一步和延迟渲染的光照一同实现的。

image-20201109180721972

于是我们在Source/Runtime/Renderer路径下搜索StandardDeferredLighting,定位到LightRendering.cpp

在文件中查找IMPLEMENT_GLOBAL_SHADER,找到所用到的shader类型为FDeferredLightPS,shader文件为/Engine/Private/DeferredLightPixelShaders.usf,入口函数为DeferredLightPixelMain

IMPLEMENT_GLOBAL_SHADER(FDeferredLightPS, "/Engine/Private/DeferredLightPixelShaders.usf", "DeferredLightPixelMain", SF_Pixel);

打开DeferredLightPixelShaders.usf中的DeferredLightPixelMain函数,发现输出为名为OutColor的变量,顺着OutColor逐个排查,找到GetDynamicLighting函数,跳到DeferredLightingCommon.ush文件,进一步找到GetDynamicLightingSplit函数,计算光照的函数应该是IntegrateBxDF

Lighting = IntegrateBxDF( GBuffer, N, V, Capsule, Shadow, LightData.bInverseSquared );

随着IntegrateBxDF跳入文件ShadingModels.ush,发现如下代码:

FDirectLighting IntegrateBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
switch( GBuffer.ShadingModelID )
{
case SHADINGMODELID_DEFAULT_LIT:
case SHADINGMODELID_SINGLELAYERWATER:
case SHADINGMODELID_THIN_TRANSLUCENT:
return DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_SUBSURFACE:
return SubsurfaceBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_PREINTEGRATED_SKIN:
return PreintegratedSkinBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_CLEAR_COAT:
return ClearCoatBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_SUBSURFACE_PROFILE:
return SubsurfaceProfileBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_TWOSIDED_FOLIAGE:
return TwoSidedBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_HAIR:
return HairBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_CLOTH:
return ClothBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_EYE:
return EyeBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
default:
return (FDirectLighting)0;
}
}

终于找到了所有着色模型的分叉点,这样以后研究着色模型就可以从这里作为入口了。


深入SubsurfaceBxDF

FDirectLighting SubsurfaceBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
FDirectLighting Lighting = DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );

float3 SubsurfaceColor = ExtractSubsurfaceColor(GBuffer);
float Opacity = GBuffer.CustomData.a;

float3 H = normalize(V + L);

// to get an effect when you see through the material
// hard coded pow constant
float InScatter = pow(saturate(dot(L, -V)), 12) * lerp(3, .1f, Opacity);
// wrap around lighting, /(PI*2) to be energy consistent (hack do get some view dependnt and light dependent effect)
// Opacity of 0 gives no normal dependent lighting, Opacity of 1 gives strong normal contribution
float NormalContribution = saturate(dot(N, H) * Opacity + 1 - Opacity);
float BackScatter = GBuffer.GBufferAO * NormalContribution / (PI * 2);

// lerp to never exceed 1 (energy conserving)
Lighting.Transmission = AreaLight.FalloffColor * ( Falloff * lerp(BackScatter, 1, InScatter) ) * SubsurfaceColor;

return Lighting;
}

可以看出,SubsurfaceBxDF中首先调用了一遍默认的延迟光照DefaultLitBxDF,然后从GBuffer中提取出SubsurfaceColorOpacity进行进一步的次表面散射计算。

我们可以将lerp(BackScatter, 1, InScatter)这一项展开,在极坐标中画出其Transmission分布图像,其中$α$为入射角弧度,$o$为不透明度Opacity,下图为入射角约为$\frac\pi4$时的Transmission分布:

1  < 2π  0.77  0.5  cos  α  2π  12  —cos θ> 0}  5Π/6  1.5  7TT/6  0.5  0.5

可以看出,其能量大部分分布在透射方向,小部分分布在明暗分界线方向,且不透明度$o$越小,透射能量越大。


ShadowTerm&LightAttenuation

计算完BxDF之后,在光照累加时还用到了Shadow,这也是影响SSS的一个关键。

LightAccumulator_AddSplit( LightAccumulator, Lighting.Diffuse, Lighting.Specular, Lighting.Diffuse, LightColor * LightMask * Shadow.SurfaceShadow, bNeedsSeparateSubsurfaceLightAccumulation );
LightAccumulator_AddSplit( LightAccumulator, Lighting.Transmission, 0.0f, Lighting.Transmission, LightColor * LightMask * Shadow.TransmissionShadow, bNeedsSeparateSubsurfaceLightAccumulation );

我们看一下Shadow的定义:

struct FShadowTerms
{
float SurfaceShadow;
float TransmissionShadow;
float TransmissionThickness;
FHairTransmittanceData HairTransmittance;
};

DeferredLightingCommon.ush下的GetShadowTerms中可以找到shadow来源于LightAttenuation

// Remapping the light attenuation buffer (see ShadowRendering.cpp)
// Also fix up the fade between dynamic and static shadows
// to work with plane splits rather than spheres.

float DynamicShadowFraction = DistanceFromCameraFade(GBuffer.Depth, LightData, WorldPosition, View.WorldCameraOrigin);
// For a directional light, fade between static shadowing and the whole scene dynamic shadowing based on distance + per object shadows
Shadow.SurfaceShadow = lerp(LightAttenuation.x, StaticShadowing, DynamicShadowFraction);
// Fade between SSS dynamic shadowing and static shadowing based on distance
Shadow.TransmissionShadow = min(lerp(LightAttenuation.y, StaticShadowing, DynamicShadowFraction), LightAttenuation.w);

Shadow.SurfaceShadow *= LightAttenuation.z;
Shadow.TransmissionShadow *= LightAttenuation.z;

// Need this min or backscattering will leak when in shadow which cast by non perobject shadow(Only for directional light)
Shadow.TransmissionThickness = min(LightAttenuation.y, LightAttenuation.w);

LightAttenuation是从ScreenShadowMaskTexture中采样得到,我们可以用RenderDoc抓取ScreenShadowMaskTexture查看。

当前场景的ScreenShadowMaskTexture

image-20201113151304160

当前场景的ScreenShadowMaskTextureBA通道均为1

image-20201113145649320

R通道记录了阴影信息:

image-20201113152138594

G通道记录了次表面阴影信息:

image-20201113145752848

RG通道叠加,可以看到球上隐约绿色的部分即为次表面散射照亮区域:

image-20201113154837234

换一个场景更加明显,其中立方体使用的是另一套次表面散射着色模型SubsurfaceProfile

image-20201209155543621

image-20201209155523608


ScreenShadowMaskTexture写入

从RenderDoc中可以找到ScreenShadowMaskTexture写入的地方

image-20201113160559787

ShadowRendering.cppFSceneRenderer::RenderShadowProjections()中:

// Normal deferred shadows render to the shadow mask
FRHIRenderPassInfo RPInfo(ScreenShadowMaskTexture->GetRenderTargetItem().TargetableTexture, ERenderTargetActions::Load_Store);
...

TransitionRenderPassTargets(RHICmdList, RPInfo);
RHICmdList.BeginRenderPass(RPInfo, TEXT("RenderShadowProjection"));
RenderShadowMask(nullptr);
RHICmdList.SetScissorRect(false, 0, 0, 0, 0);
RHICmdList.EndRenderPass();

该函数在LightRendering.cpp中的FDeferredShadingSceneRenderer::RenderLights函数中被每个光源调用一次:

ShadowRendering.cpp的注释中还可以找到不同通道的含义:

// Light Attenuation channel assignment:
// R: WholeSceneShadows, non SSS
// G: WholeSceneShadows, SSS
// B: non WholeSceneShadows, non SSS
// A: non WholeSceneShadows, SSS
//
// SSS: SubsurfaceScattering materials
// non SSS: shadow for opaque materials
// WholeSceneShadows: directional light CSM
// non WholeSceneShadows: spotlight, per object shadows, translucency lighting, omni-directional lights

继续沿着RenderShadowMask()找到FProjectedShadowInfo::RenderProjection(),其中调用了函数BindShadowProjectionShaders(),从名称可以看出在这里绑定了shader,进一步找到绑定的shader的类为TShadowProjectionPS,在下面的宏定义中可以找到所用的shader文件为ShadowProjectionPixelShader.usf

#define IMPLEMENT_SHADOW_PROJECTION_PIXEL_SHADER(Quality,UseFadePlane,UseTransmission, SupportSubPixel) \
typedef TShadowProjectionPS<Quality, UseFadePlane, false, UseTransmission, SupportSubPixel> FShadowProjectionPS##Quality##UseFadePlane##UseTransmission##SupportSubPixel; \
IMPLEMENT_SHADER_TYPE(template<>,FShadowProjectionPS##Quality##UseFadePlane##UseTransmission##SupportSubPixel,TEXT("/Engine/Private/ShadowProjectionPixelShader.usf"),TEXT("Main"),SF_Pixel);

进入shader文件,找到OutColor的来源:

float FadedSSSShadow = lerp(1.0f, Square(SSSTransmission), ShadowFadeFraction * PerObjectDistanceFadeFraction);
OutColor = EncodeLightAttenuation(half4(FadedShadow, FadedSSSShadow, FadedShadow, FadedSSSShadow));

接下来找到SHADINGMODELID_SUBSURFACE分支下次表面分量的来源为ManualPCF采样函数:

SSSTransmission = ManualPCF(ShadowPosition.xy, Settings);

采样的数据来源于CalculateOcclusion函数计算出的Occlusion,其中计算方式为:

float4 Thickness = max(Settings.SceneDepth - ShadowmapDepth, 0);
float4 Occlusion = saturate(FastExp(-Thickness * Settings.DensityMulConstant));

return ShadowmapDepth > .99f ? 1 : Occlusion;

其中Settings.DensityMulConstant的计算方式为:

float Opacity = GBufferData.CustomData.a;
// Derive density from a heuristic using opacity, tweaked for useful falloff ranges and to give a linear depth falloff with opacity
float Density = -.05f * log(1 - min(Opacity, .999f));
...
Settings.DensityMulConstant = Density * ProjectionDepthBiasParameters.w;

于是可以画出Occlusion项的函数图像:

SSSTransmission

可以看出当Opacity从1下降为0,Density随着从无穷大下降至0;而随着Thickness * Density的增大,Occlusion从1下降至趋近于0。当Occlusion为1时表示没有遮蔽,为0时完全在阴影中,这与常识相符。

以上就是光栅管线中SSS相关的部分,接下来会有一篇解决RayTracing管线SSS错误问题的相关文档。