博客 / 詳情

返回

UE4.26 Emissive Decal(發光貼花)模擬Light Function

【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)

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.