Getting started with draw call optimization in Unity
Written by Jesper Tingvall, Product Expert, Simplygon
Disclaimer: The code in this post is written using version 10.4.232.0 of Simplygon and Unity 2022.3.37f1. If you encounter this post at a later stage, some of the API calls might have changed. However, the core concepts should still remain valid.
Introduction
In this blog we'll have a look at how to optimize draw calls in Unity using material merging. We'll cover how to both UI and C# scripting.
Prerequisites
This example will use the Simplygon integration in Unity with the URP render pipeline, but the same concepts can be applied to all other integrations and Unity render pipelines.
Problem to solve
We have a character model that we would like to optimize in terms of draw calls and texture usage. A good use case for this is optimizing our game for a weaker platform where we are struggling with draw calls and texture budget. The model uses three Universal Render Pipeline/Lit
shaders based materials; body, eyes and clothes. The materials has following channels.
- Base map
- Metallic map
- Normal map
- Occlusion map
- Emission map (only body material)
Texture sizes:
- Eyes 1024x1024
- Body 4096x4096
- Clothes 2048x2048
Solution
We are going to use aggregation with material baking to merge the materials.
User interface
To access the aggregator pipeline we open up the Simplygon window in Unity (Windows → Simplygon) and click Add LOD Component → Template → Basic → Aggregation with Material Baking.
Here we are going to uncheck Enable Geometry Culling as we do not want to remove any geometry from our model. The output texture resolution can be changed under MappingImageSettings → OutputMaterialSettings → Texture Width & Texture Height. We set this to 512. We are now reducing the texture quality of the model so can expect to notice that in output.
We now need to add material casters for all of the material channels in our model. In Unity we use compute casters to cast materials. They support a subset of the shaders in Unity.
A material caster casts one texture channel. We need one caster per material channel we want in the output material. Simplygon can automatically detect which material channels are in the input material and add appropriate casters. To do this click the Gear Icon → Automatic.
We now have material casters for each material channel.
We initiate processing by clicking the yellow Simplygon logo.
Scripted Aggregation
It is possible to perform the exact same operation using the Simplygon C# API. Here we'll just do a little example of it. The real benefit of scripting aggregation is that it allows you to automate your asset pipeline.
We start by creating an AggregationPipeline
and set the same settings as we had on our previous pipeline, EnableGeometryCulling = false
and MergeGeometries = true
.
We do the same for the mapping image which handles material casting from the old model to the new aggregated one. Here we also set TextureHeight
and TextureWidth
.
private static spAggregationPipeline CreateAggregationPipeline(ISimplygon simplygon, uint textureHeight, uint textureWidth)
{
// Create a aggregation pipeline
var simplygonAggregationPipeline = simplygon.CreateAggregationPipeline();
// Set aggregation settings
var simplygonAggregationSettings = simplygonAggregationPipeline.GetAggregationSettings();
simplygonAggregationSettings.SetEnableGeometryCulling(false);
simplygonAggregationSettings.SetMergeGeometries(true);
// Enable creation of mapping image which allows us to do material casting
var simplygonMappingImageSettings = simplygonAggregationPipeline.GetMappingImageSettings();
simplygonMappingImageSettings.SetGenerateMappingImage(true);
simplygonMappingImageSettings.SetGenerateTexCoords(true);
simplygonMappingImageSettings.SetGenerateTangents(true);
simplygonMappingImageSettings.SetUseFullRetexturing(true);
simplygonMappingImageSettings.SetApplyNewMaterialIds(true);
// Set output texture size
var simplygonOutputMaterialSettings = simplygonMappingImageSettings.GetOutputMaterialSettings(0);
simplygonOutputMaterialSettings.SetTextureHeight(textureHeight);
simplygonOutputMaterialSettings.SetTextureWidth(textureWidth);
return simplygonAggregationPipeline;
}
In Unity we use compute casters to bake materials. We do some setup behind the scenes making material casting work for the following shaders.
- Standard Shader
- Universal Rendering Pipeline (URP) Lit shaders
- High Definition Render Pipeline (HDRP) Lit shaders
The only thing we need to specify in the compute caster is MaterialChannel
and OutputColorSpace
. Consult this table on which color spaces that are supported.
private static spMaterialCaster CreateMaterialCaster(ISimplygon simplygon, string channel, EImageColorSpace colorSpace)
{
var caster = simplygon.CreateComputeCaster();
var casterSettings = caster.GetComputeCasterSettings();
casterSettings.SetMaterialChannel(channel);
casterSettings.SetOutputColorSpace(colorSpace);
return caster;
}
Let's now put it all together. First we initialize Simplygon. After that we create an aggregation pipeline using function we created earlier and add material casters for all channels we want to have in our resulting material. Once that is done we create a SimplygonProcessing
that handles exporting from Unity, processing in Simplygon and import of result. We tell it to start the processing.
using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon
(out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
{
// if Simplygon handle is valid
if (simplygonErrorCode == Simplygon.EErrorCodes.NoError)
{
var simplygonAggregationPipeline = CreateAggregationPipeline(simplygon, TextureSize, TextureSize);
// Add material casters for URP render pipeline.
// If you are using another render pipeline you need to change the channel names.
// Consult following: https://documentation.simplygon.com/SimplygonSDK_10.3.2100.0/unity/concepts/materialmapping.html
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "BaseMap", EImageColorSpace.sRGB), 0);
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "NormalMap", EImageColorSpace.Linear), 0);
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "EmissionMap", EImageColorSpace.sRGB), 0);
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "MetallicMap", EImageColorSpace.Linear), 0);
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "OcclusionMap", EImageColorSpace.Linear), 0);
// Run Simplygon processing.
var simplygonProcessing = new SimplygonProcessing(simplygon);
simplygonProcessing.Run(simplygonAggregationPipeline, Selection.gameObjects.ToList(), EPipelineRunMode.RunInThisProcess, true);
...
We can make an easy entry point to our script with MenuItem
.
[MenuItem("Simplygon/Simple Scripted Aggregation")]
static void EntryPoint()
{
...
This adds a new menu option to Unity which runs our script on the selected GameObject.
Result
After processing we get this result. As expected we get lower texture quality, but that is our intention.


Here is the resulting texture. We can see that it contains parts from both the eyes, clothes and body.
So what have we gained with performing aggregation with material merging? Let's compare original and optimized model. We have gone down to only one draw call which will help runtime performance on weaker platforms. It also uses way less texture memory which helps with storage requirements.
Model | Draw calls | Texture usage |
---|---|---|
Original | 3 | 5x 4096x4096, 5x 2048x2048 & 5x 1024x1024 |
Optimized | 1 | 5x 512x512 |
Next step
We have now taken the first steps to optimize a model using aggregation with material casting. Here are some next steps that could be interesting to investigate.
Currently the eye texture uses up a very large portion of our texture space. This is because Simplygon preserves the original texture density in the original Chart Aggregation Mode. If this is not something which we want we can change Chart Aggregation Mode into Surface area. In this mode the geometry size of the objects influences how the UV charts are scaled during aggregation.
If we are porting the game to a more low end platform we probably also want to reduce the triangle count. This can be achieved with our triangle reducer which also can be configured to bake materials. There is a basic pipeline for this Template->Basic->Reduction with Material Baking.
Complete script
using Simplygon;
using Simplygon.Unity.EditorPlugin;
using System.Linq;
using UnityEditor;
using UnityEngine;
public class SimpleAggregation
{
static private uint TextureSize = 512;
/// <summary>
/// Add material caster for specified material channel name.
/// </summary>
private static spMaterialCaster CreateMaterialCaster(ISimplygon simplygon, string channel, EImageColorSpace colorSpace)
{
var caster = simplygon.CreateComputeCaster();
var casterSettings = caster.GetComputeCasterSettings();
casterSettings.SetMaterialChannel(channel);
casterSettings.SetOutputColorSpace(colorSpace);
return caster;
}
/// <summary>
/// Create an aggregation pipeline with material casting for specified texture resolution.
/// </summary>
private static spAggregationPipeline CreateAggregationPipeline(ISimplygon simplygon, uint textureHeight, uint textureWidth)
{
// Create a aggregation pipeline
var simplygonAggregationPipeline = simplygon.CreateAggregationPipeline();
// Set aggregation settings
var simplygonAggregationSettings = simplygonAggregationPipeline.GetAggregationSettings();
simplygonAggregationSettings.SetEnableGeometryCulling(false);
simplygonAggregationSettings.SetMergeGeometries(true);
// Enable creation of mapping image which allows us to do material casting
var simplygonMappingImageSettings = simplygonAggregationPipeline.GetMappingImageSettings();
simplygonMappingImageSettings.SetGenerateMappingImage(true);
simplygonMappingImageSettings.SetGenerateTexCoords(true);
simplygonMappingImageSettings.SetGenerateTangents(true);
simplygonMappingImageSettings.SetUseFullRetexturing(true);
simplygonMappingImageSettings.SetApplyNewMaterialIds(true);
// Set output texture size
var simplygonOutputMaterialSettings = simplygonMappingImageSettings.GetOutputMaterialSettings(0);
simplygonOutputMaterialSettings.SetTextureHeight(textureHeight);
simplygonOutputMaterialSettings.SetTextureWidth(textureWidth);
return simplygonAggregationPipeline;
}
/// <summary>
/// Aggregates and bakes material for selected asset.
/// </summary>
[MenuItem("Simplygon/Simple Scripted Aggregation")]
static void EntryPoint()
{
if (Selection.gameObjects.Length > 0)
{
using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon
(out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
{
// if Simplygon handle is valid
if (simplygonErrorCode == Simplygon.EErrorCodes.NoError)
{
var simplygonAggregationPipeline = CreateAggregationPipeline(simplygon, TextureSize, TextureSize);
// Add material casters for URP render pipeline.
// If you are using another render pipeline you need to change the channel names.
// Consult following: https://documentation.simplygon.com/SimplygonSDK_10.3.2100.0/unity/concepts/materialmapping.html
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "BaseMap", EImageColorSpace.sRGB), 0);
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "NormalMap", EImageColorSpace.Linear), 0);
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "EmissionMap", EImageColorSpace.sRGB), 0);
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "MetallicMap", EImageColorSpace.Linear), 0);
simplygonAggregationPipeline.AddMaterialCaster(CreateMaterialCaster(simplygon, "OcclusionMap", EImageColorSpace.Linear), 0);
// Run Simplygon processing.
var simplygonProcessing = new SimplygonProcessing(simplygon);
simplygonProcessing.Run(simplygonAggregationPipeline, Selection.gameObjects.ToList(), EPipelineRunMode.RunInThisProcess, true);
}
// if invalid handle, output error message to the Unity console
else
{
Debug.LogError("Simplygon initializing failed!");
}
}
}
}
}