diff --git a/Editor/EditModePreview.meta b/Editor/EditModePreview.meta
new file mode 100644
index 000000000..b2b130867
--- /dev/null
+++ b/Editor/EditModePreview.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ebb5beda53504474adcc5086a8409ba7
+timeCreated: 1695047925
\ No newline at end of file
diff --git a/Editor/EditModePreview/BlendShapePreviewContext.cs b/Editor/EditModePreview/BlendShapePreviewContext.cs
new file mode 100644
index 000000000..e54c78fc9
--- /dev/null
+++ b/Editor/EditModePreview/BlendShapePreviewContext.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Jobs;
+using UnityEngine;
+
+namespace Anatawa12.AvatarOptimizer.EditModePreview
+{
+ ///
+ /// Preview Context that holds information about BlendShapes
+ ///
+ class BlendShapePreviewContext : IDisposable
+ {
+ private readonly int _vertexCount;
+ // blendshape vertex transforms. _blendShapeVertices[vertexIndex + frameIndex * vertexCount]
+ private NativeArray _blendShapeVertices;
+ // frame info. _blendShapeFrameInfo[blendShapeIndex][frameIndexInShape]
+ private readonly (float weight, int globalIndex)[][] _blendShapeFrameInfo;
+
+ public BlendShapePreviewContext(Mesh originalMesh)
+ {
+ _vertexCount = originalMesh.vertexCount;
+
+ // BlendShapes
+ var blendShapeFrameCount = 0;
+ for (var i = 0; i < originalMesh.blendShapeCount; i++)
+ blendShapeFrameCount += originalMesh.GetBlendShapeFrameCount(i);
+
+ _blendShapeFrameInfo = new (float weight, int globalIndex)[originalMesh.blendShapeCount][];
+ _blendShapeVertices = new NativeArray(_vertexCount * blendShapeFrameCount, Allocator.Persistent);
+ var frameIndex = 0;
+ var getterBuffer = new Vector3[_vertexCount];
+ for (var i = 0; i < originalMesh.blendShapeCount; i++)
+ {
+ var frameCount = originalMesh.GetBlendShapeFrameCount(i);
+ var thisShapeInfo = _blendShapeFrameInfo[i] = new (float weight, int globalIndex)[frameCount];
+ for (var j = 0; j < frameCount; j++)
+ {
+ originalMesh.GetBlendShapeFrameVertices(i, j, getterBuffer, null, null);
+ getterBuffer.AsSpan()
+ .CopyTo(_blendShapeVertices.AsSpan().Slice(frameIndex * _vertexCount, _vertexCount));
+ thisShapeInfo[j] = (originalMesh.GetBlendShapeFrameWeight(i, j), frameIndex);
+ frameIndex++;
+ }
+ }
+ }
+
+ public void ComputeBlendShape(
+ float[] blendShapeWeights,
+ NativeSlice originalVertices,
+ NativeSlice outputVertices)
+ {
+ System.Diagnostics.Debug.Assert(originalVertices.Length == _vertexCount);
+ System.Diagnostics.Debug.Assert(outputVertices.Length == _vertexCount);
+
+ var frameIndices = new List();
+ var frameWeights = new List();
+
+ for (var i = 0; i < blendShapeWeights.Length; i++)
+ GetBlendShape(i, blendShapeWeights[i], frameIndices, frameWeights);
+
+ if (frameWeights.Count == 0)
+ {
+ outputVertices.CopyFrom(originalVertices);
+ return;
+ }
+
+ using (var blendShapeFrameIndices = new NativeArray(frameIndices.ToArray(), Allocator.TempJob))
+ using (var blendShapeFrameWeights = new NativeArray(frameWeights.ToArray(), Allocator.TempJob))
+ {
+ new ApplyBlendShapeJob
+ {
+ OriginalVertices = originalVertices,
+ BlendShapeVertices = _blendShapeVertices,
+ BlendShapeFrameIndices = blendShapeFrameIndices,
+ BlendShapeFrameWeights = blendShapeFrameWeights,
+ ResultVertices = outputVertices
+ }.Schedule(outputVertices.Length, 1).Complete();
+ }
+ }
+
+ [BurstCompile]
+ struct ApplyBlendShapeJob: IJobParallelFor
+ {
+ [ReadOnly]
+ public NativeSlice OriginalVertices;
+ [ReadOnly]
+ public NativeArray BlendShapeVertices;
+ [ReadOnly]
+ public NativeArray BlendShapeFrameIndices;
+ [ReadOnly]
+ public NativeArray BlendShapeFrameWeights;
+ public NativeSlice ResultVertices;
+
+ public void Execute (int vertexIndex)
+ {
+ var vertexCount = OriginalVertices.Length;
+ var original = OriginalVertices[vertexIndex];
+
+ for (var indicesIndex = 0; indicesIndex < BlendShapeFrameIndices.Length; indicesIndex++)
+ {
+ var frameIndex = BlendShapeFrameIndices[indicesIndex];
+ var weight = BlendShapeFrameWeights[indicesIndex];
+ var delta = BlendShapeVertices[frameIndex * vertexCount + vertexIndex];
+ original += delta * weight;
+ }
+
+ ResultVertices[vertexIndex] = original;
+ }
+ }
+
+ public void Dispose()
+ {
+ _blendShapeVertices.Dispose();
+ }
+
+ private void GetBlendShape(int shapeIndex, float weight, List frameIndices, List frameWeights)
+ {
+ const float blendShapeEpsilon = 0.0001f;
+ var frames = _blendShapeFrameInfo[shapeIndex];
+
+ if (Mathf.Abs(weight) <= blendShapeEpsilon && ZeroForWeightZero())
+ {
+ // the blendShape is not active
+ return;
+ }
+
+ bool ZeroForWeightZero()
+ {
+ if (frames.Length == 1) return true;
+ var first = frames.First();
+ var end = frames.Last();
+
+ // both weight are same sign, zero for 0 weight
+ if (first.weight <= 0 && end.weight <= 0) return true;
+ if (first.weight >= 0 && end.weight >= 0) return true;
+
+ return false;
+ }
+
+ if (frames.Length == 1)
+ {
+ // simplest and likely
+ var frame = frames[0];
+ frameIndices.Add(frame.globalIndex);
+ frameWeights.Add(weight / frame.weight);
+ }
+ else
+ {
+ // multi frame
+
+ var firstFrame = frames[0];
+ var lastFrame = frames.Last();
+
+ if (firstFrame.weight > 0 && weight < firstFrame.weight)
+ {
+ // if all weights are positive and the weight is less than first weight: lerp 0..first
+ frameIndices.Add(firstFrame.globalIndex);
+ frameWeights.Add(weight / firstFrame.weight);
+ }
+
+ if (lastFrame.weight < 0 && weight > lastFrame.weight)
+ {
+ // if all weights are negative and the weight is more than last weight: lerp last..0
+ frameIndices.Add(lastFrame.globalIndex);
+ frameWeights.Add(weight / lastFrame.weight);
+ }
+
+ // otherwise, lerp between two surrounding frames OR nearest two frames
+ var (lessFrame, greaterFrame) = FindFrame();
+ var weightDiff = greaterFrame.weight - lessFrame.weight;
+ var lessFrameWeight = (weight - lessFrame.weight) / weightDiff;
+ var graterFrameWeight = (greaterFrame.weight - weight) / weightDiff;
+
+ if (!(Mathf.Abs(lessFrameWeight) < blendShapeEpsilon))
+ {
+ frameIndices.Add(lessFrame.globalIndex);
+ frameWeights.Add(lessFrameWeight);
+ }
+
+ if (!(Mathf.Abs(graterFrameWeight) < blendShapeEpsilon))
+ {
+ frameIndices.Add(greaterFrame.globalIndex);
+ frameWeights.Add(graterFrameWeight);
+ }
+ }
+
+ return;
+
+ // TODO: merge this logic with it in MeshInfo2
+ ((float weight, int globalIndex), (float weight, int globalIndex)) FindFrame()
+ {
+ for (var i = 1; i < frames.Length; i++)
+ {
+ if (weight <= frames[i].weight)
+ return (frames[i - 1], frames[i]);
+ }
+
+ return (frames[frames.Length - 2], frames[frames.Length - 1]);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/EditModePreview/BlendShapePreviewContext.cs.meta b/Editor/EditModePreview/BlendShapePreviewContext.cs.meta
new file mode 100644
index 000000000..bae6bed02
--- /dev/null
+++ b/Editor/EditModePreview/BlendShapePreviewContext.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e053c35a488c4e40ae440d1619782165
+timeCreated: 1695053866
\ No newline at end of file
diff --git a/Editor/EditModePreview/MeshPreviewController.cs b/Editor/EditModePreview/MeshPreviewController.cs
new file mode 100644
index 000000000..245ba1e4a
--- /dev/null
+++ b/Editor/EditModePreview/MeshPreviewController.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using JetBrains.Annotations;
+using UnityEditor;
+using UnityEngine;
+using Object = UnityEngine.Object;
+#if !UNITY_2020_1_OR_NEWER
+using AnimationModeDriver = UnityEngine.Object;
+#endif
+
+namespace Anatawa12.AvatarOptimizer.EditModePreview
+{
+ class MeshPreviewController : ScriptableSingleton
+ {
+ [CanBeNull] private RemoveMeshPreviewController _previewController;
+ public static bool Previewing => instance.previewing;
+
+ [SerializeField] private bool previewing;
+ [SerializeField] private Mesh previewMesh;
+ [SerializeField] private Mesh originalMesh;
+ [SerializeField] private SkinnedMeshRenderer targetRenderer;
+ [SerializeField] private AnimationModeDriver driverCached;
+
+ private AnimationModeDriver DriverCached => driverCached ? driverCached : driverCached = CreateDriver();
+
+ public bool Enabled
+ {
+ get => EditorPrefs.GetBool("com.anatawa12.avatar-optimizer.mesh-preview.enabled", true);
+ set => EditorPrefs.SetBool("com.anatawa12.avatar-optimizer.mesh-preview.enabled", value);
+ }
+
+ private void OnEnable()
+ {
+ EditorApplication.update -= Update;
+ EditorApplication.update += Update;
+ }
+
+ private void OnDisable()
+ {
+ _previewController?.Dispose();
+ EditorApplication.update -= Update;
+ }
+
+ private Object ActiveEditor()
+ {
+ var editors = ActiveEditorTracker.sharedTracker.activeEditors;
+ return editors.Length == 0 ? null : editors[0].target;
+ }
+
+ private void Update()
+ {
+ if (previewing)
+ {
+ if (_previewController == null)
+ _previewController = new RemoveMeshPreviewController(targetRenderer, originalMesh, previewMesh);
+
+ if (targetRenderer == null || ActiveEditor() != targetRenderer.gameObject)
+ {
+ StopPreview();
+ return;
+ }
+
+ if (_previewController.UpdatePreviewing())
+ StopPreview();
+ }
+ else
+ {
+ if (Enabled && StateForImpl(null) == PreviewState.PreviewAble)
+ {
+ var editorObj = ActiveEditor();
+ if (editorObj is GameObject go &&
+ RemoveMeshPreviewController.EditorTypes.Any(t => go.GetComponent(t)))
+ {
+ StartPreview(go);
+ }
+ }
+ }
+ }
+
+ public enum PreviewState
+ {
+ PreviewAble,
+ PreviewingThat,
+
+ PreviewingOther,
+ ActiveEditorMismatch,
+ }
+
+ public static PreviewState StateFor([CanBeNull] Component component) => instance.StateForImpl(component);
+
+ private PreviewState StateForImpl([CanBeNull] Component component)
+ {
+ var gameObject = component ? component.gameObject : null;
+
+ if (previewing && targetRenderer && targetRenderer.gameObject == gameObject)
+ return PreviewState.PreviewingThat;
+
+ if (AnimationMode.InAnimationMode())
+ return PreviewState.PreviewingOther;
+
+ if (gameObject && ActiveEditor() as GameObject != gameObject)
+ return PreviewState.ActiveEditorMismatch;
+
+ return PreviewState.PreviewAble;
+ }
+
+ public static void ShowPreviewControl(Component component) => instance.ShowPreviewControlImpl(component);
+
+ private void ShowPreviewControlImpl(Component component)
+ {
+ switch (StateForImpl(component))
+ {
+ case PreviewState.PreviewAble:
+ if (GUILayout.Button("Preview"))
+ {
+ Enabled = true;
+ StartPreview();
+ }
+ break;
+ case PreviewState.PreviewingThat:
+ if (GUILayout.Button("Stop Preview"))
+ {
+ StopPreview();
+ Enabled = false;
+ }
+ break;
+ case PreviewState.PreviewingOther:
+ EditorGUI.BeginDisabledGroup(true);
+ GUILayout.Button("Preview (other Previewing)");
+ EditorGUI.EndDisabledGroup();
+ break;
+ case PreviewState.ActiveEditorMismatch:
+ EditorGUI.BeginDisabledGroup(true);
+ GUILayout.Button("Preview (not the active object)");
+ EditorGUI.EndDisabledGroup();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ public void StartPreview(GameObject expectedGameObject = null)
+ {
+ // Already in AnimationMode of other object
+ if (AnimationMode.InAnimationMode())
+ throw new Exception("Already in Animation Mode");
+
+ var targetGameObject = ActiveEditor() as GameObject;
+ if (targetGameObject == null)
+ throw new Exception("Active Editor is not GameObject");
+ if (expectedGameObject != null && expectedGameObject != targetGameObject)
+ throw new Exception("Unexpected GameObject");
+
+ targetRenderer = targetGameObject.GetComponent();
+ _previewController = new RemoveMeshPreviewController(targetRenderer);
+ targetRenderer = _previewController.TargetRenderer;
+ previewMesh = _previewController.PreviewMesh;
+ originalMesh = _previewController.OriginalMesh;
+
+ previewing = true;
+ AnimationMode.StartAnimationMode(DriverCached);
+ try
+ {
+ AnimationMode.BeginSampling();
+
+ AnimationMode.AddPropertyModification(
+ EditorCurveBinding.PPtrCurve("", typeof(SkinnedMeshRenderer), "m_Mesh"),
+ new PropertyModification
+ {
+ target = _previewController.TargetRenderer,
+ propertyPath = "m_Mesh",
+ objectReference = _previewController.OriginalMesh,
+ },
+ true);
+
+ _previewController.TargetRenderer.sharedMesh = _previewController.PreviewMesh;
+ }
+ finally
+ {
+ AnimationMode.EndSampling();
+ }
+ }
+
+ public void StopPreview()
+ {
+ previewing = false;
+ AnimationMode.StopAnimationMode(DriverCached);
+ _previewController?.Dispose();
+ _previewController = null;
+ }
+
+#if !UNITY_2020_1_OR_NEWER
+ private static AnimationModeDriver CreateDriver() =>
+ ScriptableObject.CreateInstance(
+ typeof(UnityEditor.AnimationMode).Assembly.GetType("UnityEditor.AnimationModeDriver"));
+#else
+ private static AnimationModeDriver CreateDriver() => ScriptableObject.CreateInstance();
+#endif
+
+#if !UNITY_2020_1_OR_NEWER
+ public static class AnimationMode
+ {
+ public static void BeginSampling() => UnityEditor.AnimationMode.BeginSampling();
+ public static void EndSampling() => UnityEditor.AnimationMode.EndSampling();
+ public static bool InAnimationMode() => UnityEditor.AnimationMode.InAnimationMode();
+ public static void StartAnimationMode(AnimationModeDriver o) => StartAnimationMode("StartAnimationMode", o);
+ public static void StopAnimationMode(AnimationModeDriver o) => StartAnimationMode("StopAnimationMode", o);
+
+ public static void AddPropertyModification(EditorCurveBinding binding, PropertyModification modification,
+ bool keepPrefabOverride) =>
+ UnityEditor.AnimationMode.AddPropertyModification(binding, modification, keepPrefabOverride);
+
+ private static void StartAnimationMode(string name, AnimationModeDriver o)
+ {
+ var method = typeof(UnityEditor.AnimationMode).GetMethod(name,
+ BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
+ null,
+ new[] { typeof(AnimationModeDriver) },
+ null);
+ System.Diagnostics.Debug.Assert(method != null, nameof(method) + " != null");
+ method.Invoke(null, new object[] { o });
+ }
+ }
+#endif
+ }
+}
diff --git a/Editor/EditModePreview/MeshPreviewController.cs.meta b/Editor/EditModePreview/MeshPreviewController.cs.meta
new file mode 100644
index 000000000..b9c1a14a5
--- /dev/null
+++ b/Editor/EditModePreview/MeshPreviewController.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2a9aa96e10a54b7cb7c238f3656a6cdf
+timeCreated: 1695026721
\ No newline at end of file
diff --git a/Editor/EditModePreview/RemoveMeshByBlendShapePreviewContext.cs b/Editor/EditModePreview/RemoveMeshByBlendShapePreviewContext.cs
new file mode 100644
index 000000000..44fe836ef
--- /dev/null
+++ b/Editor/EditModePreview/RemoveMeshByBlendShapePreviewContext.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using Unity.Collections;
+using UnityEngine;
+
+namespace Anatawa12.AvatarOptimizer.EditModePreview
+{
+ class RemoveMeshByBlendShapePreviewContext : IDisposable
+ {
+ // PerVertexBlendShapeRemoveFlags[vertexIndex / 32 + blendShapeIndex * _rowVertexCount / 32] & 1 << (vertexIndex % 32)
+ // blendshape vertex transforms. _blendShapeVertices[vertexIndex + blendShapeIndex * vertexCount]
+ public NativeArray BlendShapeMovements => _blendShapeMovements;
+
+ private NativeArray _blendShapeMovements;
+
+ private float _previousTolerance = float.NaN;
+ private HashSet _previousRemovingShapeKeys;
+
+ public RemoveMeshByBlendShapePreviewContext(BlendShapePreviewContext blendShapePreviewContext,
+ Mesh originalMesh)
+ {
+ var vertexCount = originalMesh.vertexCount;
+ var blendShapeCount = originalMesh.blendShapeCount;
+
+ _blendShapeMovements = new NativeArray(blendShapeCount * vertexCount, Allocator.Persistent);
+
+ try
+ {
+ using (var zeros = new NativeArray(vertexCount, Allocator.TempJob))
+ {
+ var weights = new float[blendShapeCount];
+ for (var i = 0; i < blendShapeCount; i++)
+ {
+ weights[i] = 100;
+ blendShapePreviewContext.ComputeBlendShape(weights,
+ zeros,
+ _blendShapeMovements.Slice(i * vertexCount, vertexCount)
+ );
+ weights[i] = 0;
+ }
+ }
+ }
+ catch
+ {
+ _blendShapeMovements.Dispose();
+ throw;
+ }
+ }
+
+ public void Dispose()
+ {
+ _blendShapeMovements.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/EditModePreview/RemoveMeshByBlendShapePreviewContext.cs.meta b/Editor/EditModePreview/RemoveMeshByBlendShapePreviewContext.cs.meta
new file mode 100644
index 000000000..f7697ed1e
--- /dev/null
+++ b/Editor/EditModePreview/RemoveMeshByBlendShapePreviewContext.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: fb9b892b24954095aebd8a18ee194a04
+timeCreated: 1695053774
\ No newline at end of file
diff --git a/Editor/EditModePreview/RemoveMeshPreviewController.cs b/Editor/EditModePreview/RemoveMeshPreviewController.cs
new file mode 100644
index 000000000..e574ef11f
--- /dev/null
+++ b/Editor/EditModePreview/RemoveMeshPreviewController.cs
@@ -0,0 +1,384 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Jobs;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.Rendering;
+using Object = UnityEngine.Object;
+
+namespace Anatawa12.AvatarOptimizer.EditModePreview
+{
+ class RemoveMeshPreviewController : IDisposable
+ {
+ public static Type[] EditorTypes =
+ {
+ typeof(RemoveMeshByBlendShape),
+ typeof(RemoveMeshInBox),
+ };
+
+ public RemoveMeshPreviewController([NotNull] SkinnedMeshRenderer targetRenderer, Mesh originalMesh = null, Mesh previewMesh = null)
+ {
+ if (targetRenderer == null) throw new ArgumentNullException(nameof(targetRenderer));
+
+ // Previewing object
+ TargetGameObject = targetRenderer.gameObject;
+ _targetRenderer = new ComponentHolder(targetRenderer);
+
+ OriginalMesh = originalMesh ? originalMesh : _targetRenderer.Value.sharedMesh;
+ _blendShapePreviewContext = new BlendShapePreviewContext(OriginalMesh);
+
+ _removeMeshInBox = default;
+ _removeMeshByBlendShape = default;
+
+ var subMeshes = new SubMeshDescriptor[OriginalMesh.subMeshCount];
+ _subMeshTriangleEndIndices = new int[OriginalMesh.subMeshCount];
+ var totalTriangles = 0;
+ for (var i = 0; i < subMeshes.Length; i++)
+ {
+ subMeshes[i] = OriginalMesh.GetSubMesh(i);
+ totalTriangles += subMeshes[i].indexCount / 3;
+ _subMeshTriangleEndIndices[i] = totalTriangles;
+ }
+
+ _blendShapeNames = new string[OriginalMesh.blendShapeCount];
+ for (var i = 0; i < OriginalMesh.blendShapeCount; i++)
+ _blendShapeNames[i] = OriginalMesh.GetBlendShapeName(i);
+
+ var originalTriangles = OriginalMesh.triangles;
+
+
+ _triangles = new NativeArray(totalTriangles, Allocator.Persistent);
+ _indexBuffer = new List();
+
+ var trianglesIndex = 0;
+ foreach (var subMeshDescriptor in subMeshes)
+ {
+ var indexStart = subMeshDescriptor.indexStart;
+ for (var i = 0; i < subMeshDescriptor.indexCount / 3; i++)
+ {
+ _triangles[trianglesIndex++] = new Triangle(
+ originalTriangles[indexStart + i * 3 + 0],
+ originalTriangles[indexStart + i * 3 + 1],
+ originalTriangles[indexStart + i * 3 + 2],
+ subMeshDescriptor.baseVertex);
+ }
+ }
+
+ if (previewMesh)
+ {
+ PreviewMesh = previewMesh;
+ }
+ else
+ {
+ PreviewMesh = Object.Instantiate(OriginalMesh);
+ PreviewMesh.name = OriginalMesh.name + " (AAO Preview)";
+ PreviewMesh.indexFormat = IndexFormat.UInt32;
+ }
+ }
+
+ public readonly GameObject TargetGameObject;
+ public readonly Mesh OriginalMesh;
+ public readonly Mesh PreviewMesh;
+ public SkinnedMeshRenderer TargetRenderer => _targetRenderer.Value;
+
+ private ComponentHolder _targetRenderer;
+ private ComponentHolder _removeMeshInBox;
+ private ComponentHolder _removeMeshByBlendShape;
+
+ private readonly BlendShapePreviewContext _blendShapePreviewContext;
+ private readonly int[] _subMeshTriangleEndIndices;
+ private NativeArray _triangles;
+ [CanBeNull] private RemoveMeshWithBoxPreviewContext _removeMeshWithBoxPreviewContext;
+ [CanBeNull] private RemoveMeshByBlendShapePreviewContext _removeMeshByBlendShapePreviewContext;
+ private readonly string[] _blendShapeNames;
+ private readonly List _indexBuffer;
+
+ struct Triangle
+ {
+ public int First;
+ public int Second;
+ public int Third;
+
+ public Triangle(int first, int second, int third)
+ {
+ First = first;
+ Second = second;
+ Third = third;
+ }
+
+ public Triangle(int first, int second, int third, int baseIndex) : this(first + baseIndex,
+ second + baseIndex, third + baseIndex)
+ {
+ }
+ }
+
+ /// True if this is no longer valid
+ public bool UpdatePreviewing()
+ {
+ bool ShouldStopPreview()
+ {
+ // target GameObject disappears
+ if (TargetGameObject == null || _targetRenderer.Value == null) return true;
+ // animation mode externally exited
+ if (!AnimationMode.InAnimationMode()) return true;
+ // Showing Inspector changed
+ if (ActiveEditorTracker.sharedTracker.activeEditors[0].target != TargetGameObject) return true;
+
+ return false;
+ }
+
+ if (ShouldStopPreview()) return true;
+
+ var modified = false;
+
+ if (_targetRenderer.Update(null) != Changed.Nothing)
+ {
+ _removeMeshWithBoxPreviewContext?.OnUpdateSkinnedMeshRenderer(_targetRenderer.Value);
+ modified = true;
+ }
+
+ switch (_removeMeshInBox.Update(TargetGameObject))
+ {
+ default:
+ case Changed.Updated:
+ modified = true;
+ break;
+ case Changed.Removed:
+ Debug.Assert(_removeMeshWithBoxPreviewContext != null,
+ nameof(_removeMeshWithBoxPreviewContext) + " != null");
+ _removeMeshWithBoxPreviewContext.Dispose();
+ _removeMeshWithBoxPreviewContext = null;
+ modified = true;
+ break;
+ case Changed.Created:
+ Debug.Assert(_removeMeshWithBoxPreviewContext == null,
+ nameof(_removeMeshWithBoxPreviewContext) + " == null");
+ _removeMeshWithBoxPreviewContext =
+ new RemoveMeshWithBoxPreviewContext(_blendShapePreviewContext, OriginalMesh);
+ modified = true;
+ break;
+ case Changed.Nothing:
+ break;
+ }
+
+ switch (_removeMeshByBlendShape.Update(TargetGameObject))
+ {
+ default:
+ case Changed.Updated:
+ modified = true;
+ break;
+ case Changed.Removed:
+ modified = true;
+ break;
+ case Changed.Created:
+ if (_removeMeshByBlendShapePreviewContext == null)
+ _removeMeshByBlendShapePreviewContext =
+ new RemoveMeshByBlendShapePreviewContext(_blendShapePreviewContext, OriginalMesh);
+ modified = true;
+ break;
+ case Changed.Nothing:
+ break;
+ }
+
+ if (modified)
+ UpdatePreviewMesh();
+
+ // modifier component not found
+ if (!(_removeMeshInBox.Value || _removeMeshByBlendShape.Value)) return false;
+
+ return false;
+ }
+
+ private void UpdatePreviewMesh()
+ {
+ var removeBlendShapeIndicesList = new List();
+ if (_removeMeshByBlendShape.Value)
+ {
+ var blendShapes = _removeMeshByBlendShape.Value.RemovingShapeKeys;
+
+ for (var i = 0; i < _blendShapeNames.Length; i++)
+ if (blendShapes.Contains(_blendShapeNames[i]))
+ removeBlendShapeIndicesList.Add(i);
+ }
+
+ using (var flags = new NativeArray(_triangles.Length, Allocator.TempJob))
+ {
+ using (var boxes = new NativeArray(
+ _removeMeshInBox.Value != null
+ ? _removeMeshInBox.Value.boxes
+ : Array.Empty(), Allocator.TempJob))
+ using (var blendShapeIndices =
+ new NativeArray(removeBlendShapeIndicesList.ToArray(), Allocator.TempJob))
+ using (var empty = new NativeArray(0, Allocator.TempJob))
+ {
+ var blendShapeAppliedVertices = _removeMeshWithBoxPreviewContext?.Vertices ?? empty;
+ var blendShapeMovements = _removeMeshByBlendShapePreviewContext?.BlendShapeMovements ?? empty;
+ var tolerance =
+ (float)(_removeMeshByBlendShape.Value ? _removeMeshByBlendShape.Value.tolerance : 0);
+
+ new FlagTrianglesJob
+ {
+ Triangles = _triangles,
+ RemoveFlags = flags,
+ VertexCount = OriginalMesh.vertexCount,
+
+ Boxes = boxes,
+ BlendShapeAppliedVertices = blendShapeAppliedVertices,
+
+ BlendShapeIndices = blendShapeIndices,
+ ToleranceSquared = tolerance * tolerance,
+ BlendShapeMovements = blendShapeMovements,
+ }.Schedule(_triangles.Length, 1).Complete();
+ }
+
+ var subMeshIdx = 0;
+
+ _indexBuffer.Clear();
+
+ for (var triIdx = 0; triIdx < _triangles.Length; triIdx++)
+ {
+ if (!flags[triIdx])
+ {
+ _indexBuffer.Add(_triangles[triIdx].First);
+ _indexBuffer.Add(_triangles[triIdx].Second);
+ _indexBuffer.Add(_triangles[triIdx].Third);
+ }
+
+ while (subMeshIdx < _subMeshTriangleEndIndices.Length &&
+ triIdx + 1 == _subMeshTriangleEndIndices[subMeshIdx])
+ {
+ PreviewMesh.SetTriangles(_indexBuffer, subMeshIdx);
+ _indexBuffer.Clear();
+ subMeshIdx++;
+ }
+ }
+ }
+ }
+
+ [BurstCompile]
+ struct FlagTrianglesJob : IJobParallelFor
+ {
+ [ReadOnly]
+ public NativeArray Triangles;
+ public int VertexCount;
+ public NativeArray RemoveFlags;
+
+ // Remove Mesh in Box
+ [ReadOnly]
+ public NativeArray Boxes;
+ [ReadOnly]
+ public NativeArray BlendShapeAppliedVertices;
+
+ // Remove Mesh by BlendShape
+ [ReadOnly]
+ public NativeArray BlendShapeIndices;
+ [ReadOnly]
+ public NativeArray BlendShapeMovements;
+
+ public float ToleranceSquared { get; set; }
+
+ public void Execute(int index) => RemoveFlags[index] = TestTriangle(Triangles[index]);
+
+ private bool TestTriangle(Triangle triangle)
+ {
+ // return true if remove
+ if (BlendShapeIndices.Length != 0)
+ {
+ // for RemoveMesh by BlendShape, *any* of vertex is moved, remove the triangle
+ foreach (var blendShapeIndex in BlendShapeIndices)
+ {
+ var movementBase = blendShapeIndex * VertexCount;
+
+ if (TestBlendShape(movementBase, triangle.First)) return true;
+ if (TestBlendShape(movementBase, triangle.Second)) return true;
+ if (TestBlendShape(movementBase, triangle.Third)) return true;
+ }
+ }
+
+ if (Boxes.Length != 0)
+ {
+ foreach (var boundingBox in Boxes)
+ {
+ if (boundingBox.ContainsVertex(BlendShapeAppliedVertices[triangle.First])
+ && boundingBox.ContainsVertex(BlendShapeAppliedVertices[triangle.First])
+ && boundingBox.ContainsVertex(BlendShapeAppliedVertices[triangle.First]))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private bool TestBlendShape(int movementBase, int index) =>
+ BlendShapeMovements[movementBase + index].sqrMagnitude > ToleranceSquared;
+ }
+
+ public void Dispose()
+ {
+ _triangles.Dispose();
+ _removeMeshWithBoxPreviewContext?.Dispose();
+ _removeMeshByBlendShapePreviewContext?.Dispose();
+ _blendShapePreviewContext?.Dispose();
+ }
+
+ public struct ComponentHolder where T : Component
+ {
+ public T Value => _value;
+ // preview version + 1 to make default value != EditorUtility.GetDirtyCount(new object)
+ private int _previousVersion;
+ private T _value;
+
+ public ComponentHolder(T value)
+ {
+ _value = value;
+ if (value) _previousVersion = EditorUtility.GetDirtyCount(value) + 1;
+ else _previousVersion = 0;
+ }
+
+ public Changed Update(GameObject gameObject)
+ {
+ if (!_value)
+ {
+ _value = gameObject ? gameObject.GetComponent() : null;
+ if (_value)
+ {
+ // newly created
+ _previousVersion = EditorUtility.GetDirtyCount(_value) + 1;
+ return Changed.Created;
+ }
+ else
+ {
+ if (_previousVersion == 0) return Changed.Nothing;
+
+ // it seem component is removed
+ _previousVersion = 0;
+ return Changed.Removed;
+ }
+ }
+ else
+ {
+ var currentVersion = EditorUtility.GetDirtyCount(_value) + 1;
+ if (_previousVersion == currentVersion)
+ return Changed.Nothing;
+
+ _previousVersion = currentVersion;
+ return Changed.Updated;
+ }
+
+ }
+ }
+
+ internal enum Changed
+ {
+ Nothing,
+ Updated,
+ Removed,
+ Created,
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/EditModePreview/RemoveMeshPreviewController.cs.meta b/Editor/EditModePreview/RemoveMeshPreviewController.cs.meta
new file mode 100644
index 000000000..425bb36b5
--- /dev/null
+++ b/Editor/EditModePreview/RemoveMeshPreviewController.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8e07879318a84723897e9f7b39e3ad73
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/EditModePreview/RemoveMeshWithBoxPreviewContext.cs b/Editor/EditModePreview/RemoveMeshWithBoxPreviewContext.cs
new file mode 100644
index 000000000..055795829
--- /dev/null
+++ b/Editor/EditModePreview/RemoveMeshWithBoxPreviewContext.cs
@@ -0,0 +1,60 @@
+using System;
+using JetBrains.Annotations;
+using Unity.Collections;
+using UnityEngine;
+
+namespace Anatawa12.AvatarOptimizer.EditModePreview
+{
+ ///
+ /// Preview Context for RemoveMesh with Box.
+ /// RuntimePreview of Remove Mesh with Box needs BlendShape applied vertex position so
+ /// this class holds that information and update if nesessary
+ ///
+ class RemoveMeshWithBoxPreviewContext : IDisposable
+ {
+ public NativeArray Vertices => _vertices;
+
+ private readonly BlendShapePreviewContext _blendShapePreviewContext;
+ // not transformed vertices
+ private NativeArray _originalVertices;
+ // this should be blendShape transformed
+ private NativeArray _vertices;
+ // configured BlendShape weights
+ [NotNull] private readonly float[] _blendShapeWeights;
+
+ public RemoveMeshWithBoxPreviewContext(BlendShapePreviewContext blendShapePreviewContext, Mesh originalMesh)
+ {
+ _blendShapePreviewContext = blendShapePreviewContext;
+ _originalVertices = new NativeArray(originalMesh.vertices, Allocator.Persistent);
+
+ // initialize with original vertices
+ _vertices = new NativeArray(_originalVertices, Allocator.Persistent);
+
+ _blendShapeWeights = new float[originalMesh.blendShapeCount];
+ }
+
+ public void OnUpdateSkinnedMeshRenderer(SkinnedMeshRenderer renderer)
+ {
+ var modified = false;
+ for (var i = 0; i < _blendShapeWeights.Length; i++)
+ {
+ var currentWeight = renderer.GetBlendShapeWeight(i);
+ if (Math.Abs(currentWeight - _blendShapeWeights[i]) > Mathf.Epsilon)
+ {
+ _blendShapeWeights[i] = currentWeight;
+ modified = true;
+ }
+ }
+
+ if (!modified) return;
+
+ _blendShapePreviewContext.ComputeBlendShape(_blendShapeWeights, _originalVertices, _vertices);
+ }
+
+ public void Dispose()
+ {
+ _originalVertices.Dispose();
+ _vertices.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/EditModePreview/RemoveMeshWithBoxPreviewContext.cs.meta b/Editor/EditModePreview/RemoveMeshWithBoxPreviewContext.cs.meta
new file mode 100644
index 000000000..6e766024e
--- /dev/null
+++ b/Editor/EditModePreview/RemoveMeshWithBoxPreviewContext.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 50c50f2cd0fa4899a3dfe97e787c6e03
+timeCreated: 1695047935
\ No newline at end of file
diff --git a/Editor/RemoveMeshByBlendShapeEditor.cs b/Editor/RemoveMeshByBlendShapeEditor.cs
index 3f67a2cff..49c7f1caa 100644
--- a/Editor/RemoveMeshByBlendShapeEditor.cs
+++ b/Editor/RemoveMeshByBlendShapeEditor.cs
@@ -1,4 +1,5 @@
using CustomLocalization4EditorExtension;
+using Unity.Collections;
using UnityEditor;
using UnityEngine;
@@ -14,6 +15,7 @@ internal class RemoveMeshByBlendShapeEditor : AvatarTagComponentEditorBase
private void OnEnable()
{
+ NativeLeakDetection.Mode = NativeLeakDetectionMode.EnabledWithStackTrace;
_renderer = targets.Length == 1 ? ((Component)target).GetComponent() : null;
var nestCount = PrefabSafeSet.PrefabSafeSetUtil.PrefabNestCount(serializedObject.targetObject);
_shapeKeysSet = PrefabSafeSet.EditorUtil.Create(
@@ -28,6 +30,8 @@ protected override void OnInspectorGUIInner()
{
var component = (RemoveMeshByBlendShape)target;
+ EditModePreview.MeshPreviewController.ShowPreviewControl(component);
+
if (!_renderer)
{
EditorGUI.BeginDisabledGroup(true);
diff --git a/Editor/RemoveMeshInBoxEditor.cs b/Editor/RemoveMeshInBoxEditor.cs
index 676c46cc7..f99e634e2 100644
--- a/Editor/RemoveMeshInBoxEditor.cs
+++ b/Editor/RemoveMeshInBoxEditor.cs
@@ -22,6 +22,8 @@ private void OnEnable()
protected override void OnInspectorGUIInner()
{
+ EditModePreview.MeshPreviewController.ShowPreviewControl((Component)target);
+
// size prop
_boxes.isExpanded = true;
using (new BoundingBoxEditor.EditorScope(this))
diff --git a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef
index 41396b371..004330a75 100644
--- a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef
+++ b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef
@@ -9,7 +9,8 @@
"GUID:f69eeb3e25674f4a9bd20e6d7e69e0e6",
"GUID:2633ab9fa94544a69517fc9a1bc143c9",
"GUID:b9880ca0b6584453a2627bd3c038759f",
- "GUID:8542dbf824204440a818dbc2377cb4d6"
+ "GUID:8542dbf824204440a818dbc2377cb4d6",
+ "GUID:2665a8d13d1b3f18800f46e256720795"
],
"includePlatforms": [
"Editor"
diff --git a/Runtime/RemoveMeshInBox.cs b/Runtime/RemoveMeshInBox.cs
index 649536200..898451d4e 100644
--- a/Runtime/RemoveMeshInBox.cs
+++ b/Runtime/RemoveMeshInBox.cs
@@ -1,5 +1,6 @@
using System;
using CustomLocalization4EditorExtension;
+using Unity.Burst;
using UnityEngine;
namespace Anatawa12.AvatarOptimizer
@@ -14,19 +15,27 @@ internal class RemoveMeshInBox : EditSkinnedMeshComponent
private void Reset()
{
- boxes = new[] { new BoundingBox() };
+ boxes = new[] { BoundingBox.Default };
}
[Serializable]
- public class BoundingBox
+ public struct BoundingBox
{
[CL4EELocalized("RemoveMeshInBox:BoundingBox:prop:center")]
public Vector3 center;
[CL4EELocalized("RemoveMeshInBox:BoundingBox:prop:size")]
- public Vector3 size = new Vector3(1, 1, 1);
+ public Vector3 size;
[CL4EELocalized("RemoveMeshInBox:BoundingBox:prop:rotation")]
- public Quaternion rotation = Quaternion.identity;
+ public Quaternion rotation;
+ public static BoundingBox Default = new BoundingBox
+ {
+ center = Vector3.zero,
+ size = new Vector3(1, 1, 1),
+ rotation = Quaternion.identity,
+ };
+
+ [BurstCompile]
public bool ContainsVertex(Vector3 point)
{
var positionInBox = Quaternion.Inverse(rotation) * (point - center);
diff --git a/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef b/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef
index 9d9f9ecf3..05092ff79 100644
--- a/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef
+++ b/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef
@@ -3,7 +3,8 @@
"references": [
"GUID:b23bdfe8d18741a29e869995d47026d0",
"GUID:8264e72376854221affe9980c91c2fff",
- "GUID:8542dbf824204440a818dbc2377cb4d6"
+ "GUID:8542dbf824204440a818dbc2377cb4d6",
+ "GUID:2665a8d13d1b3f18800f46e256720795"
],
"includePlatforms": [],
"excludePlatforms": [],