User Rating: 5 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Active
 

Combine Decals at Runtime

Sometimes it's necessary to combine decals at runtime to a shared mesh to save draw calls. 

The method below makes use of Unity's built-in combine functionality to get the job done. Note that if the keepMaterials argument is set to true and the decals use different materials you may not be able to reduce draw calls.

    /// <summary>
    /// Combines decals to one mesh at runtime.
    /// </summary>
    /// <param name="decals">The decals to combine.</param>
    /// <param name="meshRenderer">The target mesh renderer</param>
    /// <param name="meshFilter">The target mesh filter</param>
    /// <param name="keepMaterials">When set to <c>false</c> the first material gets used, otherwise all materials get preserved. 
    /// Note that preserving all materials may result in more drawcalls</param>
    public static void Combine(IList<EasyDecal> decals, MeshRenderer meshRenderer, MeshFilter meshFilter, bool keepMaterials)
    {
        if (decals.Count == 0) { Debug.LogError("No decals to combine."); return; }

        List<MeshFilter> meshFilters = new List<MeshFilter>();
        List<Material> materials = new List<Material>();

        foreach (EasyDecal decal in decals)
        {
            meshFilters.Add(decal.MeshFilter);
            materials.Add(decal.DecalMaterial);
        }

        CombineInstance[] combineInstances = new CombineInstance[meshFilters.Count];

        for (int i = 0; i < meshFilters.Count; i++)
        {
            combineInstances[i].mesh = meshFilters[i].sharedMesh;

            // Transform from model to world space
            combineInstances[i].transform = meshFilters[i].transform.localToWorldMatrix;
        }

        meshFilter.mesh = new Mesh();

        if (keepMaterials)
        {
            meshFilter.mesh.CombineMeshes(combineInstances, false);

            meshRenderer.materials = materials.ToArray();
        }
        else
        {       
            meshFilter.mesh.CombineMeshes(combineInstances, true);

            meshRenderer.material = decals[0].DecalMaterial;
        }
    }

The Combine() method above can be used as the following:

 

using ch.sycoforge.Decal;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class RuntimeCombine : MonoBehaviour
{
    //------------------------------------
    // Exposed Fields
    //------------------------------------
    public EasyDecal[] Decals;

    //------------------------------------
    // Private Fields
    //------------------------------------
    private MeshRenderer meshRenderer;
    private MeshFilter meshFilter;

    //------------------------------------
    // Unity Methods
    //------------------------------------
    private void Start()
    {
        meshRenderer = GetComponent<MeshRenderer>();
        meshFilter = GetComponent<MeshFilter>();
    }

    private void Update()
    {
        // Combine when pressing Enter/Return
        if(Input.GetKeyUp(KeyCode.Return))
        {
            Combine(Decals, meshRenderer, meshFilter, false);
        }
    }

    private void OnDestroy()
    {
        // Cleanup runtime instance
        Destroy(meshFilter.mesh);
    }

}

While the method above will work for decals placed in the scene root (no parent GameObject). The method will fail if the decals are parented to a transformed GameObject. To account this fact one could rewrite the method to reflect the parent's world transformation.

/// <summary>
/// Combines decals to one mesh at runtime and creates a new <c>GameObject</c> as container.
/// </summary>
/// <param name="decals">The decals to combine.</param>
/// <param name="parent">The parent <c>GameObject</c> of the decals.</param>
/// <param name="keepMaterials">When set to <c>false</c> the first material gets used, otherwise all materials get preserved. 
/// Note that preserving all materials may result in more drawcalls</param>
/// <param name="deleteOriginals">When set to <c>true</c> the original decals get deleted. 
public static GameObject CombineToChild(IList<EasyDecal> decals, GameObject parent, bool keepMaterials, bool deleteOriginals)
{
    if (decals.Count == 0) { Debug.LogError("No decals to combine."); return null; }

    GameObject container = new GameObject("[Decal Container]");

    MeshRenderer meshRenderer = container.AddComponent<MeshRenderer>();
    MeshFilter meshFilter = container.AddComponent<MeshFilter>();


    List<MeshFilter> meshFilters = new List<MeshFilter>();
    List<Material> materials = new List<Material>();


    foreach (EasyDecal decal in decals)
    {
        meshFilters.Add(decal.MeshFilter);
        materials.Add(decal.DecalMaterial);
    }

    CombineInstance[] combineInstances = new CombineInstance[meshFilters.Count];

    for (int i = 0; i < meshFilters.Count; i++)
    {
        combineInstances[i].mesh = meshFilters[i].sharedMesh;

        Transform decalTransform = meshFilters[i].transform;

        // Create local-to-world transformation matrix
        Matrix4x4 localDecalTransform = decalTransform.localToWorldMatrix;

        // Transform from model to world space
        combineInstances[i].transform = localDecalTransform;
    }

    // Create new mesh
    meshFilter.mesh = new Mesh();
    meshFilter.mesh.name = "Combined Decals";

    if (keepMaterials)
    {
        meshFilter.mesh.CombineMeshes(combineInstances, false, true);

        meshRenderer.materials = materials.ToArray();
    }
    else
    {
        meshFilter.mesh.CombineMeshes(combineInstances, true, true);

        meshRenderer.material = decals[0].DecalMaterial;
    }

    container.transform.parent = parent.transform;

    if(deleteOriginals)
    {
        foreach(EasyDecal decal in decals)
        {
            Destroy(decal.gameObject);
        }
    }

    return container;
}