Created
June 22, 2018 20:02
-
-
Save lostfictions/c6bd35bcc8f4b01d297bc1d2a78e1545 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 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