Meshes, Instances and Optimization

For a personal project of mine, I’m working/playing;) with LEGO bricks in the engine and want some advice regarding optimizations. Get ready for a wall of text. (Note that this is entirely personal and non-commercial, and I don’t plan on releasing any game without asking the LEGO Group)

My current workflow is to make a LEGO model using either Digital Designer or LDraw which are designed for working with LEGO units. Once done, I reassemble the model in 3ds Max using my own custom library of bricks modelled after the LDraw library (since LDraw itself is unsuitable due to a very specific 3D format).

Each brick that I model will have three LODs, the first one will be the normal brick as optimized as possible. The outer edges are beveled and use custom normals to fake rounded corners without using smoothing groups or normal maps so that each brick stands out when stacked together. The second LOD is just the basic convex shape of the brick (so no studs and no interior) but with the beveled edges whereas the third LOD is just a heavily optimized convex shape. The final model in 3ds Max is optimized as best as possible by using a script that replaces all selected models with a single source instance, according to how visible the bricks are and where the model itself is used (studs are rarely visible so can be removed, the same for the interior/underside). Rough estimates as to poly-count, including bevels, are around 60-150 at minimum (for the simple bricks) to around 1,000 for the more complex bricks (which comes mostly from geometry that can be removed when used in certain ways, which results in around 400-700 max).

As for the materials, I have a single master material with a sprite sheet containing every official LEGO color. It uses a custom material function based on the flipbook function to select an index from the palette. Every color is then a material instance with the index set correctly. Since the bricks don’t use normal textures, those materials can be reused everywhere (decals are going to be done differently, perhaps as either engine decals or offset faces).

However, the optimization process can only go so far and I don’t want to/can’t spend hours on removing every single unnecessary triangle for a whole model. In some cases the ingame models aren’t too big or are simple enough to remove non-visible triangles quickly.

I’m planning to test a few things out, but that’s going to take time, so I also wanted some advice ahead of time. One case study I plan to try out is having every brick as a separate static mesh imported into the engine. I’ll hopefully be able to create a blueprint or c++ class that can import an LDraw file (a text file with lists of bricks used) or an LDD file (an xml file with bricks used) that automatically assembles the final model by placing the correct bricks (named using the official part IDs) and assigning the correct materials. I don’t have a plan for that system yet, but that is not the most critical part of the case study. The most important part is about draw calls, geometry and culling. If I assemble a LEGO model using individual bricks (with LODs) rather than a single static mesh from 3ds Max (with multiple materials), either with (hierarchical) instanced static meshes or not, could this provide some optimization? Each model would then be a blueprint class.

TL;DR, I know. But I wanted to provide as much information as I could. At the same time, I’m also aware of the standard optimization rules (I have worked for a mobile developer so I know a thing or two about draw calls and poly-count), and normally I would remove as much geometry as I could, but I also want to optimize the time spent so I have more time for other parts of the project as it’s entirely done in my spare time. If it doesn’t work, then no harm no fowl.

As a small impression, I’ve added a picture of a cutscene model that I’m working on (it has almost 180,000 triangles and 200,000 vertices, but it’s also a lot more complex than the ingame models will be):

I dont think that approach will support large and complex models in terms of performance.
The main bottlenecks I see is: One drawcall, per brick!. Even if you only use meshes at simplest LOD level, a model would still require houndrets of drawcalls.
You could however make several version of the model (as you did with the bricks) and use them also distance based, as blueprints, as you suggested.
Another optimization step here would be not using several material IDs (again, drawcalls), you could maks them out and lerp the final material together.
Especially for things like lego, this would be optimal.

If they are all using the same material, then I think you would be able to use Blueprints to make them all one draw call. Or, it might be better to just export it all as one in the first place, depending on what your needs are for it. It sounds like you have some advanced technical skill in 3ds Max so maybe you could have it automatically optimize a whole build and make the LOD’s for it to export to UE4. My guess is that you’ll probably end up with something that’s less optimized than doing things by hand, but you might be able to get close.

Lego does not hold patent to basic Lego brick anymore. So you can use those. Colors and specific models might be different story.

Optimization strategies just depends how dynamic those models are. If they rarely change then you have exactly same problem that every Minecraft clone has. This is good thing because there are lot’s of good material out there. There are also good algorithms for this kind of geometry. Just to save your efforts I can say that you can’t treat those bricks as draw calls. Even instancing is not good enough. You need to build big chunks and remove all invisible geometry from interiors. This is well studied problem so don’t try to reinvent wheel here.

I can program, but I’m not that experienced. As far as I understand Minecraft and its clones, they use procedural geometry to render polygonal blocks. Adjacent blocks have their hidden faces removed and the blocks are connected together. But LEGO bricks have an arbitrary shape that’s mostly not even aligned to a normal grid (particularly in the up direction). Now, I don’t need to have entire worlds rendered out of LEGO, that will take a lot more work. As far as I understand it, theoretically, I would have to remove the occluded triangles when rendering the bricks, on the fly. But that means I’ll have to modify the engine’s rendering system, which I can’t.

What I understand about normal ISMs is that they are all rendered if only one of them is visible and they don’t support LODs, whereas hierarchical ISMs do support LODs and are not all rendered at the same time, but rather only in a single actor/blueprint, is that right? Also, regarding occlusion culling, wouldn’t it help if all the bricks of a single model were separate? That way, only the bricks that were actually visible would be rendered. If the model was a single static mesh, then the whole model would be rendered. And if each copy of a brick was the same static mesh, wouldn’t the engine understand that and load only a single copy of that brick in memory? Or is that really only what an (H)ISM can do?

GPU’s are not very efficient to render instanced meshes that have very low poly count. So even if you would have super clever automatic instancing system(which ue4 doesn’t.) You would have big overhead with all those lego bricks. You can see how bad instancing is with two triangle models here. Vertex Shader Tricks by Bill Bilodeau - AMD at GDC14

Also occlusion culling does not help much with this kind of geometry. You have almost as big performance cost per block than by just rendering it.

Maybe you just could use 3d Max tools to remove all unvisible triangles and batch all those blocks as single StaticMesh. This would be much less work for you.

Just a question on the general purpose:

Do you intend to :

A: Have the lego models being assembled/disassembled dynamically during gameplay
B: Do you want to use “complete”, unbreakable models that just happend to be designed in Max, based on individual bricks?

@Kalle_H: Unfortunately, there are no tools that automatically optimize a model by removing all geometry that isn’t visible. The developers of LEGO Universe wrote scripts in Maya to do that, but I’ve managed to look at the actual models using NifSkope, at tool normally used for modding Fallout 3 (both games used the same engine). However, I don’t have the time to make those tools, or any money to buy them if they even existed.

@KVogler: Most models are static and unbreakable, especially the larger models such as buildings. There are times when models would be built or smashed during gameplay, but those are typically small and simple, such as cars or containers. It is likely that some larger buildings will be destroyed or built in realtime during gameplay or cutscenes, but that would be very localized.

Also, my current workflow is that all models are made entirely in Max, then exported as a single static mesh to the engine. At first, the entire model is made using the bricks from my library. Once finished, I plan to create LODs by replacing every brick with their optimized LODs. Most bricks that will never be completely visible will just be a convex shape without studs or interio, so a 1x1 brick is just a box (in most cases, the studs and interior contain the most triangles as they are cylindrical). But the outer edges are beveled/chamfered with custom normals so every brick stands out when stacked together. The lowest LOD will then be the same but without the rounded edges.

I’ll try to show some pictures of the LODs later on, and possibly a simple example of a model.

Still, you have the cluttered geomoetry in terms of connected faces and tesselation.
What would be ideal, for a wall for example, not having it made out of individual bricks (even on simple LOD, you have the top/bottom faces that will never show), but instead make the wall one solid piece of geometry and use normal mapping to create the inter-brick grooves.

I also tried a bit with lego meshes (a while ago), and found out that individual bricks, no matter how simple they are (just boxes), are just not feasable… Also in terms of lightmapping, its a lot easier to work with a larger surface…

Unless… you write a Max script that:

  • checks all bricks for non visible faces and removes them
  • welds all congruent vertices
  • merge all coplanar faces to large quads
  • deletes all now freefloating vertices

I agree, normally I would spend time on optimizing models as much as possible. However, I don’t have a lot of spare time on my hands to optimize everything by hand. I want to spend my time as efficiently as possible. I’m aware that there is a lot of unnecessary geometry.

For large walls and interiors (like buildings, or space stations) I would build a small modular piece of wall out of bricks, then I manually optimize everything, or indeed use a normal map. That’s fine in this case, but some models are large and complex with bricks placed in such positions that it would require a lot of tedious work to optimize it and to check that all holes are closed. Regarding lightmapping, I also planned to use fully dynamic lighting, possibly with distance fields to bypass the triangle count using a replacement mesh. If it was feasible to import each brick separately and copy the bricks to build a model in the engine directly, I could create UVs for lightmaps and give each brick a low lightmap resolution. But yes, the triangle count is the main issue here. Making a script also takes a lot of time, obviously, but I don’t even know where to start. I planned to create a script to assemble a model automatically based on the LDraw/LDD file anyway, but that will already take a lot of time.

I don’t know how TT Games are doing it, but the bricks they have are all normal mapped to fake the round corners and use a baked AO map to fake the underside/interior. The problem for me is that it would again take a lot of time as I would have to unwrap every single brick, and I end up with dozens of textures. I could create an atlas out of it and such, but that would be more of a puzzle to get right.

Don’t get me wrong about the “too much work/time”. I just want to spend my time as efficiently as possible, so I try stuff out to see how far I can go with optimizing/baking, in as little time as possible.