头图

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


主要是想用Emissive Decal(发光贴花)来模拟出SpotLight的Light Function效果。

原因是SpotLight的Light Function依赖于阴影,而SpotLight开阴影比较费,且UE4移动端似乎不支持Light Function:
[mobile] - Light function for caustics effect is not rendered on mobile (directional stationary light)

下面是SpotLight和贴花(Emissive模式)的效果差异(左:SpotLight,右:贴花):

可见右面贴花明显发闷,我们希望贴花能出接近SpotLight的效果。

一、PC端

在PC端,这个Emissive贴花是通过两个DrawCall来绘制的,第一个DrawCall的Write Mask是____,不写SceneColor。只操作Stencil:将zpass条件设置为Less Equal(由于Depth是近白远黑,所以是墙内部分),对zpass部分以0x01(0000 0001)为Mask进行翻转(即将最后一位翻转),即将原本的0x80(1000 0000)变为0x81(1000 0001):

第二个Pass的Write Mask是RGB__,写SceneColor,且Blend Function是(Src_Alpha,One)(Add模式)。Stencil操作是将zpass条件设置为Greater Equal(由于Depth是近白远黑,所以是墙外部分),以0x01为Mask读取模板值(即读最后一位),与0x00作比较,相等则进行绘制,无论模板测试是否通过,都将模板值以0x01为Mask进行清零(即最后一位清零):

一般来说Alpha Blend会使效果变闷,但现在它已经是Add模式了,却还闷,为啥呢?因为没有最闷,只有更闷。Add模式确实已经比Alpha Blend好了,但跟光源比还是差了些东西。

光源打在地板上产生的颜色=SceneColor+光源颜色*地板固有色*NdotL*衰减

贴花印在地板上的颜色=SceneColor+贴花颜色*遮罩

对比两个公式,假设我们用贴花颜色模拟光源颜色,用遮罩模拟衰减,并且无视NdotL(以后也可以考虑),则两者还差一个地板固有色,正是因为贴花在Add到SceneColor之前没有乘地板固有色,所以显得闷。

于是解法就清晰了,我们只需修改Emissive贴花的Shader,使其在输出前乘以GBufferC.rgb。

于是问题就是GBufferC在Decal Pass能否访问,一是看GBufferC是否传进了Decal Pass,二是要确保GBufferC没有作为Decal Pass的渲染目标(因为同一个Pass里不能对GBufferC既读又写)。

从截帧可以看到,第二个DrawCall的输入输出均无GBufferC,说明GBufferC没有被设为渲染目标。但GBufferC是否传入了Decal Pass还不能确定,也可能是传进来了,但是没采。

为了看GBufferC是否传入了Decal Pass,由帧可见Emissive Decal所在Pass位于BasePass和Lights之间,名为DeferredDecals DBS_BeforeLighting:

去源码中搜相关的EVENT,找到PostProcessDeferredDecals.cpp的AddDeferredDecalPass函数里,且断点也能找到:

在此函数中找到PassParameters处,看GetDeferredDecalPassParameters函数:

在其中下断点:

可见GBufferC非空,已经赋给PassParameters了,所以GBufferC是传进Pass了,只需在Shader中对其采样即可。其在Shader中的名字,可以从断点数据中看到:

或者从截帧里看:

另外从截帧中也可以看到Shader文件名和函数名:

即PixelShaderOutputCommon.ush的MainPS函数中又调用DeferredDecal.usf的FPixelShaderInOut_MainPS函数。

由于PixelShaderOutputCommon.ush的MainPS有可能是公用的,改它影响范围不太可控,但DeferredDecal.usf的FPixelShaderInOut_MainPS显然只是Decal用,所以改它相对安全。

在其末尾添加如下语句(即采样GBufferC.rgb,乘到输出结果上,Out.MRT[0]显然就是对应的SceneColor):

float4 gbufferC=Texture2DSampleLevel(SceneTexturesStruct.GBufferCTexture, SceneTexturesStruct.PointClampSampler, ScreenUV, 0);
Out.MRT[0]=float4(Out.MRT[0].rgb*gbufferC.rgb,Out.MRT[0].a)

修改后效果如下:

可见,两边效果接近了。

然后把新增语句用宏包起来,让它只对Emissive贴花起作用,且可以在材质球中开关:

#if DECAL_BLEND_MODE== DECALBLENDMODEID_EMISSIVE && MATERIAL_MY_SAMPLEBASECOLOR
    #if SHADING_PATH_MOBILE

    #else//pc
        float4 gbufferC=Texture2DSampleLevel(SceneTexturesStruct.GBufferCTexture,SceneTexturesStruct.PointClampSampler,ScreenUV,0);
        Out.MRT[0]=float4(Out.MRT[0].rgb*gbufferC.rgb,Out.MRT[0].a);
    #endif
#endif

<font color="SteelBlue">二、移动端</font>

移动端贴花Pass名为DeferredDecals,也是在BasePass和Lights之间,而且没有像PC端那样搞单独的Stencil Pass:

其输入输出分别为:

显然,这是一种一刀切的做法,即虽然Emissive Decal只需要渲染到SceneColor,而无需渲染到GBuffer,但由于其它类型的贴花可能渲染到GBuffer的某张图上,所以索性把全部GBuffer都绑为渲染目标了。

对于Emissive类型的Decal,我们想改为采样GBufferC,所以需要将GBufferC从渲染目标中去除,又因为其它GBuffer渲染目标对Emissive Decal也没用,所以可一并去除。

代码中搜DeferredDecals相关EVENT,定位到MobileDecalRendering.cpp的RenderDeferredDecalsMobile函数,并通过断点确认:

接下来的问题就是要找到这个Pass绑定RenderTarget的代码。

沿堆栈向上层找,可以找到绑定RenderTarget处。移动端代码跟PC端有点儿差异,它不是在Pass中去指定RenderTarget,而是在更上层提前指定好存到xxxPassInfo结构体里,再通过BeginRenderPass(xxxPassInfo)传进去。可以看到DecalPass是复用的BasePassInfo,而BasePassInfo是在RenderDeferred函数开头创建的,绑定了SceneColor、GBuffer和SceneDepthAux:

Occlusion之后的Pass(DecalPass、LightingPass、Translucencypass)根据bRequiresMultiPass分成两路,multiPass模式和非multiPass模式,当前走的是multiPass模式,只有multiPass模式可在Pass开始前用BeginRenderPass指定PassInfo。

可以看到DecalPass用的是BasePassInfo,而LightingPass和TranslucencyPass用的是ShadingPassInfo。

可以考虑专门为DecalPass构造一个DecalPassInfo,但更简单的方法是在BasePassInfo传入DecalPass之前将BasePassInfo.ColorRenderTargets[0](sceneColor)之外元素都置空 :

//yang chao begin                        
for(int32 Index=0;Index< UE_ARRAY_COUNT(ColorTargets);++Index)
{
    if(Index>0){
        BasePassInfo.ColorRenderTargets[Index].RenderTarget=nullptr;
    }
}
//yang chao end
RHICmdList.BeginRenderPass(BasePassInfo, TEXT("AfterBasePass"));

if(ViewFamily.EngineShowFlags.Decals)
{
    CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
    RenderDecals(RHICmdList,EMyDecalGroup::Emissive);
}

RHICmdList.EndRenderPass();

此时截帧的输入输出变为:

然后再看GBufferC是否传进了Pass,由RenderDecals一路向里找->RenderDeferredDecalsMobile->CreateMobileSceneTextureUniformBuffer,在其中断点,可以看到GBuffer是传进来了的:

所以,只需在Shader中采样即可,将之前DeferredDecal.usf的代码改为:

#if DECAL_BLEND_MODE == DECALBLENDMODEID_EMISSIVE && MATERIAL_MY_SAMPLEBASECOLOR
    #if SHADING_PATH_MOBILE
        #if MOBILE_DEFERRED_SHADING
            float4 gbufferC=Texture2DSampleLevel(MobileSceneTextures.GBufferCTexture,MobileSceneTextures.GBufferCTextureSampler,ScreenUV,0);
            Out.MRT[0]=float4(Out.MRT[0].rgb*gbufferC.rgb,Out.MRT[0].a);
        #endif
    #else//pc
        float4 gbufferC=Texture2DSampleLevel(SceneTexturesStruct.GBufferCTexture,SceneTexturesStruct.PointClampSampler,ScreenUV,0);
        Out.MRT[0]=float4(Out.MRT[0].rgb*gbufferC.rgb,Out.MRT[0].a);
    #endif
#endif

改后移动端效果:

但这样改对Emissive贴花没问题,其它混合模式的贴花,比如那些需要写GBuffer的贴花就不对了。为了让其它模式的贴花不受影响,需将Emissive贴花单独分离出一个Pass,即将代码改为:

SceneRendering.h

//yang chao begin 
enum EMyDecalGroup
{
    All,
    Emissive,
    NonEmissive,
};
//yang chao end

MobileShadingRenderer.cpp

if (!bRequiresMultiPass)
{
    ...
}
else
{
    ...
    // SceneColor + GBuffer write, SceneDepth is read only
    {
        ...
        RHICmdList.BeginRenderPass(BasePassInfo, TEXT("AfterBasePass"));
        if(ViewFamily.EngineShowFlags.Decals)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
            RenderDecals(RHICmdList,EMyDecalGroup::NonEmissive);
        }
        RHICmdList.EndRenderPass();
        //yang chao begin
        for(int32 Index=0;Index< UE_ARRAY_COUNT(ColorTargets);++Index)
        {
            if(Index>0){
            BasePassInfo.ColorRenderTargets[Index].RenderTarget=nullptr;
        }
    }
    RHICmdList.BeginRenderPass(BasePassInfo, TEXT("AfterBasePass"));
    if(ViewFamily.EngineShowFlags.Decals)
    {
        CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
        RenderDecals(RHICmdList,EMyDecalGroup::Emissive);
    }
    RHICmdList.EndRenderPass();
    //yang chao end

  }

MobileDecalRendering.cpp

void FMobileSceneRenderer::RenderDecals(FRHICommandListImmediate&RHICmdList,
    EMyDecalGroup myDecalGroup//yang chao
)
{
    ...

    // Deferred decals
    if(Scene->Decals.Num()>0)
    {
        for(int32 ViewIndex=0;ViewIndex<Views.Num();ViewIndex++)
        {
            constFViewInfo&View=Views[ViewIndex];
            RenderDeferredDecalsMobile(RHICmdList,*Scene,View,
                myDecalGroup //yang chao
            );
        }
    }
    ...
}

voidRenderDeferredDecalsMobile(FRHICommandList&RHICmdList,constFScene&Scene,constFViewInfo&View,
EMyDecalGroup myDecalGroup //yang chao
)
{
    ...
    if(SortedDecals.Num())
    {
        SCOPED_DRAW_EVENT(RHICmdList,DeferredDecals);
        INC_DWORD_STAT_BY(STAT_Decals,SortedDecals.Num());
        ...
        for(int32 DecalIndex=0,DecalCount=SortedDecals.Num();DecalIndex<DecalCount;DecalIndex++)
        {
            constFTransientDecalRenderData&DecalData=SortedDecals[DecalIndex];
            //yang chao begin
            if(myDecalGroup==EMyDecalGroup::All
                ||(myDecalGroup ==EMyDecalGroup::Emissive&&DecalData.FinalDecalBlendMode== DBM_Emissive)
                ||(myDecalGroup ==EMyDecalGroup::NonEmissive&&DecalData.FinalDecalBlendMode!= DBM_Emissive)
                )
            //yang chao end
            {
                ...
                RHICmdList.DrawIndexedPrimitive(GetUnitCubeIndexBuffer(),0,0,8,0, UE_ARRAY_COUNT(GCubeIndices)/3,1);
            }
        }
    }
}

这样,即使有多种贴花,也能显示正常(左:SpotLight,中:Emissive贴花,右:Normal贴花):

Normal 贴花

Emissive 贴花

加上Light Function:
PC端:

移动端:(不支持Light Function)

这样就实现了Emissive贴花与Light Function的大体对齐,但忽略了NdotL项,所以仅投到平面上时效果与灯光比较接近,而投到物体上时不会产生明暗,比如投到box上,如下图,box全亮了:

为贴花添加NdotL,Normal可以直接采GBufferA获得,根据上文可知,是可以采到的。于是问题只剩如何获得贴花的投射方向,也就是贴花图标上那个紫色箭头朝向。

浏览贴花相关的Shader代码,可以在DeferredDecal.usf看到提供了几个现成矩阵:

其中SvPositionToDecal是裁剪空间转Decal空间,DecalToWorld和WorldToDecal是世界空间与Decal空间互转。截帧可以看到其具体值:

经试验,那个紫色箭头就是Decal空间的-x轴,所以其世界朝向就是normalize(mul( float4(-1,0,0,0),DecalToWorld).xyz),通过显示为颜色可以确认:

注意:要看到准确的颜色,最好将各种光源、大气雾、后处理都关掉,并且把skyLight上的CubeMap clear掉,排除干扰。

于是代码改为:

//yang chao begin 
#if DECAL_BLEND_MODE == DECALBLENDMODEID_EMISSIVE && MATERIAL_MY_SAMPLEBASECOLOR
    #if SHADING_PATH_MOBILE
        #if MOBILE_DEFERRED_SHADING
            float4 gbufferA=Texture2DSampleLevel(MobileSceneTextures.GBufferATexture,MobileSceneTextures.GBufferATextureSampler,ScreenUV,0);
            float3 worldNormal=DecodeNormal( gbufferA.xyz );
            float3 dir=normalize(-DecalToWorld[0].xyz);//normalize(mul( float4(-1,0,0,0),DecalToWorld).xyz);
            float ndl=dot(worldNormal,dir);
            float4 gbufferC=Texture2DSampleLevel(MobileSceneTextures.GBufferCTexture,MobileSceneTextures.GBufferCTextureSampler,ScreenUV,0);
            Out.MRT[0]=float4(Out.MRT[0].rgb*gbufferC.rgb*max(0,ndl*0.55+0.45),Out.MRT[0].a);
        #endif
    #else//pc
        float4 gbufferA=Texture2DSampleLevel(SceneTexturesStruct.GBufferATexture,SceneTexturesStruct.PointClampSampler,ScreenUV,0);
        float3 worldNormal=DecodeNormal( gbufferA.xyz );
        float3 dir=normalize(-DecalToWorld[0].xyz);//normalize(mul( float4(-1,0,0,0),DecalToWorld).xyz);
        float ndl=dot(worldNormal,dir);
        float4 gbufferC=Texture2DSampleLevel(SceneTexturesStruct.GBufferCTexture,SceneTexturesStruct.PointClampSampler,ScreenUV,0);
        Out.MRT[0]=float4(Out.MRT[0].rgb*gbufferC.rgb*max(0,ndl*0.55+0.45),Out.MRT[0].a);
    #endif
#endif
//yang chao end

其中用normalize(-DecalToWorld[0].xyz)代替normalize(mul( float4(-1,0,0,0),DecalToWorld).xyz),稍微优化一点儿。

注意:UE4 Shader里矩阵是行主序,第1,2,3,4行分别为x轴,y轴,z轴和位移。也是因为行主序,向量与矩阵相乘时矩阵放右边,即mul(v, m)。

效果:


这是侑虎科技第1681篇文章,感谢作者杨超wantnon供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/wantnon

再次感谢杨超wantnon的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)


侑虎科技
65 声望21 粉丝

UWA官网:[链接]