181 lines
7.5 KiB
C#

using System.IO;
using UnityEditor;
using UnityEngine;
using UnityMeshSimplifier;
namespace VATPipeline.EditorTools
{
/// <summary>
/// Step 1 of VAT pipeline: decimate (reduce polygons on) a SkinnedMesh while
/// preserving bone weights / bind poses so the animation still aligns with skeleton.
/// </summary>
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<MeshDecimatorWindow>("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<SkinnedMeshRenderer>();
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<SkinnedMeshRenderer>();
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;
}
}
}
}