From 38bd1941a0499ddd9d72286768c0a8fa40bbf8c8 Mon Sep 17 00:00:00 2001 From: bd_ Date: Thu, 3 Oct 2024 20:02:01 -0700 Subject: [PATCH] fix: incorrect handling of shape key deletion This change reworks delete handling to be more consistent with other properties, by treating it as a virtual property (`deletedShape.{blendshapeName}`) instead of a weird additional field of blendshape keys. This then fixes a number of issues (e.g. broken preview for delete keys). Fixes: #1253 --- .../AnimationGeneration/AnimatedProperty.cs | 1 - .../AnimationGeneration/ReactionRule.cs | 10 +- .../ReactiveObjectAnalyzer.LocateReactions.cs | 75 +- .../ReactiveObjectAnalyzer.cs | 29 +- .../AnimationGeneration/ReactiveObjectPass.cs | 77 +- Editor/ReactiveObjects/ShapeChangerPreview.cs | 54 +- .../ReactiveObjects/Simulator/ROSimulator.cs | 5 +- .../ReactiveComponent/DeletionTest.prefab | 666 ++++++++++++++++++ .../DeletionTest.prefab.meta | 7 + .../ShapeDeletionAnalysis.cs | 98 +++ .../ShapeDeletionAnalysis.cs.meta | 11 + .../shape deletion test mesh.fbx | Bin 0 -> 17116 bytes .../shape deletion test mesh.fbx.meta | 109 +++ 13 files changed, 1040 insertions(+), 102 deletions(-) create mode 100644 UnitTests~/ReactiveComponent/DeletionTest.prefab create mode 100644 UnitTests~/ReactiveComponent/DeletionTest.prefab.meta create mode 100644 UnitTests~/ReactiveComponent/ShapeDeletionAnalysis.cs create mode 100644 UnitTests~/ReactiveComponent/ShapeDeletionAnalysis.cs.meta create mode 100644 UnitTests~/ReactiveComponent/shape deletion test mesh.fbx create mode 100644 UnitTests~/ReactiveComponent/shape deletion test mesh.fbx.meta diff --git a/Editor/ReactiveObjects/AnimationGeneration/AnimatedProperty.cs b/Editor/ReactiveObjects/AnimationGeneration/AnimatedProperty.cs index c3bded7d..f56a59af 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/AnimatedProperty.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/AnimatedProperty.cs @@ -8,7 +8,6 @@ namespace nadena.dev.modular_avatar.core.editor public TargetProp TargetProp { get; } public string ControlParam { get; set; } - public bool alwaysDeleted; public object currentState; // Objects which trigger deletion of this shape key. diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs index dcf040ea..12bea0cb 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using nadena.dev.modular_avatar.animation; using UnityEngine; namespace nadena.dev.modular_avatar.core.editor @@ -9,8 +8,8 @@ namespace nadena.dev.modular_avatar.core.editor { public ReactionRule(TargetProp key, float value) : this(key, (object)value) { } - - public ReactionRule(TargetProp key, UnityEngine.Object value) + + public ReactionRule(TargetProp key, Object value) : this(key, (object)value) { } private ReactionRule(TargetProp key, object value) @@ -31,15 +30,15 @@ namespace nadena.dev.modular_avatar.core.editor public bool InitiallyActive => ((ControllingConditions.Count == 0) || ControllingConditions.All(c => c.InitiallyActive)) ^ Inverted; - public bool IsDelete; public bool Inverted; public bool IsConstant => ControllingConditions.Count == 0 || ControllingConditions.All(c => c.IsConstant) || ControllingConditions.Any(c => c.IsConstant && !c.InitiallyActive); - public bool IsConstantOn => IsConstant && InitiallyActive; + public bool IsConstantActive => IsConstant && InitiallyActive ^ Inverted; + public override string ToString() { return $"AGK: {TargetProp}={Value}"; @@ -57,7 +56,6 @@ namespace nadena.dev.modular_avatar.core.editor } else return false; if (!ControllingConditions.SequenceEqual(other.ControllingConditions)) return false; - if (IsDelete || other.IsDelete) return false; return true; } diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs index f84d0b30..f2e500e7 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs @@ -124,50 +124,55 @@ namespace nadena.dev.modular_avatar.core.editor var key = new TargetProp { TargetObject = renderer, - PropertyName = "blendShape." + shape.ShapeName, + PropertyName = BlendshapePrefix + shape.ShapeName }; + var currentValue = renderer.GetBlendShapeWeight(shapeId); var value = shape.ChangeType == ShapeChangeType.Delete ? 100 : shape.Value; - if (!shapeKeys.TryGetValue(key, out var info)) + + RegisterAction(key, renderer, currentValue, value, changer, shape); + + key = new TargetProp { - info = new AnimatedProperty(key, renderer.GetBlendShapeWeight(shapeId)); - shapeKeys[key] = info; + TargetObject = renderer, + PropertyName = DeletedShapePrefix + shape.ShapeName + }; - // Add initial state - var agk = new ReactionRule(key, value); - agk.Value = renderer.GetBlendShapeWeight(shapeId); - info.actionGroups.Add(agk); - } - - var action = ObjectRule(key, changer, value); - action.Inverted = _computeContext.Observe(changer, c => c.Inverted); - var isCurrentlyActive = changer.gameObject.activeInHierarchy; - - if (shape.ChangeType == ShapeChangeType.Delete) - { - action.IsDelete = true; - - if (isCurrentlyActive) info.currentState = 100; - - info.actionGroups.Add(action); // Never merge - - continue; - } - - if (changer.gameObject.activeInHierarchy) info.currentState = action.Value; - - if (info.actionGroups.Count == 0) - { - info.actionGroups.Add(action); - } - else if (!info.actionGroups[^1].TryMerge(action)) - { - info.actionGroups.Add(action); - } + value = shape.ChangeType == ShapeChangeType.Delete ? 1 : 0; + RegisterAction(key, renderer, 0, value, changer, shape); } } return shapeKeys; + + void RegisterAction(TargetProp key, SkinnedMeshRenderer renderer, float currentValue, float value, + ModularAvatarShapeChanger changer, ChangedShape shape) + { + if (!shapeKeys.TryGetValue(key, out var info)) + { + info = new AnimatedProperty(key, currentValue); + shapeKeys[key] = info; + + // Add initial state + var agk = new ReactionRule(key, value); + agk.Value = currentValue; + info.actionGroups.Add(agk); + } + + var action = ObjectRule(key, changer, value); + action.Inverted = _computeContext.Observe(changer, c => c.Inverted); + + if (changer.gameObject.activeInHierarchy) info.currentState = action.Value; + + if (info.actionGroups.Count == 0) + { + info.actionGroups.Add(action); + } + else if (!info.actionGroups[^1].TryMerge(action)) + { + info.actionGroups.Add(action); + } + } } private void FindMaterialSetters(Dictionary objectGroups, GameObject root) diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs index 745c5184..1c5b97b2 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs @@ -18,6 +18,9 @@ namespace nadena.dev.modular_avatar.core.editor private readonly ndmf.BuildContext _context; private readonly AnimationServicesContext _asc; private Dictionary _simulationInitialStates; + + public const string BlendshapePrefix = "blendShape."; + public const string DeletedShapePrefix = "deletedShape."; public ImmutableDictionary ForcePropertyOverrides { get; set; } = ImmutableDictionary.Empty; @@ -58,7 +61,6 @@ namespace nadena.dev.modular_avatar.core.editor { public Dictionary Shapes; public Dictionary InitialStates; - public HashSet DeletedShapes; } private static PropCache _analysisCache; @@ -86,7 +88,6 @@ namespace nadena.dev.modular_avatar.core.editor /// /// The avatar root /// A dictionary of target property to initial state (float or UnityEngine.Object) - /// A hashset of blendshape properties which are always deleted /// public AnalysisResult Analyze( GameObject root @@ -98,7 +99,6 @@ namespace nadena.dev.modular_avatar.core.editor { result.Shapes = new(); result.InitialStates = new(); - result.DeletedShapes = new(); return result; } @@ -109,7 +109,7 @@ namespace nadena.dev.modular_avatar.core.editor ApplyInitialStateOverrides(shapes); AnalyzeConstants(shapes); ResolveToggleInitialStates(shapes); - PreprocessShapes(shapes, out result.InitialStates, out result.DeletedShapes); + PreprocessShapes(shapes, out result.InitialStates); result.Shapes = shapes; return result; @@ -165,7 +165,7 @@ namespace nadena.dev.modular_avatar.core.editor group.actionGroups.RemoveAll(agk => agk.IsConstant && !agk.InitiallyActive); // Remove all action groups up until the last one where we're always on - var lastAlwaysOnGroup = group.actionGroups.FindLastIndex(ag => ag.IsConstantOn); + var lastAlwaysOnGroup = group.actionGroups.FindLastIndex(ag => ag.IsConstantActive); if (lastAlwaysOnGroup > 0) group.actionGroups.RemoveRange(0, lastAlwaysOnGroup - 1); } @@ -264,18 +264,17 @@ namespace nadena.dev.modular_avatar.core.editor } /// - /// Determine initial state and deleted shapes for all properties + /// Determine initial state for all properties /// /// /// - /// - private void PreprocessShapes(Dictionary shapes, out Dictionary initialStates, out HashSet deletedShapes) + private void PreprocessShapes(Dictionary shapes, + out Dictionary initialStates) { // For each shapekey, determine 1) if we can just set an initial state and skip and 2) if we can delete the // corresponding mesh. If we can't, delete ops are merged into the main list of operations. initialStates = new Dictionary(); - deletedShapes = new HashSet(); foreach (var (key, info) in shapes.ToList()) { @@ -285,18 +284,6 @@ namespace nadena.dev.modular_avatar.core.editor shapes.Remove(key); continue; } - - var deletions = info.actionGroups.Where(agk => agk.IsDelete).ToList(); - if (deletions.Any(d => d.InitiallyActive)) - { - // always deleted - shapes.Remove(key); - deletedShapes.Add(key); - continue; - } - - // Move deleted shapes to the end of the list, so they override all Set actions - info.actionGroups = info.actionGroups.Where(agk => !agk.IsDelete).Concat(deletions).ToList(); var initialState = info.actionGroups.Where(agk => agk.InitiallyActive) .Select(agk => agk.Value) diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs index 69a0ede7..3ff0fe4a 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using nadena.dev.modular_avatar.animation; using UnityEditor; @@ -42,10 +41,11 @@ namespace nadena.dev.modular_avatar.core.editor var shapes = analysis.Shapes; var initialStates = analysis.InitialStates; - var deletedShapes = analysis.DeletedShapes; GenerateActiveSelfProxies(shapes); + ProcessMeshDeletion(initialStates, shapes); + ProcessInitialStates(initialStates, shapes); ProcessInitialAnimatorVariables(shapes); @@ -53,8 +53,6 @@ namespace nadena.dev.modular_avatar.core.editor { ProcessShapeKey(groups); } - - ProcessMeshDeletion(deletedShapes); } private void GenerateActiveSelfProxies(Dictionary shapes) @@ -225,30 +223,65 @@ namespace nadena.dev.modular_avatar.core.editor #region Mesh processing - private void ProcessMeshDeletion(HashSet deletedKeys) + private void ProcessMeshDeletion(Dictionary initialStates, + Dictionary shapes) { - ImmutableDictionary> renderers = deletedKeys - .GroupBy( - v => (SkinnedMeshRenderer) v.TargetObject - ).ToImmutableDictionary( - g => (SkinnedMeshRenderer) g.Key, - g => g.ToList() - ); + var renderers = initialStates + .Where(kvp => kvp.Key.PropertyName.StartsWith(ReactiveObjectAnalyzer.DeletedShapePrefix)) + .Where(kvp => kvp.Key.TargetObject is SkinnedMeshRenderer) + .Where(kvp => kvp.Value is float f && f > 0.5f) + // Filter any non-constant keys + .Where(kvp => + { + if (!shapes.ContainsKey(kvp.Key)) + { + // Constant value + return true; + } - foreach (var (renderer, infos) in renderers) + var lastGroup = shapes[kvp.Key].actionGroups.LastOrDefault(); + return lastGroup?.IsConstantActive == true && lastGroup.Value is float f && f > 0.5f; + }) + .GroupBy(kvp => kvp.Key.TargetObject as SkinnedMeshRenderer) + .Select(grouping => (grouping.Key, grouping.Select( + kvp => kvp.Key.PropertyName.Substring(ReactiveObjectAnalyzer.DeletedShapePrefix.Length) + ).ToList())) + .ToList(); + foreach (var (renderer, shapeNamesToDelete) in renderers) { if (renderer == null) continue; var mesh = renderer.sharedMesh; if (mesh == null) continue; - renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes( - mesh, - infos - .Select(i => mesh.GetBlendShapeIndex(i.PropertyName.Substring("blendShape.".Length))) - .Where(k => k >= 0) - .ToList() - ); + var shapesToDelete = shapeNamesToDelete + .Select(shape => mesh.GetBlendShapeIndex(shape)) + .Where(k => k >= 0) + .ToList(); + + renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes(mesh, shapesToDelete); + + foreach (var name in shapeNamesToDelete) + { + // Don't need to animate this anymore...! + shapes.Remove(new TargetProp + { + TargetObject = renderer, + PropertyName = ReactiveObjectAnalyzer.BlendshapePrefix + name + }); + + shapes.Remove(new TargetProp + { + TargetObject = renderer, + PropertyName = ReactiveObjectAnalyzer.DeletedShapePrefix + name + }); + + initialStates.Remove(new TargetProp + { + TargetObject = renderer, + PropertyName = ReactiveObjectAnalyzer.BlendshapePrefix + name + }); + } } } @@ -257,10 +290,6 @@ namespace nadena.dev.modular_avatar.core.editor private void ProcessShapeKey(AnimatedProperty info) { // TODO: prune non-animated keys - - // Check if this is non-animated and skip most processing if so - if (info.alwaysDeleted || info.actionGroups[^1].IsConstant) return; - var asm = GenerateStateMachine(info); ApplyController(asm, "MA Responsive: " + info.TargetProp.TargetObject.name); } diff --git a/Editor/ReactiveObjects/ShapeChangerPreview.cs b/Editor/ReactiveObjects/ShapeChangerPreview.cs index 2ca011c7..58c09131 100644 --- a/Editor/ReactiveObjects/ShapeChangerPreview.cs +++ b/Editor/ReactiveObjects/ShapeChangerPreview.cs @@ -72,8 +72,8 @@ namespace nadena.dev.modular_avatar.core.editor var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot); var shapes = analysis.Shapes; - ImmutableDictionary>.Builder rendererStates = - ImmutableDictionary.CreateBuilder>( + var rendererStates = + ImmutableDictionary.CreateBuilder>( ); var avatarRootTransform = avatarRoot.transform; @@ -83,16 +83,29 @@ namespace nadena.dev.modular_avatar.core.editor var target = prop.TargetProp; if (target.TargetObject == null || target.TargetObject is not SkinnedMeshRenderer r) continue; if (!r.transform.IsChildOf(avatarRootTransform)) continue; - if (!target.PropertyName.StartsWith("blendShape.")) continue; + var isDelete = false; + string shapeName = null; + if (target.PropertyName.StartsWith(ReactiveObjectAnalyzer.DeletedShapePrefix)) + { + isDelete = true; + shapeName = target.PropertyName.Substring(ReactiveObjectAnalyzer.DeletedShapePrefix.Length); + } + else if (target.PropertyName.StartsWith(ReactiveObjectAnalyzer.BlendshapePrefix)) + { + shapeName = target.PropertyName.Substring(ReactiveObjectAnalyzer.BlendshapePrefix.Length); + } + else + { + continue; + } + var mesh = r.sharedMesh; if (mesh == null) continue; - var shapeName = target.PropertyName.Substring("blendShape.".Length); - if (!rendererStates.TryGetValue(r, out var states)) { - states = ImmutableList<(int, float)>.Empty; + states = ImmutableDictionary.Empty; rendererStates[r] = states; } @@ -103,15 +116,30 @@ namespace nadena.dev.modular_avatar.core.editor if (activeRule == null || activeRule.Value is not float value) continue; if (activeRule.ControllingObject == null) continue; // default value is being inherited - value = Math.Clamp(value, 0, 100); - - if (activeRule.IsDelete) value = -1; - - states = states.Add((index, value)); + if (isDelete) + { + if (value < 0.5f) continue; + value = -1; + } + else + { + if (states.ContainsKey(index)) + { + // Delete takes precedence over set in preview + continue; + } + + value = Math.Clamp(value, 0, 100); + } + + states = states.SetItem(index, value); rendererStates[r] = states; } - - return rendererStates.ToImmutableDictionary(); + + return rendererStates.ToImmutableDictionary( + kvp => kvp.Key, + kvp => kvp.Value.Select(shapePair => (shapePair.Key, shapePair.Value) + ).ToImmutableList()); } private IEnumerable ShapesToGroups(GameObject avatarRoot, ImmutableDictionary> shapes) diff --git a/Editor/ReactiveObjects/Simulator/ROSimulator.cs b/Editor/ReactiveObjects/Simulator/ROSimulator.cs index 3aaf3237..84896612 100644 --- a/Editor/ReactiveObjects/Simulator/ROSimulator.cs +++ b/Editor/ReactiveObjects/Simulator/ROSimulator.cs @@ -471,7 +471,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator var f_set_inactive = effectGroup.Q("effect__set-inactive"); var f_value = effectGroup.Q("effect__value"); var f_material = effectGroup.Q("effect__material"); - var f_delete = effectGroup.Q("effect__deleted"); + var f_delete = effectGroup.Q("effect__deleted"); f_target_component.style.display = DisplayStyle.None; f_target_component.SetEnabled(false); @@ -504,9 +504,10 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator f_property.value = targetProp.PropertyName; f_property.style.display = DisplayStyle.Flex; - if (reactionRule.IsDelete) + if (reactionRule.TargetProp.PropertyName.StartsWith(ReactiveObjectAnalyzer.DeletedShapePrefix)) { f_delete.style.display = DisplayStyle.Flex; + f_delete.value = reactionRule.Value is > 0.5f ? "DELETE" : "RETAIN"; } else if (reactionRule.Value is float f) { f_value.SetValueWithoutNotify(f); diff --git a/UnitTests~/ReactiveComponent/DeletionTest.prefab b/UnitTests~/ReactiveComponent/DeletionTest.prefab new file mode 100644 index 00000000..6cefc636 --- /dev/null +++ b/UnitTests~/ReactiveComponent/DeletionTest.prefab @@ -0,0 +1,666 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &2464504760772767737 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3251791125987375227} + - component: {fileID: 6611954401356246169} + - component: {fileID: 4257580493320060063} + - component: {fileID: 7095484051158404692} + m_Layer: 0 + m_Name: DeletionTest + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3251791125987375227 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2464504760772767737} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0.06867766, y: 0.7869835, z: -0.57959247} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 1 + m_Children: + - {fileID: 8671858138418525756} + - {fileID: 3787319563290092876} + - {fileID: 2780879708549973278} + - {fileID: 6867583134219554799} + - {fileID: 3617623734196600728} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!95 &6611954401356246169 +Animator: + serializedVersion: 5 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2464504760772767737} + m_Enabled: 1 + m_Avatar: {fileID: 0} + m_Controller: {fileID: 0} + m_CullingMode: 0 + m_UpdateMode: 0 + m_ApplyRootMotion: 0 + m_LinearVelocityBlending: 0 + m_StabilizeFeet: 0 + m_WarningMessage: + m_HasTransformHierarchy: 1 + m_AllowConstantClipSamplingOptimization: 1 + m_KeepAnimatorStateOnDisable: 0 + m_WriteDefaultValuesOnDisable: 0 +--- !u!114 &4257580493320060063 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2464504760772767737} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 542108242, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3} + m_Name: + m_EditorClassIdentifier: + Name: + ViewPosition: {x: 0, y: 1.6, z: 0.2} + Animations: 0 + ScaleIPD: 1 + lipSync: 0 + lipSyncJawBone: {fileID: 0} + lipSyncJawClosed: {x: 0, y: 0, z: 0, w: 1} + lipSyncJawOpen: {x: 0, y: 0, z: 0, w: 1} + VisemeSkinnedMesh: {fileID: 0} + MouthOpenBlendShapeName: Facial_Blends.Jaw_Down + VisemeBlendShapes: [] + unityVersion: + portraitCameraPositionOffset: {x: 0, y: 0, z: 0} + portraitCameraRotationOffset: {x: 0, y: 1, z: 0, w: -0.00000004371139} + networkIDs: [] + customExpressions: 0 + expressionsMenu: {fileID: 0} + expressionParameters: {fileID: 0} + enableEyeLook: 0 + customEyeLookSettings: + eyeMovement: + confidence: 0.5 + excitement: 0.5 + leftEye: {fileID: 0} + rightEye: {fileID: 0} + eyesLookingStraight: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + eyesLookingUp: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + eyesLookingDown: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + eyesLookingLeft: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + eyesLookingRight: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + eyelidType: 0 + upperLeftEyelid: {fileID: 0} + upperRightEyelid: {fileID: 0} + lowerLeftEyelid: {fileID: 0} + lowerRightEyelid: {fileID: 0} + eyelidsDefault: + upper: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + lower: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + eyelidsClosed: + upper: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + lower: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + eyelidsLookingUp: + upper: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + lower: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + eyelidsLookingDown: + upper: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + lower: + linked: 1 + left: {x: 0, y: 0, z: 0, w: 0} + right: {x: 0, y: 0, z: 0, w: 0} + eyelidsSkinnedMesh: {fileID: 0} + eyelidsBlendshapes: + customizeAnimationLayers: 0 + baseAnimationLayers: + - isEnabled: 0 + type: 0 + animatorController: {fileID: 0} + mask: {fileID: 0} + isDefault: 1 + - isEnabled: 0 + type: 4 + animatorController: {fileID: 0} + mask: {fileID: 0} + isDefault: 1 + - isEnabled: 0 + type: 5 + animatorController: {fileID: 0} + mask: {fileID: 0} + isDefault: 1 + specialAnimationLayers: + - isEnabled: 0 + type: 6 + animatorController: {fileID: 0} + mask: {fileID: 0} + isDefault: 1 + - isEnabled: 0 + type: 7 + animatorController: {fileID: 0} + mask: {fileID: 0} + isDefault: 1 + - isEnabled: 0 + type: 8 + animatorController: {fileID: 0} + mask: {fileID: 0} + isDefault: 1 + AnimationPreset: {fileID: 0} + animationHashSet: [] + autoFootsteps: 1 + autoLocomotion: 1 + collider_head: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_torso: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_footR: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_footL: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_handR: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_handL: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_fingerIndexL: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_fingerMiddleL: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_fingerRingL: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_fingerLittleL: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_fingerIndexR: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_fingerMiddleR: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_fingerRingR: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} + collider_fingerLittleR: + isMirrored: 1 + state: 0 + transform: {fileID: 0} + radius: 0 + height: 0 + position: {x: 0, y: 0, z: 0} + rotation: {x: 0, y: 0, z: 0, w: 1} +--- !u!114 &7095484051158404692 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2464504760772767737} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + m_Name: + m_EditorClassIdentifier: + launchedFromSDKPipeline: 0 + completedSDKPipeline: 0 + blueprintId: + contentType: 0 + assetBundleUnityVersion: + fallbackStatus: 0 +--- !u!1 &3134446681435896768 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2780879708549973278} + - component: {fileID: 2470606632396626262} + m_Layer: 0 + m_Name: Delete + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2780879708549973278 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3134446681435896768} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 3251791125987375227} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &2470606632396626262 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3134446681435896768} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2db441f589c3407bb6fb5f02ff8ab541, type: 3} + m_Name: + m_EditorClassIdentifier: + m_inverted: 0 + m_shapes: + - Object: + referencePath: shape deletion test mesh + targetObject: {fileID: 0} + ShapeName: bottom + ChangeType: 0 + Value: 50 +--- !u!1 &7874409458034691206 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3787319563290092876} + - component: {fileID: 8462455628590652122} + m_Layer: 0 + m_Name: PriorSet + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3787319563290092876 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7874409458034691206} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 3251791125987375227} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &8462455628590652122 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7874409458034691206} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2db441f589c3407bb6fb5f02ff8ab541, type: 3} + m_Name: + m_EditorClassIdentifier: + m_inverted: 0 + m_shapes: + - Object: + referencePath: shape deletion test mesh + targetObject: {fileID: 0} + ShapeName: bottom + ChangeType: 1 + Value: 50 +--- !u!1 &7956182162252432618 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3617623734196600728} + - component: {fileID: 4167915178638071617} + - component: {fileID: 3280847981733507148} + m_Layer: 0 + m_Name: MenuSet + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!4 &3617623734196600728 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7956182162252432618} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 3251791125987375227} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &4167915178638071617 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7956182162252432618} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2db441f589c3407bb6fb5f02ff8ab541, type: 3} + m_Name: + m_EditorClassIdentifier: + m_inverted: 0 + m_shapes: + - Object: + referencePath: shape deletion test mesh + targetObject: {fileID: 0} + ShapeName: bottom + ChangeType: 1 + Value: 0 +--- !u!114 &3280847981733507148 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7956182162252432618} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3b29d45007c5493d926d2cd45a489529, type: 3} + m_Name: + m_EditorClassIdentifier: + Control: + name: + icon: {fileID: 0} + type: 102 + parameter: + name: + value: 1 + style: 0 + subMenu: {fileID: 0} + subParameters: [] + labels: [] + MenuSource: 1 + menuSource_otherObjectChildren: {fileID: 0} + isSynced: 1 + isSaved: 1 + isDefault: 0 + automaticValue: 1 +--- !u!1 &8389945206789797712 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6867583134219554799} + - component: {fileID: 8099891503683627458} + m_Layer: 0 + m_Name: NullSet + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!4 &6867583134219554799 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8389945206789797712} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 3251791125987375227} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &8099891503683627458 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8389945206789797712} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2db441f589c3407bb6fb5f02ff8ab541, type: 3} + m_Name: + m_EditorClassIdentifier: + m_inverted: 0 + m_shapes: + - Object: + referencePath: shape deletion test mesh + targetObject: {fileID: 0} + ShapeName: bottom + ChangeType: 1 + Value: 0 +--- !u!1001 &9210451080691405271 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 3251791125987375227} + m_Modifications: + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalPosition.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalRotation.w + value: 0.7071067 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalRotation.x + value: -0.7071068 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -3887185075125053422, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_DirtyAABB + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -3887185075125053422, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_AABB.m_Extent.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -3887185075125053422, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_AABB.m_Extent.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -3887185075125053422, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_AABB.m_Extent.z + value: 2 + objectReference: {fileID: 0} + - target: {fileID: -3887185075125053422, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_BlendShapeWeights.Array.data[0] + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 919132149155446097, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + propertyPath: m_Name + value: shape deletion test mesh + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: fe5b76dae94c07345b74d51e9a9a8440, type: 3} +--- !u!4 &8671858138418525756 stripped +Transform: + m_CorrespondingSourceObject: {fileID: -8679921383154817045, guid: fe5b76dae94c07345b74d51e9a9a8440, + type: 3} + m_PrefabInstance: {fileID: 9210451080691405271} + m_PrefabAsset: {fileID: 0} diff --git a/UnitTests~/ReactiveComponent/DeletionTest.prefab.meta b/UnitTests~/ReactiveComponent/DeletionTest.prefab.meta new file mode 100644 index 00000000..0197de59 --- /dev/null +++ b/UnitTests~/ReactiveComponent/DeletionTest.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a82669288fc87d94db320a2494fd76c5 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnitTests~/ReactiveComponent/ShapeDeletionAnalysis.cs b/UnitTests~/ReactiveComponent/ShapeDeletionAnalysis.cs new file mode 100644 index 00000000..7cb78e50 --- /dev/null +++ b/UnitTests~/ReactiveComponent/ShapeDeletionAnalysis.cs @@ -0,0 +1,98 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using modular_avatar_tests; +using nadena.dev.modular_avatar.core; +using nadena.dev.modular_avatar.core.editor; +using NUnit.Framework; +using UnityEngine; + +public class ShapeDeletionAnalysis : TestBase +{ + [Test] + public void BasicShapeDeletionAnalysis() + { + var root = CreatePrefab("DeletionTest.prefab"); + + var mesh = AssertPreviewDeletion(root); + + AssertBuildDeletion(mesh, root); + } + + [Test] + public void WhenShapeDeletionIsConditionedOnSubsequentChanger_DoesNotDelete() + { + var root = CreatePrefab("DeletionTest.prefab"); + root.transform.Find("MenuSet").gameObject.SetActive(true); + + AssertPreviewDeletion(root); + AssertNoMeshDeletion(root); + + var mesh = root.GetComponentInChildren(); + Assert.AreEqual(100, mesh.GetBlendShapeWeight(mesh.sharedMesh.GetBlendShapeIndex("bottom"))); + } + + + [Test] + public void WhenShapeDeletionIsConditionedOnItself_DoesNotDelete() + { + var root = CreatePrefab("DeletionTest.prefab"); + root.transform.Find("Delete").gameObject.AddComponent().InitSettings(); + + AssertNoPreviewDeletion(root); + AssertNoMeshDeletion(root); + + var mesh = root.GetComponentInChildren(); + // deletion action is initially off, so we use the shape changer above it, which is set to 50. + Assert.AreEqual(50f, mesh.GetBlendShapeWeight(mesh.sharedMesh.GetBlendShapeIndex("bottom"))); + } + + private static void AssertBuildDeletion(SkinnedMeshRenderer mesh, GameObject root) + { + var originalSharedMesh = mesh.sharedMesh; + AvatarProcessor.ProcessAvatar(root); + Assert.AreNotEqual(originalSharedMesh, mesh.sharedMesh); + + Assert.IsTrue(mesh.sharedMesh.vertices.All(v => v.z >= 0)); + } + + private static SkinnedMeshRenderer AssertPreviewDeletion(GameObject root) + { + var mesh = root.GetComponentInChildren(); + var analysis = new ReactiveObjectAnalyzer().Analyze(root); + var deletedShape = analysis.Shapes.GetValueOrDefault(new TargetProp() + { + TargetObject = mesh, + PropertyName = "deletedShape.bottom" + }); + Assert.IsNotNull(deletedShape); + var activeGroup = deletedShape.actionGroups.LastOrDefault(ag => ag.InitiallyActive); + Assert.AreEqual(1.0f, activeGroup?.Value); + return mesh; + } + + private static void AssertNoPreviewDeletion(GameObject root) + { + var mesh = root.GetComponentInChildren(); + var analysis = new ReactiveObjectAnalyzer().Analyze(root); + var deletedShape = analysis.Shapes.GetValueOrDefault(new TargetProp() + { + TargetObject = mesh, + PropertyName = "deletedShape.bottom" + }); + if (deletedShape != null) + { + var activeGroup = deletedShape.actionGroups.LastOrDefault(ag => ag.InitiallyActive); + Assert.IsFalse(activeGroup?.Value is float f && f > 0); + } + + } + + private static void AssertNoMeshDeletion(GameObject root) + { + var mesh = root.GetComponentInChildren(); + var originalSharedMesh = mesh.sharedMesh; + AvatarProcessor.ProcessAvatar(root); + Assert.AreEqual(originalSharedMesh, mesh.sharedMesh); + } +} diff --git a/UnitTests~/ReactiveComponent/ShapeDeletionAnalysis.cs.meta b/UnitTests~/ReactiveComponent/ShapeDeletionAnalysis.cs.meta new file mode 100644 index 00000000..024b7f11 --- /dev/null +++ b/UnitTests~/ReactiveComponent/ShapeDeletionAnalysis.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 18eb55e1b66a00243a91142456dfd5f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnitTests~/ReactiveComponent/shape deletion test mesh.fbx b/UnitTests~/ReactiveComponent/shape deletion test mesh.fbx new file mode 100644 index 0000000000000000000000000000000000000000..52fa8bb9d39297d235bc9146d397de02a8c87a50 GIT binary patch literal 17116 zcmc&+3y>Ved7eX$J4uIx1O|Z+Iur>#j7}#ZUJ^+6mJaPbxV;kyQibMr=k5mfW;VOC z(w(sl7@3MG5>ip95E56}AxSC522u&OgGr!JB?gLA2#`RCNkYJ}2oNc;j1>bW-`D-m zZtv~R?&{)Dy9rY7qG>*$4+^@sIV zzAQoeF2_zfnXbve@v^S({VE#kaLmDRR=#P|TEz@A9|JzB!C>+`S}@`pyl?7fbi={X81J~nFS z^!i>f-ZLLnivba$%<8nKdH`UpF)C;8@^hJF!glJDW9`xO`HXGO@DMwkTr+;13hNdme zo0~T`#fIXzzPx$U=H@LAK*%#dvj)e!c*60V9&gl-e+IWZQ~rpZ;;mIzXlX#paMsCm z+JSvBD9P2CIA+rTakF)+w9?FZRVQ&XpW2er(CY*?vBsl%-(fV&8FGV^6DRG1MsRgv#RACvt`iP?{XfhmQRe9`VN0$!twMV z1k9ITH>P{zK*moynZR|jSGL>=K`H&!IO+!C3vlZ66FH&`XHs#J?T!Z-*BgsdC9K2A zQXMyr`cQwSd30p*F9hZqv;k%zyJQkGLHbW1%?9}@jYMnpXWTKY-jp&OL`?fRFR9r; ztJR9`6d{HKV_$$%Nb&i$bUNiG)cV?k83#R^5}~K1akvb^IoVBkMe|*{pL+~)S?Cc#< zdJHQHdW@6VmQ5S+Z`0LRwluZ0Y*qi7TUuIvN1(;Ni%sZmH|11N0jHEPQ@#%*NN%mh z0i9BSn4Jyc_I@WBVc?Z>i{zrF@+7Be;X4I-Ky!0`6m$joDwy~cLGRM&*cqZ2SNlpW zHr|MN1`S$$8uHvC$k3)UH1h3&yE|9_|EHh*_b7*k zc@X@xcAsY(fgoj}-f!Yg5Wpd3Ck3DO$nPFU;lG|RXo|e;bD&^v?yi{pm>$+}x^2?U zD(C9Lb3;z6#|xY>M{fx|6krxs3CT9#02#M;A#=yP%B0r{DK}kNZrvFK36%%Mo?_iD zlr5T)8)$*D>bOsoj@#j5JF7e{#;|@|I&Oio>bRSs9IA>ohrHZ~!Z}<`)1RW`g#2<% z&TU~Dc~uU+ClFU@h($OJdu|X<*eR#mP6XJ4X!oR_L*Sz>`*IT_Vs35*D0^Gy!EI8G zRveInvpmnpsiWNjG|sGD8jaJlZDPd5Cg0(wz!0XTrLG2dw#PZFuEvEW9F@CxRCsJd zH}J?A(&>!axm3`B9Wi4^xQBDl4foE$(05;>c+mR6+jW?1eGdfUKmbgk6~2$1&ThW1K~=b z+{BO2nL*le91UUT*lpij+>8oK$)bYf!qrm-e37H_&_In{fDN&#XRu!g zXq8zW1C3#fv(cqZ8Me)HCt!}mZ54J=bS^G$0F5%j8(0m4z8~~q9>j|Kl)GEXB`;Jz|iB z;;rKAr2rhLsGPvY$4c77MB_r(>G%^)kePb76o(f7k&t3uSZ7IiKQ-h`q*I)MT>0H2 zlf6!M{0(ldQi=@zIru5|Yr_G9(~i|$!$ZHv^6rk#l8YAwu2aJixxL+=#KWcXHG8n) z{mf;ZYH{crHHFw+mC}ogz&T;2BifTQMZ6h^lyf5vh)iEUkZ~uNCD?=Ks}VQl22;xO zjQGAh_MnfY*4iNTRii}DTFURP z(zA%($(%)$l&&;&DT4QKk{izBUZJZvg}4U4fR14)nnI5Pn4^djX13^eqB>od%|@Lf`>+zdvK# z@08+lmq%!Ldi*YgKcOOeG{3Zn;GPz{*8!IHY1Xi?mNk^Iy)0}36>C>rZrY-s^{}vH z*v6nejb;6DW#E~w?kf;xP+DW56?1%Jl&tA$lzXEnzfq0yrYOpHDpT5%k=$N{byh)6 zdoq&S8>>-9a{F6CX*j=dLG@>l)>BfJS>dk=-4(D4Ryd!wLpp?P38&l7#M}ut==V}( z9WVM@%kjuddAH#ZubsyT+|g0b$!2e?9&}mOXbnJnYXN$!2B4d20s26ITC`|%0eoDF zIbL<@RjnO(Mj(mA_zaH+GEQDMj3Q>R?LH)^hA$fMv%%@Yz5-ZU@v}Il`TEL^XdRMp z)1(Y}ISns#M+MZGyP$6al|ru5Leh^Lyhg6*4=R#HF6UKPPSK-9uH~C6kX3Lyzg>|m zlF8>Ql0`E4sgSMJn&jN>@y4Bu8>o0YBA~busbFfva8t66R-R+`fsS)*ryh-wl+c~V z6niTgw6+;`d`Bl3K`}1QLoSpO8hR;k8acswD^W$7?(Rxdk(T?*XsQac>sTeKNDfyc zI~bu2doq&4gP@|dug3vbgpD|S+Hw;{#4yv~^WO!&Ps20nl7~4r5qg7}VsFEfK3VV8 z*m$%kV9PwH1=^TU`6+e>psC9%G&EyoO#V*1o*cBjF{h>-_DE%nnl{&ZWT7kYQPaM< zy9|asd1|uUUm2q&Szf7(QIjketkC+nTHA#t7rWpso#TxK<29Mat_o9=M2D-w)Fjlg zGBDaerzF%BO{KaihN(%YFI0u8NvLN9=1Q1ILL9_n%ts~Lys=bmOQ`;0Em9OnO#;QL zfYc<;mjJ?jR<;^GoyUCtwzeQlH>ntPdQR|3p^`0_({(tl!_bO!?+VK}QF$CVKJFhV z_xMzYwv8*bny%8_v3pp3&KRC+_d7^1}OT>eo>L0;5Q z{tYA34%}4D6kP-Ek&X5kpxrO#)x-7q)@o83;7yN zPP6sm1(F-HrzDS1X2q<>wTN;v#5ton-~5U5gW zyB`Q|+t3d6(RLk|N$UKgZ<3%X*rxlmLmez-a^m0wg>5(Vx znoRp1}VZ>1XivR}gE` zhyp&vKZjZi1oiq2aCSRb;9k8NM??7HHn!qF?rdgX^9+i(t z2Rh>hGz2c*wkNq-NQTvJP1Ys$S$8e^aNoTxf3>1_`PLPq3u704^`7UCj6ZnI))kj5 zeE8R1m}MRN)=GC96hxv497_iL)YO>o5z(1cVYf?AmVN54ejK4FpCIa{XlLNy{Yl(& za`BqJ^k=vy|HNiLw)2{WG#^Vl`?CBT_xB1f*W!LoS8~kBx{az4pGeA?5F{rgNjV&c zosVMy4)#A22dCosAYy;Au|&l-T`A>D_1@8gYU}$lB>UvQK5%gJrqM84eM7vhp)?x4)h^+u3*W@fE{=@Wy{FU21i`)bRU_B0J66D5T{LiVpcgt(ph5 zVjdjVJZJ&)pk>Shw}tXNf|<}93r%wj?@^{0G|W|GiV10;jkPJr)9@bX|Bs~U6z%aZ zLUxMw*d`KOkKxZ4-jhU@QAyN^vtDq^KHN9I@0zx^kG!+7^>6I|ZTWfK;ghr0o*7^C z_K{bQob+FM^~i~*TVKK5i6bXlK55zh;$JTs_@w10E8efW`_S?QPYFJaRu8T4xwmKm zhO?bBo!XcuXU?2(rBKDRA?-lhP*?2NAlJkQX~UJoI`%6n-^DH$+Up$CUg2YovRZ@D ztH^2>84ZO6)GM{*HfjNJJ-4M&aeT~mP`WQc)hR5TM}%xK$mWIYW?f3dLba5XP1r#~ zapx#;m3&79it5SBm&DB=BRwgNrh3+)fk2Fo^sgVtz1c&r$1dqdI&uO4Y3uFR2!_dHVTD61fUSc>HNND%NkNJimmGe-$Ee4e4giV}t6l%79A ze8bs`;M~~9pMCP~t?a8J47~{(J2@4oQcYhsMUYgN`oRTh^W{l{%OZ=uCk@D;TlLBo z*nq;hauzCSRWhgM0?rd)mUUdOg>5)oi2G1mz8ru07ghh?1_${aqB;ZRhd=GwCy+6v zepcfs1Y!U9=07n-XsK^i5eb#E+|7(+q!a~VVWm0;=oK!}BFp`;G(=jClPvLi7Ve3k1RjVb8UghMu^03+_Ea2w~Q}Yq%?$0o=|Wfo2|Y{{n9so zcnNzgM0c|A8#v~|4`tlRB$q17o$4?DM42bwVcJFH0*x^9H1hmyrEz~#RE~!-d^ju_ z$k@eSUVJghjVG3{O<6+yUP?K;yF~I5_2x(C#W?&X4U5@kjB@*1=QDP(;@mkxe~KT= z<%izqHI!nhL$YxM7`uu|a2F~FZy^xH5m9wcT)(gT~i6TF!F4kr+LCJq{V%6c{ zhhnY7fnS#5+tFA!*VXU;^itGmUh0mIu=fcUMG_oE&`vT zXepF?S_{6kAq37Y4sYvp+2-JzW55@C?B4R z$Q^f?+EeoQVXu%Qd?h(`cPbfKJYmDzQ#E83C)w72RVtuRD23I>ExEcz%z8UEw z1w`tD9vq1D*agHIG=l!MmE&!NehWH$>}4wDnSHrcuhjF=T-OojvhVNDyW3bu^nYc$ zg}KFi|0aw=&s~fbZs_@3S*PyM?=89_^{J-NMK`7WRL; zY`4&L%acwpTGG2yNz$>?q9dy{{c=!45t79 literal 0 HcmV?d00001 diff --git a/UnitTests~/ReactiveComponent/shape deletion test mesh.fbx.meta b/UnitTests~/ReactiveComponent/shape deletion test mesh.fbx.meta new file mode 100644 index 00000000..0f60ec10 --- /dev/null +++ b/UnitTests~/ReactiveComponent/shape deletion test mesh.fbx.meta @@ -0,0 +1,109 @@ +fileFormatVersion: 2 +guid: fe5b76dae94c07345b74d51e9a9a8440 +ModelImporter: + serializedVersion: 22200 + internalIDToNameTable: [] + externalObjects: {} + materials: + materialImportMode: 2 + materialName: 0 + materialSearch: 1 + materialLocation: 1 + animations: + legacyGenerateAnimations: 4 + bakeSimulation: 0 + resampleCurves: 1 + optimizeGameObjects: 0 + removeConstantScaleCurves: 0 + motionNodeName: + rigImportErrors: + rigImportWarnings: + animationImportErrors: + animationImportWarnings: + animationRetargetingWarnings: + animationDoRetargetingWarnings: 0 + importAnimatedCustomProperties: 0 + importConstraints: 0 + animationCompression: 1 + animationRotationError: 0.5 + animationPositionError: 0.5 + animationScaleError: 0.5 + animationWrapMode: 0 + extraExposedTransformPaths: [] + extraUserProperties: [] + clipAnimations: [] + isReadable: 0 + meshes: + lODScreenPercentages: [] + globalScale: 1 + meshCompression: 0 + addColliders: 0 + useSRGBMaterialColor: 1 + sortHierarchyByName: 1 + importPhysicalCameras: 1 + importVisibility: 1 + importBlendShapes: 1 + importCameras: 1 + importLights: 1 + nodeNameCollisionStrategy: 1 + fileIdsGeneration: 2 + swapUVChannels: 0 + generateSecondaryUV: 0 + useFileUnits: 1 + keepQuads: 0 + weldVertices: 1 + bakeAxisConversion: 0 + preserveHierarchy: 0 + skinWeightsMode: 0 + maxBonesPerVertex: 4 + minBoneWeight: 0.001 + optimizeBones: 1 + meshOptimizationFlags: -1 + indexFormat: 0 + secondaryUVAngleDistortion: 8 + secondaryUVAreaDistortion: 15.000001 + secondaryUVHardAngle: 88 + secondaryUVMarginMethod: 1 + secondaryUVMinLightmapResolution: 40 + secondaryUVMinObjectScale: 1 + secondaryUVPackMargin: 4 + useFileScale: 1 + strictVertexDataChecks: 0 + tangentSpace: + normalSmoothAngle: 60 + normalImportMode: 0 + tangentImportMode: 3 + normalCalculationMode: 4 + legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0 + blendShapeNormalImportMode: 1 + normalSmoothingSource: 0 + referencedClips: [] + importAnimation: 1 + humanDescription: + serializedVersion: 3 + human: [] + skeleton: [] + armTwist: 0.5 + foreArmTwist: 0.5 + upperLegTwist: 0.5 + legTwist: 0.5 + armStretch: 0.05 + legStretch: 0.05 + feetSpacing: 0 + globalScale: 1 + rootMotionBoneName: + hasTranslationDoF: 0 + hasExtraRoot: 0 + skeletonHasParents: 1 + lastHumanDescriptionAvatarSource: {instanceID: 0} + autoGenerateAvatarMappingIfUnspecified: 1 + animationType: 2 + humanoidOversampling: 1 + avatarSetup: 0 + addHumanoidExtraRootOnlyWhenUsingAvatar: 1 + importBlendShapeDeformPercent: 1 + remapMaterialsIfMaterialImportModeIsNone: 0 + additionalBone: 0 + userData: + assetBundleName: + assetBundleVariant: