Add Introduction

This commit is contained in:
Haï~
2022-02-28 15:24:37 +01:00
parent 276c8d3ddf
commit 739292efdb
+556 -1
View File
@@ -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 doesnt 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 dont 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)