Skip to main content

Playing Animations

The anim component plays animation clips on the entities below it. This page plays the clips that come with a model: it adds the component, gives each clip a state, switches between them and controls their playback.

Adding the Anim Component​

Add the anim component to the root of the hierarchy that the clips animate, usually the root entity of a model. A clip finds the nodes it animates by name, among the descendants of the component's entity. If the nodes are below a different entity, point the component's Root Bone (rootBone) at it.

Load the GLB as a container asset, create its entities, and add the component to their root. Register the anim component system and the container handler with your application:

const asset = new pc.Asset('robot-arm', 'container', {
url: 'https://developer.playcanvas.com/assets/robot-arm.glb'
});
asset.ready(() => {
const arm = asset.resource.instantiateRenderEntity();
arm.addComponent('anim', { activate: true });
app.root.addChild(arm);
});
app.assets.add(asset);
app.assets.load(asset);

activate: true starts playback as soon as the component has a clip to play.

Playing Clips​

The component holds each clip in a state. To play the clips of a model by name, give each one a state of its own, named after the clip. The animations of a GLB are named in the file, and the component starts with the first one you add. To decide which clip plays when, from parameters such as a character's speed, use a state graph.

A container's animations are animation assets, whose resources are the clips, as AnimTrack objects. assignAnimation() creates a state for a clip:

for (const animation of asset.resource.animations) {
arm.anim.assignAnimation(animation.resource.name, animation.resource);
}
console.log(arm.anim.baseLayer.states); // ['START', 'idle', 'pick', 'place', 'inspect', 'stow']

Switching Clips​

Switching to another state can cut straight to it, or cross-fade to it over a time you choose. A cross-fade of a few tenths of a second hides the jump between two poses.

arm.anim.baseLayer.transition('pick', 0.4); // cross-fade over 0.4 seconds
arm.anim.baseLayer.play('idle'); // cut straight to the start of idle

Example​

This Engine app loads a robot arm with five animations, gives each one a state, and cross-fades to the next one each time you click the canvas.

Live Example
import * as pc from 'playcanvas';

// create the application with the component systems and resource handlers it uses
const canvas = document.getElementById('application');
const device = await pc.createGraphicsDevice(canvas, {
deviceTypes: [pc.DEVICETYPE_WEBGPU]
});
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);

const options = new pc.AppOptions();
options.graphicsDevice = device;
options.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.AnimComponentSystem
];
options.resourceHandlers = [pc.ContainerHandler, pc.TextureHandler];

const app = new pc.AppBase(canvas);
app.init(options);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
window.addEventListener('resize', () => app.resizeCanvas());
app.start();

app.scene.ambientLight = new pc.Color(0.35, 0.35, 0.4);

// load the robot arm, a GLB with five animations
const asset = new pc.Asset('robot-arm', 'container', {
url: 'https://developer.playcanvas.com/assets/robot-arm.glb'
});
await new Promise((resolve) => {
asset.ready(resolve);
app.assets.add(asset);
app.assets.load(asset);
});

const camera = new pc.Entity('camera');
camera.addComponent('camera', {
clearColor: new pc.Color(0.11, 0.12, 0.17)
});
camera.setPosition(1.75, 1.4, 2.15);
camera.lookAt(0, 0.75, 0);
app.root.addChild(camera);

const light = new pc.Entity('light');
light.addComponent('light', {
castShadows: true,
shadowBias: 0.2,
normalOffsetBias: 0.05
});
light.setEulerAngles(48, 38, 0);
app.root.addChild(light);

const floor = new pc.Entity('floor');
const floorMaterial = new pc.StandardMaterial();
floorMaterial.diffuse = new pc.Color(0.24, 0.26, 0.31);
floorMaterial.update();
floor.addComponent('render', { type: 'plane', material: floorMaterial });
floor.setLocalScale(20, 1, 20);
app.root.addChild(floor);

// create the arm's entities, and give its root an anim component
const arm = asset.resource.instantiateRenderEntity({ castShadows: true });
arm.addComponent('anim', { activate: true });
app.root.addChild(arm);

// give each animation in the GLB a state of its own, named after it
const clips = asset.resource.animations.map((animation) => {
arm.anim.assignAnimation(animation.resource.name, animation.resource);
return animation.resource.name;
});

// click to cross-fade to the next clip over 0.4 seconds
let current = 0;
canvas.addEventListener('click', () => {
current = (current + 1) % clips.length;
arm.anim.baseLayer.transition(clips[current], 0.4);
});

const label = document.createElement('div');
label.style.cssText = 'position: absolute; left: 12px; bottom: 10px; color: #ccc; font: 14px sans-serif;';
document.body.appendChild(label);
app.on('update', () => {
label.textContent = `Playing: ${arm.anim.baseLayer.activeState}. Click for the next clip.`;
});

Some things to try:

  • Raise the cross-fade time from 0.4 to 2 to see the two clips blend.
  • Replace transition() with play(), and drop the time, to cut between clips instead.
  • Add arm.anim.speed = 0.5; after the component is created to play every clip at half speed.

Speed, Pausing and Looping​

Playback speed multiplies at two levels. Each state has a speed of its own, and the component's speed multiplies every state's. A speed of 0 freezes playback. A negative state speed plays a clip backwards, but a negative component speed does not: it holds every clip at its first frame.

arm.anim.speed = 0.5; // everything at half speed
arm.anim.assignAnimation('pick', pickTrack, undefined, 2); // this state at double speed

A state loops by default. A state that does not loop holds the last frame of its clip until it is left. Pass false as the last argument of assignAnimation() for a state that plays once. In the Editor, set Speed and Loop on the state in the state graph, and in Web Components, set speed and loop on the <pc-anim-clip>.

To pause, set the component's playing to false, and set it back to true to carry on. That pauses every layer at once. Each layer also has pause() and play() methods of its own.

In React, set the component's speed and playing as props of <Anim>, such as <Anim playing={!paused} speed={0.5} />. <Anim> sets its props again each time it renders, so values set on the engine component from code do not last.

caution

With Activate turned off, setting playing to true is not enough to start playback, because no layer has been started either. Start each layer as well: entity.anim.playing = true followed by entity.anim.baseLayer.play().

Knowing When a Clip Has Finished​

The component fires no event when a clip ends. A clip that does not loop holds its last frame and stays in its state. There are three ways to act when it finishes:

  • Move on in the state graph. A transition with an exit time of 1 leaves the state as soon as its clip has played through.
  • Add an event to the end of the clip. An animation event at the clip's duration fires when playback reaches it.
  • Compare the time in the state with its duration. A layer's activeStateCurrentTime is the time its clip has played, in the clip's own seconds, and activeStateDuration is the clip's length:
const layer = arm.anim.baseLayer;
const finished = layer.activeStateCurrentTime >= layer.activeStateDuration;

This works for a clip that plays forwards once. In a looping state, activeStateCurrentTime keeps growing rather than wrapping back to 0, and a clip played backwards counts down from 0. Avoid activeStateProgress for this: with a state speed other than 1, it applies the speed a second time.

See Also​