using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Animations; using UnityEditorInternal; using UnityEngine; using UnityEngine.UIElements; using VF.Builder; using VF.Feature.Base; using VF.Inspector; using VF.Model.Feature; using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType; namespace VF.Feature { public class DirectTreeOptimizerBuilder : FeatureBuilder { [FeatureBuilderAction(FeatureOrder.DirectTreeOptimizer)] public void Apply() { var fx = GetFx(); var bindingsByLayer = fx.GetLayers() .ToDictionary(layer => layer, layer => GetBindingsAnimatedInLayer(layer)); var floatTrue = fx.NewFloat("floatTrue", def: 1); var eligibleLayers = new List(); var debugLog = new List(); foreach (var (layer,i) in fx.GetLayers().Select((layer,i) => (layer,i))) { void AddDebug(string msg) { debugLog.Add($"{layer.name} - {msg}"); } if (i == 0) { AddDebug("Not optimizing (base layer)"); continue; } var weight = fx.GetWeight(layer); if (!Mathf.Approximately(weight, 1)) { AddDebug($"Not optimizing (layer weight is {weight}, not 1)"); continue; } if (layer.stateMachines.Length > 0) { AddDebug("Not optimizing (contains submachine)"); continue; } var hasBehaviour = false; AnimatorIterator.ForEachBehaviour(layer, (b, add) => { hasBehaviour = true; return true; }); if (hasBehaviour) { AddDebug($"Not optimizing (contains behaviours)"); continue; } var hasNonstaticClips = false; AnimatorIterator.ForEachClip(layer, clip => { hasNonstaticClips |= !ClipBuilder.IsStaticMotion(clip); }); var usedBindings = bindingsByLayer[layer]; var someOtherLayerAnimatesTheSameThing = bindingsByLayer .Any(pair => pair.Key != layer && pair.Value.Any(b => usedBindings.Contains(b))); if (someOtherLayerAnimatesTheSameThing) { AddDebug($"Not optimizing (shares animations with some other layer)"); continue; } Motion onClip; Motion offClip; string param; var states = layer.states; if (states.Length == 1) { var state = states[0].state; if (hasNonstaticClips) { var dualState = ClipBuilder.SplitRangeClip(state.motion); if (dualState == null) { AddDebug($"Not optimizing (contains single clip that is not static and not a single time range)"); continue; } if (!state.timeParameterActive || string.IsNullOrWhiteSpace(state.timeParameter)) { AddDebug($"Not optimizing (contains a time range clip but doesn't use motion time)"); continue; } offClip = dualState.Item1; offClip.name = state.motion.name + " (OFF)"; AssetDatabase.AddObjectToAsset(offClip, state.motion); onClip = dualState.Item2; onClip.name = state.motion.name + " (ON)"; AssetDatabase.AddObjectToAsset(onClip, state.motion); param = state.timeParameter; } else { offClip = null; onClip = states[0].state.motion; param = floatTrue.Name(); } } else { if (hasNonstaticClips) { AddDebug($"Not optimizing (contains non-static clips)"); continue; } ICollection GetTransitionsTo(AnimatorState state) { var output = new List(); AnimatorIterator.ForEachTransition(layer, t => { if (t.destinationState == state || (t.isExit && layer.defaultState == state)) { output.Add(t); } }); return output.ToArray(); } if (states.Length == 3) { bool IsJunkState(AnimatorState state) { return layer.defaultState == state && GetTransitionsTo(state).Count == 0; } states = states.Where(child => !IsJunkState(child.state)).ToArray(); } if (states.Length != 2) { AddDebug($"Not optimizing (contains {states.Length} states)"); continue; } var state0 = states[0].state; var state1 = states[1].state; var state0Condition = GetSingleCondition(GetTransitionsTo(state0)); var state1Condition = GetSingleCondition(GetTransitionsTo(state1)); if (state0Condition == null || state1Condition == null) { AddDebug($"Not optimizing (state conditions are not basic)"); continue; } if (state0Condition.Value.parameter != state1Condition.Value.parameter) { AddDebug($"Not optimizing (state conditions do not use same parameter)"); continue; } var state0EffectiveCondition = EstimateEffectiveCondition(state0Condition.Value); var state1EffectiveCondition = EstimateEffectiveCondition(state1Condition.Value); AnimatorState onState; AnimatorState offState; if ( state0EffectiveCondition == EffectiveCondition.WHEN_0 && state1EffectiveCondition == EffectiveCondition.WHEN_1) { offState = state0; onState = state1; } else if ( state0EffectiveCondition == EffectiveCondition.WHEN_1 && state1EffectiveCondition == EffectiveCondition.WHEN_0) { offState = state1; onState = state0; } else { AddDebug($"Not optimizing (state conditions are not an inversion of each other)"); continue; } offClip = offState.motion; onClip = onState.motion; param = state0Condition.Value.parameter; } var paramUsedInOtherLayer = false; foreach (var other in fx.GetLayers()) { AnimatorIterator.ForEachTransition(other, t => { paramUsedInOtherLayer |= layer != other && t.conditions.Any(c => c.parameter == param); }); } if (paramUsedInOtherLayer) { AddDebug($"Not optimizing (parameter used in some other layer)"); continue; } eligibleLayers.Add(new EligibleLayer { offState = offClip, onState = onClip, param = param }); AddDebug("OPTIMIZING"); fx.RemoveLayer(layer); } Debug.Log("Optimization report:\n\n" + string.Join("\n", debugLog)); if (eligibleLayers.Count > 0) { var tree = manager.GetClipStorage().NewBlendTree("Optimized Toggles"); tree.blendType = BlendTreeType.Direct; foreach (var toggle in eligibleLayers) { var offEmpty = ClipBuilder.IsEmptyMotion(toggle.offState, avatarObject); var onEmpty = ClipBuilder.IsEmptyMotion(toggle.onState, avatarObject); if (offEmpty && onEmpty) continue; string param; Motion motion; if (!offEmpty) { var subTree = manager.GetClipStorage().NewBlendTree("Optimized Toggle " + toggle.offState.name); subTree.useAutomaticThresholds = false; subTree.blendType = BlendTreeType.Simple1D; subTree.AddChild(toggle.offState, 0); subTree.AddChild( !onEmpty ? toggle.onState : manager.GetClipStorage().GetNoopClip(), 1); subTree.blendParameter = toggle.param; param = floatTrue.Name(); motion = subTree; } else { param = toggle.param; motion = toggle.onState; } tree.AddChild(motion); var children = tree.children; var child = children[children.Length - 1]; child.directBlendParameter = param; children[children.Length - 1] = child; tree.children = children; var fxRaw = fx.GetRaw(); fxRaw.parameters = fxRaw.parameters.Select(p => { if (p.name == toggle.param) { if (p.type == AnimatorControllerParameterType.Bool) p.defaultFloat = p.defaultBool ? 1 : 0; if (p.type == AnimatorControllerParameterType.Int) p.defaultFloat = p.defaultInt; p.type = AnimatorControllerParameterType.Float; } return p; }).ToArray(); } var layer = fx.NewLayer("Optimized Toggles"); layer.NewState("Optimized Toggles").WithAnimation(tree); } } private ICollection GetBindingsAnimatedInLayer(AnimatorStateMachine sm) { var usedBindings = new HashSet(); AnimatorIterator.ForEachClip(sm, clip => { usedBindings.UnionWith(AnimationUtility.GetCurveBindings(clip)); usedBindings.UnionWith(AnimationUtility.GetObjectReferenceCurveBindings(clip)); }); return usedBindings; } public enum EffectiveCondition { WHEN_1, WHEN_0, INVALID } private static EffectiveCondition EstimateEffectiveCondition(AnimatorCondition cond) { if (cond.mode == AnimatorConditionMode.If) return EffectiveCondition.WHEN_1; if (cond.mode == AnimatorConditionMode.IfNot) return EffectiveCondition.WHEN_0; if (cond.mode == AnimatorConditionMode.Equals && Mathf.Approximately(cond.threshold, 1)) return EffectiveCondition.WHEN_1; if (cond.mode == AnimatorConditionMode.Equals && Mathf.Approximately(cond.threshold, 0)) return EffectiveCondition.WHEN_0; if (cond.mode == AnimatorConditionMode.NotEqual && Mathf.Approximately(cond.threshold, 1)) return EffectiveCondition.WHEN_0; if (cond.mode == AnimatorConditionMode.NotEqual && Mathf.Approximately(cond.threshold, 0)) return EffectiveCondition.WHEN_1; if (cond.mode == AnimatorConditionMode.Greater && Mathf.Approximately(cond.threshold, 0)) return EffectiveCondition.WHEN_1; if (cond.mode == AnimatorConditionMode.Less && Mathf.Approximately(cond.threshold, 1)) return EffectiveCondition.WHEN_0; return EffectiveCondition.INVALID; } private static AnimatorCondition? GetSingleCondition(IEnumerable transitions) { var allConditions = transitions .SelectMany(t => t.conditions) .Distinct() .ToList(); if (allConditions.Count != 1) return null; return allConditions[0]; } public class EligibleLayer { public Motion offState; public Motion onState; public string param; } public override string GetEditorTitle() { return "Direct Tree Optimizer"; } public override VisualElement CreateEditor(SerializedProperty prop) { var content = new VisualElement(); content.Add(VRCFuryEditorUtils.Info( "This feature will automatically convert all non-conflicting toggle layers into a single direct blend tree layer." + "\n\nWarning: Toggles may not work in Av3 emulator when using this feature. This is a bug in Av3 emulator. Use Gesture Manager for testing instead." )); return content; } public override bool AvailableOnProps() { return false; } } }