Getting started with draw call optimization in Unity

Green alien

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

Character model

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.

Unity UI selecting aggregation  with material baking pipeline

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.

Aggregation optimization settings

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.

Gear icon → Automatic for material casters

We now have material casters for each material channel.

Material channel casters

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.

Original
Aggregation

Here is the resulting texture. We can see that it contains parts from both the eyes, clothes and body.

Aggregated BaseMap, NormalMap, EmissionMap, MetallicMap and OcclusionMap

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!");
                }
            }
        }
    }
}
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*