Skip to content

Instantly share code, notes, and snippets.

@louisgv
Forked from luciditee/BatchBurner.cs
Created December 23, 2016 09:20
Show Gist options
  • Save louisgv/56d27302f3c4cd324e7c4b2a94496b16 to your computer and use it in GitHub Desktop.
Save louisgv/56d27302f3c4cd324e7c4b2a94496b16 to your computer and use it in GitHub Desktop.
Batch Burner, a script designed to reduce static mesh draw calls in Unity scenes with a large number of static mesh entities.
/*
* 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>
/// 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>
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) {
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.
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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment