【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)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。