内存中的mesh是如何进行渲染的,主要涉及几个重要的数据结构

UPrimitiveComponent

PrimitiveSceneProxy

FMeshBatch

FMeshDrawCommand

一.UPrimitiveComponent

UPrimitiveComponent存在于Game Thread内的数据UPrimitiveComponent。

继承自USceneComponent,而USceneComponent的意思,主要是带有3D变化(位置、缩放、朝向)的ActorComponentUPrimitiveComponent 更近一步,是带有图形(比如网格mesh或者particle system粒子)表现的UsceneComponent。我们所有的mesh数据结构,都会继承自UPrimitiveComponent。而这也是我们征程的开始。

UPrimitiveComponent包含的数据并不会直接在渲染线程使用,这里不使用原数据的主要原因也是显而易见的:主线程和渲染线程的同步问题。

在最开始的阶段world里会遍历所有Actor,Actor遍历所有的UActorComponent,然后执行函数

UActorComponent::ExecuteRegisterEvents();

在这里,会调用然后通知World中的Scene进行AddPrimitive

FPrimitiveSceneProxy和FPrimitiveSceneInfo

FScene::AddPrimitive中,会根据UPrimitiveComponent,进行FPrimitiveSceneProxyFPrimitiveSceneInfo的创建,创建完成后,调用

Scene->AddPrimitiveSceneInfo_RenderThread(RHICmdList, PrimitiveSceneInfo);

之后的调用顺序为:

AddPrimitiveSceneInfo_RenderThread开始,就进入了渲染线程当中,这时我们已经脱离了UPrimitiveComponent,在渲染线程中仅能接触到的是FPrimitiveSceneInfoFPrimitiveSceneProxy

在这里,FScene中的成员变量会对FPrimitiveSceneProxyFPrimitiveSceneInfo进行相当多的注册。

关于FPrimitiveSceneInfoFPrimitiveSceneProxy,互为双向指针,二者的区别,我表示,打扰了

FPrimitiveSceneProxy is the rendering thread version of UPrimitiveComponent that is intended to be subclassed depending on the component type. It lives in the Engine module and has functions called during rendering passes.

FPrimitiveSceneInfo is the primitive component state that is private to the renderer module.

  • FPrimitiveSceneProxy: UPrimitiveComponent的渲染线程版本
  • FPrimitiveSceneInfo: UPrimitiveComponent在渲染器的内部状态,与FPrimitiveSceneProxy一一映射

按照我肤浅的观察,应该是FPrimitiveSceneInfo是在render最初创建时发挥作用,在真正渲染使用时,使用的数据结构是FPrimitiveSceneProxy 。当我们进入FPrimitiveSceneInfo::AddToScene时,事情变得复杂了起来,会进行非常多的状态更新,AABB的创建。 FPrimitiveSceneProxy生成FMeshBatch有两个途径

  • Cache
  • Dynamic

这里先说cache的,其会调用FPrimitiveSceneInfo::AddStaticMeshes,这里仅是Static Mesh的添加。而动态物体创建并不在这里

CacheMeshDrawCommands(RHICmdList);  

对于StaticMesh来说就是Cache的方式,在Proxy添加到场景时调用DrawStaticElements

生成的FMeshBatch被保存在FPrimitiveSceneInfo的StaticMeshes中

每帧进行重用直到Proxy从场景中移除。

这里就是一个全新分界线,我们将从FPrimitiveSceneInfo跳出来,通过FMeshPassProcessor进入到一个全新的领域。

二.FMeshBatch

FMeshBatch作为中间阶段,是FPrimitiveSceneProxyFMeshDrawCommand的中间桥梁,那么他的作用是什么呢,

FMeshPassProcessor的输入就是FMeshBatch,输出是MeshDrawCommand。

其最主要的作用就是把FPrimitiveSceneProxy和最终的渲染结构分离。它包含pass需要渲染数据的全部信息,因此FPrimitiveSceneProxy永远不知道将Pass渲染的啥。

可以看到,FMeshBatch具有一组MeshBatchElements,一个可以视为顶点缓冲区的顶点工厂,和代表材质的材质渲染代理。

有时会有多个mesh。对于某些平台,它不支持实例化。因此,unreal改为将实例分成许多批次进行绘制。在这种情况下,FMeshBatchElements数组将用于存储其他元素。但是通常,它仅使用第一个元素。

FMaterialRenderProxy是真正的着色器的三级结构。首先,它包含一组FMaterial。它可以根据给定的RHI等级选择不同的材质。然后,每个FMaterial都有一个FMaterialShaderMap,其中包含很多FShader。

在unreal中,每种材质将被编译为一组着色器,而不只是一个。那是因为对于不同的通过,不同的平台和各种条件,unreal将尝试提供最优化的着色器。例如,材质中的静态开关节点将被编译为两个版本,而不是“if-else”语句。 FShader结构包含像素着色器,顶点着色器等。

为什么还是继续划分为FMeshDrawCommand

为什么不直接使用FMeshBatch进行渲染,这个原因应当是:unreal中包含多种pass,所以同样的FMeshBatch在不同的pass下虽然有相同的部分,但是大多数情况下并不相同。也就是说FMeshBatch包含完整的对所有pass充分的信息,但是对某些pass来说相当的冗余,所以需要继续划分为充分必要。

从FMeshBatch到FMeshDrawCommand

FMeshPassProcessor是建立FMeshDrawCommand的最重要的工具类,我们要实现不同的渲染方案,就需要在这里进行操作。如果需要自定义的定制,我们需要手动进行修改。FMeshPassProcessor最主要的作用,就是生产FMeshDrawCommand,那么

我们为什么需要FMeshDrawCommand呢?

  1. 动态合并instance,也就是动态判断绘制的物体材质,VB等是否一样,一样的物体合并到一起绘制。但是对于4.21前的管线,动态物体和静态物体没法合并。然后运行时判断是否合并instance效率又太低。

  2. 一些新技术的需求,之前的架构也是难以胜任的。DXR技术,GPU Driven Culling的技术, Rendering Graph或者说可编程的渲染管线技术。

总结起来就是,我们需要一种更简洁,更紧凑,更利于CPU访问的数据结构去表示场景数据,包括Transform,Material(可以理解成shader参数),RenderState,顶点Buffer等等。而他们最好是一个数组,并不需要包括太多GameThread的信息。而我们的Renderer代码需要在渲染每一个Frame开始前cache全场景的数据。

FMeshDrawCommand类缓存了一个drawcall所需要的最少资源集合:

  • VertexBuffer和IndexBuffer
  • ShaderBindings(Constant,SRV,UAV等)
  • PipelineStateObject(VS/PS/HS/GS/CS 和 RenderState)

FMeshDrawCommand的地方

具体由哪个途径生成FMeshBatch通过FPrimitiveSceneProxy::GetViewRelevance决定. 目前已知DynamicMesh需要每帧生成MeshDrawCommand StaticMesh根据StaticMeshBatchRelevance决定是否需要重新生成静态物体的FMeshDrawCommand的创建是在FSceneRenderer::ComputeViewVisibility中会首先调用

GatherDynamicMeshElements(Views, Scene, ViewFamily, DynamicIndexBuffer,
    DynamicVertexBuffer, DynamicReadBuffer,HasDynamicMeshElementsMasks,
    HasDynamicEditorMeshElementsMasks, HasViewCustomDataMasks, MeshCollector);

然后调用SetupMeshPass

FParallelMeshDrawCommandPass::DispatchPassSetup中最终完成动态的Mesh的创建GenerateDynamicMeshDrawCommands

什么时候进行合批操作

当调用函数**BuildMeshDrawCommands时,**调用FinalizeCommand,在scene中,有一成员变量为

倘若,新增的东西和之前缓存的一致,那么我们就将这些合批操作。不然就新增 。

2019.12.31补充

三.顶点输入

关于Mesh中一个比较重要的问题,之前好像没有讨论过就是,Vs的顶点信息FVertexFactoryInput从C++到shader到底是如何进行传入的。

最重要的类是FVertexFactory,我们的顶点信息将变为FVertexFactory传入渲染之中。而我们最常用的子类就是FLocalVertexFactory,而我们的staticmesh是使用的FStaticMeshVertexFactories。我们抛开静态的扩展类函数不谈,我们只看它的数据是如何构建的。

对于UStaticMeshComponent,其中会有一个UStaticMesh

265cee09d18a84092e5a1df13041268d

这里的StaticMesh就是我们将要进行渲染的数据,我们需要的渲染数据就是在这里组装的。在序列化时,在UStaticMesh里面还有一个存储数据的结构RenderData,是真正的渲染数据的存储

bc4da3f73c25df71ad3dfa731192236d

在里面会有FStaticMeshLODResources,针对每个lod的renderdata

0de0e9597d968cab355efe176ff1cace

这里一共有两个,一个是顶点数据,还有一个是vertexFactories,它是在同时进行初始化的 d8a1ab3c1aacd78ae978cd60b14b1c4f

LODResources.InitResources

其主要的初始化的内容有

  1. IndexBuffer

  2. VertexBuffers

  3. DepthOnlyIndexBuffer

  4. FAdditionalStaticMeshIndexBuffers

LODVertexFactories[LODIndex].InitResources

1951322c67847f46a392484c08d1ef94

FStaticMeshVertexFactories::InitVertexFactory
FVertexFactory::InitDeclaration

四.RenderData的数据传输流程

在构建MeshSceneProxy,直接会把这个传入

之后会变为meshbatch,主要有两个情况

  1. GetShadowMeshElement

  2. GetMeshElement

BuildMeshDrawCommands

e89ab209b92c61b6768ddef211dd9700

DrawStaticElements