181 lines
7.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|