UE贴花光照烘培难点分析

背景

项目里, 经常使用很多贴花去补充一些细节
所以想尝试一下移动端带光照的贴花, 截至UE 4.27版本移动端贴花仅支持平行光光照, 而不支持烘焙

分析

其中涉及的几个比较重要的分支宏, 控制同一套Shader的不同处理流程

宏\同场景下测试平台值PCMobile备注
DECAL_RENDERTARGET_COUNT31控制GBuffer缓存的个数
DECAL_OUTPUT_NORMALfalse
DECAL_RENDERSTAGE230:before base pass,
1:after base pass,
2:before lighting,
3:mobile
DECAL_BLEND_MODEDBM_TranslucentDBM_Translucent
PC平台
管线

PC贴花预灯光

PC贴花灯光

C++部分

计算最后的贴花混合模式等, 当满足诸如移动延迟渲染等条件, 会触发一定强制转换

/** Returns if the GBuffer is used. Only valid for the current platform. */
inline bool IsUsingGBuffers(const FStaticShaderPlatform Platform)
{
    if (IsMobilePlatform(Platform))
    {
        return IsMobileDeferredShadingEnabled(Platform);
    }
    ...
}

static EDecalBlendMode ComputeFinalDecalBlendMode(EShaderPlatform Platform, EDecalBlendMode DecalBlendMode, bool bUseNormal)
{
    // 
    const bool bShouldConvertToDBuffer = !IsUsingGBuffers(Platform) && !IsSimpleForwardShadingEnabled(Platform) && IsUsingDBuffers(Platform);
    ...
}

贴花渲染自发光相比常规延迟贴花多一些参数设置

// DecalRenderingShared.cpp
void FDecalRendering::SetShader(FRHICommandList& RHICmdList, ...)
{
	// When in shader complexity, decals get rendered as emissive even though there might not be emissive decals.
	// FDeferredDecalEmissivePS might not be available depending on the decal blend mode.
	TShaderRef<FDeferredDecalPS> PixelShader = (DecalRenderStage == DRS_Emissive && DebugViewMode == DVSM_None)
		? TShaderRef<FDeferredDecalPS>(MaterialShaderMap->GetShader<FDeferredDecalEmissivePS>())
		: MaterialShaderMap->GetShader<FDeferredDecalPS>();

	TShaderMapRef<FDeferredDecalVS> VertexShader(View.ShaderMap);
    PixelShader->SetParameters(...);
    VertexShader->SetParameters(...);
}
Shader部分
// DeferredDecal.usf
void FPixelShaderInOut_MainPS(inout FPixelShaderIn In, inout FPixelShaderOut Out)
{
    ...
    FGBufferData GBufferData;
    GBufferData.BaseColor = BaseColor;
    GBufferData.Metallic = Metallic;
    GBufferData.Specular = Specular;
    ...

    DecalCommonOutput(In, Out, Color, Opacity, GBufferData);
}
void DecalCommonOutput(inout FPixelShaderIn In, inout FPixelShaderOut Out, ... FGBufferData Data)
{
    // RETURN_COLOR not needed unless writing to SceneColor
    Out.MRT[0] = float4(Color, Opacity);
    ...
    #if DECAL_RENDERTARGET_COUNT > 1
        #if DECAL_RENDERSTAGE == 0
        // some MRT rendering
        #elif DECAL_RENDERSTAGE == 1 || DECAL_RENDERSTAGE == 2 || DECAL_RENDERSTAGE == 3
        {
            // 1 == after base pass (GBuffer) || 2 = before lighting (GBuffer)

            #if DECAL_OUTPUT_NORMAL
                EncodeGBuffer(Data, Out.MRT[1], Out.MRT[2], Out.MRT[3], OutTarget4, OutTarget5, OutTarget6);
            #else
                EncodeGBuffer(Data, OutTarget1, Out.MRT[1], Out.MRT[2], OutTarget4, OutTarget5, OutTarget6);
            #endif
        }
    #endif
    ...
}
// DecalCommon.ush
/** Populates OutGBufferA, B and C */
void EncodeGBuffer(
	FGBufferData GBuffer,
	out float4 OutGBufferA,
	out float4 OutGBufferB,
	out float4 OutGBufferC,
        ...)
{
    ...
    OutGBufferB.r = GBuffer.Metallic;
    OutGBufferB.g = GBuffer.Specular;
    OutGBufferB.b = GBuffer.Roughness;
    OutGBufferB.a = EncodeShadingModelIdAndSelectiveOutputMask(GBuffer.ShadingModelID, GBuffer.SelectiveOutputMask);

    OutGBufferC.rgb = EncodeBaseColor( GBuffer.BaseColor );
#if ALLOW_STATIC_LIGHTING
    // No space for AO. Multiply IndirectIrradiance by AO instead of storing.
    OutGBufferC.a = EncodeIndirectIrradiance(GBuffer.IndirectIrradiance * GBuffer.GBufferAO) + QuantizationBias * (1.0 / 255.0);
#else
    OutGBufferC.a = GBuffer.GBufferAO;
#endif
    ...
}
// DeferredLightPixelShaders.usf
void DeferredLightPixelMain(
    ...
    out float4 OutColor		: SV_Target0
    )
{
    ...
    FScreenSpaceData ScreenSpaceData = GetScreenSpaceData(InputParams.ScreenUV);
    ...
    const float4 Radiance = GetDynamicLighting(..., ScreenSpaceData.GBuffer, ...);
    const float  Attenuation = ComputeLightProfileMultiplier(...);
    OutColor += (Radiance * Attenuation);
    ...
}
// DeferredShadingCommon.ush
// @param UV - UV space in the GBuffer textures (BufferSize resolution)
FScreenSpaceData GetScreenSpaceData(float2 UV, bool bGetNormalizedNormal = true)
{
    FScreenSpaceData Out;
    Out.GBuffer = GetGBufferData(UV, bGetNormalizedNormal);
    ...
    return Out;
}

// @param UV - UV space in the GBuffer textures (BufferSize resolution)
FGBufferData GetGBufferData(float2 UV, bool bGetNormalizedNormal = true)
{
    ...
    float4 GBufferC = Texture2DSampleLevel(SceneTexturesStruct.GBufferCTexture, SceneTexturesStruct_GBufferCTextureSampler, UV, 0);
    ...
    return DecodeGBufferData(GBufferA, GBufferB, GBufferC, ...);
}
// DeferredLightingCommon.usf

/** Calculates lighting for a given position, normal, etc with a fully featured lighting model designed for quality. */
FDeferredLightingSplit GetDynamicLightingSplit(..., FGBufferData GBuffer, ...)
{
    ...
    #if NON_DIRECTIONAL_DIRECT_LIGHTING
        float Lighting;

        Lighting = IntegrateLight(...);

        float3 LightingDiffuse = Diffuse_Lambert( GBuffer.DiffuseColor ) * Lighting;
        LightAccumulator_AddSplit(LightAccumulator, LightingDiffuse, 0.0f, 0, ...);
    #else
        FDirectLighting Lighting;
        Lighting = IntegrateBxDF( GBuffer, ...);
        Lighting.Specular *= LightData.SpecularScale;
				
        LightAccumulator_AddSplit( LightAccumulator, Lighting.Diffuse, Lighting.Specular, Lighting.Diffuse, LightColor * LightMask * Shadow.SurfaceShadow, ...);
        LightAccumulator_AddSplit( LightAccumulator, Lighting.Transmission, 0.0f, Lighting.Transmission, LightColor * LightMask * Shadow.TransmissionShadow, ...);
    #endif
    ...
}
移动平台
管线

移动端地板

移动端贴花

C++相关

准备好Decal Pass所需Shader

// MobileDecalRendering.cpp: Decals for mobile renderer
void RenderDeferredDecalsMobile(FRHICommandList& RHICmdList, ...)
{
    ...
    // Set shader params
    FDecalRendering::SetShader(...);
    ...
}

计算移动平台所需的参数, 诸如贴花混合模式、RenderTarget类型等

// DecalRenderingCommon.h

static ERenderTargetMode ComputeRenderTargetMode(EShaderPlatform Platform, EDecalBlendMode DecalBlendMode, bool bHasNormal)
{
    if (IsMobilePlatform(Platform))
    {
        return IsMobileDeferredShadingEnabled(Platform) ? RTM_SceneColorAndGBufferWithNormal : RTM_SceneColor;
    }
    ...
}

// @return DECAL_RENDERTARGET_COUNT for the shader
static uint32 ComputeRenderTargetCount(EShaderPlatform Platform, ERenderTargetMode RenderTargetMode)
{
    // has to be SceneColor on mobile 
    check(!IsMobilePlatform(Platform) || RenderTargetMode == RTM_SceneColor || IsMobileDeferredShadingEnabled(Platform));
    ...

    return 0;
}

可行性方案探究

移动端贴花的渲染仅以一定透明度的方式, 叠到SceneColor上去

  • 步骤
    • 延迟贴花渲染Pass上, 转换贴花uv到接受贴花的mesh uv空间, 采样烘培纹理混合
  • 问题
    • 烘培纹理难获取
    • uv跨mesh转换难, 尤其针对一贴花跨多mesh
  • 步骤
    • 在BasePass前插入Decal Pass, 单独渲染贴花到纹理T0上, (类似PC上贴花渲染到GBufferC上)
    • 以混合T0及SceneColor纹理, 替代延迟贴花渲染Pass
    • 后续Decal Pass去掉
  • 问题
    • 贴花的着色计算复杂度较高, 后续的着色步骤基本按需移植
  • 能否使用LightProbe方式?

无论哪种方式, 若解决上述问题, 均可保证贴花在移动过程中, 能做到与烘培的投影面混合