Skip to content

Instantly share code, notes, and snippets.

@zanders3
Created April 20, 2016 08:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zanders3/cd0b920aab47ec77b5ec4b9fa2942714 to your computer and use it in GitHub Desktop.
Save zanders3/cd0b920aab47ec77b5ec4b9fa2942714 to your computer and use it in GitHub Desktop.
Unity Optimisation Tools
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;
}
}
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));
}
}
}
}
}
}
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