fix: incorrect handling of shape key deletion (#1258)

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
This commit is contained in:
bd_ 2024-10-03 20:16:53 -07:00 committed by GitHub
parent c379d730ca
commit 30cafb21e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1040 additions and 102 deletions

View File

@ -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.

View File

@ -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;
}

View File

@ -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<TargetProp, AnimatedProperty> objectGroups, GameObject root)

View File

@ -18,6 +18,9 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly ndmf.BuildContext _context;
private readonly AnimationServicesContext _asc;
private Dictionary<string, float> _simulationInitialStates;
public const string BlendshapePrefix = "blendShape.";
public const string DeletedShapePrefix = "deletedShape.";
public ImmutableDictionary<string, float> ForcePropertyOverrides { get; set; } = ImmutableDictionary<string, float>.Empty;
@ -58,7 +61,6 @@ namespace nadena.dev.modular_avatar.core.editor
{
public Dictionary<TargetProp, AnimatedProperty> Shapes;
public Dictionary<TargetProp, object> InitialStates;
public HashSet<TargetProp> DeletedShapes;
}
private static PropCache<GameObject, AnalysisResult> _analysisCache;
@ -86,7 +88,6 @@ namespace nadena.dev.modular_avatar.core.editor
/// </summary>
/// <param name="root">The avatar root</param>
/// <param name="initialStates">A dictionary of target property to initial state (float or UnityEngine.Object)</param>
/// <param name="deletedShapes">A hashset of blendshape properties which are always deleted</param>
/// <returns></returns>
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
}
/// <summary>
/// Determine initial state and deleted shapes for all properties
/// Determine initial state for all properties
/// </summary>
/// <param name="shapes"></param>
/// <param name="initialStates"></param>
/// <param name="deletedShapes"></param>
private void PreprocessShapes(Dictionary<TargetProp, AnimatedProperty> shapes, out Dictionary<TargetProp, object> initialStates, out HashSet<TargetProp> deletedShapes)
private void PreprocessShapes(Dictionary<TargetProp, AnimatedProperty> shapes,
out Dictionary<TargetProp, object> 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<TargetProp, object>();
deletedShapes = new HashSet<TargetProp>();
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)

View File

@ -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<TargetProp, AnimatedProperty> shapes)
@ -225,30 +223,65 @@ namespace nadena.dev.modular_avatar.core.editor
#region Mesh processing
private void ProcessMeshDeletion(HashSet<TargetProp> deletedKeys)
private void ProcessMeshDeletion(Dictionary<TargetProp, object> initialStates,
Dictionary<TargetProp, AnimatedProperty> shapes)
{
ImmutableDictionary<SkinnedMeshRenderer, List<TargetProp>> 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);
}

View File

@ -72,8 +72,8 @@ namespace nadena.dev.modular_avatar.core.editor
var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot);
var shapes = analysis.Shapes;
ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>>.Builder rendererStates =
ImmutableDictionary.CreateBuilder<SkinnedMeshRenderer, ImmutableList<(int, float)>>(
var rendererStates =
ImmutableDictionary.CreateBuilder<SkinnedMeshRenderer, ImmutableDictionary<int, float>>(
);
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<int, float>.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<RenderGroup> ShapesToGroups(GameObject avatarRoot, ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>> shapes)

View File

@ -471,7 +471,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
var f_set_inactive = effectGroup.Q<VisualElement>("effect__set-inactive");
var f_value = effectGroup.Q<FloatField>("effect__value");
var f_material = effectGroup.Q<ObjectField>("effect__material");
var f_delete = effectGroup.Q("effect__deleted");
var f_delete = effectGroup.Q<TextField>("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);

View File

@ -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}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a82669288fc87d94db320a2494fd76c5
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<SkinnedMeshRenderer>();
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<ModularAvatarMenuItem>().InitSettings();
AssertNoPreviewDeletion(root);
AssertNoMeshDeletion(root);
var mesh = root.GetComponentInChildren<SkinnedMeshRenderer>();
// 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<SkinnedMeshRenderer>();
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<SkinnedMeshRenderer>();
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<SkinnedMeshRenderer>();
var originalSharedMesh = mesh.sharedMesh;
AvatarProcessor.ProcessAvatar(root);
Assert.AreEqual(originalSharedMesh, mesh.sharedMesh);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 18eb55e1b66a00243a91142456dfd5f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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: