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