Skip to content

Commit 24f13b0

Browse files
committed
Let Follow custom animations use the average of two parameters
New Follow animations pair a left/right parameter automatically. The generator computes the average in the Inputs layer and the preview uses it too.
1 parent 604c122 commit 24f13b0

3 files changed

Lines changed: 93 additions & 10 deletions

File tree

‎Packages/net.pawlygon.unitytools/Editor/FTExtrasCustomAnimations.cs‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ public class FTExtrasCustomAnimation
9191

9292
// --- Follow ---
9393
public string followParameter;
94+
[Tooltip("Follow the average of Follow Parameter and this one (e.g. left and right brow).")]
95+
public bool followAverage;
96+
public string followParameterB;
9497
public FTExtrasFollowStyle followStyle = FTExtrasFollowStyle.FadeIn;
9598
public float fromValue;
9699
public float toValue = 1f;
@@ -119,9 +122,19 @@ public class FTExtrasCustomAnimation
119122
/// <summary>Every parameter this animation reads.</summary>
120123
public IEnumerable<string> UsedParameters =>
121124
mode == FTExtrasCustomMode.Follow
122-
? new[] { followParameter }.Where(p => !string.IsNullOrEmpty(p))
125+
? new[] { followParameter, IsAveraged ? followParameterB : null }.Where(p => !string.IsNullOrEmpty(p)).Distinct()
123126
: conditions.Select(c => c.parameter).Where(p => !string.IsNullOrEmpty(p)).Distinct();
124127

128+
/// <summary>Follow: whether the value is the average of two parameters.</summary>
129+
public bool IsAveraged => followAverage && !string.IsNullOrEmpty(followParameterB);
130+
131+
/// <summary>Follow: the value that drives the animation (one parameter, or the average of two).</summary>
132+
public float FollowValue(Func<string, float> valueOf)
133+
{
134+
float a = string.IsNullOrEmpty(followParameter) ? 0f : valueOf(followParameter);
135+
return IsAveraged ? (a + valueOf(followParameterB)) * 0.5f : a;
136+
}
137+
125138
/// <summary>
126139
/// Whether the conditions start the animation (All: every one met; Any: at least one).
127140
/// </summary>

‎Packages/net.pawlygon.unitytools/Editor/FTExtrasGenerator.cs‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,9 @@ private static void BuildInputsLayer(ControllerBuilder builder, ClipStore clips,
478478
/// </summary>
479479
private static BlendTree InputsTree(ControllerBuilder builder, ClipStore clips, FTExtrasGenerationSettings settings, CustomAnimationPlan customs, bool includeJaw)
480480
{
481-
var outputs = new[] { ParamGazeLeftX, ParamGazeRightX, ParamGazeY, ParamMood, ParamJaw };
481+
var outputs = new[] { ParamGazeLeftX, ParamGazeRightX, ParamGazeY, ParamMood, ParamJaw }
482+
.Concat(customs.Animations.Where(a => a.HasComputedDriver).Select(a => a.Driver))
483+
.ToArray();
482484
AnimationClip zero = clips.Parameters("Inputs - Zero", outputs.Select(p => (p, 0f)).ToArray());
483485

484486
BlendTree root = builder.NewTree(includeJaw ? "Inputs (Jaw On)" : "Inputs (Jaw Off)", BlendTreeType.Direct, null);
@@ -497,6 +499,13 @@ private static BlendTree InputsTree(ControllerBuilder builder, ClipStore clips,
497499
children.Add(Direct(RangeTree(builder, clips, settings.jawX, settings.jawFullAt, ParamJaw, 1f), settings.lipTrackingActive));
498500
}
499501

502+
// Followed averages: each parameter adds half of its value.
503+
foreach (CustomAnimationSpec spec in customs.Animations.Where(a => a.HasComputedDriver))
504+
{
505+
children.Add(Direct(HalfTree(builder, clips, spec.Animation.followParameter, spec.Driver, $"{spec.SafeName} A"), ParamOne));
506+
children.Add(Direct(HalfTree(builder, clips, spec.Animation.followParameterB, spec.Driver, $"{spec.SafeName} B"), ParamOne));
507+
}
508+
500509
// Scrubbed custom animations: map the parameter's from..to range onto a 0..1 playback position.
501510
foreach (CustomAnimationSpec spec in customs.Animations.Where(a => a.ScrubParameter != null))
502511
{
@@ -508,15 +517,31 @@ private static BlendTree InputsTree(ControllerBuilder builder, ClipStore clips,
508517
}
509518

510519
/// <summary>
511-
/// 1D tree mapping the Follow parameter's from..to range to a 0..1 playback position (clamped outside).
520+
/// 1D tree writing half of the input into the output, linear over the parameter's range (at least
521+
/// -1..1); two of them in the Direct tree make the average.
522+
/// </summary>
523+
private static BlendTree HalfTree(ControllerBuilder builder, ClipStore clips, string input, string output, string label)
524+
{
525+
FTExtrasParameterCatalog.Entry info = FTExtrasParameterCatalog.Describe(input);
526+
float low = Mathf.Min(-1f, info.Min);
527+
float high = Mathf.Max(1f, info.Max);
528+
529+
BlendTree tree = builder.NewTree($"Custom - {label} half of {input}", BlendTreeType.Simple1D, input);
530+
tree.AddChild(clips.Parameters($"Custom - {label} - Low", (output, low * 0.5f)), low);
531+
tree.AddChild(clips.Parameters($"Custom - {label} - High", (output, high * 0.5f)), high);
532+
return tree;
533+
}
534+
535+
/// <summary>
536+
/// 1D tree mapping the Follow value's from..to range to a 0..1 playback position (clamped outside).
512537
/// </summary>
513538
private static BlendTree PositionTree(ControllerBuilder builder, ClipStore clips, CustomAnimationSpec spec)
514539
{
515540
FTExtrasCustomAnimation animation = spec.Animation;
516541
AnimationClip start = clips.Parameters($"Custom - {spec.SafeName} - Position 0", (spec.ScrubParameter, 0f));
517542
AnimationClip end = clips.Parameters($"Custom - {spec.SafeName} - Position 1", (spec.ScrubParameter, 1f));
518543

519-
BlendTree tree = builder.NewTree($"{animation.name} position from {animation.followParameter}", BlendTreeType.Simple1D, animation.followParameter);
544+
BlendTree tree = builder.NewTree($"{animation.name} position from {spec.Driver}", BlendTreeType.Simple1D, spec.Driver);
520545
AddSorted(tree, (animation.fromValue, start), (animation.toValue, end));
521546
return tree;
522547
}
@@ -771,6 +796,10 @@ private class CustomAnimationSpec
771796
public List<string> Gates = new List<string>();
772797
public string Toggle;
773798
public string ScrubParameter;
799+
800+
/// <summary>Follow: the parameter the layer reads; a computed average when following two.</summary>
801+
public string Driver;
802+
public bool HasComputedDriver => Driver != null && Driver != Animation.followParameter;
774803
}
775804

776805
/// <summary>
@@ -819,6 +848,11 @@ public CustomAnimationPlan(FTExtrasProfile profile, FTExtrasGenerationSettings s
819848
Toggles.Add(new CustomToggle { Label = animation.name, Parameter = spec.Toggle, DefaultOn = animation.toggleDefaultOn });
820849
}
821850

851+
if (animation.mode == FTExtrasCustomMode.Follow)
852+
{
853+
spec.Driver = animation.IsAveraged ? CustomParamPrefix + safeName + "/Value" : animation.followParameter;
854+
}
855+
822856
if (animation.mode == FTExtrasCustomMode.Follow && animation.followStyle == FTExtrasFollowStyle.Scrub)
823857
{
824858
spec.ScrubParameter = CustomParamPrefix + safeName + "/Position";
@@ -836,6 +870,7 @@ public void AddParameters(ControllerBuilder builder)
836870
foreach (string gate in spec.Gates) builder.AddFloat(gate, 0f);
837871
if (spec.Toggle != null) builder.AddBool(spec.Toggle, spec.Animation.toggleDefaultOn);
838872
if (spec.ScrubParameter != null) builder.AddFloat(spec.ScrubParameter, 0f);
873+
if (spec.HasComputedDriver) builder.AddFloat(spec.Driver, 0f);
839874
}
840875
}
841876

@@ -845,6 +880,7 @@ private static string DescribeProblem(FTExtrasCustomAnimation animation)
845880
if (animation.mode == FTExtrasCustomMode.Follow)
846881
{
847882
if (string.IsNullOrWhiteSpace(animation.followParameter)) return "no parameter is set.";
883+
if (animation.followAverage && string.IsNullOrWhiteSpace(animation.followParameterB)) return "the parameter to average with is not set.";
848884
if (Mathf.Approximately(animation.fromValue, animation.toValue)) return "'From' and 'To' are the same value.";
849885
}
850886
else
@@ -901,14 +937,14 @@ private static void BuildFollowLayer(ControllerBuilder builder, ClipStore clips,
901937
{
902938
case FTExtrasFollowStyle.FadeIn:
903939
{
904-
BlendTree tree = builder.NewTree($"{animation.name} (fade in)", BlendTreeType.Simple1D, animation.followParameter);
940+
BlendTree tree = builder.NewTree($"{animation.name} (fade in)", BlendTreeType.Simple1D, spec.Driver);
905941
AddSorted(tree, (animation.fromValue, empty), (animation.toValue, animation.clip));
906942
active.motion = tree;
907943
break;
908944
}
909945
case FTExtrasFollowStyle.TwoSided:
910946
{
911-
BlendTree tree = builder.NewTree($"{animation.name} (two-sided)", BlendTreeType.Simple1D, animation.followParameter);
947+
BlendTree tree = builder.NewTree($"{animation.name} (two-sided)", BlendTreeType.Simple1D, spec.Driver);
912948
float middle = (animation.fromValue + animation.toValue) * 0.5f;
913949
AddSorted(tree, (animation.fromValue, animation.negativeClip != null ? (Motion)animation.negativeClip : empty), (middle, empty), (animation.toValue, animation.clip));
914950
active.motion = tree;
@@ -947,8 +983,8 @@ private static void BuildFollowLayer(ControllerBuilder builder, ClipStore clips,
947983
float span = Mathf.Abs(animation.toValue - animation.fromValue);
948984
float enter = animation.fromValue + direction * span * 0.02f;
949985
float leave = animation.fromValue + direction * span * 0.01f;
950-
start.AddCondition(direction > 0f ? AnimatorConditionMode.Greater : AnimatorConditionMode.Less, enter, animation.followParameter);
951-
Transition(active, off, Blend).AddCondition(direction > 0f ? AnimatorConditionMode.Less : AnimatorConditionMode.Greater, leave, animation.followParameter);
986+
start.AddCondition(direction > 0f ? AnimatorConditionMode.Greater : AnimatorConditionMode.Less, enter, spec.Driver);
987+
Transition(active, off, Blend).AddCondition(direction > 0f ? AnimatorConditionMode.Less : AnimatorConditionMode.Greater, leave, spec.Driver);
952988
}
953989

954990
AddStopTransitions(sm, off, spec, Blend);

‎Packages/net.pawlygon.unitytools/Editor/FaceTrackingExtras.Custom.cs‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ private void AddCustomAnimation(FTExtrasCustomMode mode)
170170
if (first != null)
171171
{
172172
animation.followParameter = first.Name;
173+
174+
// Left/right pairs are usually followed together.
175+
string otherSide = MatchingSide(first.Name, catalog);
176+
animation.followAverage = otherSide != first.Name;
177+
animation.followParameterB = animation.followAverage ? otherSide : null;
178+
173179
animation.fromValue = Mathf.Max(0f, first.Min);
174180
animation.toValue = first.Max;
175181
}
@@ -330,7 +336,31 @@ private void DrawCustomAnimationEditor(int index, FTExtrasCustomAnimation animat
330336

331337
private void DrawFollowSettings(FTExtrasCustomAnimation animation, List<FTExtrasParameterCatalog.Entry> catalog)
332338
{
339+
string previous = animation.followParameter;
333340
animation.followParameter = DrawParameterField("Parameter", animation.followParameter, catalog);
341+
342+
// Keep a left/right pair together when the first parameter changes.
343+
if (animation.followParameter != previous && animation.followParameterB == MatchingSide(previous, catalog))
344+
{
345+
animation.followParameterB = MatchingSide(animation.followParameter, catalog);
346+
}
347+
348+
bool average = EditorGUILayout.Toggle(
349+
new GUIContent("Average with", "Follow the average of two parameters, e.g. the left and right brow."), animation.followAverage);
350+
if (average && !animation.followAverage && string.IsNullOrEmpty(animation.followParameterB))
351+
{
352+
animation.followParameterB = MatchingSide(animation.followParameter, catalog);
353+
}
354+
animation.followAverage = average;
355+
if (average)
356+
{
357+
animation.followParameterB = DrawParameterField(" ", animation.followParameterB, catalog);
358+
if (animation.followParameterB == animation.followParameter)
359+
{
360+
EditorGUILayout.HelpBox("Both parameters are the same; pick the other one to average.", MessageType.Info);
361+
}
362+
}
363+
334364
FTExtrasParameterCatalog.Entry info = FTExtrasParameterCatalog.Describe(animation.followParameter);
335365

336366
animation.followStyle = (FTExtrasFollowStyle)EditorGUILayout.Popup("Style", (int)animation.followStyle, FollowStyleLabels);
@@ -503,7 +533,10 @@ private static string DescribeCustomAnimation(FTExtrasCustomAnimation animation)
503533
if (animation.mode == FTExtrasCustomMode.Follow)
504534
{
505535
string style = FollowStyleLabels[(int)animation.followStyle].Split(' ')[0].ToLowerInvariant();
506-
return $"{clip} follows {ParameterLabel(animation.followParameter)} ({style}, {animation.fromValue:0.##} → {animation.toValue:0.##})";
536+
string source = animation.IsAveraged
537+
? $"the average of {ParameterLabel(animation.followParameter)} and {ParameterLabel(animation.followParameterB)}"
538+
: ParameterLabel(animation.followParameter);
539+
return $"{clip} follows {source} ({style}, {animation.fromValue:0.##} → {animation.toValue:0.##})";
507540
}
508541

509542
string joiner = animation.combine == FTExtrasConditionCombine.All ? " and " : " or ";
@@ -521,6 +554,7 @@ private static string DescribeCustomProblem(FTExtrasCustomAnimation animation)
521554
if (animation.mode == FTExtrasCustomMode.Follow)
522555
{
523556
if (string.IsNullOrWhiteSpace(animation.followParameter)) return "no parameter is set.";
557+
if (animation.followAverage && string.IsNullOrWhiteSpace(animation.followParameterB)) return "the parameter to average with is not set.";
524558
if (Mathf.Approximately(animation.fromValue, animation.toValue)) return "'From' and 'To' are the same value.";
525559
}
526560
else
@@ -658,7 +692,7 @@ float ClipTimeAt(AnimationClip clip) => clip == null ? 0f
658692

659693
if (animation.mode == FTExtrasCustomMode.Follow)
660694
{
661-
float value = ValueOf(animation.followParameter);
695+
float value = animation.FollowValue(ValueOf);
662696
switch (animation.followStyle)
663697
{
664698
case FTExtrasFollowStyle.FadeIn:

0 commit comments

Comments
 (0)