diff --git a/README.md b/README.md index 0ffd695..06266a2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,559 @@ -## Animator As Code (AacV0) +# Animator As Code + +**Animator As Code** is a small Unity Editor facility to generate Avatars 3.0 Animator layers and animations from a [fluent builder](https://en.wikipedia.org/wiki/Fluent_interface) syntax written in C#. + +Describing your animators as code provides the following advantages: + +- you do not need to edit your animations by hand every time you add remove or change the location of a component in your hierarchy +- you will not need to edit a hundred transitions by hand if you need to rectify your animator + +It is written with VRChat Avatars 3.0 use cases in mind; the API is opinionated to facilitate writing such animators in a concise way, hopefully requiring as little additional tweaking. + +# Interested? Join my Discord Server + +This is a work in progress, I am looking for feedback! + +[Join the Invitation Discord Server!](https://discord.com/invite/58fWAUTYF8) + +# Install + +There are currently no releases. + +Clone the repository within a subfolder of your Unity project, or download the source code and install in any subfolder of your project. + +The project can be located within `Assets/AnimatorAsCodeFramework` but you can choose any location. + +# Examples + +## #0 Toggle a GameObject + +This example shows: + +- Creating a FX layer +- Creating states +- Creating animations +- Declaring a Bool parameter +- Creating transitions with conditions +- +![sx_2022-02-25_00-47-15_dDwKV4bsE2](https://user-images.githubusercontent.com/60819407/155912947-7e5ac988-71f7-44cd-b526-2e245faffcd9.png) + +```csharp +public class GenExample0_ToggleGo : MonoBehaviour +{ + public VRCAvatarDescriptor avatar; + public AnimatorController assetContainer; + public string assetKey; + public GameObject item; +} + +private void Create() +{ + var my = (GenExample0_ToggleGo) target; + // The avatar is used here: + // - to find the FX playable layer animator, where a new layer will be created. + // - to resolve the relative animation path to the item. + // The generated animation files are stored in the asset container. + var aac = AacExample.AnimatorAsCode("Example 0", my.avatar, my.assetContainer, my.assetKey, AacExample.Options().WriteDefaultsOff()); + + // Create a layer in the FX animator. + // Additional layers can be created in the FX animator (see later in the manual). + var fx = aac.CreateMainFxLayer(); + + // The first created state is the default one connected to the "Entry" node. + // States are automatically placed on the grid (see later in the manual). + var hidden = fx.NewState("Hidden") + // Animation assets are generated as sub-assets of the asset container. + // The animation path to my.skinnedMesh is relative to my.avatar + .WithAnimation(aac.NewClip().Toggling(my.item, false)); + var shown = fx.NewState("Shown") + .WithAnimation(aac.NewClip().Toggling(my.item, true)); + + // Creates a Bool parameter in the FX layer. + // Parameters are added to the Animator if a parameter with the same name + // does not exist yet. + var itemParam = fx.BoolParameter("EnableItem"); + + // Transitions are created with a set of default values + // That can be changed in the Generator settings (see later in the manual). + hidden.TransitionsTo(shown).When(itemParam.IsTrue()); + shown.TransitionsTo(hidden).When(itemParam.IsFalse()); +} +``` + +## #1 Toggle a SkinnedMeshRenderer with two conditions + +This example shows: + +- Simple toggle animations +- Groups of Bool parameters +- Simple transitions + +![sx_2022-02-25_00-39-05_DbvwrouqFc](https://user-images.githubusercontent.com/60819407/155912964-ffdfe363-6c1f-41d4-b474-9f18e1749c4d.png) + +```csharp +public class GenExample1_ToggleSmr : MonoBehaviour +{ + public VRCAvatarDescriptor avatar; + public AnimatorController assetContainer; + public string assetKey; + public SkinnedMeshRenderer skinnedMesh; +} + +private void Create() +{ + var my = (GenExample1_ToggleSmr) target; + var aac = AacExample.AnimatorAsCode(SystemName, my.avatar, my.assetContainer, my.assetKey); + + var fx = aac.CreateMainFxLayer(); + var hidden = fx.NewState("Hidden") + // The runtime type of my.skinnedMesh is used within the animation. + // In this case, the "SkinnedMeshRenderer" component is disabled. + .WithAnimation(aac.NewClip().TogglingComponent(my.skinnedMesh, false)); + var shown = fx.NewState("Shown") + .WithAnimation(aac.NewClip().TogglingComponent(my.skinnedMesh, true)); + + // This creates two Bool parameters in the animator. + // The resulting value can be used in conditions. + var accessoriesParams = fx.BoolParameters("EnableAccessories", "AccessoryThing"); + + // The following line creates one transition. + // The conditions are "EnableAccessories is true" and "AccessoryThing is true" + hidden.TransitionsTo(shown).When(accessoriesParams.AreTrue()); + + // The following line creates two transitions: + // - The first transition is "EnableAccessories is false" + // - The second transition is "AccessoryThing is false" + shown.TransitionsTo(hidden).When(accessoriesParams.IsAnyFalse()); +} +``` + +## #2 Animate a SkinnedMesh with Motion time + +This example shows: + +- Describing animations with simplified keyframes +- Animating a state with Motion Time (formerly known as Normalized Time) + +![sx_2022-02-25_01-25-53_2KNRPmra4o](https://user-images.githubusercontent.com/60819407/155912980-53ba1528-0872-4b65-a491-2e9442f48bae.png) + + +```csharp +public class GenExample2_Animate : MonoBehaviour +{ + public VRCAvatarDescriptor avatar; + public AnimatorController assetContainer; + public string assetKey; + public SkinnedMeshRenderer wedgeMesh; +} + +private void Create() +{ + var my = (GenExample2_Animate) target; + var aac = AacExample.AnimatorAsCode(SystemName, my.avatar, my.assetContainer, my.assetKey); + + var fx = aac.CreateMainFxLayer(); + + fx.NewState("Motion") + .WithAnimation(aac.NewClip().Animating(clip => + { + clip.Animates(my.wedgeMesh, "blendShape.Wedge").WithFrameCountUnit(keyframes => + keyframes.Easing(0, 100f).Easing(28, 0).Easing(29, 0).Easing(30, 0).Easing(31, 0).Easing(32, 0).Easing(60, 100f) + ); + clip.Animates(my.wedgeMesh, "material._Metallic").WithFrameCountUnit(keyframes => + keyframes.Constant(0, 1f).Constant(28, 0).Constant(60, 0) + ); + })) + .MotionTime(fx.FloatParameter("WedgeAmount")); +} +``` + +## #3 Using Parameter Drivers + +This example shows: + +- Driving parameters using VRC Parameter Driver behavior +- VRChat Parameters +- Creating systems with multiple FX layers +- Complex transitions +- *Show code...* + + ```csharp + public class GenExample3_Gesturing : MonoBehaviour + { + public VRCAvatarDescriptor avatar; + public AnimatorController assetContainer; + public string assetKey; + public SkinnedMeshRenderer iconMesh; + } + + private GenExample3_Gesturing my; + private AacFlBase aac; + + private void Create() + { + my = (GenExample3_Gesturing) target; + aac = AacExample.AnimatorAsCode(SystemName, my.avatar, my.assetContainer, my.assetKey); + + CreateMainLayer(); + CreateSupportingLayer(); + } + + private void CreateMainLayer() + { + var layer = aac.CreateMainFxLayer(); + + var dirtyCheckParameter = layer.BoolParameter("AAC_INTERNAL_GesturingIcon_DirtyCheck"); + + // ### Create states + var lackOfChangeDetected = layer.NewState("Animate To NoChange") + .WithAnimation(IconAppears()); + + // By default, states have an animation that animates a dummy object for 1 frame. + var noChange = layer.NewState("NoChange", 1, 0).RightOf(); + + var changeDetected = layer.NewState("Animate To Changing").Under() + .WithAnimation(IconDisappears()); + + // This creates a clip that animates a dummy object for 1.5f seconds. + var changing = layer.NewState("Changing", 0, 1).LeftOf() + .WithAnimation(aac.DummyClipLasting(1.5f, AacFlUnit.Seconds)); + + // When this state is entered, the parameter is driven to the value of false. + var stillChanging = layer.NewState("Still Changing", 0, 2).Under() + .Drives(dirtyCheckParameter, false); + + // ------ + + // ### Create transitions + lackOfChangeDetected.TransitionsTo(changeDetected).AfterAnimationIsAtLeastAtPercent(0.7f).When(dirtyCheckParameter.IsTrue()); + + // The transition duration is 30% of the animation duration. + lackOfChangeDetected.TransitionsTo(noChange).AfterAnimationFinishes().WithTransitionDurationPercent(0.3f); + + noChange.TransitionsTo(changeDetected).When(dirtyCheckParameter.IsTrue()); + + // By using AfterAnimationFinishes, the transition will trigger after the animation + // for the icon appearing finishes. + changeDetected.TransitionsTo(changing).AfterAnimationFinishes(); + + changing.TransitionsTo(stillChanging).When(dirtyCheckParameter.IsTrue()); + // By using AfterAnimationFinishes, the transition will trigger after 1.5 seconds, + // which is the length of the animation in Changing. + changing.TransitionsTo(lackOfChangeDetected).AfterAnimationFinishes(); + + // The transition will immediately happen upon entering, by using Exit time set to 0. + stillChanging.AutomaticallyMovesTo(changing); + } + + private void CreateSupportingLayer() + { + // Create an additional FX layer. + var layer = aac.CreateSupportingFxLayer("Detection"); + var reevaluating = layer.NewState("Reevaluating", -1, 0); + + foreach (var left in Enumerable.Range(0, 8)) + { + foreach (var right in Enumerable.Range(0, 8)) + { + var state = layer.NewState($"Gesture {left} {right}", left, right) + // When this state is entered, the parameter is driven to the value of true. + .Drives(layer.BoolParameter("AAC_INTERNAL_GesturingIcon_DirtyCheck"), true); + + reevaluating.TransitionsTo(state) + // Use ".Av3" to access VRChat standard parameters. + // Accessing these parameters will create the corresponding parameter in the animator. + .When(layer.Av3().GestureLeft.IsEqualTo(left)) + .And(layer.Av3().GestureRight.IsEqualTo(right)); + state.TransitionsTo(reevaluating) + .When(layer.Av3().GestureLeft.IsNotEqualTo(left)) + .Or() + .When(layer.Av3().GestureRight.IsNotEqualTo(right)); + } + } + } + + private AacFlClip IconAppears() + { + return aac.NewClip().Animating(clip => + { + clip.Animates(my.iconMesh, "blendShape.Wedge") + .WithFrameCountUnit(keyframes => keyframes.Easing(0, 0f).Easing(10, 100f)); + }); + } + + private AacFlClip IconDisappears() + { + return aac.NewClip().Animating(clip => + { + clip.Animates(my.iconMesh, "blendShape.Wedge") + .WithFrameCountUnit(keyframes => keyframes.Easing(0f, 100f).Easing(10, 0f)); + }); + } + ``` + + +--- + +# Reference + +## Typical animator creation steps + +Animator As Code is generally used in the following steps: + +- Declare an Animator As Code +- Create one or multiple layers +- Create the states + - Create the animations at the same time +- Create the transitions + +## Declare an Animator As Code (AAC) + +In order to use Animator As Code (AAC), first, declare it with a configuration. + +The AAC configuration requires the following: + +- A system name. + - Animator As Code describes systems. A system can have multiple layers, not only across playable layer animators, but also within a single playable layer animator. All created layers will be prefixed with this system name. + - It is up to you to decide where the boundaries of the system lies. +- An avatar descriptor, animator root, and default value root. + - This is used to select the playable layer animators to use. + - This is also used to select the root transform that animations will use for relative paths. + - This is used to collect the default values for some animations. + - *In general, all three of them are the same object, but this is not mandatory.* +- An asset container. + - Animations are generated by Animator As Code, and the large quantity of generated assets can be quite messy. To limit littering your project, such assets will be generated as sub-assets of a container. The container is of type Animator Controller, but it doesn’t need to have any layers within it. +- An asset key. + - The asset key a prefix that all generated assets will use. When creating the layers, all generated assets that uses that prefix will be removed upon invocation of `aac.ClearPreviousAssets()`. +- A provider of defaults. + - Animator As Code is opinionated, and sometimes you want to tweak the default values. The provider of defaults is executed when a state is created, an animation is created, a transition is created. This will let you tweak the generation process. + +To declare it: + +```csharp +string systemName; +VRCAvatarDescriptor avatar; +AnimatorController assetContainer; +string assetKey; + +var aac = AacV0.Create(new AacConfiguration +{ + SystemName = systemName, + AvatarDescriptor = avatar, + AnimatorRoot = avatar.transform, + DefaultValueRoot = avatar.transform, + AssetContainer = assetContainer, + AssetKey = assetKey, + DefaultsProvider = new AacDefaultsProvider(writeDefaults: false) +}); +// Remove all previously generated assets from the asset container +// that match the asset key. +aac.ClearPreviousAssets(); +``` + +## Create one or multiple layers + +Using AAC, create layers in your animators. A system can have multiple layers across animators. + +There is one main layer, and multiple supporting layers, per animator. + +- The main layer will be named exactly after your system name. +- The supporting layers will be prefixed by the system name, and appended with a suffix of your choice. + +You are in no obligation to create a main layer. If you think several layers of the same animators are of equal importance, you can declare them as being supporting layers. + +```csharp +var fx = aac.CreateMainFxLayer(); +var detection = aac.CreateSupportingFxLayer("Detection"); +``` + +## Create states + +Using your layer, you may now create states. Your state will be configured with defaults based on your AAC configuration, most notably, the Write Defaults setting. + +By default, states have a dummy animation that lasts one frame (1/60th of a second). + +It is intended to create the animation clip directly while declaring the state, this will be explained later on. + +```csharp +var hidden = fx.NewState("Hidden") + .WithAnimation(aac.NewClip().Toggling(my.item, false)); +``` + +## Creating parameters for use within layers + +Using your layer, you can create parameters. + +The parameter will be added to the animator the layer belongs to. + +If you need to reuse a parameter across multiple layers, you need to invoke it on all relevant layers. + +*(Once done, you are not forced to reuse the resulting parameter instance on the same layer it was created from)* + +```csharp + +fx.NewState("Motion") + // This creates a Float parameter on the FX layer. + .MotionTime(fx.FloatParameter("WedgeAmount")) + // ... +``` + +If a parameter with the same name already exists in the animator, it will not be created again. + +*This is also true if the animator already has a parameter with the same name but with a different type, and no error will be raised.* + +There are also dedicated functions to obtain Avatars 3.0 parameters, such as. + +```csharp +var gestureLeftWeight = fx.Av3().GestureLeftWeight +``` + +## Forcing the value of parameters + +In some rare cases you may wish to override the default value of animator parameters, for example in use for special conditions, for use in blend trees as a constant value. + +Using your layer: + +```csharp +fx.OverrideValue(fx.FloatParameter("SmoothingAmount"), 0.7f) +``` + +## Visually organize your states + +By default, a newly created state will be visually placed under the previously created state. + +For more control, states have `LeftOf`, `RightOf`, `Over`, `Under`, `Shift` operators, which let you move a state to be visually next to another state. + +The value is in grid units. + +![Untitled](https://user-images.githubusercontent.com/60819407/155912996-89369e2d-435d-4619-b5ee-3ac4b6f8827c.png) + +```csharp +var init = fx.NewState("Init"); // This is the first state. By default it is at (0, 0) +var a = fx.NewState("A"); // This will be placed under Init. +var b = fx.NewState("B"); // This will be placed under A. +var c = fx.NewState("C").RightOf(a); // This will be placed right of A. +var d = fx.NewState("D"); // This will be placed under C. +var e = fx.NewState("E").RightOf(); // This will be placed right of D. +var alternate = fx.NewState("Alternate").Over(c); // This will be placed over C. + +// This will be placed relative to Alternate: 2 blocks over, and 1 to the right. +var reset = fx.NewState("Reset").Shift(alternate, 1, -2); +``` + +## Create an animation + +By default, states have a dummy animation that lasts one frame (1/60th of a second). If you want the state to play an animation of your choice, there is usually an invocation to the `.WithAnimation(...)` function: + +```csharp +var hidden = fx.NewState("Hidden") + .WithAnimation(aac.NewClip().Toggling(my.item, false)); +``` + +The invocation uses AAC to create a new clip inside the asset container. You don’t need to specify a name for this clip, it is irrelevant. + +Animations use object references instead of paths. They are converted to paths in the animation clip asset by resolving the relative path to the animator root of the AAC configuration. + +Most of the functions lets you create single-frame constant animations. + +For more complex animations, use the `.Animating(...)` function. + +```csharp +fx.NewState("Motion") + .WithAnimation(aac.NewClip().Animating(clip => + { + clip.Animates(my.wedgeMesh, "blendShape.Wedge").WithFrameCountUnit(keyframes => + keyframes.Easing(0, 100f).Easing(28, 0).Easing(29, 0).Easing(30, 0).Easing(31, 0).Easing(32, 0).Easing(60, 100f) + ); + clip.Animates(my.wedgeMesh, "material._Metallic").WithFrameCountUnit(keyframes => + keyframes.Constant(0, 1f).Constant(28, 0).Constant(60, 0) + ); + })) +``` + +## Create transitions and define conditions + +Using the states, you can create transitions between states. Your transition will be configured with defaults based on your AAC configuration, defined within the DefaultsProvider. + +To create a transition from Any, Exit, or Entry, there are some functions in the states like `.TransitionsFromAny()` or `.Exits()`. + +Start defining conditions for that transition using the `.When(...)` operator. + +Parameters have functions that generate conditions once invoked. + +```csharp +reevaluating.TransitionsTo(state) + // Use ".Av3" to access VRChat standard parameters. + // Accessing these parameters will create the corresponding parameter in the animator. + .When(layer.Av3().GestureLeft.IsEqualTo(left)) + .And(layer.Av3().GestureRight.IsEqualTo(right)); +state.TransitionsTo(reevaluating) + .When(layer.Av3().GestureLeft.IsNotEqualTo(left)) + .Or() + .When(layer.Av3().GestureRight.IsNotEqualTo(right)); +``` + +## When(), And(), and Or() operators + +In animator transitions, all conditions must be verified for the transition to occur; this is effectively a “AND” of all of the conditions (A && B && C). + +This is done by using the `.And(...)` operator: `.When(A).And(B).And(C)` + +To represent “OR”, new transitions need to be created; ((A && B && C) || (D && E)) results in: + +- Transition 1: (A && B && C) +- Transition 2: (D && E) + +This is done by using the `.Or(...)` operator: `.When(A).And(B).And(C).Or().When(D).And(E)` + +From this limitation, conditions with nested OR cannot be expressed easily, such as: + +- 🚫 (F && (G || H) && (J || K)) + +## Conditions that generate multiple transitions + +Some conditions generate multiple transitions, such as `boolParameters.IsAnyFalse()` + +In order to still let you express these expressions easily, the following applies: + +- These conditions can only be used in a `.When(...)` operator, and not within a `.And(...)` +- If such a condition is used, calling `.And(...)` will apply the condition to all transitions generated by the `.When(...)` operator up until the next `Or(...)` operator if any exists. + - For example: + `.When(fx.BoolParameters(I, J).IsAnyFalse()).And(K.IsTrue())` + - Is equivalent to: + `.When(I.IsFalse()).And(K.IsTrue()).Or().When(J.IsFalse()).And(K.IsTrue())` + +## Use the WhenConditions() operator to build in a `for` loop + +The presence of the `.When(...)` operator can make it difficult to build conditions iteratively in a `for` loop. + +For this purpose, use the `.WhenConditions()` operator. This will let you build conditions using the `.And(...)` operator. + +```csharp +var conditions = state.TransitionsFromEntry().WhenConditions(); +for (var i = 0; i < numberOfBits; i++) +{ + conditions.And(parameter[i].IsEqualTo(bitMask[i])); +} +``` + +## Create many-to-one transitions using a `foreach` loop + +There is no facility to create multiple identical transitions from multiple states to another state. + +Use a regular `foreach` loop to achieve this. + +```csharp +foreach (var cancelWhenNotAllowed in new[] {auto, reverse, manual, custom, done}) +{ + cancelWhenNotAllowed.TransitionsTo(idle).When(allowSystemParameter.IsFalse()); +} +``` + +--- + +# Reference manual + +## Animator As Code (AacV0) - `static AacFlBase Create(AacConfiguration configuration)` /// Create an Animator As Code (AAC) base. # Base (AacFlBase)