Skip to content

Commit

Permalink
fix(parameter-slot): prevent flickering by switching between control …
Browse files Browse the repository at this point in the history
…states directly
  • Loading branch information
poi-vrc committed May 6, 2024
1 parent 797d786 commit 249f233
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 30 deletions.
113 changes: 83 additions & 30 deletions Editor/Animations/SmartControlComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,24 @@ internal class SmartControlComposer
private const string VRCPhysBoneStretchSuffix = "_Stretch";
private const string VRCPhysBoneSquishSuffix = "_Squish";

private class ParameterSlotLayerInfo
{
public AnimatorLayerBuilder layer;
public AnimatorStateBuilder entryState;
public Dictionary<DTSmartControl, AnimatorStateBuilder> scEnabledStates;
public Dictionary<DTSmartControl, AnimatorStateBuilder> scPrepareDisabledStates;

public ParameterSlotLayerInfo()
{
layer = null;
entryState = null;
scEnabledStates = new Dictionary<DTSmartControl, AnimatorStateBuilder>();
scPrepareDisabledStates = new Dictionary<DTSmartControl, AnimatorStateBuilder>();
}
}

private readonly HashSet<DTSmartControl> _controls;
private readonly Dictionary<DTParameterSlot, Tuple<AnimatorLayerBuilder, AnimatorStateBuilder>> _parameterSlotLayers;
private readonly Dictionary<DTParameterSlot, ParameterSlotLayerInfo> _parameterSlotLayerInfos;
private readonly AnimatorOptions _options;
private readonly AnimatorController _controller;

Expand All @@ -44,7 +60,7 @@ public SmartControlComposer(AnimatorOptions options, AnimatorController controll
_options = options;
_controller = controller;
_controls = new HashSet<DTSmartControl>();
_parameterSlotLayers = new Dictionary<DTParameterSlot, Tuple<AnimatorLayerBuilder, AnimatorStateBuilder>>();
_parameterSlotLayerInfos = new Dictionary<DTParameterSlot, ParameterSlotLayerInfo>();
}

private void AddParameterConfig(Transform transform, string parameterName, float defaultValue, bool networkSynced, bool saved)
Expand Down Expand Up @@ -105,11 +121,11 @@ private void HandleDriverMenuItem(DTSmartControl ctrl)
}
}

private Tuple<AnimatorLayerBuilder, AnimatorStateBuilder> PrepareOrGetParameterSlotLayer(DTParameterSlot parameterSlot)
private ParameterSlotLayerInfo PrepareOrGetParameterSlotLayer(DTParameterSlot parameterSlot)
{
if (_parameterSlotLayers.ContainsKey(parameterSlot))
if (_parameterSlotLayerInfos.ContainsKey(parameterSlot))
{
return _parameterSlotLayers[parameterSlot];
return _parameterSlotLayerInfos[parameterSlot];
}

// generate parameter name
Expand Down Expand Up @@ -143,7 +159,11 @@ private Tuple<AnimatorLayerBuilder, AnimatorStateBuilder> PrepareOrGetParameterS
animator.FloatParameter(parameterSlot.ParameterName, parameterSlot.ParameterDefaultValue);
}

return _parameterSlotLayers[parameterSlot] = new Tuple<AnimatorLayerBuilder, AnimatorStateBuilder>(layer, entryState);
return _parameterSlotLayerInfos[parameterSlot] = new ParameterSlotLayerInfo()
{
layer = layer,
entryState = entryState
};
}

private void HandleDriverParameterSlot(DTSmartControl ctrl)
Expand All @@ -156,9 +176,9 @@ private void HandleDriverParameterSlot(DTSmartControl ctrl)
}

// prepare and get value slot anystate layer
var tuple = PrepareOrGetParameterSlotLayer(parameterSlot);
var layer = tuple.Item1;
var entryState = tuple.Item2;
var info = PrepareOrGetParameterSlotLayer(parameterSlot);
var layer = info.layer;
var entryState = info.entryState;

// generate menu item
if (ctrl.ParameterSlotConfig.GenerateMenuItem)
Expand Down Expand Up @@ -195,17 +215,51 @@ private void HandleDriverParameterSlot(DTSmartControl ctrl)

var enabledState = layer.NewState($"{ctrl.name} Enabled");
var prepareDisabledState = layer.NewState($"{ctrl.name} Prepare Disabled");
info.scEnabledStates[ctrl] = enabledState;
info.scPrepareDisabledStates[ctrl] = prepareDisabledState;

entryState.AddTransition(enabledState)
.Equals(parameterSlot.ParameterName, ctrl.ParameterSlotConfig.MappedValue);
enabledState.AddTransition(prepareDisabledState)
.NotEquals(parameterSlot.ParameterName, ctrl.ParameterSlotConfig.MappedValue);
prepareDisabledState.AddTransition(entryState)
.NotEquals(parameterSlot.ParameterName, ctrl.ParameterSlotConfig.MappedValue);
// we add the condition to entry state in FillParameterSlotInterControlTransitions
// to prioritize switching between control states instead

ComposeBinaryToggles(_controller, prepareDisabledState, enabledState, ctrl);
}

private void FillParameterSlotInterControlTransitions()
{
// this fills conditions in each controls' state to allow
// switching to each other directly without going to entry state
foreach (var psKvp in _parameterSlotLayerInfos)
{
var parameterSlot = psKvp.Key;
var info = psKvp.Value;
foreach (var prepareDisabledKvp in info.scPrepareDisabledStates)
{
var ctrl = prepareDisabledKvp.Key;
var prepareDisabledState = prepareDisabledKvp.Value;

foreach (var enabledKvp in info.scEnabledStates)
{
var anotherCtrl = enabledKvp.Key;
var enabledState = enabledKvp.Value;
if (anotherCtrl == ctrl)
{
continue;
}
prepareDisabledState.AddTransition(enabledState)
.Equals(parameterSlot.ParameterName, anotherCtrl.ParameterSlotConfig.MappedValue);
}

// only if no conditions match, it falls back to entry state
prepareDisabledState.AddTransition(info.entryState)
.NotEquals(parameterSlot.ParameterName, ctrl.ParameterSlotConfig.MappedValue);
}
}
}

private string VRCPhysBoneDataSourceToParam(string prefix, DTSmartControl.SCVRCPhysBoneDriverConfig.DataSource source)
{
if (source == DTSmartControl.SCVRCPhysBoneDriverConfig.DataSource.Angle)
Expand Down Expand Up @@ -340,6 +394,11 @@ public void Compose(DTSmartControl ctrl)
// EditorUtility.SetDirty(ctrl.Controller);
}

public void Finish()
{
FillParameterSlotInterControlTransitions();
}

private void ComposeBinary(DTSmartControl ctrl, List<string> conditionalParams)
{
if (HasCrossControlCycle(ctrl))
Expand Down Expand Up @@ -507,7 +566,7 @@ private static bool TryGetComponentProperty(Component comp, string propertyName,
return true;
}

private void ComposePropertyGroupToggles(AnimationClipBuilder disabledClip, AnimationClipBuilder enabledClip, DTSmartControl.PropertyGroup propGp)
private void ComposePropertyGroupToggles(AnimationClipBuilder prepareDisabledClip, AnimationClipBuilder enabledClip, DTSmartControl.PropertyGroup propGp)
{
// search through all objects and animate them
var searchTrans = propGp.SelectionType == DTSmartControl.PropertyGroup.PropertySelectionType.AvatarWide ? _options.context.AvatarGameObject.transform : propGp.SearchTransform;
Expand All @@ -534,24 +593,20 @@ private void ComposePropertyGroupToggles(AnimationClipBuilder disabledClip, Anim
time = 0.0f,
value = propVal.ValueObjectReference
}};
enabledClip.SetCurve(comp.transform, compType, propVal.Name, enabledFrames);
if (!_options.writeDefaults)
{
var disabledFrames = new ObjectReferenceKeyframe[] { new ObjectReferenceKeyframe() {
var disabledFrames = new ObjectReferenceKeyframe[] { new ObjectReferenceKeyframe() {
time = 0.0f,
value = obj
}};
disabledClip.SetCurve(comp.transform, compType, propVal.Name, disabledFrames);
}
enabledClip.SetCurve(comp.transform, compType, propVal.Name, enabledFrames);
// in write defaults mode, we want to keep the enabled frames in prepare disabled to prevent flickering in parameter slot
prepareDisabledClip.SetCurve(comp.transform, compType, propVal.Name, _options.writeDefaults ? enabledFrames : disabledFrames);
}
else if (type == typeof(float))
{
var f = (float)originalValue;
enabledClip.SetCurve(comp.transform, compType, propVal.Name, AnimationCurve.Constant(0.0f, 0.0f, propVal.Value));
if (!_options.writeDefaults)
{
disabledClip.SetCurve(comp.transform, compType, propVal.Name, AnimationCurve.Constant(0.0f, 0.0f, f));
}
// in write defaults mode, we want to keep the enabled value in prepare disabled to prevent flickering in parameter slot
prepareDisabledClip.SetCurve(comp.transform, compType, propVal.Name, AnimationCurve.Constant(0.0f, 0.0f, _options.writeDefaults ? propVal.Value : f));
}
}
}
Expand Down Expand Up @@ -632,9 +687,9 @@ private void ComposeCrossControlValueActions(AnimatorController controller, Anim
}
#endif

private void ComposeBinaryToggles(AnimatorController controller, AnimatorStateBuilder disabledState, AnimatorStateBuilder enabledState, DTSmartControl ctrl)
private void ComposeBinaryToggles(AnimatorController controller, AnimatorStateBuilder prepareDisabledState, AnimatorStateBuilder enabledState, DTSmartControl ctrl)
{
var disabledClip = disabledState.WithNewAnimation();
var prepareDisabledClip = prepareDisabledState.WithNewAnimation();
var enabledClip = enabledState.WithNewAnimation();

foreach (var toggle in ctrl.ObjectToggles)
Expand All @@ -645,20 +700,18 @@ private void ComposeBinaryToggles(AnimatorController controller, AnimatorStateBu
}

ClipToggle(enabledClip, toggle.Target, toggle.Enabled);
if (!_options.writeDefaults)
{
ClipToggle(disabledClip, toggle.Target, GetComponentOrGameObjectOriginalState(toggle.Target));
}
// in write defaults mode, we want to keep the enabled value in prepare disabled to prevent flickering in parameter slot
ClipToggle(prepareDisabledClip, toggle.Target, _options.writeDefaults ? toggle.Enabled : GetComponentOrGameObjectOriginalState(toggle.Target));
}

foreach (var propGp in ctrl.PropertyGroups)
{
ComposePropertyGroupToggles(disabledClip, enabledClip, propGp);
ComposePropertyGroupToggles(prepareDisabledClip, enabledClip, propGp);
}

#if DT_VRCSDK3A
// cross-controls are currently only available in VRC environments
ComposeCrossControlValueActions(controller, disabledState, ctrl.CrossControlActions.ValueActions.ValuesOnDisable);
ComposeCrossControlValueActions(controller, prepareDisabledState, ctrl.CrossControlActions.ValueActions.ValuesOnDisable);
ComposeCrossControlValueActions(controller, enabledState, ctrl.CrossControlActions.ValueActions.ValuesOnEnable);
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public override bool Invoke(Context ctx)
{
composer.Compose(ctrl);
}
composer.Finish();

EditorUtility.SetDirty(fx);

Expand Down

0 comments on commit 249f233

Please sign in to comment.