The goal of this optimization guide is to learn about the combination of methods that the Sandstorm development team uses to optimize its levels.
The Sandstorm editor comes with all official maps containing their respective development sublevels, which can also be viewed for additional reference.
You’ll want to limit draw calls around the map as much as possible, as they’re heavy on the CPU. It's important to keep the communication between CPU to GPU to a minimum. Every new material that contains a unique set of textures is an expensive draw call. The CPU constantly has to tell the GPU what the specifications are of the new material that needs to be rendered. We can reduce the communication between the CPU to GPU by using less unique materials. Say we have a building with a brick material, plaster material, concrete and wood. All those materials are unique, if we want to create variations of those materials, different color bricks, plasters etc. If these materials all use their own unique textures a simple building can become very expensive for the engine to render and will cost a lot of memory. In this guide we describe several ways to reduce draw calls.
Color Masked Materials
Materials that have a color mask can be duplicated multiple times. Each duplicate can then have different values for its color mask and this allows us to make color variations of the same material.
While these color masks create additional draw calls, they are essentially very cheap draw calls because these materials reuse the same textures, only the color changes.
color variations of the same material using a color mask
The editor comes with plenty of color masked materials, many of which can be found when typing “masked” in the Content Browser’s search bar.
Modular kits using the same texture atlas
The modding tools come with several themed modular kits that each use only a single material, often with additional color variations (using color masks) available.
Below you can see a few examples of this, both show only a selection of the available meshes in each kit.
Metal Shack Kit:
Prop sets Materials
There are also a big number of (themed) prop sets available, each created in such a way they work great together and share the same material. Below you can see a few examples.
fastfood and spiceshop prop sets each use only one material
In some of the earlier Sandstorm maps rock props usually had their own material, based on Megascans. These materials would therefore only be compatible with one or sometimes a few assets.
For optimization reasons, we moved away from this later on and started using tiling rock materials instead. The benefit of the tiling rock materials is that they’re compatible with a much wider range of rock props. The below screenshot explains this.
Left are two rocks with unique non-tiling materials, middle are two rocks with the same non-tiling material (notice the black spots) and right are the two rocks with a tiling material
While tiling rock materials tend to lose some of the fidelity compared to the unique non-tiling materials, the benefit of reduced materials (especially in large landscapes) far outweighs this.
You can quickly find the tiling materials when typing “rocktiling” in the Content Browser’s search bar.
Fade out distance
Individual objects like static meshes or decals can all have a fade out distance applied to them.
Fade distances help to reduce draw calls by not rendering small objects at medium or large distances.
Foliage that is added to the landscape using the Foliage Tool gets grouped into batches that use instancing, so their meshes use only one draw call to render. However, their amount of unique materials can become expensive.
That’s why it’s best to limit the amount of FoliageType assets for landscape foliage to 2 to 3, unless they share the same material.
As previously mentioned, actor merging is a great way to reduce the number of draw calls for meshes. Let’s consider the following basic building as an example. It is made of 68 modular meshes, not including windows, doors and the balcony.
Basic building made out of 68 modular meshes
Divide the building in such a way that there’s a different group for the front, back, two sides and the top. The green corners roughly show where one group ends and the next one starts.
Left shows actor groups for back and front, right show actor groups for left, right and top
Before actually starting to merge actors together, we want to make sure there is a backup, in case we need to make adjustments. So we duplicate whatever we want to merge into a separate hidden sublevel.
When that’s done we can merge each group together into a new mesh. Select a group and right click on it, then click “Merge Actors” and a new window will pop up. Enable both “Pivot Point at Zero” and “Replace Source Actors”.
Merge Actors window
After merging, our example building is made out of only 5 meshes, excluding windows, doors and the balcony.
Note: in some occasions it’s better to merge buildings into one mesh. For example, backdrop buildings outside the playable area visible to the player will always be fully rendered anyway.
Actor merged meshes can be further optimized by altering their lightmap values. For example, a mesh that is constructed with six to eight individual default wall pieces (400*400) can have its lightmap reduced to 128. If it’s smaller or made of very simple geometry, it can even be lowered to 64. This can sometimes be a bit of a trial & error process until you find the best setting.
Another method to reduce draw calls is using Instanced Static Meshes. This is an optimization method that turns multiple meshes into a single actor by using offset duplicates of the same static mesh. Because of this, it can only be used on actors using the same static mesh.
static mesh actors all using the same modular window
Like with actor merging, we need to make sure everything we want to instance is backed up in a separate sublevel. Instancing will replace the separate static meshes with an InstancedStaticMeshComponent, which will prevent you from making any changes afterwards.
When the static meshes we want to instance are grouped together, we right click and select “Merge Actors”. A new window opens and at the top we click on the third tab.
In this tab we can create InstancedStaticMeshComponents. It’ll list the Mesh Components that are to be instanced. Uncheck “Use HLOD Volumes”.
When everything is set up correctly, we click “Merge Actors” in the bottom right of the window.
We now have a single draw call for the entire facade, not taking into account draw calls for materials.
Because instancing is about offsetting the same static mesh, it’s more likely to suffer from lighting inaccuracies or glitches. Therefore, instancing may not always be the best optimization method for base geometry, like walls, floors and ceilings.
However, it’s great for things like trims, fences, decorative pieces, etc.
Note: The engine has an automated instance system, but there is a small overhead on the CPU to calculate what meshes need to be instanced in a frame. In more dense levels containing a lot of objects it's advisable to instance meshes manually.
Light actors’ Mobility can be set to three different options: Static, Stationary and Movable. Static means the light gets baked, a Stationary light is baked lighting and casts shadows for moving objects and Movable is fully dynamic. This makes Static light actors the cheapest by far and in 99% of cases that’s what we should use. We should also limit the attenuation radius of light actors to the minimum that is needed.
Wherever possible, “Cast Shadows” should be disabled. This works great for fill lights, for example. This is an option for all light actors, but can also be disabled on individual objects.
It stands for Hierarchical Level Of Detail and is a way to combine multiple static meshes into one mesh to reduce draw calls over longer distance. The process consists of two parts: generating clusters and generating proxy meshes.
Before we can do start doing that, we need to enable “Enable Hierarchical LODSystem” in “World Settings”.
Generating clusters can be done automatically or manually. The former will include anything in its radius though, which is not what we want. We only want the exterior walls of buildings to be included, as whatever is inside will be culled automatically depending on the angle.
Think of the exterior walls of each building as a “shell” that hides what’s inside. Check out some of the official maps in the editor to get a sense of cluster size. Once the clusters are set up properly, it’s time to generate the proxy meshes. Fortunately, this can be done automatically. Just hit “Generate Proxy Meshes” and wait for it to finish.
If it crashes during this process, group the clusters together in smaller groups instead of one big list. Don’t forget to save after each smaller group has been generated.
For more in-depth information about cluster generation and proxy mesh building, we refer to:
Useful (editor) console commands
allows you to inspect what is being rendered from a specific location + angle in the viewport
stat rhi - DrawPrimitive calls
Depending on the complexity of the map, the numbers below can be used as guidelines:
2000 to 3000 is reasonable
5000 is high
10000 is problematic