Root Motion / Animation looping

Hey there,
I am trying to use root motion animations in Playcanvas for the first time:
I read and looked at @whydoidoit project but other than him I couldn’t find any other examples of similar projects…
any references?

the method I chose to try is to update the location of the player in the end of each loop so:

  1. I calculate the offset of the movement of the player hip between two frames
  2. I increase the location in the offset
  3. I want to actually set the location only when the loop is done (does that make sense?)

problems:

  1. my calculation seems to be right but I cant be too sure, if someone has the time to look at it
  2. I cant find a method to pick the exact frame where is animation ends, if I use my own timer than I have to count on the dt object, and there is some bias all the time…

so, how do you pick this exact moment?

thanks in advance!

also, what are the pros cons of counting frames?

Hi.

Can you please drop a link to your project?

it will take me some time to create a copy I can publish…
but I am working on it :blush:

Hey Daphna I can take a look if you like. Probably the best bet on looping is to grab the engine source code, look at pc.Skeleton.addTime and monkey patch it to fire an event or call a callback when it loops. That code is pretty simple to understand. I remember all of the nightmare of trying to get the looping working. My version is all messed up with looping split animations now (which I needed for my current project).

Here’s a monkey patch to fire events when the skeleton loops:

    pc.Skeleton = pc.inherits(function Skeleton(graph) {
        this._super.call(this, graph);
        pc.events.attach(this);
    }, pc.Skeleton);

   

    /**
     * @function
     * @name pc.Skeleton#addTime
     * @description Progresses the animation assigned to the specified skeleton by the
     * supplied time delta. If the delta takes the animation passed its end point, if
     * the skeleton is set to loop, the animation will continue from the beginning.
     * Otherwise, the animation's current time will remain at its duration (i.e. the
     * end).
     * @param {Number} delta The time in seconds to progress the skeleton's animation.
     * @author Will Eastcott
     */
    pc.Skeleton.prototype.addTime = function (delta) {
        if (this._animation !== null) {
            var i;
            var node, nodeName;
            var keys, interpKey;
            var k1, k2, alpha;
            var nodes = this._animation._nodes;
            var duration = this._animation.duration;

            // Check if we can early out
            if ((this._time === duration) && !this.looping) {
                return;
            }

            // Step the current time and work out if we need to jump ahead, clamp or wrap around
            this._time += delta;

            if (this._time > duration) {
                this._time = this.looping ? 0.0 : duration;
                for (i = 0; i < nodes.length; i++) {
                    node = nodes[i];
                    nodeName = node._name;
                    this._currKeyIndices[nodeName] = 0;
                }
                this.fire('loop', true)
            } else if (this._time < 0) {
                this._time = this.looping ? duration : 0.0;
                for (i = 0; i < nodes.length; i++) {
                    node = nodes[i];
                    nodeName = node._name;
                    this._currKeyIndices[nodeName] = node._keys.length - 2;
                
                }
                this.fire('loop', false);
            }


            // For each animated node...

            // keys index offset
            var offset = (delta >= 0 ? 1 : -1);

            var foundKey;
            for (i = 0; i < nodes.length; i++) {
                node = nodes[i];
                nodeName = node._name;
                keys = node._keys;

                // Determine the interpolated keyframe for this animated node
                interpKey = this._interpolatedKeyDict[nodeName];

                // If there's only a single key, just copy the key to the interpolated key...
                foundKey = false;
                if (keys.length !== 1) {
                    // Otherwise, find the keyframe pair for this node
                    for (var currKeyIndex = this._currKeyIndices[nodeName]; currKeyIndex < keys.length-1 && currKeyIndex >= 0; currKeyIndex += offset) {
                        k1 = keys[currKeyIndex];
                        k2 = keys[currKeyIndex + 1];

                        if ((k1.time <= this._time) && (k2.time >= this._time)) {
                            alpha = (this._time - k1.time) / (k2.time - k1.time);

                            interpKey._pos.lerp(k1.position, k2.position, alpha);
                            interpKey._quat.slerp(k1.rotation, k2.rotation, alpha);
                            interpKey._scale.lerp(k1.scale, k2.scale, alpha);
                            interpKey._written = true;

                            this._currKeyIndices[nodeName] = currKeyIndex;
                            foundKey = true;
                            break;
                        }
                    }
                }
                if (keys.length === 1 || (!foundKey && this._time === 0.0 && this.looping)) {
                    interpKey._pos.copy(keys[0].position);
                    interpKey._quat.copy(keys[0].rotation);
                    interpKey._scale.copy(keys[0].scale);
                    interpKey._written = true;
                }
            }
            this.fire('frame');
        }
    };

So with that you can do this.entity.animation.data.skeleton.on('loop', function(direction) { ... })

The loop event will happen before the animation is played out.

You could then wait for the animation to be played on the character. Or also monkey patch updateGraph…

var updateGraph = pc.Skeleton.prototype.updateGraph
pc.Skeleton.prototype.updateGraph = function() {
    updateGraph.call(this);
    this.fire('updated');
};

So now you can have an event for that too… this.entity.animation.data.skeleton.on('updated', function() { ... })

To make things easier you could just wait for that event after a loop like this:


var skeleton = this.entity.animation.data.skeleton
skeleton.on('loop', function(direction) {
      //Loop has happened
      skeleton.once('updated', function() {
         //Model updated after the loop
      })
})

That’s all a bit “cobbled together” out of my current stuff and not fully tested - let me know if you use it and something doesn’t work right!

hey, first of all thank you so much!
I am starting to feel like you are right about grabbing the source code…
at the moment I had the simple Idea of never looping any animation, but checking if an animation is done by

    .animation.currentTime == .animation.duration

which does happen everytime if we don’t loop, and immediatly call the next one
(yes it is not smooth, and no I dont know how to add blending to it at the moment :slight_smile: )
but It seems to get almost the same results meanwhile, for less work…

will post an update it something changes
thanks for the help again!