Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge blend tree #870

Merged
merged 11 commits into from
Jan 29, 2024
2 changes: 2 additions & 0 deletions CHANGELOG-PRERELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog].
- Current Optimizer includes the following optimization
- Remove meaningless properties `#854`
- Converts Entry / Exit to 1D BlendTree `#854` `#867`
- Merges multiple Direct BlendTree to single Direct BlendTree `#870`
- Removes meaningless Animator Layers `#870`

### Changed
- Project is slightly renamed to AAO: Avatar Optimizer `#830`
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog].
- Current Optimizer includes the following optimization
- Remove meaningless properties `#854`
- Converts Entry / Exit to 1D BlendTree `#854` `#867`
- Merges multiple Direct BlendTree to single Direct BlendTree `#870`
- Removes meaningless Animator Layers `#870`

### Changed
- AvatarOptimizer now uses ErrorReporting API of NDMF instead of our own API `#805`
Expand Down
2 changes: 2 additions & 0 deletions Editor/OptimizerPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ protected override void Configure()
// EntryExit to BlendTree optimization heavily depends on VRChat's behavior
.Then.Run(Processors.AnimatorOptimizer.EntryExitToBlendTree.Instance)
#endif
.Then.Run(Processors.AnimatorOptimizer.MergeDirectBlendTree.Instance)
.Then.Run(Processors.AnimatorOptimizer.RemoveMeaninglessLayer.Instance)
;
}

Expand Down
83 changes: 78 additions & 5 deletions Editor/Processors/AnimatorOptimizer/AnimOptPassBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@
using UnityEngine.Profiling;
using Object = UnityEngine.Object;

#if AAO_VRCSDK3_AVATARS
using VRC.SDKBase;
using VRC.SDK3.Avatars.Components;
#endif

namespace Anatawa12.AvatarOptimizer.Processors.AnimatorOptimizer
{
class AnimatorOptimizerState
{
private List<AOAnimatorController> _contollers = new List<AOAnimatorController>();
public IEnumerable<AOAnimatorController> Controllers => _contollers;

public void Add(AOAnimatorController cloned)
{
_contollers.Add(cloned);
}
public void Add(AOAnimatorController wrapper) => _contollers.Add(wrapper);

private Dictionary<AnimationClip, bool> _isTimeDependentClipCache = new Dictionary<AnimationClip, bool>();
public bool IsTimeDependentClip(AnimationClip clip)
Expand Down Expand Up @@ -90,6 +92,19 @@ protected override void Execute(BuildContext context, TraceAndOptimizeState stat

var animatorState = context.GetState<AnimatorOptimizerState>();

#if AAO_VRCSDK3_AVATARS
// According to VRCSDK 3.5.0, default animation controllers doesn't have AnimatorLayerWeightControl so
// we don't have to care about them.
var changerBehaviours = new AnimatorLayerMap<HashSet<VRC_AnimatorLayerControl>>();
{
changerBehaviours[VRCAvatarDescriptor.AnimLayerType.Action] = new HashSet<VRC_AnimatorLayerControl>();
changerBehaviours[VRCAvatarDescriptor.AnimLayerType.FX] = new HashSet<VRC_AnimatorLayerControl>();
changerBehaviours[VRCAvatarDescriptor.AnimLayerType.Gesture] = new HashSet<VRC_AnimatorLayerControl>();
changerBehaviours[VRCAvatarDescriptor.AnimLayerType.Additive] = new HashSet<VRC_AnimatorLayerControl>();
}
#endif
var clonedToController = new Dictionary<AnimatorController, AOAnimatorController>();

foreach (var component in context.AvatarRootObject.GetComponents<Component>())
{
using (var serializedObject = new SerializedObject(component))
Expand All @@ -99,14 +114,72 @@ protected override void Execute(BuildContext context, TraceAndOptimizeState stat
if (property.objectReferenceValue is RuntimeAnimatorController runtimeController)
{
var cloned = AnimatorControllerCloner.Clone(context, runtimeController);
animatorState.Add(new AOAnimatorController(cloned));
var wrapper = new AOAnimatorController(cloned);
animatorState.Add(wrapper);
property.objectReferenceValue = cloned;
clonedToController.Add(cloned, wrapper);

#if AAO_VRCSDK3_AVATARS
foreach (var behaviour in ACUtils.StateMachineBehaviours(cloned))
{
switch (behaviour)
{
case VRC_AnimatorLayerControl control:
if (control.playable.ToAnimLayerType() is VRCAvatarDescriptor.AnimLayerType l)
changerBehaviours[l].Add(control);
break;
}
}
#endif
}
}

serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
}

#if AAO_VRCSDK3_AVATARS
{
var descriptor = context.AvatarDescriptor;
if (descriptor.customizeAnimationLayers)
{
foreach (var playableLayer in descriptor.baseAnimationLayers)
{
if (playableLayer.isDefault || !playableLayer.animatorController ||
changerBehaviours[playableLayer.type] == null) continue;

var wrapper = clonedToController[(AnimatorController)playableLayer.animatorController];

foreach (var control in changerBehaviours[playableLayer.type])
{
if (control.layer < 0 || wrapper.layers.Length <= control.layer) continue;

var ourChange =
AnimatorWeightChanges.ForDurationAndWeight(control.blendDuration, control.goalWeight);

var layer = wrapper.layers[control.layer];

layer.WeightChange = layer.WeightChange.Merge(ourChange);
layer.LayerIndexUpdated += index => control.layer = index;
}

// process MMD world compatibility
if (playableLayer.type == VRCAvatarDescriptor.AnimLayerType.FX && state.MmdWorldCompatibility)
{
for (var i = 1; i <= 2; i++)
{
if (wrapper.layers.Length > i)
{
wrapper.layers[i].MarkUnRemovable();
wrapper.layers[i].WeightChange = wrapper.layers[i].WeightChange
.Merge(AnimatorWeightChange.EitherZeroOrOne);
}
}
}
}
}
}
#endif
}
}

Expand Down
115 changes: 115 additions & 0 deletions Editor/Processors/AnimatorOptimizer/MergeDirectBlendTree.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes;
using JetBrains.Annotations;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;

namespace Anatawa12.AvatarOptimizer.Processors.AnimatorOptimizer
{
internal class MergeDirectBlendTree : AnimOptPassBase<MergeDirectBlendTree>
{
protected override void Execute(BuildContext context, AOAnimatorController controller,
TraceAndOptimizeState settings)
{
if (!settings.SkipMergeDirectBlendTreeLayers) return;
Execute(controller);
}

public static void Execute(AOAnimatorController controller)
{
var directBlendTrees = new List<(int layerIndex, BlendTree tree)>();

var modifiedProperties = new HashSet<EditorCurveBinding>();

for (var i = controller.layers.Length - 1; i >= 0; i--)
{
var layer = controller.layers[i];

var blendTree = GetSingleDirectBlendTree(layer);

if (blendTree != null)
{
var blendTreeModified = ACUtils.AllClips(blendTree).Aggregate(new HashSet<EditorCurveBinding>(), (set, clip) =>
{
modifiedProperties.UnionWith(AnimationUtility.GetCurveBindings(clip));
modifiedProperties.UnionWith(AnimationUtility.GetObjectReferenceCurveBindings(clip));
return set;
});
// nothing is animated in higher priority layer
if (!blendTreeModified.Any(modifiedProperties.Contains))
directBlendTrees.Add((i, blendTree));

modifiedProperties.UnionWith(blendTreeModified);
}
else
{
foreach (var motion in layer.GetMotions())
foreach (var clip in ACUtils.AllClips(motion))
{
modifiedProperties.UnionWith(AnimationUtility.GetCurveBindings(clip));
modifiedProperties.UnionWith(AnimationUtility.GetObjectReferenceCurveBindings(clip));
}
}
}

// if we found only one, leave it as is
if (directBlendTrees.Count < 2) return;

directBlendTrees.Reverse();

// create merged layer
var newLayer = controller.AddLayer("Merged Direct BlendTrees");
var newState = new AnimatorState { name = "Merged Direct BlendTrees" };
newLayer.stateMachine.states = new[] { new ChildAnimatorState { state = newState } };
newLayer.stateMachine.defaultState = newState;
newLayer.defaultWeight = 1f;

var newBlendTree = new BlendTree() { name = "Merged Direct BlendTrees" };
newState.motion = newBlendTree;
newBlendTree.blendType = BlendTreeType.Direct;
newBlendTree.children = directBlendTrees.SelectMany(x => x.tree.children).ToArray();

// clear original layers
foreach (var (layerIndex, _) in directBlendTrees)
{
var layer = controller.layers[layerIndex];
layer.stateMachine.states = Array.Empty<ChildAnimatorState>();
layer.stateMachine.defaultState = null;
layer.defaultWeight = 0f;
layer.WeightChange = AnimatorWeightChange.NotChanged;
}
}

[CanBeNull]
private static BlendTree GetSingleDirectBlendTree(AOAnimatorControllerLayer layer)
{
if (layer.IsSyncedToOtherLayer || layer.IsSynced || layer.avatarMask != null) return null;
if (!layer.IsOverride) return null;
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (layer.defaultWeight != 1) return null;
if (layer.WeightChange != AnimatorWeightChange.NotChanged &&
layer.WeightChange != AnimatorWeightChange.AlwaysOne) return null;
if (!layer.stateMachine) return null;
var stateMachine = layer.stateMachine;

if (stateMachine.states.Length != 1) return null;
if (stateMachine.stateMachines.Length != 0) return null;

var state = stateMachine.states[0].state;
if (stateMachine.defaultState != state) return null;

// state configuration
if (!state.writeDefaultValues) return null;
if (state.behaviours.Length != 0) return null;
if (state.timeParameterActive) return null;
if (!(state.motion is BlendTree blendTree)) return null;

// validate blend tree
if (blendTree.blendType != BlendTreeType.Direct) return null;
return blendTree;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions Editor/Processors/AnimatorOptimizer/RemoveMeaninglessLayer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes;
using nadena.dev.ndmf;

namespace Anatawa12.AvatarOptimizer.Processors.AnimatorOptimizer
{
class RemoveMeaninglessLayer : AnimOptPassBase<RemoveMeaninglessLayer>
{
protected override void Execute(BuildContext context, AOAnimatorController controller, TraceAndOptimizeState settings)
{
if (settings.SkipRemoveMeaninglessAnimatorLayer) return;
Execute(controller);
}

public static void Execute(AOAnimatorController controller)
{
var newLayers = new List<AOAnimatorControllerLayer>();
for (var i = 0; i < controller.layers.Length; i++)
{
var layer = controller.layers[i];
if (!layer.IsRemovable || !IsMeaningless(layer))
{
if (i != newLayers.Count)
layer.OnLayerIndexUpdated(newLayers.Count);
newLayers.Add(layer);
}
}
controller.SetLayersUnsafe(newLayers.ToArray());
}

private static bool IsMeaningless(AOAnimatorControllerLayer layer)
{
if (layer.avatarMask != null) return false;
var stateMachine = layer.IsSynced ? layer.SyncedLayer?.stateMachine : layer.stateMachine;
if (stateMachine == null) return true;
return stateMachine.states.Length == 0 && stateMachine.stateMachines.Length == 0;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading