首页 > 其他分享 >UE4.26 Emissive Decal(发光贴花)模拟Light Function

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

时间:2024-10-10 12:33:27浏览次数:8  
标签:Function UE4.26 MRT Emissive 贴花 Pass float4 Out

【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

   

 

二、移动端

移动端贴花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)

标签:Function,UE4.26,MRT,Emissive,贴花,Pass,float4,Out
From: https://www.cnblogs.com/uwatech/p/18456072

相关文章

  • std::binary_function 未定义问题
    使用高版本C++编译器编译旧的SDK的时候,SDK代码中会含有一些已经废弃的函数;如std::binary_function修改方式:原始代码:namespace{structNameCompare:std::binary_function<constchar*,constchar*,bool>{booloperator()(constchar*x,constchar*y)c......
  • 深度学习No module named ‘torchvision.transforms.functional_tensor‘问题解决
    问题在进行深度学习训练过程中出现ModuleNotFoundError:Nomodulenamed'torchvision.transforms.functional_tensor'报错,多方查阅资料后得到了解决方案。关于我的环境:CUDA==12.1torch==2.4.1GPU==4090D原先进行深度学习用的CUDA11.3,torch1.2.1,但是在训练时出现nvrtc......
  • [Javascript] Check whether a function is call with new
    The new.target meta-propertyletsyoudetectwhetherafunctionorconstructorwascalledusingthe new operator.Inconstructorsandfunctionsinvokedusingthe new operator, new.target returnsareferencetotheconstructororfunctionthat new wa......
  • 前端模块化进化史:从全局 function 到 ES Modules
    目前,前端开发已经离不开由CommonJS、ESModules和Webpack构建的模块化开发环境。无论是JavaScript、CSS、图片还是其他资源,都可以作为一个模块来处理。那么,模块化究竟是如何发展到今天的呢?全局函数模式最初的前端模块化尝试是通过全局函数来实现的。例如,在一个util.js文......
  • Jupyter - Magic Function Usage
     %%writefiledata_preparation/v0.pydevice='cuda'iftorch.cuda.is_available()else'cpu'#OurdatawasinNumpyarrays,butweneedtotransformthem#intoPyTorch'sTensorsandthenwesendthemtothechosendevicex_tr......
  • 本地环境PHP帝国备份王备份报错mysql_escape_string(): This function is
    在使用帝国备份王进行备份和恢复时,如果遇到PHP5.5环境下的报错,通常是因为一些旧的MySQL函数已经被弃用或移除。具体来说,mysql_escape_string 函数在PHP5.5中已经被废弃,应该使用 mysql_real_escape_string 替代。解决方案定位问题文件:找到 function.php 文件的位置。......
  • C4996 'scanf': This function or variable may be unsafe. Consider using scanf_s i
    错误原因VS平台认为scanf函数不安全,要求换成scanf_s函数解决方案方案一:将scanf换成scanf_s[不建议]将scanf换成scanf_s但是,scanf_s函数只能在vs上使用,其他平台无法使用,故修改后代码无法移植,不建议方案二:#define_CRT_SECURE_NO_WARNINGS在头文件之前增加预处理器指令#defin......
  • OpenAi FunctionCalling 案例详解
    源码详细讲解pdf及教学视频下载链接:点击这里下载FunctionCalling的单一函数调用天气预报查询(今天长沙的天气如何?)1importjson2importrequests3fromopenaiimportOpenAI45client=OpenAI()67location="长沙"89defget_current_weather(c......
  • C++ Data Member Pointers 和 Member Function Pointers
    https://itanium-cxx-abi.github.io/cxx-abi/abi.html#member-pointers2.3MemberPointers2.3.1DataMemberPointersThebasicABIpropertiesofdatamemberpointertypesarethoseofptrdiff_t.Adatamemberpointerisrepresentedasthedatamember's......
  • Learning Continuous Image Representation with Local Implicit Image Function
    LearningContinuousImageRepresentationwithLocalImplicitImageFunction(阅读笔记)11.03局部隐式图像函数(LIIF)表示连续中的图像,可以以任意高分辨率表示。摘要:如何表示图像?当视觉世界以连续的方式呈现时,机器用二维像素数组以离散的方式存储和观看图像。本文中,试图学习......