-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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