whoimi

A geek blog

View on GitHub

UE4Shader代码初探

BasePassPixelShader.usf

BasePassPixelShader.usf是理解Unreal Shader最好的切入点。

下面是Unreal官方文档shader开发给出的简单说明:

典型的材质像素着色器类型将先通过调用 GetMaterialPixelParameters 顶点工厂函数来创建 FMaterialPixelParameters 构造。GetMaterialPixelParameters 将特定于顶点工厂的输入转换为任何过程可能想访问的属性,例如 WorldPosition 和 TangentNormal 等等。然后,材质着色器将调用 CalcMaterialParameters,后者将写出 FMaterialPixelParameters 的其余成员,之后 FMaterialPixelParameters 完全初始化。然后,材质着色器将通过 MaterialTemplate.usf 中的函数来访问该材质的某些输入(例如,通过 GetMaterialEmissive 访问材质的自发光输入),执行一些明暗处理,然后输出该过程的最终颜色。

上文中出现的函数和结构体均可以在BasePassPixelShader.usf文件当中找到对应的位置。而提到的MaterialTemplate.usf 文件则可以查看各种函数的实现。

GetMaterialPixelParameters
FMaterialPixelParameters
CalcMaterialParameters
GetMaterialEmissive

首先观察函数FPixelShaderInOut_MainPS

这里主要是在对Gbuffer(FPixelShaderOut)进行填充。

// is called in MainPS() from PixelShaderOutputCommon.usf
void FPixelShaderInOut_MainPS(FVertexFactoryInterpolantsVSToPS Interpolants,
FBasePassInterpolantsVSToPS BasePassInterpolants,
in FPixelShaderIn In, 
inout FPixelShaderOut Out)
{
	//像素着色器的主要计算过程
}

PixelShaderOutputCommon.ush

根据上一段代码的注释可以延展到这个文件:PixelShaderOutputCommon.ush

文件的内容很少,就是一个通用的Shader文件,当中包括了所有Pixel的公用入口:

void MainPS
(...)
{
    
}

里面大量的宏控制了输入的输出的结构体类型

MaterialTemplate.usf

从这个文件名字可以看出这是定义材质模板的头文件。

包括了VS-PS结构体的定义:

struct FMaterialPixelParameters
{
    ...
	/** Interpolated vertex color, in linear color space. */
	half4 VertexColor;

	/** Normalized world space normal. */
	half3 WorldNormal;
	... ...
};

定义了重要的空间转换函数:

float3 GetTranslatedWorldPosition_NoMaterialOffsets(FMaterialPixelParameters Parameters)
float4 GetScreenPosition(FMaterialVertexParameters Parameters)
float4 GetScreenPosition(FMaterialPixelParameters Parameters)
float2 GetSceneTextureUV(FMaterialVertexParameters Parameters)

各种材质函数:

MaterialFloat2 GetLightmapUVs(FMaterialPixelParameters Parameters)
half3 GetMaterialNormal(FMaterialPixelParameters Parameters, FPixelMaterialInputs PixelMaterialInputs)
half3 GetMaterialEmissive(FPixelMaterialInputs PixelMaterialInputs)
half GetMaterialMetallic(FPixelMaterialInputs PixelMaterialInputs)

从函数和变量名可以看到代码很好理解。

TiledDeferredLightShaders.usf

Unreal默认时使用Deferred渲染,上面的三个文件都是Geometry阶段的代码。而这个文件就是LIght阶段的代码。

Unreal使用TiledDeferredLight策略,和Unity HDRP的光照策略接近。

入口函数,可以看到这个和Unity的HDRP一样使用了ComputeShader来计算:

[numthreads(THREADGROUP_SIZEX, THREADGROUP_SIZEY, 1)]
void TiledDeferredLightingMain(
	uint3 GroupId : SV_GroupID,
	uint3 DispatchThreadId : SV_DispatchThreadID,
    uint3 GroupThreadId : SV_GroupThreadID) 
{
    ...
}

除了线程同步之外的操作,关键的就是TIle的灯光列表的获取,通过边界剪裁获取影响每一个tile的灯光列表

	// Compute per-tile lists of affecting lights through bounds culling
	// Each thread now operates on a sample instead of a pixel
	// 计算影响tile的灯光列表,这里的计算以Tile为单位而非pixel
	LOOP
	for (uint LightIndex = ThreadIndex; LightIndex < NumLights && LightIndex < MAX_LIGHTS; LightIndex += THREADGROUP_TOTALSIZE)
	{
       	... 
	}
//GPU线程同步
GroupMemoryBarrierWithGroupSync();

在灯光列表计算完成之后,就是再次遍历这个灯光列表,进行光照计算:

	...	
	BRANCH
	if (InGBufferData.ShadingModelID != SHADINGMODELID_UNLIT)
	{
         // 遍历每一个灯光,读取灯光信息,填充LightData,这里和Unity出奇的相似。
		LOOP
		for (uint TileLightIndex = 0; TileLightIndex < NumLightsAffectingTile; TileLightIndex++) 
		{
			uint LightIndex = TileLightIndices[TileLightIndex];
			FDeferredLightData LightData = (FDeferredLightData)0;
			...
			// Lights requiring light attenuation are not supported tiled for now
			CompositedLighting += GetDynamicLighting(WorldPosition, CameraVector, ScreenSpaceData.GBuffer, ScreenSpaceData.AmbientOcclusion, InGBufferData.ShadingModelID, LightData, float4(1, 1, 1, 1), 0.5, uint2(0,0));
		}

		// 遍历每一个灯光,读取灯光信息,填充LightData,这里和Unity出奇的相似。和上面区别是灯光类型不同光照复杂程度不同
		LOOP
		for (uint TileLightIndex = 0; TileLightIndex < NumSimpleLightsAffectingTile; TileLightIndex++) 
		{
			uint LightIndex = TileSimpleLightIndices[TileLightIndex];

			FSimpleDeferredLightData LightData = (FSimpleDeferredLightData)0;
            	...
			// todo: doesn't support ScreenSpaceSubsurfaceScattering yet (not using alpha)
			CompositedLighting.rgb += GetSimpleDynamicLighting(...);
		}
	}

最终写出光照结果:

	BRANCH
    if (all(DispatchThreadId.xy < ViewDimensions.zw)) 
	{
		// One some hardware we can read and write from the same UAV with a 32 bit format. We don't do that yet.
		RWOutTexture[PixelPos.xy] = InTexture[PixelPos.xy] + CompositedLighting;
    }

DeferredLightingCommon.ush

这个文件延展自上一小节,TiledDeferredLightShaders.usf文件中的光照计算代码GetDynamicLighting和GetSimpleDynamicLighting就是来自这个文件。

GetDynamicLighting函数简要代码如下:

/** Calculates lighting for a given position, normal, etc with a fully featured lighting model designed for quality. */
float4 GetDynamicLighting(float3 WorldPosition, float3 CameraVector, FGBufferData GBuffer, float AmbientOcclusion, uint ShadingModelID, FDeferredLightData LightData, float4 LightAttenuation, float Dither, uint2 Random)
{
	FLightAccumulator LightAccumulator = (FLightAccumulator)0;
	float3 V = ...;
	float3 N = ...;
	float3 L = ...;	// Already normalized
	float3 ToLight = ...;
	float NoL = ...;
	float DistanceAttenuation = 1;
	float LightRadiusMask = 1;
	float SpotFalloff = 1;

	...
	BRANCH
	if (LightRadiusMask > 0 && SpotFalloff > 0)
	{
		float SurfaceShadow = 1;
		float SubsurfaceShadow = 1;
		BRANCH
		if (LightData.ShadowedBits)
			GetShadowTerms(...);
		else
			SurfaceShadow = AmbientOcclusion;
		...
		if( LightData.ShadowedBits < 2 && GBuffer.ShadingModelID == SHADINGMODELID_HAIR )
		{
			SubsurfaceShadow = ShadowRayCast(...);
		}
	
		...
		float3 LobeEnergy = AreaLightSpecular(LightData, LobeRoughness, ToLight, L, V, N);
		float3 SurfaceLighting = SurfaceShading(GBuffer, LobeRoughness, LobeEnergy, L, V, N, Random);
		float3 SubsurfaceLighting = SubsurfaceShading(GBuffer, L, V, N, SubsurfaceShadow, Random);
		...
	}

	return LightAccumulator_GetResult(LightAccumulator);
}

​ 从化简后的代码可以清楚的看出他的代码结构,而AreaLightSpecularSurfaceShadingSubsurfaceShading就是光照计算的关键,我们的讨论到此为止,而接下来的内容涉及到光照函数、阴影算法、光照策略之后再讨论。