using System.IO; using UnityEditor; using UnityEngine; using UnityMeshSimplifier; namespace VATPipeline.EditorTools { /// /// Step 1 of VAT pipeline: decimate (reduce polygons on) a SkinnedMesh while /// preserving bone weights / bind poses so the animation still aligns with skeleton. /// public class MeshDecimatorWindow : EditorWindow { private GameObject _targetGo; private float _quality = 0.3f; private bool _preserveBorderEdges = true; private bool _preserveSurfaceCurvature = false; private bool _enableSmartLinking = true; private string _outputFolder = "Assets/Generated/VAT/Mesh"; private bool _replaceSharedMesh = true; [MenuItem("Tools/VAT Pipeline/1. Decimate SkinnedMesh", priority = 1)] public static void Open() { var w = GetWindow("Mesh Decimator"); w.minSize = new Vector2(420, 360); } private void OnGUI() { EditorGUILayout.LabelField("Step 1: Mesh Decimation (UnityMeshSimplifier)", EditorStyles.boldLabel); EditorGUILayout.Space(); _targetGo = (GameObject)EditorGUILayout.ObjectField( new GUIContent("Target", "GameObject (or its child) carrying a SkinnedMeshRenderer."), _targetGo, typeof(GameObject), true); EditorGUILayout.Space(); EditorGUILayout.LabelField("Decimation Settings", EditorStyles.boldLabel); _quality = EditorGUILayout.Slider( new GUIContent("Quality", "0.1 = keep ~10% of triangles | 1.0 = no reduction."), _quality, 0.05f, 1.0f); _preserveBorderEdges = EditorGUILayout.Toggle( new GUIContent("Preserve Border Edges", "Keep open-edge silhouettes intact (recommended)."), _preserveBorderEdges); _preserveSurfaceCurvature = EditorGUILayout.Toggle( new GUIContent("Preserve Surface Curvature", "Bias collapse cost on highly-curved areas (slower, higher quality)."), _preserveSurfaceCurvature); _enableSmartLinking = EditorGUILayout.Toggle( new GUIContent("Enable Smart Linking", "Merge duplicate vertices across submeshes before reduction."), _enableSmartLinking); EditorGUILayout.Space(); EditorGUILayout.LabelField("Output", EditorStyles.boldLabel); _outputFolder = EditorGUILayout.TextField("Output Folder", _outputFolder); _replaceSharedMesh = EditorGUILayout.Toggle( new GUIContent("Replace SMR.sharedMesh", "After saving, also swap the SkinnedMeshRenderer to use the decimated mesh in this prefab/scene."), _replaceSharedMesh); EditorGUILayout.Space(); DrawTargetInfo(); EditorGUILayout.Space(); using (new EditorGUI.DisabledScope(_targetGo == null)) { if (GUILayout.Button("Decimate", GUILayout.Height(32))) { Decimate(); } } EditorGUILayout.Space(); EditorGUILayout.HelpBox( "Pipeline:\n" + " 1. (here) Decimate SkinnedMesh\n" + " 2. Bake VAT -> Window/AnimMapBaker\n" + "After step 2 you get a Mesh + Material + Prefab driven by texture-fetched vertices, fully GPU-instanced.", MessageType.Info); } private void DrawTargetInfo() { if (_targetGo == null) return; var smr = _targetGo.GetComponentInChildren(); if (smr == null || smr.sharedMesh == null) { EditorGUILayout.HelpBox("No SkinnedMeshRenderer / Mesh found on target.", MessageType.Warning); return; } var src = smr.sharedMesh; int tris = src.triangles.Length / 3; int verts = src.vertexCount; int afterTris = Mathf.Max(1, (int)(tris * _quality)); EditorGUILayout.HelpBox( $"Mesh: {src.name}\n" + $"Verts: {verts} Tris: {tris}\n" + $"Bones: {src.bindposes.Length}\n" + $"After decimation (~{_quality:P0}): ~{afterTris} tris", MessageType.None); } private void Decimate() { var smr = _targetGo.GetComponentInChildren(); if (smr == null || smr.sharedMesh == null) { EditorUtility.DisplayDialog("Decimator", "Target has no SkinnedMeshRenderer / sharedMesh.", "OK"); return; } var src = smr.sharedMesh; int srcTris = src.triangles.Length / 3; try { EditorUtility.DisplayProgressBar("Decimating mesh...", $"Reducing {src.name} to {_quality:P0}", 0.2f); var simplifier = new MeshSimplifier(); simplifier.PreserveBorderEdges = _preserveBorderEdges; simplifier.PreserveSurfaceCurvature = _preserveSurfaceCurvature; simplifier.EnableSmartLink = _enableSmartLinking; simplifier.Initialize(src); EditorUtility.DisplayProgressBar("Decimating mesh...", "Running simplification...", 0.5f); simplifier.SimplifyMesh(_quality); EditorUtility.DisplayProgressBar("Decimating mesh...", "Building result mesh...", 0.85f); var dst = simplifier.ToMesh(); dst.name = $"{src.name}_q{Mathf.RoundToInt(_quality * 100)}"; dst.bindposes = src.bindposes; dst.RecalculateBounds(); EnsureFolder(_outputFolder); var path = $"{_outputFolder}/{dst.name}.asset"; AssetDatabase.CreateAsset(dst, path); AssetDatabase.SaveAssets(); if (_replaceSharedMesh) { Undo.RecordObject(smr, "Decimate Mesh"); smr.sharedMesh = dst; EditorUtility.SetDirty(smr); if (PrefabUtility.IsPartOfPrefabInstance(smr)) { PrefabUtility.RecordPrefabInstancePropertyModifications(smr); } } int dstTris = dst.triangles.Length / 3; Debug.Log($"[MeshDecimator] {src.name}: {srcTris} -> {dstTris} tris ({(float)dstTris/srcTris:P0}) saved at {path}"); EditorUtility.DisplayDialog("Decimator", $"OK\n\nSource: {srcTris} tris\nResult: {dstTris} tris\n\nSaved: {path}", "Continue to Step 2"); } catch (System.Exception ex) { Debug.LogException(ex); EditorUtility.DisplayDialog("Decimator failed", ex.Message, "OK"); } finally { EditorUtility.ClearProgressBar(); } } private static void EnsureFolder(string path) { if (AssetDatabase.IsValidFolder(path)) return; var parts = path.Split('/'); var cur = parts[0]; for (int i = 1; i < parts.Length; i++) { var next = $"{cur}/{parts[i]}"; if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]); cur = next; } } } }