Skip to content

Instantly share code, notes, and snippets.

@lostfictions
Created June 22, 2018 20:02
Show Gist options
  • Save lostfictions/c6bd35bcc8f4b01d297bc1d2a78e1545 to your computer and use it in GitHub Desktop.
Save lostfictions/c6bd35bcc8f4b01d297bc1d2a78e1545 to your computer and use it in GitHub Desktop.
using UnityEngine;
using System;
using System.Collections.Generic;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
using System.Linq;
using System.IO;
using UnityEditor;
public class ModelPostprocessor : AssetPostprocessor
{
const string VERTEX_MATERIAL_PATH = "Assets/Materials/VertexColoured.mat";
static readonly string[] prefabDirs = {
"Assets/Prefabs/"
};
static Material vertexColourMaterial;
static Material VertexColourMaterial
{
get
{
if(vertexColourMaterial == null) {
vertexColourMaterial = (Material)AssetDatabase.LoadMainAssetAtPath(VERTEX_MATERIAL_PATH);
if(vertexColourMaterial == null) {
Debug.LogError("Can't find material at " + VERTEX_MATERIAL_PATH + "!");
}
}
return vertexColourMaterial;
}
}
#region Preprocessor Methods
void OnPreprocessModel()
{
var pathsToPreActions = new Dictionary<string, Action<ModelImporter>> {
{ "Assets/Models/Props", PreprocessProp },
{ "Assets/Models/Levels", PreprocessStatic },
{ "Assets/Models/Static", PreprocessStatic },
{ "Assets/Models/RiggedProps", PreprocessRiggedProp },
{ "Assets/Models/Clothes", _ => { }},
{ "Assets/Models/Characters", _ => { }}
};
var importer = (ModelImporter)assetImporter;
CheckPreprocessorMaterialImportSettings(importer);
bool didPreprocess = false;
foreach(var kvp in pathsToPreActions) {
if(assetPath.StartsWith(kvp.Key, StringComparison.InvariantCultureIgnoreCase)) {
kvp.Value(importer);
didPreprocess = true;
break;
}
}
if(!didPreprocess) {
Debug.LogWarning("Didn't preprocess model " + assetPath + " because it didn't match any preprocessing folders.");
}
}
static void PreprocessStatic(ModelImporter importer)
{
//importer.addCollider = true;
importer.importAnimation = false;
importer.animationType = ModelImporterAnimationType.None;
}
static void PreprocessProp(ModelImporter importer)
{
importer.addCollider = false; //we'll add our own later!
importer.importAnimation = false;
importer.animationType = ModelImporterAnimationType.None;
}
static void PreprocessRiggedProp(ModelImporter importer)
{
importer.addCollider = false; //we'll add our own later!
importer.importAnimation = true;
importer.animationType = ModelImporterAnimationType.Legacy;
}
static void CheckPreprocessorMaterialImportSettings(ModelImporter importer)
{
var path = Path.GetFileNameWithoutExtension(importer.assetPath);
if(string.IsNullOrEmpty(path)) {
Debug.LogWarning("Uhh");
return;
}
if(path.EndsWith("-vc")) {
importer.importMaterials = true;
importer.materialName = ModelImporterMaterialName.BasedOnModelNameAndMaterialName;
importer.materialSearch = ModelImporterMaterialSearch.Local;
}
else if(path.EndsWith("-vcbaked")) {
importer.importMaterials = false;
}
}
#endregion
#region Postprocessor Methods
void OnPostprocessModel(GameObject model)
{
var pathsToPostActions = new Dictionary<string, Action<GameObject>[]> {
{ "Assets/Models/Levels", new Action<GameObject>[] {
ReplaceInstances,
ReplacePrefabs,
SetStaticFlag
} },
{ "Assets/Models/Props", new Action<GameObject>[] {
ConfigureColliders,
TryConvertToVertexColoursAndReduce
} },
{ "Assets/Models/RiggedProps", new Action<GameObject>[] {
ConfigureColliders,
TryConvertToVertexColoursAndReduce
} },
{ "Assets/Models/Static", new Action<GameObject>[] {
SetStaticFlag,
TryConvertToVertexColoursAndReduce
} },
{ "Assets/Models/Clothes", new Action<GameObject>[] {
TryConvertToVertexColoursAndReduce
} },
{ "Assets/Models/Character", new Action<GameObject>[] { } }
};
bool didPostprocess = false;
foreach(var kvp in pathsToPostActions) {
if(assetPath.StartsWith(kvp.Key, StringComparison.InvariantCultureIgnoreCase)) {
foreach(var postAction in kvp.Value) {
postAction(model);
}
didPostprocess = true;
break;
}
}
if(!didPostprocess) {
Debug.LogWarning("Didn't postprocess model " + assetPath + " because it didn't match any postprocessing folders.");
}
}
static void ReplaceInstances(GameObject model)
{
//Find all meshes that start with our instance prefix, "i_"
var instances = model.GetComponentsInChildren<MeshFilter>(true)
.Where(mf => mf.name.StartsWith("i_", StringComparison.InvariantCultureIgnoreCase));
//Now group them, keyed on the "original" reference.
var groupedInstances = instances
.GroupBy(mf => instances.First(i => i.name == mf.name.Substring(0, mf.name.IndexOf('-') + 1)));
foreach(var group in groupedInstances) {
foreach(var mfInstance in group) {
mfInstance.sharedMesh = group.Key.sharedMesh;
mfInstance.GetComponent<MeshCollider>().sharedMesh = group.Key.sharedMesh;
}
Object.DestroyImmediate(group.Key.gameObject);
}
}
static void ReplacePrefabs(GameObject model)
{
//Find all meshes that start with our prefab prefix, "p_"
var replaceables = model.GetComponentsInChildren<MeshFilter>(true)
.Where(mf => mf.name.StartsWith("p_", StringComparison.InvariantCultureIgnoreCase));
var nameToPrefab = new Dictionary<string, GameObject>();
foreach(var mf in replaceables) {
string prefabName = mf.name.Substring("p_".Length, mf.name.IndexOf('-') - "p_".Length);
Debug.Log(prefabName);
GameObject prefab;
if(!nameToPrefab.TryGetValue(prefabName, out prefab)) {
foreach(var path in prefabDirs) {
//TODO: search in subdirectories.
prefab = (GameObject)AssetDatabase.LoadMainAssetAtPath(path + prefabName + ".prefab");
if(prefab != null) {
nameToPrefab.Add(prefabName, prefab);
break;
}
}
}
if(prefab == null) {
Debug.LogError("Prefab " + prefabName + " not found in any of the search directories!");
}
else {
var inst = (GameObject)Object.Instantiate(prefab, mf.transform.position, mf.transform.rotation);
inst.name = mf.name;
inst.transform.parent = mf.transform.parent;
}
Object.DestroyImmediate(mf.gameObject);
}
}
static void SetStaticFlag(GameObject model)
{
foreach(var t in model.GetComponentsInChildren<Transform>(true)) {
t.gameObject.isStatic = true;
}
}
static void ConfigureColliders(GameObject model)
{
var renderers = new List<Renderer>(model.GetComponentsInChildren<Renderer>(true));
var boxColliderRenderers = renderers.Where(r => r.name.StartsWith("boxcollider_", StringComparison.InvariantCultureIgnoreCase)).ToList();
var meshColliderRenderers = renderers.Where(r => r.name.StartsWith("meshcollider_", StringComparison.InvariantCultureIgnoreCase)).ToList();
bool hasOwnColliders = boxColliderRenderers.Concat(meshColliderRenderers).Any();
if(hasOwnColliders) {
foreach(var r in boxColliderRenderers) {
r.gameObject.AddComponent<BoxCollider>();
Object.DestroyImmediate(r.GetComponent<MeshFilter>());
Object.DestroyImmediate(r);
}
foreach(var r in meshColliderRenderers) {
var mc = r.gameObject.AddComponent<MeshCollider>();
mc.convex = true;
mc.sharedMesh = r.GetComponent<MeshFilter>().sharedMesh;
Object.DestroyImmediate(r);
}
}
else {
foreach(var r in renderers) {
// We have to check the mesh name here rather than the gameobject name, since the
// underscore seem to get suppressed on objects at the root of the hierarchy.
if(r.GetComponent<MeshFilter>() && r.GetComponent<MeshFilter>().sharedMesh.name.StartsWith("_")) {
continue;
}
var collider = r.gameObject.AddComponent<MeshCollider>();
collider.convex = true;
}
}
}
void TryConvertToVertexColoursAndReduce(GameObject go)
{
var path = Path.GetFileNameWithoutExtension(assetPath);
if(path.EndsWith("-vc")) {
var renderers = go.GetComponentsInChildren<Renderer>(true);
foreach(var r in renderers) {
ProcessVertexColoursAndUpdateMesh(r);
AssignNewMaterials(r);
}
}
else if(path.EndsWith("-vcbaked"))
{
var renderers = go.GetComponentsInChildren<Renderer>(true);
foreach(var r in renderers) {
AssignNewMaterials(r);
}
}
}
void ProcessVertexColoursAndUpdateMesh(Renderer r)
{
var vertexColourCombines = new List<CombineInstance>();
var texturedCombines = new List<CombineInstance>();
bool isSkinnedMesh = false;
Mesh mesh;
var smr = r as SkinnedMeshRenderer;
if(smr != null) {
mesh = smr.sharedMesh;
isSkinnedMesh = true;
}
else {
mesh = r.GetComponent<MeshFilter>().sharedMesh;
}
//TODO: figure this out if it's worthwhile...
if(isSkinnedMesh) {
Debug.LogWarning("Vertex colour conversion doesn't support skinned meshes yet!\nCheck back later.");
return;
}
var mats = r.sharedMaterials;
//Extract submeshes by type: textured, plain colour
for(int i = 0; i < mats.Length; i++) {
if(mats[i].mainTexture != null) {
var ci = new CombineInstance { mesh = ExtractSubmesh(mesh, i, true) };
texturedCombines.Add(ci);
}
else {
var colorMesh = ExtractSubmesh(mesh, i);
Color[] colors = new Color[colorMesh.vertexCount];
Color vertexColor = mats[i].color;
for(int j = 0; j < colors.Length; j++) {
colors[j] = vertexColor;
}
colorMesh.colors = colors;
var ci = new CombineInstance { mesh = colorMesh };
vertexColourCombines.Add(ci);
}
}
//Now combine each of our submesh instances in turn, and then combine those to get a final multimaterial mesh.
var finalCombine = new List<CombineInstance>();
//If there are textures, we don't combine the submeshes for them...
bool hasTextures = texturedCombines.Count > 0;
if(hasTextures) {
var ci = new CombineInstance();
var ciMesh = new Mesh();
ciMesh.CombineMeshes(texturedCombines.ToArray(), false, false);
ciMesh.Optimize();
ciMesh.name = "Textured";
ci.mesh = ciMesh;
finalCombine.Add(ci);
}
//...but for vertex colours, we squash them all down to a single submesh.
bool hasVertexColours = vertexColourCombines.Count > 0;
if(hasVertexColours) {
var ci = new CombineInstance();
var ciMesh = new Mesh();
ciMesh.CombineMeshes(vertexColourCombines.ToArray(), true, false);
ciMesh.Optimize();
ciMesh.name = "VertexColours";
ci.mesh = ciMesh;
finalCombine.Add(ci);
}
mesh = new Mesh();
mesh.CombineMeshes(finalCombine.ToArray(), false, false);
mesh.Optimize();
var newDir = assetPath.Substring(0, assetPath.IndexOf(Path.GetExtension(assetPath), StringComparison.Ordinal));
if(!Directory.Exists(newDir)) {
Directory.CreateDirectory(newDir);
}
var combinedMeshAssetPath = newDir + "/CombinedMesh-" + r.name + ".asset";
var existingCombinedMeshAsset = (Mesh)AssetDatabase.LoadMainAssetAtPath(combinedMeshAssetPath);
if(existingCombinedMeshAsset != null) {
// Tried using EditorUtility.CopySerialized, as suggested in
// http://answers.unity3d.com/questions/24929/assetdatabase-replacing-an-asset-but-leaving-refer.html
// ...but it seems to explode the mesh sometimes, so let's just copy all the properties??
existingCombinedMeshAsset.Clear();
existingCombinedMeshAsset.vertices = mesh.vertices;
existingCombinedMeshAsset.colors32 = mesh.colors32;
existingCombinedMeshAsset.normals = mesh.normals;
existingCombinedMeshAsset.tangents = mesh.tangents;
existingCombinedMeshAsset.uv = mesh.uv;
existingCombinedMeshAsset.uv2 = mesh.uv2;
existingCombinedMeshAsset.subMeshCount = mesh.subMeshCount;
existingCombinedMeshAsset.triangles = mesh.triangles;
existingCombinedMeshAsset.RecalculateBounds();
AssetDatabase.SaveAssets();
mesh = existingCombinedMeshAsset;
}
else {
AssetDatabase.CreateAsset(mesh, combinedMeshAssetPath);
}
{
var mf = r.GetComponent<MeshFilter>();
if(mf != null) {
mf.sharedMesh = mesh;
}
else {
var sm = r.GetComponent<SkinnedMeshRenderer>();
if(sm != null) {
sm.sharedMesh = mesh;
}
else {
Debug.LogWarning("Unrecognized renderer type on " + r.name + "! Aborting.");
return;
}
}
}
}
static void AssignNewMaterials(Renderer r)
{
var mats = r.sharedMaterials;
var newMats = new List<Material>();
newMats.AddRange(mats.Where(mat => mat.mainTexture != null));
newMats.Add(VertexColourMaterial);
r.sharedMaterials = newMats.ToArray();
}
static Mesh ExtractSubmesh(Mesh mesh, int subMeshIndex, bool includeUVs = false)
{
int[] tris = mesh.GetTriangles(subMeshIndex);
var newVerts = new List<Vector3>();
var newNormals = new List<Vector3>();
var newUVs = new List<Vector2>();
var newTris = new List<int>();
var oldTrisToNew = new Dictionary<int, int>();
foreach(int oldTriIndex in tris) {
int newTriIndex;
if(!oldTrisToNew.TryGetValue(oldTriIndex, out newTriIndex)) {
newTriIndex = newVerts.Count;
newVerts.Add(mesh.vertices[oldTriIndex]);
newNormals.Add(mesh.normals[oldTriIndex]);
if(includeUVs) {
newUVs.Add(mesh.uv[oldTriIndex]);
}
oldTrisToNew.Add(oldTriIndex, newTriIndex);
}
newTris.Add(newTriIndex);
}
var newMesh = new Mesh {
vertices = newVerts.ToArray(),
normals = newNormals.ToArray()
};
if(includeUVs) {
newMesh.uv = newUVs.ToArray();
}
newMesh.triangles = newTris.ToArray();
return newMesh;
}
#endregion
static void OnPostprocessAllAssets(
// ReSharper disable once ParameterTypeCanBeEnumerable.Local
string[] importedAssets,
// ReSharper disable UnusedParameter.Local
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths)
// ReSharper restore UnusedParameter.Local
{
foreach(string fullPath in importedAssets) {
var ext = Path.GetExtension(fullPath);
if(ext.ToLowerInvariant() == ".fbx") {
var fn = Path.GetFileNameWithoutExtension(fullPath);
if(fn.EndsWith("-vc")) {
var matPaths = Directory.GetFiles(Path.GetDirectoryName(fullPath) + "/Materials", "*.mat");
foreach(string matPath in matPaths) {
if(Path.GetFileName(matPath).StartsWith(fn, StringComparison.InvariantCultureIgnoreCase)) {
var mat = (Material)AssetDatabase.LoadMainAssetAtPath(matPath);
if(mat.mainTexture == null) {
//Debug.Log("Cleaning up " + matPath);
AssetDatabase.DeleteAsset(matPath);
}
}
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment