Unity Optimisation Tools
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
using System.Collections.Generic; | |
using System.Linq; | |
using UnityEngine; | |
public class MeshProxy | |
{ | |
public List<Vector3> Vertices = new List<Vector3>(); | |
public List<Vector3> Normals = new List<Vector3>(); | |
public List<Vector2> UVs = new List<Vector2>(); | |
public List<int> Tris = new List<int>(); | |
public Material Material; | |
static Dictionary<int, int> indMappings = new Dictionary<int, int>(); | |
public static void MergeBySharedMaterial(GameObject meshRoot, MeshFilter[] meshes, System.Action<List<MeshProxy>> remapMaterials = null) | |
{ | |
//Split all submeshes into independent meshes and gather all meshes and their materials into a single list | |
List<MeshProxy> allMeshes = new List<MeshProxy>(); | |
foreach (MeshFilter meshFilter in meshes) | |
{ | |
allMeshes.AddRange( | |
FromMesh( | |
meshFilter.sharedMesh, | |
meshFilter.GetComponent<MeshRenderer>().sharedMaterials, | |
meshFilter.transform.localToWorldMatrix | |
)); | |
} | |
//Remap materials or modify meshes if needed (used for e.g. texture mapping or automatic material replacement/merging) | |
if (remapMaterials != null) | |
remapMaterials(allMeshes); | |
//Merge meshes that share the same material | |
List<MeshProxy> finalMeshes = new List<MeshProxy>(); | |
foreach (var group in allMeshes.GroupBy(m => m.Material)) | |
finalMeshes.Add(Merge(group.ToList())); | |
//Merge the final meshes into a single mesh | |
//If the mesh has more than ushort.MaxValue verts then split it into separate objects | |
List<MeshProxy> outputMeshes = new List<MeshProxy>(); | |
int vertCount = 0; | |
foreach (MeshProxy proxy in finalMeshes) | |
{ | |
if (vertCount + proxy.Vertices.Count >= ushort.MaxValue) | |
{ | |
ToMeshFilter(outputMeshes, meshRoot); | |
outputMeshes.Clear(); | |
} | |
outputMeshes.Add(proxy); | |
} | |
ToMeshFilter(outputMeshes, meshRoot); | |
} | |
//Splits each submesh into its own mesh proxy and only copies used verts | |
static List<MeshProxy> FromMesh(Mesh mesh, Material[] materials, Matrix4x4 transform) | |
{ | |
Debug.Assert(materials.Length == mesh.subMeshCount); | |
List<MeshProxy> meshes = new List<MeshProxy>(mesh.subMeshCount); | |
for (int i = 0; i<mesh.subMeshCount; i++) | |
{ | |
Vector3[] verts = mesh.vertices; | |
Vector3[] normals = mesh.normals; | |
Vector2[] uvs = mesh.uv; | |
MeshProxy proxy = new MeshProxy(); | |
proxy.Material = materials[i]; | |
foreach (int ind in mesh.GetTriangles(i)) | |
{ | |
int newInd; | |
if (!indMappings.TryGetValue(ind, out newInd)) | |
{ | |
newInd = proxy.Vertices.Count; | |
indMappings.Add(ind, newInd); | |
proxy.Vertices.Add(transform.MultiplyPoint(verts[ind])); | |
if (uvs.Length > 0) | |
proxy.UVs.Add(uvs[ind]); | |
if (normals.Length > 0) | |
proxy.Normals.Add(transform.MultiplyVector(normals[ind])); | |
} | |
proxy.Tris.Add(newInd); | |
} | |
indMappings.Clear(); | |
meshes.Add(proxy); | |
} | |
return meshes; | |
} | |
//Merges the list of meshes into the first mesh proxy instance | |
static MeshProxy Merge(List<MeshProxy> meshes) | |
{ | |
bool hasNormals = meshes.Any(mesh => mesh.Normals.Count > 0); | |
bool hasUVs = meshes.Any(mesh => mesh.UVs.Count > 0); | |
MeshProxy proxy = meshes[0]; | |
if (hasNormals && proxy.Normals.Count == 0) | |
proxy.Normals.AddRange(Enumerable.Range(0, proxy.Vertices.Count).Select(i => Vector3.up)); | |
if (hasUVs && proxy.UVs.Count == 0) | |
proxy.UVs.AddRange(Enumerable.Range(0, proxy.Vertices.Count).Select(i => Vector2.zero)); | |
for (int i = 1; i<meshes.Count; i++) | |
{ | |
proxy.Tris.AddRange(meshes[i].Tris.Select(ind => ind + proxy.Vertices.Count)); | |
proxy.Vertices.AddRange(meshes[i].Vertices); | |
if (hasNormals && meshes[i].Normals.Count == 0) | |
proxy.Normals.AddRange(Enumerable.Range(0, meshes[i].Vertices.Count).Select(j => Vector3.up)); | |
else | |
proxy.Normals.AddRange(meshes[i].Normals); | |
if (hasUVs && meshes[i].UVs.Count == 0) | |
proxy.UVs.AddRange(Enumerable.Range(0, meshes[i].Vertices.Count).Select(j => Vector2.zero)); | |
else | |
proxy.UVs.AddRange(meshes[i].UVs); | |
} | |
return proxy; | |
} | |
static void ToMeshFilter(List<MeshProxy> meshes, GameObject meshRoot) | |
{ | |
string meshName = meshes[0].Material.name; | |
GameObject meshChild = new GameObject(meshName); | |
meshChild.AddComponent<MeshFilter>().sharedMesh = ToMesh(meshName, meshes); | |
meshChild.AddComponent<MeshRenderer>().sharedMaterials = meshes.Select(m => m.Material).ToArray(); | |
meshChild.transform.SetParent(meshRoot.transform); | |
} | |
static Mesh ToMesh(string name, List<MeshProxy> meshes) | |
{ | |
Debug.Assert(meshes.Sum(m => m.Vertices.Count) < ushort.MaxValue); | |
List<Vector3> verts = meshes[0].Vertices; | |
List<Vector3> normals = meshes[0].Normals; | |
List<Vector2> uvs = meshes[0].UVs; | |
int vertOffset = meshes[0].Vertices.Count; | |
for (int i = 1; i<meshes.Count; i++) | |
{ | |
Debug.Assert(meshes[i].Vertices.Count == meshes[i].Normals.Count && meshes[i].Vertices.Count == meshes[i].UVs.Count); | |
verts.AddRange(meshes[i].Vertices); | |
normals.AddRange(meshes[i].Normals); | |
uvs.AddRange(meshes[i].UVs); | |
} | |
Mesh mesh = new Mesh(); | |
mesh.name = name; | |
mesh.SetVertices(verts); | |
mesh.SetNormals(normals); | |
mesh.SetUVs(0, uvs); | |
mesh.SetTriangles(meshes[0].Tris, 0); | |
mesh.subMeshCount = meshes.Count; | |
for (int i = 1; i < meshes.Count; i++) | |
{ | |
int[] tris = meshes[i].Tris.Select(tri => tri + vertOffset).ToArray(); | |
mesh.SetTriangles(tris, i); | |
vertOffset += meshes[i].Vertices.Count; | |
} | |
mesh.Optimize(); | |
mesh.UploadMeshData(true); | |
return mesh; | |
} | |
} |
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
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using UnityEditor; | |
using UnityEngine; | |
public static class ObjExporter | |
{ | |
[MenuItem("Util/Optimise/Merge")] | |
public static void Merge() | |
{ | |
if (Selection.activeGameObject == null) | |
return; | |
MeshFilter[] meshes = Selection.activeGameObject.GetComponentsInChildren<MeshFilter>(); | |
MeshProxy.MergeBySharedMaterial(Selection.activeGameObject, meshes); | |
foreach (MeshFilter filter in meshes) | |
GameObject.DestroyImmediate(filter); | |
} | |
[MenuItem("Util/Optimise/Merge and Atlas")] | |
public static void MergeAndAtlas() | |
{ | |
if (Selection.activeGameObject == null) | |
return; | |
MeshFilter[] meshes = Selection.activeGameObject.GetComponentsInChildren<MeshFilter>(); | |
MeshProxy.MergeBySharedMaterial(Selection.activeGameObject, meshes, TextureAtlaser.AtlasMeshes); | |
foreach (MeshFilter filter in meshes) | |
GameObject.DestroyImmediate(filter); | |
} | |
static string SaveTextureIfNeeded(string path, Texture2D texture) | |
{ | |
string texturePath = AssetDatabase.GetAssetPath(texture); | |
if (!string.IsNullOrEmpty(texturePath)) | |
return texturePath; | |
string savePath = Path.GetDirectoryName(path) + "/" + texture.name + ".png"; | |
File.WriteAllBytes(savePath, texture.EncodeToPNG()); | |
return savePath; | |
} | |
[MenuItem("Util/Optimise/Export OBJ")] | |
public static void Export() | |
{ | |
if (Selection.activeGameObject == null) | |
return; | |
string path = EditorUtility.SaveFilePanel("Export OBJ", "Assets", "mesh.obj", "obj"); | |
if (string.IsNullOrEmpty(path)) | |
return; | |
MeshFilter[] meshes = Selection.activeGameObject.GetComponentsInChildren<MeshFilter>(); | |
string matLibPath = Path.GetDirectoryName(path) + Path.GetFileNameWithoutExtension(path) + ".mtl"; | |
{ | |
List<Material> materials = meshes.SelectMany(m => m.GetComponent<MeshRenderer>().sharedMaterials).ToList(); | |
for (int i = 0; i < materials.Count; i++) | |
materials[i].name = "mat" + i; | |
using (StreamWriter writer = new StreamWriter(matLibPath)) | |
{ | |
writer.WriteLine("# " + Selection.activeGameObject.name); | |
foreach (Material material in materials) | |
{ | |
writer.WriteLine("newmtl " + material.name); | |
if (material.mainTexture != null) | |
{ | |
string texturePath = SaveTextureIfNeeded(path, (Texture2D)material.mainTexture); | |
if (!string.IsNullOrEmpty(texturePath)) | |
writer.WriteLine("map_Ka " + texturePath); | |
} | |
} | |
} | |
} | |
using (StreamWriter writer = new StreamWriter(path)) | |
{ | |
writer.WriteLine("# " + Selection.activeGameObject.name); | |
writer.WriteLine("mtllib " + Path.GetFileName(matLibPath)); | |
foreach (MeshFilter mesh in Selection.activeGameObject.GetComponentsInChildren<MeshFilter>()) | |
{ | |
writer.WriteLine("o " + mesh.name); | |
foreach (Vector3 vert in mesh.sharedMesh.vertices) | |
writer.WriteLine("v " + vert.x + " " + vert.y + " " + vert.z); | |
foreach (Vector2 uv in mesh.sharedMesh.uv) | |
writer.WriteLine("vt " + uv.x + " " + uv.y); | |
foreach (Vector3 norm in mesh.sharedMesh.normals) | |
writer.WriteLine("vn " + norm.x + " " + norm.y + " " + norm.z); | |
bool hasUV = mesh.sharedMesh.uv.Length > 0; | |
bool hasNorm = mesh.sharedMesh.normals.Length > 0; | |
Material[] materials = mesh.GetComponent<MeshRenderer>().sharedMaterials; | |
for (int i = 0; i<mesh.sharedMesh.subMeshCount; i++) | |
{ | |
writer.WriteLine("usemtl " + materials[i].name); | |
writer.WriteLine("g grp" + i); | |
int[] tris = mesh.sharedMesh.GetTriangles(i); | |
if (hasUV && hasNorm) | |
{ | |
for (int j = 0; j < tris.Length; j += 3) | |
writer.WriteLine("f " + (tris[j]+1) + "/" + (tris[j]+1) + "/" + (tris[j]+1) + " " + (tris[j+1]+1) + "/" + (tris[j+1]+1) + "/" + (tris[j+1]+1) + " " + (tris[j+2]+1) + "/" + (tris[j+2]+1) + "/" + (tris[j+2]+1)); | |
} | |
else if (hasUV) | |
{ | |
for (int j = 0; j < tris.Length; j += 3) | |
writer.WriteLine("f " + (tris[j]+1) + "/" + (tris[j]+1) + " " + (tris[j+1]+1) + "/" + (tris[j+1]+1) + " " + (tris[j+2]+1) + "/" + (tris[j+2]+1)); | |
} | |
else if (hasNorm) | |
{ | |
for (int j = 0; j < tris.Length; j += 3) | |
writer.WriteLine("f " + (tris[j]+1) + "//" + (tris[j]+1) + " " + (tris[j+1]+1) + "//" + (tris[j+1]+1) + " " + (tris[j+2]+1) + "//" + (tris[j+2]+1)); | |
} | |
else | |
{ | |
for (int j = 0; j < tris.Length; j += 3) | |
writer.WriteLine("f " + (tris[j]+1) + " " + (tris[j+1]+1) + " " + (tris[j+2]+1)); | |
} | |
} | |
} | |
} | |
} | |
} |
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
using System.Collections.Generic; | |
using UnityEngine; | |
using System.Linq; | |
public class TextureAtlaser | |
{ | |
//Merges and atlases meshes that share the same shader. | |
//Caveats: 1. this will only work on the .mainTexture property 2. this will trash any material specific properties | |
public static void AtlasMeshes(List<MeshProxy> meshProxy) | |
{ | |
//Group mesh proxies by the same shader | |
foreach (var group in meshProxy.GroupBy(m => m.Material.shader)) | |
{ | |
Material material = new Material(group.Key); | |
material.CopyPropertiesFromMaterial(group.First().Material); | |
material.name = group.Key.name; | |
List<MeshProxy> meshes = group.ToList(); | |
material.mainTexture = CreateAtlasAndOffset(meshes); | |
material.mainTextureOffset = Vector2.zero; | |
material.mainTextureScale = Vector2.one; | |
foreach (MeshProxy proxy in meshes) | |
proxy.Material = material; | |
break; | |
} | |
} | |
static Texture2D CreateAtlasAndOffset(List<MeshProxy> meshes) | |
{ | |
Texture2D[] textures = meshes.Select(m => (Texture2D)m.Material.mainTexture).Distinct().ToArray(); | |
Texture2D atlas = new Texture2D(1, 1); | |
atlas.name = meshes[0].Material.name + "Atlas"; | |
Rect[] rects = atlas.PackTextures(textures, 0); | |
for (int i = 0; i<meshes.Count; i++) | |
{ | |
Texture2D targetTexture = (Texture2D)meshes[i].Material.mainTexture; | |
int textureIdx = -1; | |
for (int j = 0; j<textures.Length; j++) | |
if (targetTexture == textures[j]) | |
{ | |
textureIdx = j; | |
break; | |
} | |
Debug.Assert(textureIdx != -1); | |
if (textureIdx == -1) | |
continue; | |
Rect rect = rects[textureIdx]; | |
Vector4 rectOffset = new Vector4(rect.xMin, rect.yMin, rect.width, rect.height); | |
List<Vector2> uvs = meshes[i].UVs; | |
for (int j = 0; j < uvs.Count; j++) | |
uvs[j] = new Vector2(rectOffset.x + rectOffset.z * uvs[j].x, rectOffset.y + rectOffset.w * uvs[j].y); | |
} | |
return atlas; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment