From 828e6b4548c4fb0817cc48f36b3ce520a9328901 Mon Sep 17 00:00:00 2001 From: bd_ Date: Sat, 5 Oct 2024 17:46:45 -0700 Subject: [PATCH] fix: propagate shape changer effects through BlendshapeSync (#1267) Closes: #1259 --- .../AnimationGeneration/AnimatedProperty.cs | 32 +++++- .../AnimationGeneration/ControlCondition.cs | 32 +++++- .../AnimationGeneration/ReactionRule.cs | 32 +++++- .../ReactiveObjectAnalyzer.LocateReactions.cs | 105 +++++++++++++++++- .../ReactiveObjectAnalyzer.cs | 2 + 5 files changed, 193 insertions(+), 10 deletions(-) diff --git a/Editor/ReactiveObjects/AnimationGeneration/AnimatedProperty.cs b/Editor/ReactiveObjects/AnimationGeneration/AnimatedProperty.cs index f56a59af..6a488a5a 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/AnimatedProperty.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/AnimatedProperty.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; -using UnityEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using Object = UnityEngine.Object; namespace nadena.dev.modular_avatar.core.editor { internal class AnimatedProperty { public TargetProp TargetProp { get; } - public string ControlParam { get; set; } public object currentState; @@ -24,5 +25,30 @@ namespace nadena.dev.modular_avatar.core.editor TargetProp = key; this.currentState = currentState; } + + protected bool Equals(AnimatedProperty other) + { + return Equals(currentState, other.currentState) && actionGroups.SequenceEqual(other.actionGroups) && + TargetProp.Equals(other.TargetProp); + } + + public override bool Equals(object obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((AnimatedProperty)obj); + } + + public override int GetHashCode() + { + var actionGroupHash = 0; + foreach (var ag in actionGroups) + { + actionGroupHash = HashCode.Combine(actionGroupHash, ag); + } + + return HashCode.Combine(currentState, actionGroupHash, TargetProp); + } } } \ No newline at end of file diff --git a/Editor/ReactiveObjects/AnimationGeneration/ControlCondition.cs b/Editor/ReactiveObjects/AnimationGeneration/ControlCondition.cs index 33368ea5..f61b33e0 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ControlCondition.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ControlCondition.cs @@ -1,11 +1,13 @@ -using UnityEngine; +using System; +using UnityEngine; +using Object = UnityEngine.Object; namespace nadena.dev.modular_avatar.core.editor { internal class ControlCondition { public string Parameter; - public UnityEngine.Object DebugReference; + public Object DebugReference; public string DebugName; public bool IsConstant; @@ -14,5 +16,31 @@ namespace nadena.dev.modular_avatar.core.editor public bool IsConstantActive => InitiallyActive && IsConstant; public GameObject ReferenceObject; + + protected bool Equals(ControlCondition other) + { + return Parameter == other.Parameter + && Equals(DebugReference, other.DebugReference) + && DebugName == other.DebugName + && IsConstant == other.IsConstant + && ParameterValueLo.Equals(other.ParameterValueLo) + && ParameterValueHi.Equals(other.ParameterValueHi) + && InitialValue.Equals(other.InitialValue) + && Equals(ReferenceObject, other.ReferenceObject); + } + + public override bool Equals(object obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((ControlCondition)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Parameter, DebugReference, DebugName, IsConstant, ParameterValueLo, + ParameterValueHi, InitialValue, ReferenceObject); + } } } \ No newline at end of file diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs index 12bea0cb..497046ab 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using UnityEngine; +using Object = UnityEngine.Object; namespace nadena.dev.modular_avatar.core.editor { @@ -59,5 +61,33 @@ namespace nadena.dev.modular_avatar.core.editor return true; } + + protected bool Equals(ReactionRule other) + { + return TargetProp.Equals(other.TargetProp) + && Equals(Value, other.Value) + && Equals(ControllingObject, other.ControllingObject) + && ControllingConditions.SequenceEqual(other.ControllingConditions) + && Inverted == other.Inverted; + } + + public override bool Equals(object obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((ReactionRule)obj); + } + + public override int GetHashCode() + { + var ccHash = 0; + foreach (var cc in ControllingConditions) + { + ccHash = HashCode.Combine(ccHash, cc); + } + + return HashCode.Combine(TargetProp, Value, ControllingObject, ccHash, Inverted); + } } } \ No newline at end of file diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs index f2e500e7..179e4ce4 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using nadena.dev.ndmf.preview; using UnityEngine; @@ -38,6 +39,77 @@ namespace nadena.dev.modular_avatar.core.editor return param; } } + + private readonly Dictionary<(SkinnedMeshRenderer, string), HashSet<(SkinnedMeshRenderer, string)>> + _blendshapeSyncMappings = new(); + + private void LocateBlendshapeSyncs(GameObject root) + { + var components = _computeContext.GetComponentsInChildren(root, true); + + foreach (var bss in components) + { + var localMesh = _computeContext.GetComponent(bss.gameObject); + if (localMesh == null) continue; + + foreach (var entry in _computeContext.Observe(bss, bss_ => bss_.Bindings.ToImmutableList(), + Enumerable.SequenceEqual)) + { + var src = entry.ReferenceMesh.Get(bss); + if (src == null) continue; + + var srcMesh = _computeContext.GetComponent(src); + + var localBlendshape = entry.LocalBlendshape; + if (string.IsNullOrWhiteSpace(localBlendshape)) + { + localBlendshape = entry.Blendshape; + } + + var srcBinding = (srcMesh, entry.Blendshape); + var dstBinding = (localMesh, localBlendshape); + + if (!_blendshapeSyncMappings.TryGetValue(srcBinding, out var dstSet)) + { + dstSet = new HashSet<(SkinnedMeshRenderer, string)>(); + _blendshapeSyncMappings[srcBinding] = dstSet; + } + + dstSet.Add(dstBinding); + } + } + + // For recursive blendshape syncs, we need to precompute the full set of affected blendshapes. + foreach (var (src, dsts) in _blendshapeSyncMappings) + { + var visited = new HashSet<(SkinnedMeshRenderer, string)>(); + foreach (var item in Visit(src, visited).ToList()) + { + dsts.Add(item); + } + } + + IEnumerable<(SkinnedMeshRenderer, string)> Visit( + (SkinnedMeshRenderer, string) key, + HashSet<(SkinnedMeshRenderer, string)> visited + ) + { + if (!visited.Add(key)) yield break; + + if (_blendshapeSyncMappings.TryGetValue(key, out var children)) + { + foreach (var child in children) + { + foreach (var item in Visit(child, visited)) + { + yield return item; + } + } + } + + yield return key; + } + } private void BuildConditions(Component controllingComponent, ReactionRule rule) { @@ -130,8 +202,34 @@ namespace nadena.dev.modular_avatar.core.editor var currentValue = renderer.GetBlendShapeWeight(shapeId); var value = shape.ChangeType == ShapeChangeType.Delete ? 100 : shape.Value; - RegisterAction(key, renderer, currentValue, value, changer, shape); + RegisterAction(key, currentValue, value, changer); + if (_blendshapeSyncMappings.TryGetValue((renderer, shape.ShapeName), out var bindings)) + { + // Propagate the new value through any Blendshape Syncs we might have. + // Note that we don't propagate deletes; it's common to e.g. want to delete breasts from the + // base model while retaining outerwear that matches the breast size. + foreach (var binding in bindings) + { + var bindingKey = new TargetProp + { + TargetObject = binding.Item1, + PropertyName = BlendshapePrefix + binding.Item2 + }; + var bindingRenderer = binding.Item1; + + var bindingMesh = bindingRenderer.sharedMesh; + if (bindingMesh == null) continue; + + var bindingShapeIndex = bindingMesh.GetBlendShapeIndex(binding.Item2); + if (bindingShapeIndex < 0) continue; + + var bindingInitialState = bindingRenderer.GetBlendShapeWeight(bindingShapeIndex); + + RegisterAction(bindingKey, bindingInitialState, value, changer); + } + } + key = new TargetProp { TargetObject = renderer, @@ -139,14 +237,13 @@ namespace nadena.dev.modular_avatar.core.editor }; value = shape.ChangeType == ShapeChangeType.Delete ? 1 : 0; - RegisterAction(key, renderer, 0, value, changer, shape); + RegisterAction(key, 0, value, changer); } } return shapeKeys; - void RegisterAction(TargetProp key, SkinnedMeshRenderer renderer, float currentValue, float value, - ModularAvatarShapeChanger changer, ChangedShape shape) + void RegisterAction(TargetProp key, float currentValue, float value, ModularAvatarShapeChanger changer) { if (!shapeKeys.TryGetValue(key, out var info)) { diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs index 1c5b97b2..643c4a4e 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs @@ -101,6 +101,8 @@ namespace nadena.dev.modular_avatar.core.editor result.InitialStates = new(); return result; } + + LocateBlendshapeSyncs(root); Dictionary shapes = FindShapes(root); FindObjectToggles(shapes, root);