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": [],