UE4.26 Emissive Decal(发光贴花)模拟Light Function

news/2024/10/10 12:40:33

【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//pcfloat4 gbufferC=Texture2DSampleLevel(SceneTexturesStruct.GBufferCTexture,SceneTexturesStruct.PointClampSampler,ScreenUV,0);Out.MRT[0]=float4(Out.MRT[0].rgb*gbufferC.rgb,Out.MRT[0].a);#endif
#endif

   

 

二、移动端

移动端贴花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_SHADINGfloat4 gbufferC=Texture2DSampleLevel(MobileSceneTextures.GBufferCTexture,MobileSceneTextures.GBufferCTextureSampler,ScreenUV,0);Out.MRT[0]=float4(Out.MRT[0].rgb*gbufferC.rgb,Out.MRT[0].a);#endif#else//pcfloat4 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 beginfor(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 decalsif(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 beginif(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_SHADINGfloat4 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//pcfloat4 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://www.ryyt.cn/news/69755.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

APP上架大陆应用商店记录

重点自己的手机APP想要上架到国内的应用市场必须要进行ICP备案 部分平台需要电子软著信息 大部分应用商店上架需要准备好合规的隐私政策电子软著 其中包含【APP电子版权认证证书】、【计算机软件著作权登记证书】两个证书 最便捷的方式是找专业人士带跑流程,根据时长分为不同的…

搭建Redis哨兵集群并使用RedisTemplate实现读写分离

一、理论相关通过上篇博客:搭建Redis“主-从-从”模式集群并使用 RedisTemplate 实现读写分离,我们已经搭建好了Redis“主-从-从”模式集群并且实现读写分离,这里会出现几个问题:如果主库宕机了,我们就需要运行一个新主库,比如说把一个从库切换为主库,把它当成主库。这就…

唯一客服浏览器插件: 视频号直播自动回复与循环发送话术-自动化插件

唯一客服浏览器插件 gofly.v1kf.com 我们在做视频号直播的时候,会有这种自动回复咨询问题的功能唯一客服浏览器插件现在就支持,在视频号直播后台,自动化回复用户问题,以及循环发送我们的介绍话术 十年开发经验程序员,离职全心创业中,历时三年开发出的产品《唯一客服系统》…

[编程笔记] 当前上下文中不存在名称ViewBag

当前上下文中不存在名称"ViewBag",很多ViewBag、@Html.Partial、@Html.FunctionBar() 等这些地方都报波浪线了,提示不存在这个名称,但是代码是可以运行的最近在弄另外一个项目,很长一段时间没接触MVC了,Visual Studio 2022识别cshtml文件的时候,出了一点故障!…

MSSQL-从字符串转换日期和/或时间时,转换失败

1、报错的sql为: select ID,Test_time as 时间, from ProcessData where convert(datetime,test_time,120) between convert(datetime, 2020-10-10, 120) and convert(datetime, 2024-10-11, 120) 它是将Test_time转化为datetime格式,再用between进行比较; 2、报错原因:…

Pytorch常用代码段汇总

Pytorch常用代码段来源: https://zhuanlan.zhihu.com/p/104019160 PyTorch最好的资料是官方文档。本文是PyTorch常用代码段,在参考资料[1](张皓:PyTorch Cookbook)的基础上做了一些修补,方便使用时查阅。 1. 基本配置 导入包和版本查询 import torch import torch.nn as nn…

manim边学边做--无向图

无向图属于数学中的图论这一学科, 所谓无向图G,就是由顶点集V(非空集合)和边集E(由V中元素构成的无序二元组的集合)组成的图, 可表示为G=(V,E)。 在无向图中,边没有方向,即从顶点A到顶点B的边与从顶点B到顶点A的边是相同的。 无向图简洁直观,常用于描述社交网络,交通…

[持续更新]程序员每天会阅读哪些技术网站(带链接)来提升自己?

本文原文来自[持续更新]程序员每天会阅读哪些技术网站(带链接)来提升自己? 国内的网站 这些国内技术网站和社区涵盖了编程语言、算法、职业规划、云计算、AI等多方面的内容,可以获取最新的技术资讯、学习资源和开发经验。当然目前国内的技术社区的内容还是相当的鱼龙混杂。CS…