Skip to content

Instantly share code, notes, and snippets.

@luciditee
Last active June 16, 2018 17:09

Revisions

  1. @sigtau sigtau revised this gist Dec 19, 2016. 1 changed file with 7 additions and 1 deletion.
    8 changes: 7 additions & 1 deletion BatchBurner.cs
    Original file line number Diff line number Diff line change
    @@ -48,7 +48,13 @@ public sealed class BatchBurner : MonoBehaviour {
    /// A positive unsigned value is needed for the batcher to work correctly.
    /// </summary>
    public float preferredRadius = 50f;


    /// <summary>
    /// If set to true, this forces the batcher to ONLY consider batching meshes together
    /// when they share a layer. If not, the entire scene is considered.
    /// </summary>
    public bool groupByLayer = false;

    /// <summary>
    /// A set of layer masks to be ALWAYS considered by the batching process.
    /// </summary>
  2. @sigtau sigtau revised this gist Dec 19, 2016. 1 changed file with 17 additions and 15 deletions.
    32 changes: 17 additions & 15 deletions BatchBurner.cs
    Original file line number Diff line number Diff line change
    @@ -3,13 +3,13 @@
    * with a large amount of static geometry entities.
    *
    * Copyright 2016-2017 Will Preston & Die-Cast Magic Studios, LLC.
    *
    * Licensed under the Apache License, Version 2.0 (the "License");
    * you may not use this file except in compliance with the License.
    * You may obtain a copy of the License at
    *
    * http://www.apache.org/licenses/LICENSE-2.0
    *
    * Unless required by applicable law or agreed to in writing, software
    * distributed under the License is distributed on an "AS IS" BASIS,
    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    @@ -178,20 +178,22 @@ void Burn() {
    if (current.sharedMesh == filters[i].sharedMesh
    && Vector3.Distance(current.transform.position,
    filters[i].transform.position) <= preferredRadius) {
    // ...ensure that adding this mesh won't overflow the
    // vertex count of future meshes past VERTEX_MAX.
    if ((vtxCounts[current] + filters[i].sharedMesh
    .vertexCount) >= VERTEX_MAX) {
    // If it does, just break out--this cluster is full.
    break;
    } else {
    // If it doesn't, we're good.
    // Add them to a cluster.
    cluster.Add(filters[i]);
    filters.RemoveAt(i);
    if ((groupByLayer && filters[i].gameObject.layer
    == current.gameObject.layer) || !groupByLayer) {
    // ...ensure that adding this mesh won't overflow the
    // vertex count of future meshes past VERTEX_MAX.
    if ((vtxCounts[current] + filters[i].sharedMesh
    .vertexCount) >= VERTEX_MAX) {
    // If it does, just break out--this cluster is full.
    break;
    } else {
    // If it doesn't, we're good.
    // Add them to a cluster.
    cluster.Add(filters[i]);
    filters.RemoveAt(i);
    }
    }
    }

    }

    // Add this cluster to the list of clusters if it's not singular.
  3. @sigtau sigtau created this gist Dec 19, 2016.
    230 changes: 230 additions & 0 deletions BatchBurner.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,230 @@
    /*
    * Unity Batch Burner: A script designed to reduce static mesh draw calls automatically in scenes
    * with a large amount of static geometry entities.
    *
    * Copyright 2016-2017 Will Preston & Die-Cast Magic Studios, LLC.
    * Licensed under the Apache License, Version 2.0 (the "License");
    * you may not use this file except in compliance with the License.
    * You may obtain a copy of the License at
    * http://www.apache.org/licenses/LICENSE-2.0
    * Unless required by applicable law or agreed to in writing, software
    * distributed under the License is distributed on an "AS IS" BASIS,
    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    * See the License for the specific language governing permissions and
    * limitations under the License.
    *
    */

    using System.Collections.Generic;
    using UnityEngine;

    /// <summary>
    /// A behavior that runs at scene start to combine static objects in the scene
    /// together into a single mesh to save up to 95% of draw calls.
    /// </summary>
    public sealed class BatchBurner : MonoBehaviour {

    /// <summary>
    /// The maximum number of vertices in a mesh. This is a Unity-imposed default,
    /// but a code-based value did not exist. It is the same as 1111 1111 1111 1111,
    /// or the maximum 16-bit unsigned integer.
    /// </summary>
    public static readonly int VERTEX_MAX = 65535;

    /// <summary>
    /// An array of the possible states of the burner, in human-readable format.
    /// </summary>
    public static readonly string[] StatusString = {
    "Gathering filters", "Creating clusters", "Merging meshes"
    };

    /// <summary>
    /// The radius of the clusters gathered in the batching process. A higher number
    /// entails larger clusters, so this should be considered on a per-scene basis.
    ///
    /// A positive unsigned value is needed for the batcher to work correctly.
    /// </summary>
    public float preferredRadius = 50f;

    /// <summary>
    /// A set of layer masks to be ALWAYS considered by the batching process.
    /// </summary>
    public LayerMask alwaysChoose = 0;

    /// <summary>
    /// A set of layer masks to be ALWAYS ignored by the batching process.
    /// </summary>
    public LayerMask alwaysIgnore = 0;

    /// <summary>
    /// A list of transforms for the batcher to ignore individually, regardless of layer.
    /// </summary>
    public Transform[] forceIgnore = null;

    /// <summary>
    /// A property that can be accessed to get the current human-readable state of the batcher.
    /// </summary>
    public string Status {
    get { return StatusString[statusIndex]; }
    }

    /// <summary>
    /// A singleton reference to the currently active batcher. Returns null if one is
    /// not available in the scene or has been destroyed.
    /// </summary>
    public BatchBurner Active {
    get { return current; }
    }

    /// <summary>
    /// A collection of all mesh filters in the scene.
    /// </summary>
    private List<MeshFilter> filters = new List<MeshFilter>();

    /// <summary>
    /// A collection of collections consisting of meshes that share data.
    /// </summary>
    private List<List<MeshFilter>> clusters = new List<List<MeshFilter>>();

    /// <summary>
    /// A dictionary that uses the first mesh in each cluster to keep track of the
    /// number of vertices found in this cluster, to ensure a cluster does not exceed
    /// VERTEX_MAX.
    /// </summary>
    private Dictionary<MeshFilter, int> vtxCounts = new Dictionary<MeshFilter, int>();

    /// <summary>
    /// The array index of the current human-readable status of the batcher.
    /// </summary>
    private int statusIndex = 0;

    /// <summary>
    /// The local singleton reference to the batch burner.
    /// </summary>
    private static BatchBurner current = null;

    /// <summary>
    /// Called on the object's start, this begins the batch burn process.
    /// </summary>
    void Awake() {
    current = this;
    Debug.Log("Beginning batch burn.");
    if (preferredRadius > 0) Burn();
    else Debug.LogWarning("Positive, nonzero preferred radius "
    + "is required for normal burn functionality.");
    }

    /// <summary>
    /// Performs the mesh batching process.
    /// </summary>
    void Burn() {
    // Phase 1: Gather
    // Any mesh that is not marked as Lightmap Static is discarded from the list.
    // Meshes whose layer match the mask found in alwaysChoose are exempt from this check.
    // Meshes whose layer match them ask in alwaysIgnore are never exempt.
    // Get all meshes in the scene.
    filters.AddRange(FindObjectsOfType<MeshFilter>());

    // Apply filtering to strip out nonstatic meshes.
    for (int i = filters.Count-1; i > 0; --i) {
    if (!(filters[i].gameObject.isStatic ||
    (filters[i].gameObject.layer & alwaysChoose) != 0) ||
    (filters[i].gameObject.layer & alwaysIgnore) != 0) {
    if (forceIgnore == null) {
    // Ignore list is empty
    filters.RemoveAt(i);
    } else {
    // Make sure this mesh is not included in the ignore list.
    foreach (Transform ignore in forceIgnore) {
    if (ignore == filters[i].transform) {
    filters.RemoveAt(i);
    break;
    }
    }
    }
    }
    }

    // Stop if we ended up filtering the scene completely, and pop a warning.
    if (filters.Count == 0) {
    Debug.LogWarning("Batch burner was unable to find meshes to combine.");
    return;
    }

    Debug.Log("Burner found " + filters.Count + " meshes.");
    statusIndex++;

    // Phase 2: Cluster creation.
    // Pick a mesh (starting with the first). Iterate to see what meshes are near it
    // that happen to fall within preferred radius and happen to share both a mesh
    // and a material. Add them to a cluster if they meet this criteria. Once added
    // to a cluster, the mesh is cleared from the renderer list.
    while (filters.Count > 0) {
    // Add the first we find, and give it its own cluster.
    MeshFilter current = filters[0];
    MeshRenderer currentRenderer = current.GetComponent<MeshRenderer>();
    filters.RemoveAt(0);
    List<MeshFilter> cluster = new List<MeshFilter>();
    cluster.Add(current);
    vtxCounts.Add(current, current.mesh.vertexCount);

    // Iterate over filter list to find the nearby shared meshes and strip
    // them from the filter list as needed.
    for (int i = filters.Count-1; i > 0; --i) {
    // If they meet the mesh/distance criteria...
    if (current.sharedMesh == filters[i].sharedMesh
    && Vector3.Distance(current.transform.position,
    filters[i].transform.position) <= preferredRadius) {
    // ...ensure that adding this mesh won't overflow the
    // vertex count of future meshes past VERTEX_MAX.
    if ((vtxCounts[current] + filters[i].sharedMesh
    .vertexCount) >= VERTEX_MAX) {
    // If it does, just break out--this cluster is full.
    break;
    } else {
    // If it doesn't, we're good.
    // Add them to a cluster.
    cluster.Add(filters[i]);
    filters.RemoveAt(i);
    }
    }

    }

    // Add this cluster to the list of clusters if it's not singular.
    if (cluster.Count > 1) {
    clusters.Add(cluster);
    //Debug.Log("Clustered " + cluster.Count + " meshes ( "
    // + current.gameObject.name + ")");
    }
    }

    Debug.Log("Created a total of " + clusters.Count
    + " clusters, combining.");
    statusIndex++;

    // Phase 3: Merge clusters & disable originals.
    foreach (List<MeshFilter> cluster in clusters) {
    CombineInstance[] combine = new CombineInstance[cluster.Count];
    int i = 0;
    while (i < cluster.Count) {
    combine[i].mesh = cluster[i].sharedMesh;
    combine[i].transform = cluster[i].transform.localToWorldMatrix;
    MeshRenderer rend = cluster[i].GetComponent<MeshRenderer>();
    if (rend != null) rend.enabled = false;
    i++;
    }

    GameObject clusterGroup = new GameObject();
    MeshFilter filter = clusterGroup.AddComponent<MeshFilter>();
    MeshRenderer renderer = clusterGroup.AddComponent<MeshRenderer>();
    filter.mesh = new Mesh();
    filter.mesh.CombineMeshes(combine);
    renderer.material = cluster[0].GetComponent<MeshRenderer>().material;
    }
    }

    }