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

Anim Component #2007

Merged
merged 64 commits into from
Jun 3, 2020
Merged

Anim Component #2007

merged 64 commits into from
Jun 3, 2020

Conversation

ellthompson
Copy link
Contributor

@ellthompson ellthompson commented Apr 22, 2020

This PR introduces a new Anim component to the engine which can be used to animate the entity of the component its attached to. A state graph must also be supplied to the component which defines a set of animation states that can be active, when those states should activate and what the blending between new and old states should look like. Animation assets can then be linked to each state in the graph, after which the component will become playable. These animations can be supplied as either GLB assets (for model animations) or animation clips (to animate arbitrary properties of the components entity and its children).

Anim Component API:

// play the animations in the layer state. If a name is provided then play from that state
pc.AnimComponentLayer#play: function (name);
// pause animations in the layer
pc.AnimComponentLayer#pause: function ();
// reset the layer to its initial state
pc.AnimComponentLayer#reset: function ();
// assigns a given AnimTrack to the state node defined by nodeName. Must be called after loadStateGraph.
pc.AnimComponentLayer#assignAnimation: function (nodeName, animTrack);
// clears all animations from a given state node in the provided layer.
pc.AnimComponentLayer#removeNodeAnimations: function (nodeName);
// name of the layer
pc.AnimComponentLayer#name: string;
// whether the laying is currently playing
pc.AnimComponentLayer#playing: boolean;
// whether all states have an animTrack assigned to them
pc.AnimComponentLayer#playable: boolean;
// the name of the state that is currently playing
pc.AnimComponentLayer#activeState: string;
// the name of the state that was previously playing
pc.AnimComponentLayer#previousState: string;
// the amount of time progressed through the current states animation, normalised to the animations duration
pc.AnimComponentLayer#activeStateProgress: number;
// returns whether the anim component is currently transitioning between states
pc.AnimComponentLayer#transitioning: boolean;
// if the anim component is transitioning, returns the progress of the transition
pc.AnimComponentLayer#transitionProgress: number;
// returns a list of available states in the layer
pc.AnimComponentLayer#states: string[];


// loads in a state graph object which is the resource of an animationstategraph asset
pc.AnimComponent#loadStateGraph: function (stateGraph);
// removes a state graph from the component, clearing all layers and parameters
pc.AnimComponent#removeStateGraph: function ();
// resets all layers and parameters to their initial state
pc.AnimComponent#reset: function ();
// finds an animation layer by name
pc.AnimComponent#findAnimationLayer: function (layerName);
// assigns a given AnimTrack to the state node defined by nodeName. Must be called after loadStateGraph. If layerName is omitted the DEFAULT_LAYER is used
pc.AnimComponent#assignAnimation: function (nodeName, animTrack, layerName);
// clears all animations from a given state node in the provided layer. If layerName is omitted the DEFAULT_LAYER is used
pc.AnimComponent#removeNodeAnimations: function (nodeName, layerName);
// getters and setters for state graph parameters
pc.AnimComponent#getInteger: function (name);
pc.AnimComponent#setInteger: function (name, value);
pc.AnimComponent#getFloat: function (name);
pc.AnimComponent#setFloat: function (name, value);
pc.AnimComponent#getBoolean: function (name);
pc.AnimComponent#setBoolean: function (name, value);
pc.AnimComponent#getTrigger: function (name);
pc.AnimComponent#setTrigger: function (name);
pc.AnimComponent#resetTrigger: function (name);
// resets the animation state graph to its initial state, including all parameters
pc.AnimComponent#reset: function ();
// the speed property multiplies the speed of any animation that is currently playing
pc.AnimComponent#speed: number;
// enables / disables the component
pc.AnimComponent#enabled: boolean;
// allows updates to all animation layers
pc.AnimComponent#playing: boolean;
// if set to true the anim component will begin playing the first animation once all states have been linked
pc.AnimComponent#activate: boolean;
// returns whether all component layers are currently playable
pc.AnimComponent#playable: boolean;
// the state graph asset this component should use to generate it's animation state graph
pc.AnimComponent#stateGraphAsset: number;

State Graph Asset Example:

{
    "states": [
        {
            "name": "ANIM_STATE_START"
        },
        {
            "name": "Idle",
            "speed": 1.0
        },
        {
            "name": "Walk",
            "speed": 1.0
        },
        {
            "name": "ANIM_STATE_END"
        }
    ],
    "transitions": [
        {
            "from": "ANIM_STATE_START",
            "to": "Idle"
        },
        {
            "from": "Idle",
            "to": "Walk",
            "time": 0.5,
            "conditions": [
                {
                    "parameterName": "speed",
                    "predicate": "GREATER_THAN",
                    "value": 0
                }
            ]
        },
        {
            "from": "Walk",
            "to": "Idle",
            "time": 0.5,
            "exitTime": 0.8,
            "conditions": [
                {
                    "parameterName": "speed",
                    "predicate": "LESS_THAN_EQUAL_TO",
                    "value": 0
                }
            ]
        }
    ],
    "parameters": {
        "speed": {
            "type": "INTEGER",
            "value": 0
        }
    }
}

Character animation example using the state graph asset example above:

//  add model and anim components to the character entity
character.addComponent("model", {
    type: "asset",
    asset: characterModelAsset.resource.model,
    castShadows: true
});
character.addComponent("anim", {
    speed: 1.0,
    activate: false
});

// load the state graph and fully link all states to their animations
character.anim.loadStateGraph(characterStateGraphAsset.resource);
character.anim.assignAnimation('Idle', characterIdleAnimationAsset.resource);
character.anim.assignAnimation('Walk', characterWalkAnimationAsset.resource);

// begin playing Idle animation
character.anim.playing = true;

// create a function to set the charater to run for 5 seconds
var fiveSecondRun = function() {
    character.anim.setInteger('speed', 1);
    window.setTimeout(function() {
        character.anim.setInteger('speed', 0);
    }, 5000);
}

State Graph Asset Schema:

{
    "state": {
        "type": "object",
        "description": "Defines a state which will become a node in the state graph",
        "properties": {
            "name": {
                "type": "String",
                "description": "The name of the state. Used to link animations to a specific state and find states used in transitions"
            },
            "speed": {
                "type": "Number",
                "description": "The speed at which the animation linked to this state should play at"
            }
        },
        "required": ["name"]
    }
}
{
    "transition": {
        "type": "object",
        "description": "Defines a state to state transition",
        "properties": {
            "from": {
                "type": "string",
                "description": "The state that this transition will exit from"
            },
            "to": {
                "type": "string",
                "description": "The state that this transition will transition to"
            },
            "time": {
                "type": "number",
                "description": "The duration of the transition in seconds"
            },
            "priority": {
                "type": "number",
                "description": "Used to sort all matching transitions in ascending order. The first transition in the list will be selected."
            },
            "conditions": {
                "type": "array[condition]",
                "description": "A list of conditions which must pass for this transition to be used"
            },
            "exitTime": {
                "type": "number",
                "description": "If provided, this transition will only be active for the exact frame during which the source states progress passes the time specified. Given as a normalised value of the source states duration. Values less than 1 will be checked every animation loop"
            },
            "transitionOffset": {
                "type": "number",
                "description": "If provided, the destination state will begin playing its animation at this time. Given in seconds."
            },
            "interruptionSource": {
                "type": "number",
                "description": "Defines whether another transition can interrupt this one and which of the current or previous states transitions can do so. One of pc.ANIM_INTERRUPTION_SOURCE_*"
            }
        },
        "required": ["from", "to"]
    }
}
{
    "condition": {
        "type": "object",
        "description": "A condition used to test whether a transition is active",
        "properties": {
            "parameterName": {
                "type": "string",
                "description": "The parameter that should be tested against"
            },
            "predicate": {
                "type": "number",
                "description": "Defines the operation used to test the current parameters value against. One of pc.ANIM_TRANSITION_PREDICATE_*"
            },
            "value": {
                "type": ["number", "boolean"],
                "description": "Defines the value used to test the current parameters value against."
            }
        },
        "required": ["parameterName", "predicate", "value"]
    }
}
{
    "parameter": {
        "type": "object",
        "description": "Defines a parameter used to conditionally active transitions",
        "properties": {
            "type": {
                "type": "number",
                "description": "Defines the type of the parameter. One of pc.ANIM_PARAMETER_"
            },
            "value": {
                "type": ["number", "boolean"],
                "description": "The default value to set the parameter to when the anim controller is created and reset"
            }
        },
        "required": ["type", "value"]
    }
}

Animation Clip:
These are used to create pc.AnimTrack objects which can be passed into the linkAnimationToState function. The track can be retrieved from the resource of an animation clip .json asset that has been loaded using the animationclip asset type.

The example below flashes the light component of the signalLight entity red every quarter of a second.

{
    "name": "AlertLight",
    "duration": 0.25,
    "inputs": [
        [
            0.0,
            0.5,
            1.0
        ]
    ],
    "outputs": [
        {
           "components": 1,
           "data": [
                0.0,
                1.0,
                0.0
            ]
        }

    ],
    "curves": [
        {
            "path": "signalLight/light/color.r",
            "inputIndex": 0,
            "outputIndex": 0,
            "interpolation": 1
        }
    ]
}

Each curve in an animation is associated with an input and output array via index. These arrays define the keyframe pairs for the curve (input = time, output = value). In the clip above the single curve is using the only input and output arrays defined.

Curve path strings should be encoded and decoded using an instance of pc.AnimPropertyLocator(). Using this class ensures that all names have their forward slash and full stop characters correctly escaped. It provides two functions which can be used as follows:

var propertyLocator = new pc.AnimPropertyLocator();
var lightLocation = [['signalLight'], 'light', ['color', 'r']]
// the encode function takes a three value array:
//The first value contains an array of entity names which defines the path to an entity in the scene from the current components entity.
//The second value contains the name of the component within which the animated property exists.
//The third value contains an array of property names which defines the path to the property value in the component.
var lightPath = propertyLocator.encode(lightLocation);
console.log(lightPath); // "signalLight/light/color.r"
var decodedLightLocation = propertyLocator.decode(lightPath);
console.log(decodedLightLocation); // [['signalLight'], 'light', ['color', 'r']]

Engine examples:
Done: Animate component properties
Done: Character animation

@ellthompson ellthompson self-assigned this Apr 22, 2020
@ellthompson ellthompson added area: animation Animation related issue enhancement labels Apr 22, 2020
@ellthompson ellthompson marked this pull request as draft April 22, 2020 11:41
@Maksims
Copy link
Contributor

Maksims commented Apr 23, 2020

Would it be possible to make API code examples for different use cases. Just a snippet code. Even as just concept, to see what API will be like.

  1. Simply playing animation.
  2. Define frames range per animation sequence. Long single animation, split into sequences.
  3. Playing animation for specific node and its descendants. Like for above waist, and bellow waist. Classic topdown example.
  4. Defining blends between animations.
  5. Controlling speed of animation as it goes.
  6. Find out which animations are playing.
  7. Find out % of animation played.
  8. Find out when animation stopped playing.
  9. Define nodes and weights for animation, like animating both hands 100% and 50% on chest and head. Example: reloading a gun, while running different directions.
  10. Weighted direct manipulation of nodes while animating. Example: head looking at other entity while running around.

Copy link
Member

@slimbuck slimbuck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few comments on the draft (which may have already changed).

src/framework/components/anim/component.js Outdated Show resolved Hide resolved
src/framework/components/anim/controller.js Outdated Show resolved Hide resolved
src/framework/components/anim/controller.js Outdated Show resolved Hide resolved
@ellthompson
Copy link
Contributor Author

ellthompson commented Jun 1, 2020

  • pc.AnimComponent#assignAnimation: function (stateName, animTrack, layerName);
    First parameter is stateName. Are we going to use this function to assing animations to BlendTrees as well? If so, perhaps the parameter should be 'nodeName' or something? Same for removeStateAnimations.

I've went ahead and updated state name to node name for future proofing.

@godiagonal
Copy link
Contributor

How come most of the classes and methods are made private (using @private) in
3540283? This unfortunately makes the component API unusable in TypeScript apps.

@willeastcott
Copy link
Contributor

@godiagonal They are private because we wanted to deploy the API, ensure everything was stable, maybe iterate on what's deployed a bit, and then, when we're confident we will make no more changes that break backwards compatibility, we'll make it public (probably in 1.29.0).

@godiagonal
Copy link
Contributor

@godiagonal They are private because we wanted to deploy the API, ensure everything was stable, maybe iterate on what's deployed a bit, and then, when we're confident we will make no more changes that break backwards compatibility, we'll make it public (probably in 1.29.0).

Oh I see, thanks for clarifying!

Copy link
Member

@slimbuck slimbuck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GR8TM!!! just a small comment from me

var child;
for (var j = 0; j < entityChildren.length; j++) {
if (entityChildren[j].name === entityHierarchy[i + 1])
child = entityChildren[j];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should you break here?

Copy link
Contributor

@mvaligursky mvaligursky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • pc.AnimComponent#assignAnimation: function (stateName, animTrack, layerName);
    First parameter is stateName. Are we going to use this function to assing animations to BlendTrees as well? If so, perhaps the parameter should be 'nodeName' or something? Same for removeStateAnimations.

I've went ahead and updated state name to node name for future proofing.

maybe update the PR description as well

});

// load the state graph asset resource and assign the animation glb asset resources to the appropriate states
modelEntity.findComponent('anim').loadStateGraph(stateGraph.resource);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store modelEntity.findComponent('anim') in variable and reuse on following lines

examples/animation/animated-character.html Outdated Show resolved Hide resolved
"speed": 1.0
},
{
"name": "END"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should END state be optional here (and not needed to be specified) if noting references it?

src/framework/components/anim/data.js Show resolved Hide resolved
src/anim/anim.js Outdated Show resolved Hide resolved
src/framework/components/anim/controller.js Outdated Show resolved Hide resolved
src/framework/components/anim/controller.js Show resolved Hide resolved
src/framework/components/anim/controller.js Show resolved Hide resolved
src/framework/components/anim/controller.js Show resolved Hide resolved
this._animEvaluator.findClip(animation.name).blendWeight = interpolatedTime * animation.weight / state.totalWeight;
}
} else {
this._isTransitioning = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it feels like we only allow a single transition at any time .. that seems limited?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theres only ever one transition but a transition can contain multiple blends of previous states. You can be midway through a transition from A > B and then blend those into a new transition (A > B) > C. The this._transitionPreviousStates is used to record all previous states and their weights during a single transition.

if (entityHierarchy.length === 1) {
return currEntity;
}
return currEntity._parent.findByPath(entityHierarchy.join('/'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI - I believe findByPath is sloooooow, so if this is performance-sensitive code, this may become an issue. Not blocking for merging this PR tho, IMHO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, i'd say we can test this then. The binder runs this once when the animation is first loaded and before it's played.

// else
// return null;
// }
// return currEntity;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete commented out code? 😄

// Non-serialized
this.stateGraph = null;
this.layers = [];
this.layerIndicies = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

layerIndicies -> layerIndices

@ChristianTucker
Copy link

Something here that I don't see that could be very handy is allowing for wildcard transitions. For example, if the trigger Death is set to true, you want to play the death animation regardless of which state you're currently in. Being able to create something like an ANY state that you can transition from would create a lot less "spiderweb" mapping between transitions.

Currently, the only way I can see to do this would be to reset() the state when you need to trigger a "publicly accessible" animation state, and then create a list of transitions from START with different collections for your "globals". However, considering this clears your state it's not a great solution.

@ChristianTucker
Copy link

ChristianTucker commented Aug 3, 2020

I was also told to leave a Feature Request here regarding Blend Trees. I already posted the information on the forum, but I'll do a direct Copy+Paste below

I’m working with the anim component that was introduce with PR#2007 and I’ve got to say it’s a massive improvement to the previous workflow. However, I’ve been browsing through the PR and while I’ve seen Blend Trees being mentioned a few times I haven’t found anything that explains how they would be set up.

I’m not super familliar with animations, but in Unity the type of tree I want to create is called a 2D Freeform Directional tree and it allows you to declare animations that should play on a graph via a 2D vector. Example below:

blendtree

With this tree if you were holding (W and D) [traditional WASD controls] it would blend between the run-forward and run-right animation to create a run-forward-right animation effect. This makes movement in games feel very fluid. I’d really like to finish getting the fluidity of our character ironed out so we can move forward with transferring the remainder of our client to PC.

Currently

Currently I'm using a small (not sanity-checked) utility for creating transitions, just to save me some typing and increase readability:

function createTransition(from, to, time, _conditions) {
    if(!_conditions) _conditions = [[]];
    var conditions = []; 
    
    for(var row = 0; row < _conditions.length; row++) {
        var condition = _conditions[row];
        if(condition.length != 3) continue; 
        
        conditions.push({ 
            parameterName: _conditions[row][0], 
            predicate: toPredicate(_conditions[row][1]),
            value: _conditions[row][2]
        });
    }

    return  { 
        from: from, 
        to: to, 
        time: time,
        conditions: conditions
    };
}

In order to get my animations smooth between the different WASD states it requires a lot of transitions, some with higher priority than others (which I used declaration order instead of priority here, as it seems to work the same).

Example of transitions

transitions: [
    // Initial State
    createTransition("START", "Idle", 0),

    // Idle to walking 
    createTransition("Idle", "WalkForward", 0.1, [["vSpeed", "=", 1]]),
    createTransition("Idle", "WalkBackward", 0.1, [["vSpeed", "=", -1]]),
    createTransition("Idle", "WalkRight", 0.1, [["hSpeed", "=", 1]]),
    createTransition("Idle", "WalkLeft", 0.1, [["hSpeed", "=", -1]]),

    // Walking to running
    createTransition("WalkForward", "Sprinting", 0.3, [["vSpeed", "=", 1], ["sprinting", "=", true]]),

    // Running back to walking 
    createTransition("Sprinting", "WalkForward", 0.3, [["vSpeed", "=", 1], ["sprinting", "=", false]]),

    // Running back to idle 
    createTransition("Sprinting", "Idle", 0.1, [["vSpeed", "=", 0], ["sprinting", "=", false]]),

    // Running to back-pedaling 
    createTransition("Sprinting", "WalkForward", 0.3, [["vSpeed", "=", -1], ["sprinting", "=", false]]),

    // Opposing States 
    createTransition("WalkForward", "WalkBackward", 0.25, [["vSpeed", "=", -1]]),
    createTransition("WalkBackward", "WalkForward", 0.25, [["vSpeed", "=", 1]]),
    createTransition("WalkRight", "WalkLeft", 0.25, [["hSpeed", "=", -1]]),
    createTransition("WalkLeft", "WalkRight", 0.25, [["hSpeed", "=", 1]]),

    // Diagnal state transitions 
    // -----
    // Forward bi-directional transitions
    createTransition("WalkForward", "WalkRight", 0.25, [["vSpeed", "=", 0], ["hSpeed", "=", 1]]),
    createTransition("WalkForward", "WalkLeft", 0.25, [["vSpeed", "=", 0], ["hSpeed", "=", -1]]),

    // Right bi-directional transitions 
    createTransition("WalkRight", "WalkForward", 0.25, [["hSpeed", "=", 0], ["vSpeed", "=", 1]]),
    createTransition("WalkRight", "WalkBackward", 0.25, [["hSpeed", "=", 0], ["vSpeed", "=", -1]]),

    // Backwards bi-direction transitions                     
    createTransition("WalkBackward", "WalkRight", 0.25, [["vSpeed", "=", 0], ["hSpeed", "=", 1]]),
    createTransition("WalkBackward", "WalkLeft", 0.25, [["vSpeed", "=", 0], ["hSpeed", "=", -1]]),

    // Left bi-directional transitions 
    createTransition("WalkLeft", "WalkBackward", 0.25, [["hSpeed", "=", 0], ["vSpeed", "=", -1]]),
    createTransition("WalkLeft", "WalkForward", 0.25, [["hSpeed", "=", 0], ["vSpeed", "=", 1]]),

    // Walking to Idle 
    createTransition("WalkForward", "Idle", 0.1, [["vSpeed", "=", 0]]),
    createTransition("WalkBackward", "Idle", 0.1, [["vSpeed", "=", 0]]),
    createTransition("WalkRight", "Idle", 0.1, [["hSpeed", "=", 0]]),
    createTransition("WalkLeft", "Idle", 0.1, [["hSpeed", "=", 0]]),
],

Keep in mind

While these may blend to and from each-other when the correct combination of inputs is pressed/released, there's no continuous state of blending that takes place. This is as smooth as I was able to make it, without blending between two animations if say hSpeed and vSpeed = 1. Then I'd like to play a blended animation between WalkForward and WalkRight. Instead of just having to pick one.

@ellthompson
Copy link
Contributor Author

ellthompson commented Aug 3, 2020

@ChristianTucker Thanks for the feedback, it's super appreciated! With regards to the two features you've mentioned:

A wild card transition state is something I omitted from the initial release to reduce complexity of the anim component code but it's something i'll look to support asap now that the component has had some testing. I can definitely see how larger state graphs can become 'spiderweb like' without this feature. I should be able to get this in fairly soon.

Blend trees are the next major feature in the pipeline for the Anim Component! You'll be able to create blend tree nodes which can contain standard anim states or other blend tree nodes as their children. There will be support for 1D & 2D blend trees as a minimum. They will be controllable via parameters so these blend tree states will support continuous blends based on the parameter values.

Please be sure to let me know if there's any other features you're keen to get in!

@ChristianTucker
Copy link

@ellt92 Awesome, and thank you for all of the work you've put in on the animation system. Animations are the main reason I've left PlayCanvas a few times in the past, but thanks to the StateGraph I can do quite a bit that I couldn't before. I'll be EAGERLY waiting a wildcard implementation as I'm working on rushing our Client out as soon as possible. In the mean-time I'll just write a helper to simulate a wildcard by creating transitions from all known states, then exit back to idle.

Please ping me as soon as either one of these features are ready, I'll be using this system in my upcoming game and will report any problems I notice back.

@ellthompson
Copy link
Contributor Author

@ChristianTucker The 'any' state has now been implemented in the AnimComponent. It'll go out with the next engine release!

@ChristianTucker
Copy link

@ellthompson Awesome! Any plans for skeletal bone masking for animations in the near future?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: animation Animation related issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants