Merging Meshes in Unreal and Why It Matters

“I just feel like it’s rather strange that we’re having a difficult time rendering with such a small number of objects being used.”

A developer made this comment to me a short while ago, wondering why a seemingly simple scene was turning into a slideshow in the VR headset. As he had suggested, there weren’t many objects in the scene - a few hundred at most, and most of these had fairly limited polygon counts. By all rights, this scene should have been running faster than it was.

Here’s the thing though: There may not have been that many distinct actors in the world outliner, but that wasn’t how things looked to the GPU. As far as it was concerned, the CPU was sending it thousands of objects to draw per-frame.

To understand why this was the case, we need to talk about Draw Calls.

To begin, we need to train ourselves to think of the PC architecture in terms of two distinct computing systems that communicate and coordinate with each other to draw the scene. (On consoles, this can be a much more complex architecture, but the principles remain the same.) There’s the CPU of course (and in ancient times that’s all there was, but this hasn’t been the case for quite some time), which is responsible for simulating the scene, running AI, updating animations, and doing whatever else it needs to do to figure out where every object is going to be before it’s drawn. Separate from this is the GPU, which is really a separate computing environment with its own processor and memory. The GPU is responsible for drawing polygons, rasterizing them to pixels, and shading them. (The order in which this happens differs between forward rendering and deferred rendering. In the former, shading takes place when the polygons are drawn. In the latter, shading is performed on buffers of rasterized pixels.)

When we want to draw something, therefore, we have to transfer its information from the CPU to the GPU. The process by which this happens is called a “draw call,” or a “draw primitive call,” or a “DPC,” and it takes time every time we do it.

This isn’t simply an academic point. As a developer, you have a lot more control than you probably realize over how these individual jobs get onto the card, and nearly all of this control lies in the way you construct your assets.

mergingMeshes00_drawcalls.png

Imagine, for example, that we have a stack of bricks that we needed to move from one room to another. If we do this by picking up each brick one-by-one, carrying it to the other room, and then returning to get the next one, it’s going to take a long time to move the pile. If instead we attach our bricks to each other so the pile can be moved as a single unit, we’re going to get it to the other room much faster.

In practical terms, this means that two assets could look identical on the screen - same polygons, same materials, but depending on the way they’re constructed, one could take many times longer to draw than the other. Multiply this by a few hundred as we place these objects in the world, and we can see why the number of draw calls required to render a scene will usually be one of the largest contributors to the overall time it takes to draw the scene.

This is important to understand. It’s common for developers to think about polygon count when trying to optimize scenes, and yes, polycount matters, but it’s far from the only thing that matters. GPU’s, as it turns out, are very good at drawing even very large lists of triangles amazingly fast, but not as much can be done to speed up their transfer from the CPU to the card. If your scene contains a reasonable number of polygons but isn’t running well, your problem is probably the way those polygons are getting onto the card.

Let’s look at how this works.

Looking at Draw Calls

mergingMeshes01_blueprintAsset.png

In the image above, we have a fairly simple asset that was constructed by creating a Blueprint actor and adding a series of static mesh components to it to build a single composite asset. This isn’t an uncommon scenario to encounter if your designers are using modular geometry to build their scenes. It’s easy to set up and modify, but it’s going to slow you down.

To find out how the asset is being drawn, we’ll place the asset in an empty scene containing nothing else but a directional light and a skylight to provide illumination. (Since there’s no sky sphere in this scene, we’ll have to set the skylight’s Source Type to SLS Specified Cubemap and set its Cubemap explicitly so the skylight has the information it needs to provide light.)

We can use the Stat RHI (render hardware interface) console command to see what kind of work is required to load this asset onto the render hardware.

mergingMeshes02_statRHI.png

We can see here that we’re making 250 draw primitive calls to render this scene.

(The editor is active, so a number of these DPC’s are being used to draw the editor UI. Be aware as well that the other windows and menus you have open, objects selected, and the cursor’s rollover state can affect these values too, so when comparing values, keep the state of the editor the same. It’s going to be imprecise, and if you’re looking for real numbers, always test in a stand-alone launched executable instead, but to get a general idea of how you’re doing, this test is fine.)

To find our baseline, we’ll delete the object and check the values again.

The number drops to 229 when we remove the asset. It’s taking around 21 draw calls to get this asset onto the screen (again, remember that the editor is probably putting some noise into this test). That may not sound like a lot until we consider that a reasonable draw call target for a desktop VR headset is around 2000. For a stand-alone headset like an Oculus Quest or Go, this number is significantly lower. If we place a hundred of these in our scene, we’ve just blown our budget. We’re going to need to get this draw call count down.

Now let’s place a simple cube in the scene.

mergingMeshes03_statRHI_cube.png

235 draw calls. This cube is taking around 6 draw calls to get onto the hardware. It takes somewhere around 15 extra draw calls to draw the composite asset compared to the simple cube.

If we put our composite asset back into the scene and check it using an external graphics debugger like RenderDoc, we can see what a few of these draw calls look like. (We’re not going to go into using RenderDoc here. That’s a bit more of an advanced topic, but it’s a useful tool for understanding how your scene is being drawn.)

mergingMeshes04_renderdoc01.png

Here the first element is being drawn.

Two calls later, we have the third piece.

mergingMeshes05_renderdoc02.png

It isn’t until we get to the fifth call that all five of the mesh’s pieces will have been drawn.

For comparison, the cube’s geometry gets onto the card with a single call.

mergingMeshes06_renderdoc_cube.png

It’s taking five times the work to get the combined asset onto the card as it’s taking to load the cube.

(What accounts for the rest of the draw call disparity? Material slots, mostly. Every material slot assigned to a static mesh costs another draw call, even if the same material is assigned to it. Keep this in mind and use as few material slots on a mesh as you can get away with. They add up fast.)

Merging Meshes to Reduce Draw Calls

So what do we do when we realize an asset is taking too much time to draw?

In this instance, there are two things we can do to speed this asset up.

First, it doesn’t need to be a Blueprint actor - it has no behaviors assigned to it and has its tick unnecessarily enabled so it’s consuming CPU time as well. If we replace this asset with a static mesh, it’s going to appear exactly the same on-screen.

Secondly, the component meshes can be merged into a single mesh.

Merging simple meshes

For a simple collection of static meshes, this process is pretty straightforward:

  • Select the meshes you want to merge

  • If you’d like control over where the origin of the new mesh is created, move the meshes so the world origin is where you’d like the new mesh origin, and when you merge them in the next step, set Mesh Settings | Pivot Point at Zero to True.

  • Right-click them in the viewport and select Merge Actors.

  • Save the asset in a reasonable location.

What if we’re dealing with a slightly more complicated asset, like this one that had been made into a Blueprint actor?

Merging a complex actor

Here’s an easy way to break down its component parts for easy merging:

First, place the actor we plan to replace into our empty construction scene so we can use it for reference and be sure we’re duplicating it exactly.

mergingMeshes07_simplescene.png
  • Now we’ll open it in the Blueprint editor to see how it’s constructed.

mergingMeshes08_blueprintEditor.png
  • Verify that there’s nothing in the Construction Script, Event Graph, or Functions list that would necessitate its being a Blueprint actor. In this case, there isn’t, so we can safely replace this with a static mesh without losing functionality.

  • Select the first static mesh component within the asset and take note of its Static Mesh property. Use the magnifier to navigate to this Static Mesh’s source in the Content Browser.

mergingMeshes09_findMesh.png
  • Now grab an instance of this asset and drop it into your scene.

  • Copy the component’s Transform | Location.

mergingMeshes10_CopyTransform.png
  • Paste it into the new mesh instance’s Transform | Location.

mergingMeshes11_PasteTransform.png
  • Repeat this process for its Rotation and Scale, and ensure that you’ve matched up its material assignment and any other modified properties.

  • Do this for each of the component meshes within the Blueprint.

  • When you’re done, you should have a collection of static meshes in your scene that line up exactly with the object you’re going to replace.

mergingMeshes12_RepeatPaste.png

Now you can use the process we outlined earlier.

  • With these instances selected, right-click them in-scene and select Merge Actors.

mergingMeshes13_MergeActors.png
  • In the resulting dialog, confirm that you see the actors you expected to see in the list.

mergingMeshes14_MergeDialog.png
  • Because the transforms we copied from the source Blueprint were relative to its origin, we’re going to want to set Mesh Settings | Pivot Point at Zero to True to ensure that we preserve the same origin location the Blueprint used.

    • Set Allow Distance Field to True if the mesh will be seen up-close and needs good shadowing and ambient occlusion data.

    • If the source meshes use vertex coloring or other vertex data, be sure to set Bake Vertex Data to Mesh to True as well so you don’t lose this information.

    • If you’re merging meshes that you know from past experience will merge successfully, you can set Replace Source Actors to True, which will delete the source meshes and replace them with the newly-created mesh. It’s usually a good idea, however, to leave this False so you can re-merge the meshes if something goes wrong with the result.

  • Hit Merge Actors. A dialog will now appear asking you where the asset should be saved.

  • Choose a suitable location. (For an excellent guide to project naming conventions and file organization, refer to this guide: https://github.com/Allar/ue4-style-guide)

Verifying that you got what you expected

Once the new asset has been created, open it up in the Static Mesh editor, verify that its Material slot assignments look correct and make sense, and make sure its collision matches what you expect.

mergingMeshes15_Verify01.png

If everything looks good, drop it into your construction scene and make sure it lines up correctly with the original asset when placed at 0,0,0.

mergingMeshes16_Verify02.png

Here we can see that the source asset and the newly-created asset line up correctly.

Now we can delete the source mesh and the  intermediate instances we used to create the merged mesh.

If we check the scene using Stat RHI, we can see that we’re now consuming 230 draw calls to draw the scene and editor UI.

mergingMeshes17_VerifyStatRHI.png

In RenderDoc, we can see that the mesh geo is going onto the card in a single pass.

mergingMeshes18_VerifyRenderDoc.png

Replacing References to the Old Asset

Now it’s time to replace the old asset anywhere it’s being used with a reference to the new asset. To find out where the asset is in use, right-click the old asset in your Content Browser and open the Reference Viewer.

mergingMeshes19_ReplaceRefViewer.png

Anything pointing to the asset here is using it. You can right-click an asset or level using your old actor and select Edit to jump into its editor and fix it. Do this for any level or asset pointing to the old actor and replace the reference with a reference to the new actor.

mergingMeshes20_ReplaceRefViewer02.png

In this case, it’s only being used in our construction map.

An easy way to replace an actor instance in a map is to select the instance or instances in your scene, select the new asset in your Content Browser, right-click one of the assets in scene, and select Replace Selected Actors with your new asset.

mergingMeshes21_ReplaceActor.png

When nothing uses the asset anymore, you can delete it.

mergingMeshes22_CleanRef.png

You may be warned that the asset is still being referenced in memory. You can safely force delete it in this instance.

Note that if you’re replacing an improperly-merged static mesh actor with a new one, you also have the option simply to delete it and replace references. I’m not a fan of this approach. It’s better to take the time to replace your assets in a context where you can see and verify that nothing’s breaking in the process. An unmanaged bulk replacement might work out fine, or it might not. If you do choose this approach, make sure you still check the changes it made.

mergingMeshes23_ReplaceRef.png

Wrapping up

That’s it. In this guide, we talked a bit about draw calls and why they matter so much, and we took a relatively inefficient actor and replaced it with an actor that looks identical on-screen but takes a fraction of the time to draw. For actors that are used commonly in your scene, this can make a substantial difference.

For more information about merging actors, refer to the documentation here: https://docs.unrealengine.com/en-US/Engine/Actors/Merging/index.html