fix: propagate shape changer effects through BlendshapeSync (#1267)

Closes: #1259
This commit is contained in:
bd_ 2024-10-05 17:46:45 -07:00 committed by GitHub
parent 656a401684
commit 828e6b4548
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 193 additions and 10 deletions

View File

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

View File

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

View File

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

View File

@ -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<ModularAvatarBlendshapeSync>(root, true);
foreach (var bss in components)
{
var localMesh = _computeContext.GetComponent<SkinnedMeshRenderer>(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<SkinnedMeshRenderer>(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))
{

View File

@ -101,6 +101,8 @@ namespace nadena.dev.modular_avatar.core.editor
result.InitialStates = new();
return result;
}
LocateBlendshapeSyncs(root);
Dictionary<TargetProp, AnimatedProperty> shapes = FindShapes(root);
FindObjectToggles(shapes, root);