基于 MeshPass 的模型外扩型 UE4 描边方案与实现总结

Viewed 6

说明

  • 本文总结、记录和分享一种通过新增 MeshPass 使用模型法线外扩的方法来实现的 UE4 描边方案,也借此说明和记录 UE4 新增 MeshPass 的流程,并对相关源码原理稍做解释。
  • 本文所述方案和流程基于 UE4.27.2 ,UE5 思路相似但流程略有差异,请注意识别和适配(部分步骤有提及差异)。
  • 本文涉及对 UE4 渲染管线的修改,如需了解管线的原理机制,也可参考阅读 Unreal 渲染管线原理机制源码剖析

效果展示

如视频和截图所示,支持对场景中的物体进行差异化描边,即可对任意物体进行任意的尺寸、颜色、是否描边等设置。描边物体之间的层级正确,即根据前后正常遮挡。


模型外扩描边https://www.zhihu.com/video/1676620804396343296

模型外扩描边
原理概述

网格体绘制管道


绘制之旅(图源文末)

  • 网格体绘制管道 基于保留模式的概念,其中所有场景绘制都是预先准备好的,而不是每帧都构建它们。它还具有积极缓存(caching)和绘制调用(draw call)合并功能,以便利用静态网格体的属性,这些属性很少变化,可以跨帧重用。
  • 网格体渲染从"FPrimitiveSceneProxy"开始,这是游戏线程的"UPrimitiveComponent"渲染线程表示。"FPrimitiveSceneProxy"负责通过对"GetDynamicMeshElements"和"DrawStaticElements"的回调将FMeshBatch提交给渲染器。
  • "FMeshBatch"将"FPrimitiveSceneProxy"实现(用户代码)与网格体通道(私有渲染器模块)解耦。它包含了通道确定最终着色器绑定和渲染状态所需的所有内容,因此代理永远不知道将在哪些通道中渲染。
  • 下一步是将"FMeshBatch"转换为一个特定于网格体通道的"FMeshDrawCommand"。"FMeshDrawCommand"是"FMeshBatch"和RHI之间的接口。它是一个完全无状态的绘制描述,存储了RHI需要知道的,关于网格体绘制的所有信息:
  • 使用哪些着色器
  • 他们的资源绑定
  • 绘制调用参数
  • 这允许在RHI级别之上缓存和合并绘制调用。"FMeshDrawCommand"是由一个特定于网格体通道的"FMeshPassProcessor"根据"FMeshBatch"创建的。
  • 最后,"SubmitMeshDrawCommands"用于将"FMeshDrawCommand"转换为RHICommandList上设置的一系列RHI命令。

描边

  • 描边有很多方案,如后效、模型翻转等,本文只介绍基于模型的法线外扩的方案(其他方案参见我的其他文章)。
  • 其原理较为简单,即:在 VERTEX SHADER 里沿法线外扩指定的描边宽度 OutLineSize ,在 PIXEL SHADER 里输出指定的描边颜色 OutLineColor ,把正面剔除,即可。

步骤(基于描边)

MeshPass 新增流程概要

基于 MeshPass 的描边的详细步骤

准备 Shader ( usf )

  • Engine/Shaders/Private/MarsOutline.usf

  • Shader 比较简单,在 VERTEX SHADER 里沿法线外扩指定的描边宽度 OutLineSize ,在 PIXEL SHADER 里输出指定的描边颜色 OutLineColor 。

准备 MeshPassProcessor( Pass 、 Processor )

Pass

  • Engine/Source/Runtime/Renderer/Public/MeshPassProcessor.h

  • 在 EMeshPass 里加上我们自定义的 Pass,此处名叫 MarsOutline (加前缀的目的是统一及避免跟引擎中自带的某些 Outline 变量冲突,虽然 Pass 里没有这个名字的 Pass)
  • UE4 中 EMeshPass 里可以新增的 Pass 数目默认有限制,加上原有的,不得超过 32 个。这个跟数据类型位数等有关(参见枚举后的 static_assert(EMeshPass::Num <= (1 << EMeshPass::NumBits), "EMeshPass::Num will not fit in EMeshPass::NumBits"); 及 FMeshPassMask::SkipEmpty 等),如需支持更多,可以参考“MeshPassを33以上に増える方法について”。
  • 在此处添加一个条目会在"FScene"中分配一个"FParallelMeshDrawCommandPass"。这使得"FScene"能够在"AddToScene"时间缓存通道的网格体绘制命令。"FMeshPassProcessor"必须使用"FRegisterPassProcessorCreateFunction"注册到它们的枚举中(后文有)。通道设置和调度发生在任务中。

Processor

  • Engine/Source/Runtime/Renderer/Private/MarsOutline.h

  • Engine/Source/Runtime/Renderer/Private/MarsOutline.cpp








步骤说明

  • 有些步骤跟渲染平台、管线等有关,本文默认基于 PC SM5 deferred ,实际请以需求为准。如构造函数 FMarsOutlineMeshProcessor 中的 InFeatureLevel ERHIFeatureLevel::SM5、RegisterMarsOutline 中的 InShadingPath EShadingPath::Deferred 等。
  • MeshCullMode 需要按需正确设置,也需要跟 DepthStencil 和 SetDepthStencilState 匹配。此处描边的原则是模型外扩,外扩之后需要剔除中间跟原模型重叠的部分(故为 CM_CCW),否则会导致整个模型也是描边的颜色(如下图所示,为 CM_CW 或 CM_None 的效果)。

原理阐述

FMeshPassProcessor

  • 特定的通道网格体处理器派生自"FMeshPassProcessor"基类,负责将"FMeshBatch"转换为用于给定通道的网格体绘制命令。这是最终的绘制筛选发生的地方,会选择适当的着色器并收集着色器绑定。
  • 为了创建一个自定义网格体通道处理器,它必须派生自"FMeshPassProcessor",且需要覆盖"AddMeshBatch"函数。
  • "AddMeshBatch"实现:
  • 绘制筛选 - 例如,如果一个材质具有半透明的绘制模式,那么不要在"FDepthPassMeshProcessor"中处理它。
  • 选择着色器和管道状态(深度/模具/混合状态)
  • 最后调用"BuildMeshDrawCommands()",它为通道/材质/顶点factory/基元收集着色器绑定,并将新的绘制命令添加到相关列表中。

着色器绑定

  • UE4中的着色器绑定可以是统一缓冲区、采样器、纹理、着色器资源视图或松散参数("FShaderParameter")。
  • "FMeshPassProcessor"不将着色器绑定随随RHICmdList.SetShaderParameter一起直接发送到RHI,它只将它们记录到"FMeshDrawSingleShaderBindings"类中。函数"BuildMeshDrawCommands()"在所有通道之间共享代码,它将在通道着色器上调用"GetShaderBindings()"。
  • 着色器绑定可分为几个类别:
  • 通道常量统一缓冲区,例如"ViewUniformBuffer"或"DepthPassUniformBuffer"
  • 顶点Factory绑定
  • 材质绑定
  • 基元绑定
  • 通道特定的绑定,在绘制之间会发生更改。
  • 注意,每次绘制设置不同的绑定可以防止绘制调用合并。此外,设置松散参数(即不位于统一缓冲区中的着色器参数)可以防止绘制调用合并,从而迫使绘制之间的常量缓冲区更新放慢。
  • 因为每个"FMeshPassProcessor"必须通过"BuildMeshDrawCommands()"来调用通道着色器的"GetShaderBindings()",所以我们需要一种机制来将任意数据从"FMeshPassProcessor"传递到"GetShaderBindings()"调用。这是通过"ShaderElementData"参数到"BuildMeshDrawCommands()"来完成的。

添加开关和参数

  • Engine/Source/Runtime/Engine/Classes/Components/PrimitiveComponent.h

  • Engine/Source/Runtime/Engine/Private/PrimitiveSceneProxy.cpp

  • Engine/Source/Runtime/Engine/Private/SkeletalMesh.cpp

  • Engine/Source/Runtime/Engine/Private/StaticMeshRender.cpp

  • Engine/Source/Runtime/Engine/Public/PrimitiveSceneProxy.h


  • Engine/Source/Runtime/Engine/Public/PrimitiveViewRelevance.h

步骤说明

  • 为什么也需要增加相关的参数,原因之一在与 FXXXSceneProxy::GetViewRelevance 中默认修改和返回的是 FPrimitiveViewRelevance 类型。
  • 变量名字可以考虑用特殊点的,比如在 outline 前加上项目前缀,以免在重命名或查找等时影响引擎中同名参数。

原理阐述

  • "FPrimitiveSceneProxy"有两个生成"FMeshBatches"的路径:缓存路径和动态路径。"FPrimitiveSceneProxy"实现通过"GetViewRelevance()"函数控制每个帧使用的路径。


FMeshBatch 代码路径。橙色箭头表示每帧都必须执行的操作,而蓝色箭头表示执行一次就缓存的操作。(图源文末)

  • 缓存的路径构建并重用"FMeshBatch",对于不改变每一帧(比如静态网格体)的绘制,它是快速渲染的首选路径。这是由"DrawStaticElements"实现的,当一个代理被添加到场景中时会调用此函数。创建的"FMeshBatches"存储在"FPrimitiveSceneInfo::StaticMeshes"中,并且每一帧都被重用,直到从场景中删除代理为止。
  • 动态路径每一帧重新创建"FMeshBatch"。这是最灵活的路径,用于在帧与帧之间经常会发生变化的绘制,例如粒子。它由"GetDynamicMeshElements"实现。该函数从InitViews中调用每一帧,并为每个视图创建一个临时的"FMeshBatch"。

调用绘制命令,统计 Mesh 数目

  • Engine/Source/Runtime/Renderer/Private/SceneVisibility.cpp


  • 根据相关标记,将静态网格体添加到可见网格体绘制命令列表中。在"ComputeDynamicMeshRelevance"中标记"EMeshPass"与动态绘制的相关性。

创建渲染函数,插入渲染管线

创建渲染函数

  • Engine/Source/Runtime/Renderer/Private/MarsOutline.cpp

  • 使用"FParallelMeshDrawCommandPass::DispatchDraw"绘制该特定通道。
  • 按需正确设置 DepthStencil ,此处设为 FExclusiveDepthStencil::DepthRead_StencilNop 。因为需要读深度。如果设置为 DepthNop 相关的,则可能在使用的时候就会异常中断。

插入渲染管线

  • Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp


  • Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.h

  • 根据需要,在指定管线的期望位置插入我们的渲染调用。
  • PC 的管线修改 FDeferredShadingSceneRenderer ,移动端修改 FMobileSceneRenderer 。
  • 移动端因为管线部分步骤区分 forward 和 deferred ,所以还需根据需要决定插入位置。

相关链接

**声明:**本文来自公众号:GameDevLearning,转载请附上原文链接(https://mp.weixin.qq.com/s/q9NBsvMFkVtPaiAvO8EYZg)及本声明。

0 Answers