主要是想用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)
https://forums.unrealengine.com/t/mobile-light-function-for-caustics-effect-is-not-rendered-on-mobile-directional-stationary-light/325856
下面是SpotLight和贴花(Emissive模式)的效果差异(左: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):
光源打在地板上产生的颜色=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还不能确定,也可能是传进来了,但是没采。
由于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
二、移动端
其输入输出分别为:
对于Emissive类型的Decal,我们想改为采样GBufferC,所以需要将GBufferC从渲染目标中去除,又因为其它GBuffer渲染目标对Emissive Decal也没用,所以可一并去除。
代码中搜DeferredDecals相关EVENT,定位到MobileDecalRendering.cpp的RenderDeferredDecalsMobile函数,并通过断点确认:
沿堆栈向上层找,可以找到绑定RenderTarget处。移动端代码跟PC端有点儿差异,它不是在Pass中去指定RenderTarget,而是在更上层提前指定好存到xxxPassInfo结构体里,再通过BeginRenderPass(xxxPassInfo)传进去。可以看到DecalPass是复用的BasePassInfo,而BasePassInfo是在RenderDeferred函数开头创建的,绑定了SceneColor、GBuffer和SceneDepthAux:
可以看到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();
此时截帧的输入输出变为:
#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
改后移动端效果:
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贴花):
PC端:
这样就实现了Emissive贴花与Light Function的大体对齐,但忽略了NdotL项,所以仅投到平面上时效果与灯光比较接近,而投到物体上时不会产生明暗,比如投到box上,如下图,box全亮了:
浏览贴花相关的Shader代码,可以在DeferredDecal.usf看到提供了几个现成矩阵:
于是代码改为:
//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)。
效果:
近期精彩回顾