Skip to content

Commit

Permalink
fix: incorrect handling of shape key deletion (#1258)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bdunderscore authored Oct 4, 2024
1 parent c379d73 commit 30cafb2
Show file tree
Hide file tree
Showing 13 changed files with 1,037 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ internal class AnimatedProperty
public TargetProp TargetProp { get; }
public string ControlParam { get; set; }

public bool alwaysDeleted;
public object currentState;

// Objects which trigger deletion of this shape key.
Expand Down
10 changes: 4 additions & 6 deletions Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,8 +8,8 @@ internal class ReactionRule
{
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)
Expand All @@ -31,15 +30,15 @@ private ReactionRule(TargetProp key, object value)

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}";
Expand All @@ -57,7 +56,6 @@ public bool TryMerge(ReactionRule other)
}
else return false;
if (!ControllingConditions.SequenceEqual(other.ControllingConditions)) return false;
if (IsDelete || other.IsDelete) return false;

return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,50 +124,55 @@ private Dictionary<TargetProp, AnimatedProperty> FindShapes(GameObject root)
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);
}
value = shape.ChangeType == ShapeChangeType.Delete ? 1 : 0;
RegisterAction(key, renderer, 0, value, changer, shape);
}
}

var action = ObjectRule(key, changer, value);
action.Inverted = _computeContext.Observe(changer, c => c.Inverted);
var isCurrentlyActive = changer.gameObject.activeInHierarchy;
return shapeKeys;

if (shape.ChangeType == ShapeChangeType.Delete)
{
action.IsDelete = true;

if (isCurrentlyActive) info.currentState = 100;
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;

info.actionGroups.Add(action); // Never merge
// Add initial state
var agk = new ReactionRule(key, value);
agk.Value = currentValue;
info.actionGroups.Add(agk);
}

continue;
}
var action = ObjectRule(key, changer, value);
action.Inverted = _computeContext.Observe(changer, c => c.Inverted);

if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
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);
}
if (info.actionGroups.Count == 0)
{
info.actionGroups.Add(action);
}
else if (!info.actionGroups[^1].TryMerge(action))
{
info.actionGroups.Add(action);
}
}

return shapeKeys;
}

private void FindMaterialSetters(Dictionary<TargetProp, AnimatedProperty> objectGroups, GameObject root)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ internal partial class ReactiveObjectAnalyzer
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;

Expand Down Expand Up @@ -58,7 +61,6 @@ public struct AnalysisResult
{
public Dictionary<TargetProp, AnimatedProperty> Shapes;
public Dictionary<TargetProp, object> InitialStates;
public HashSet<TargetProp> DeletedShapes;
}

private static PropCache<GameObject, AnalysisResult> _analysisCache;
Expand Down Expand Up @@ -86,7 +88,6 @@ public static AnalysisResult CachedAnalyze(ComputeContext context, GameObject ro
/// </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
Expand All @@ -98,7 +99,6 @@ GameObject root
{
result.Shapes = new();
result.InitialStates = new();
result.DeletedShapes = new();
return result;
}

Expand All @@ -109,7 +109,7 @@ GameObject root
ApplyInitialStateOverrides(shapes);
AnalyzeConstants(shapes);
ResolveToggleInitialStates(shapes);
PreprocessShapes(shapes, out result.InitialStates, out result.DeletedShapes);
PreprocessShapes(shapes, out result.InitialStates);
result.Shapes = shapes;

return result;
Expand Down Expand Up @@ -165,7 +165,7 @@ private void AnalyzeConstants(Dictionary<TargetProp, AnimatedProperty> shapes)
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);
}
Expand Down Expand Up @@ -264,18 +264,17 @@ private void ResolveToggleInitialStates(Dictionary<TargetProp, AnimatedProperty>
}

/// <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())
{
Expand All @@ -285,18 +284,6 @@ private void PreprocessShapes(Dictionary<TargetProp, AnimatedProperty> shapes, o
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)
Expand Down
79 changes: 54 additions & 25 deletions Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,19 +41,18 @@ internal void Execute()

var shapes = analysis.Shapes;
var initialStates = analysis.InitialStates;
var deletedShapes = analysis.DeletedShapes;

GenerateActiveSelfProxies(shapes);

ProcessMeshDeletion(initialStates, shapes);

ProcessInitialStates(initialStates, shapes);
ProcessInitialAnimatorVariables(shapes);

foreach (var groups in shapes.Values)
{
ProcessShapeKey(groups);
}

ProcessMeshDeletion(deletedShapes);
}

private void GenerateActiveSelfProxies(Dictionary<TargetProp, AnimatedProperty> shapes)
Expand Down Expand Up @@ -225,30 +223,65 @@ private void ProcessInitialStates(Dictionary<TargetProp, object> initialStates,

#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()
);

foreach (var (renderer, infos) in renderers)
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;
}
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
});
}
}
}

Expand All @@ -257,10 +290,6 @@ private void ProcessMeshDeletion(HashSet<TargetProp> deletedKeys)
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);
}
Expand Down
Loading

0 comments on commit 30cafb2

Please sign in to comment.