在Unity的Shaderlab中,我们经常会使用Pass{ }关键字为同一个材质声明多个RenderState不同的Pass实现一些效果,而在UE中并没有在shader/材质层面做多Pass的支持。虽然有Layer,但并无法实现不同ShadingModel / RenderState计算结果的叠加,只是对MaterialParameter的计算结果做了混合。本文将介绍如何在UE4.22的MobileSceneRenderer中实现多Pass效果。
- 注:UE4.21-4.22的更新中修改了大量的类名/接口名,所以该文章介绍的代码部分会与UE4.21及以前的版本有较大偏差,详见https://docs.unrealengine.com/en-US/Programming/Rendering/MeshDrawingPipeline/4_22_ConversionGuide/index.html。
什么是多Pass?为什么一个材质需要多Pass?
将一组 Mesh/Parameter 输入转化为Pixel Output的过程,称做一个Pass。本文中的多Pass是指将一组 Mesh/Parameter 输入在屏幕上Draw多次。
多Pass的应用场景很多,比如外扩模型式描边。第一个Pass做正常的渲染,第二个Pass用BackfaceCull,DepthLess的RenderState设置,然后在VS中将顶点沿法线方向偏移。还有在渲染半透明物体时我们也会经常用到多Pass,比如Unity文档里的这个例子:https://docs.unity3d.com/Manual/SL-CullAndDepth.html
半透明的物体一般不会写Depth,就会导致模型的内部和背面也被画出来,模型就会看起来像一个空铁皮。解决这个的方法是在第一个Pass中只写入Depth,不渲染任何像素,然后在第二个Pass中把RenderState设为DepthLEqual,这样就可以用DepthTest过滤掉我们不希望渲染出来的内部和背部了。其实多个Pass相当于给同一个Mesh不同的材质。
在UE4中多Pass怎么实现?
令我惊讶的是,UE4在手机端并没有对多Pass提供任何的支持。需要实现类似的效果的话有两种方式,一是让美术将需要多Pass的Mesh复制两份,等于有两个完全重合的Mesh,再赋予这两个Mesh不同的材质,每个材质相当于一个Pass。第二种方法就是修改引擎Render中的逻辑,新增一个组(Pass/MeshProcessor)来实现对应的功能。两种方法各有优劣,需要根据实际应用场景来评估取舍。本文介绍的是如何修改MobileSceneRenderer来实现多Pass。
UE4的Mesh渲染流程
为了实现多Pass,我们首先要知道UE4中渲染一个Mesh的流程。这里可以参考UE的官方文档:https://docs.unrealengine.com/en-US/Programming/Rendering/MeshDrawingPipeline/index.html
我们以 MobileShadingRenderer.cpp 的 Render 方法中的 MobileBasePass 为例。
第一步是资源的准备,这里以DynamicMesh为例。在 Render 方法中,我们可以看到在设置完RenderTargets后,立刻调用了 InitViews 函数来获取当前View中可见的Primitives,其实就是先对Scene中的Meshs做了可见性判断和视锥体裁剪,然后通过SceneProxy获取对应的MeshBatch,Shader,Material 等渲染需要的信息并缓存下来,共之后所有Pass使用。这里要注意在获取到需要的渲染资源后还进行了 Relevance 的计算,提前给每个Pass准备好了需要的资源。其中 Relevance 的计算和添加到各个Pass的操作是在 SceneVisibility.cpp 中的 MarkRelevant() (static mesh)和 ComputeDynamicMeshRelevance() (dynamic mesh)中完成的。
第二步是更新Pass的ConstantBuffer和渲染任务的分发。这一部分操作在 MobileBasePassRendering.cpp 的 RenderMobileBasePass 中完成,最后调用了
View.ParallelMeshDrawCommandPasses[EMeshPass::BasePass].DispatchDraw(nullptr, RHICmdList);
生成了对应的Task交给各个WorkerThread执行。
第三步是各个 WorkerThread 根据对应 Pass 的 MeshProcessor 给Mesh绑定 Shader 和 MaterialParameter,设置RenderState,生成DrawCommand。生成DrawCommand之后的Primitives是一定会被渲染的。MobileBassPass 中这一步操作是在 MobileBasePass.cpp 中的 AddMeshBatch() 和 Process() 方法中完成的,AddMeshBatch中也可以进一步对MeshBatch进行筛选,生成最少量的 MeshDrawCommand。
添加自定义Pass的方法
- 添加Shader
了解了一个Pass要完成的工作,我们就可以动手实现一个自己的 Pass 了。首先要确定的问题是 Shader。既然要把同一个模型画两次,那必然要用到不同的Shader。关于如何在UE4中添加 Shader,可以参考 DepthPass 的 VS/PS(在DepthRendering.h中) 和 UE4 的官方文档:https://docs.unrealengine.com/en-US/Programming/Rendering/ShaderDevelopment/index.html。MobileBasePass的Shader因为涉及环境光,点光源数等可开关的Defination,所以对应的 C++ 类是以 template 的形式实现的。一般来说自定义 Pass 的 Shader 会继承 FMaterialShader 并用 IMPLEMENT_MATERIAL_SHADER_TYPE 宏来绑定对应的 usf 文件。 可以完全自己写新的 usf 文件,也可以在 FMaterialShader::ModifyCompilationEnvironment() 中应用不同的 SetDefine() 来实现不同的 Shader。需要注意的是 UE4 的 Shader 编译是一个比较漫长的过程,所以最好在 FMaterialShader::ShouldCompilePremutation() 中对材质进行筛选,只编译必要的Shader。否则所有的 Material 都会编译对应的 Shader,效率很低。还有注意 Shader 要在构造函数中绑定需要的 Uniform Buffer,在 GetShaderBinding 中绑定对应的UniformBuffer,否则会出现 ResourceMiss。
2. 添加 MeshProcessor
根据对UE4的渲染流程分析我们可以看出,Pass 生成 DrawCall 的主要逻辑是在 MeshProcessor 中完成的。MeshProcessor 是 4.22 中新加入的类名,之前对应的是 DrawingPolicy。添加 MeshProcessor 很简单,只需要继承 FMeshPassProcessor 并复写其 AddMeshBatch() 方法即可。一般我们会在 AddMeshBatch 方法中获取 Material Resource 的信息并对 MeshBatch 做进一步筛选,最后调用 Process 方法绑定 Shader,Mesh 和 Material,计算 Mesh 的 CullMode,ZTest,Zwrite,BlendOP 和 SortKey等等并用 BuildMeshCommands 生成 DrawCall。
3. 添加 Pass
所有的 Pass 都可以在 Enum EMeshPass中找到,所以第一步就是在 MeshProcessor.h 的 EMeshPass 中添加对应的 Enum。然后我们要为 Pass 创建对应的 MeshProcessor,我们可以在对应 .cpp 文件中实现对应 MeshProcessor 的 Creator 方法,并定义对应的 FRegisterPassProcessorCreatFunction 在其构造函数中传入对应的 Creator 方法指针和 Pass Enum。这一部分可以参考 MobileBasePass.cpp 最后的 CreateMobileBasePassProcessor 和 RegisterMobileBasePass 部分。之后我们就要在 MobileSceneRenderer 的 Render 方法中插入自定义 Pass 的渲染流程,这一部分主要是一些 Profile 标签和 RHICmdList 的Setup 和 Flush,还有生成 Pass 的多线程 DrawTask。这一部分逻辑可以参考 MobileBasePassRendering.cpp 中的 RenderMobileBasePass 方法。
至此,我们的自定义 Pass 就添加完成了。可以通过 UE4 的 MobilePreviwer、 RenderDocPlugin 和 RenderDoc 直接在 Editor 中确认 Pass 的执行细节和结果。其实在 UE4 中实现多 Pass 并不会破坏 UE4 现有的代码结构,实现起来也比较清晰,但是在缺少 Documents 和对 UE4 源码了解的情况下会花费比较长的时间理解 UE4 生成 Mesh Draw Call 的流程。对于从 Unity 转过来的 TA/程序(比如我),可能在实现某个效果的时候很轻易地想到了多 Pass 的方案,但找不到实现多 Pass 的方法。写这篇文章主要是分析一下 UE4 的 MobileSceneRenderer,总结加入自定义 Pass 需要做的改动。希望能对其他人有所帮助。