Creating a Custom Mesh Component in UE4 | Part 3: The Mesh Component's Scene Proxy
转自:https://medium.com/realities-io/creating-a-custom-mesh-component-in-ue4-part-3-the-mesh-components-scene-proxy-6965a3ea4cc9
Introduction
In this third part of the series, we’ll explain the concept of a Scene Proxy and we’ll go over the implementation details of our custom mesh component’s scene proxy. This will give you an idea of the steps that you need to take to implement your scene proxy for your custom mesh component.
If you’re unfamiliar with this series, you can read the intro here. You can also find the example project that we reference in this repo.
Before we start, make sure that you read the previous parts and have a good grasp of concepts like vertex factory, render resource, and RHI resource. Here’s the general structure of the article:
- What’s a Scene Proxy?
- FDeformMeshSectionProxy
- FDeformMeshSceneProxy:
- Mirroring Data
- Providing Data To The Renderer
What’s a Scene Proxy?
To answer this question, we need to revisit the hierarchy of components in UE4. Here’s the same figure from the Intro of the series.
As you can see, UPrimitiveComponent is a scene component with rendering capabilities. These capabilities imply that the mesh component’s data will be used in the render thread, and hence, a proxy is needed to mirror that data. In the official UE4 docs’ words:
“It Encapsulates the data which is mirrored to render a UPrimitiveComponent parallel to the game thread.”
Every component that inherits from UPrimitiveComponent requires a scene proxy that inherits from FPrimitiveSceneProxy and overrides some virtual methods to add its logic. A good example of a scene proxy that is used by a mesh component is the FStaticMeshSceneProxy.
Even though the main concept that we’re covering here is the component's scene proxy, it’s important to note that the idea of a proxy applies to anything that you want to mirror its data from the game thread to the render thread. For example, mesh sections. Mesh Components are usually capable of rendering multiple mesh sections, where each section contains vertex data and it’s rendered with one material. So a mesh section’s scene proxy is required to pass that data to the render thread.
Before we start explaining the code, note that all the classes and functions that we’ll go over in this article are declared and defined in DeformMeshComponent.cpp.
FDeformMeshSectionProxy
This class stores the required per-section data that needs to be mirrored in the render thread, which is the following:
- Material: A pointer to a UMaterialInterface that will be used to render this mesh section.
- IndexBuffer: An instance of FRawStaticIndexBuffer which is a class that inherits from FIndexBuffer; the base class for index buffer render resources (The concept of render resource and RHI resource are covered in part 1), it includes the bare minimum logic which is just wrapping the RHI representation of an index buffer. On the other hand, derived classes like FRawStaticIndexBuffer add their logic to facilitate creating and modifying the underlying buffer (Get an array copy of the indices, append indices to the existing buffer, etc). You can create your index buffer render resource class and add whatever logic that suits you(There’s no specific interface to implement), but for our use case, we’ll use the existing FRawStaticIndexBuffer which is used by the static mesh component and has everything that we need.
- VertexFactory: An instance of our custom vertex factory FDeformMeshVertexFactory that we covered in the last part.
- bSectionVisible: A boolean to indicate whether this mesh section should be rendered in the scene or not.
- MaxVertexIndex: The maximum vertex index to be used in rendering this section.
FDeformMeshSceneProxy
As we mentioned earlier, this class inherits from FPrimitiveSceneProxy and it’s responsible for:
- Mirroring the Mesh Component’s data in the render thread.
- Providing that data along with other rendering information to the renderer when it’s requested.
Let’s start by going over the fields of this class:
- Sections: This is self-explanatory; for each mesh section we create a proxy, and we keep them in this array in the appropriate index.
- MaterialRelevance: This is an instance of FMaterialRelevance which stores information about the materials used in rendering this primitive. We’ll see later how this information is provided with other relevance information to the renderer.
- DeformTransforms: An array of FMatrix to keep track of the secondary/deform transform of each mesh section.
- DeformTransformsSB: This is an RHI reference to the structured buffer that will contain the secondary transforms on the GPU.
- DeformTransformsSRV: This an RHI reference to a shader resource view that we’ll create for the structured buffer. This is what we’ll be bound to the shader to access the transforms stored in the structured buffer.
Now we know what data is managed by this class, let’s go over the implementation of its two main responsibilities.
Mirroring Data
Most of this logic is implemented in the constructor, which takes as an argument a pointer to an instance of our mesh component UDeformMeshComponent (We’ll cover it in the next part).
The mesh component instance contains an array of mesh sections, which we will iterate over, and for each mesh section, we’ll create a mesh section proxy instance and fill it with the required data. We’ll also copy the deform transform of each mesh section and add it to the DeformTransforms array.
Now in each iteration, we fill NewSection from SrcSection:
1. Create the Vertex Factory: We do this by creating a new vertex factory instance and initializing it with the vertex buffers of the static mesh that SrcSection has a pointer to.
InitVertexFactoryData() is a helper function that initializes an instance of our custom vertex factory from an instance of FStaticMeshVertexBuffer, which is a structure that wraps different vertex buffer render resources (Position, Color, etc.). We’re resuing it because it has the vertex buffers that we’re interested in (Position and Texcoords).
The first thing that this function does is making sure that the vertex buffers render resources that we’ll use are initialized or updated. To do so, we use a helper function called InitOrUpdateResource() that takes any render resource and initializes it if it’s not initialized yet, or updates it otherwise. We do this to make sure that the RHI resources owned by these vertex buffers render resources are created and can be used.
Each vertex factory has a member called “Data” which is an instance of FStaticMeshDataType (Or a struct that derives from it); a wrapper of different vertex stream components (We also mention it here in the previous part of the series). Fortunately, vertex buffer render resources like FPositionVertexBuffer and FColorVertexBuffer have methods that allow binding them to an FStaticMeshDataType, so we use them to do so and set the data of the vertex factory.
Finally, we call InitOrUpdateResource() on the vertex factory instance to create its underlying RHI resources, and that’s it for the helper function InitVertexFactoryData().
Now back in the constructor, the vertex factory needs additional data other than the vertex data, so we set them.
2. Copy the Index Buffer: This is pretty straightforward, we get a copy of the index buffer from the source mesh section and append it to the index buffer of the proxy. They both are instances of FRawStaticIndexBuffer that we mentioned earlier.
3. Material, Visibility, and Maximum Vertex Index: The last step in filling the mesh section proxy is setting these fields.
That’s it for creating a mesh section proxy! At the end of the loop, we end up with the two arrays Sections and DeformTransforms filled. We use the latter to create the structured buffer DeformTransformsSB and its SRV DeformTransformsSRV.
That’s all for mirroring the data in the mesh component’s proxy. Other methods exist in the class to update this data after construction, but they are simple and don’t require much explanation.
Providing Data To The Renderer
The data that gets mirrored from the mesh component is required by the scene renderer, that’s why our proxy class overrides virtual methods that are inherited from the FPrimitiveSceneProxy. So we’ll go over the main methods in this section.
Previously, we mentioned that the proxy class stores an instance of FMaterialRelevance. That instance is used in this method along with other things to determine the primitive’s relevance for a specific scene view (A projection from scene space to 2D). That’s why we need to implement this method which takes a pointer to an FSceneView as an input and returns an instance of FPrimitiveViewRelevance. If the word “relevance” is a bit ambiguous in this context, think of it this way; When rendering a primitive to the screen, there are multiple aspects that should be considered(Does it cast shadows? Does it use custom depth? Is it translucent? etc), and for each primitive and scene view, the engine needs to determine which features are enabled and hence, what’s “relevant” when rendering the primitive. We provide that information through this method.
IsShown(), IsShadowCast(), ShouldRenderInMainPass() are inherited from FPrimitiveSceneProxy and you can override them to alter the behavior of your primitive for your use case. However, you should note that bDynamicRelevance should be set to true because that’s how we tell the renderer that this primitive has dynamic elements that should be rendered in this view.
In this method, we provide the renderer with the required rendering data of the dynamic elements that we mentioned above. The implementation is a bit long but we’ll cover the important parts.
The two main arguments of this method are an array of FSceneView (A primitive can be rendered in multiple scene views at the same time), and a reference to an FMeshElementCollector instance. You can consider the latter as the output since it’s responsible for collecting the mesh elements of our primitive.
For each FSceneView and each mesh section, we allocate an FMeshBatch which contains an array of FMeshBatchElement. We only use the first element (Because we render each section only once, but if your use case includes rendering the same mesh section multiple times, you can make use of the batching logic).
Now we fill the mesh batch and its first element with the relevant mirrored data; Index buffer, vertex factory, material proxy. Note that the index buffer is set per-element while the vertex factory and the material are set per-batch.
Another thing that we need to set for each dynamic mesh element is its uniform buffer that is used by the renderer to pass internal data about the primitive in the current frame and the previous one (Local to world transform, bounds, local bounds, etc).
Finally, we set some additional data about this particular batch element; first and max vertex indices, number of primitives (Triangles in this case), etc. This must be set because, in the case of batching, we can draw the same mesh section multiple times and each instance can be drawn differently by using different parts of the same index buffer, or by using a different one (This is why the index buffer is set per-element and not per-batch).
We also set some data that applies to all the elements of this mesh batch, like the primitives type (Triangles, Lines, Points, etc) and the depth group.
We add the mesh batch and the scene view as an entry in the mesh collector and that’s it.
Other Methods
Other than GetViewRelevance() and GetDynamicMeshElements(), other methods are overridden to provide miscellaneous information about the primitive. We won’t cover them here because they’re fairly simple.
Done!
In this part of the series, we covered the custom mesh component’s proxy, its two main responsibilities (Mirroring data in the render thread and providing it to the renderer) and how are they implemented.
We now covered all the code that will be running on the render thread and the GPU. In the next article, we’ll finish the series by going over the implementation of the actual mesh component and the game thread’s side of things.