播放动画
anim 组件在所属实体下方播放动画片段。本页介绍添加组件、为片段创建状态、切换和控制播放。
添加 Anim 组件
将 anim 添加到动画层级根,通常是模型根实体。片段按名称查找后代节点;若节点位于其他实体下,将 Root Bone(rootBone)指向它。
- 引擎
- 编辑器
- React
- Web Components
以 container 加载 GLB,创建实体后在根节点添加组件。应用需注册 anim 系统和 container 处理器:
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 使组件获得片段后立即播放。
选中模型根实体,通过 Add Component 添加 Anim。编辑器通过状态图播放。指定 State Graph 后,组件为各状态显示槽位,将动画资源拖入对应槽位。Activate 控制启动时播放。
为渲染模型的 <Entity> 添加 <Anim>,传入模型资源:
import { Entity } from '@playcanvas/react';
import { Anim, Render } from '@playcanvas/react/components';
import { useModel } from '@playcanvas/react/hooks';
function RobotArm() {
const { asset } = useModel('https://developer.playcanvas.com/assets/robot-arm.glb');
if (!asset) return null;
return (
<Entity name="robot-arm">
<Render type="asset" asset={asset} />
<Anim asset={asset} playing />
</Entity>
);
}
<Anim asset> 将模型所有动画放入同一状态,多个动画时只有最后一个播放。按名称播放见播放片段。
应始终传入 playing。每次渲染都会将全部 props 重新设置到组件,而 playing 默认 false,不传时再次渲染就会停止。
在 <pc-model> 中添加 <pc-anim>。无子标签时,为各动画生成同名片段,并播放第一个:
<pc-asset id="robot-arm" src="https://developer.playcanvas.com/assets/robot-arm.glb"></pc-asset>
<pc-scene>
<pc-model asset="robot-arm">
<pc-anim></pc-anim>
</pc-model>
</pc-scene>
播放片段
各片段保存在状态中。按名称播放时,为各片段创建同名状态。GLB 动画名来自文件,组件从首先添加的片段开始。需要按速度等参数选择播放项时,使用状态图。
- 引擎
- 编辑器
- React
- Web Components
container 的动画资源持有 AnimTrack 片段。assignAnimation() 为片段创建状态:
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']
为每个片段创建一个状态,分配状态图并填入资源,见创建状态图。
添加不传 asset 的 <Anim playing />,再从自定义组件添加片段。useParent() 返回外层 <Entity> 的实体:
import { useEffect } from 'react';
import { Entity } from '@playcanvas/react';
import { Anim, Render } from '@playcanvas/react/components';
import { useModel, useParent } from '@playcanvas/react/hooks';
function Clips({ asset }) {
const entity = useParent();
useEffect(() => {
for (const animation of asset.resource.animations) {
entity.anim.assignAnimation(animation.resource.name, animation.resource);
}
}, [entity, asset]);
return null;
}
function RobotArm() {
const { asset } = useModel('https://developer.playcanvas.com/assets/robot-arm.glb');
if (!asset) return null;
return (
<Entity name="robot-arm">
<Render type="asset" asset={asset} />
<Anim playing />
<Clips asset={asset} />
</Entity>
);
}
通过 <pc-anim-clip> 声明同名模型动画,以 clip 选择:
<pc-model asset="robot-arm">
<pc-anim clip="idle">
<pc-anim-clip name="idle"></pc-anim-clip>
<pc-anim-clip name="pick"></pc-anim-clip>
<pc-anim-clip name="place"></pc-anim-clip>
</pc-anim>
</pc-model>
切换片段
切换可立即跳转,也可交叉淡化。几十分之一秒的过渡通常能隐藏姿态跳变。
- 引擎
- 编辑器
- React
- Web Components
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
状态图按条件自动切换,也可在脚本中调用,例如 this.entity.anim.baseLayer.transition('pick', 0.4)。
prop 变化时在引擎组件上切换:
function Clip({ name }) {
const entity = useParent();
useEffect(() => {
entity.anim.baseLayer.transition(name, 0.4);
}, [entity, name]);
return null;
}
<Entity name="robot-arm">
<Render type="asset" asset={asset} />
<Anim playing />
<Clips asset={asset} />
<Clip name={clip} />
</Entity>
修改 clip,transition-time 指定淡化秒数:
<pc-anim id="arm-anim" clip="idle" transition-time="0.4">
<pc-anim-clip name="idle"></pc-anim-clip>
<pc-anim-clip name="pick"></pc-anim-clip>
</pc-anim>
<script>
document.getElementById('arm-anim').setAttribute('clip', 'pick');
</script>
元素 play() 和 transition() 分别执行单次直接切换和淡化。
示例
下面加载带五个动画的机械臂,每个片段一个状态,点击画布淡化到下一个。
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.`;
});
可以尝试:
- 将
0.4改为2,观察混合。 - 用
play()替换transition()并移除时长,立即切换。 - 创建组件后添加
arm.anim.speed = 0.5;,半速播放。
速度、暂停与循环
状态速度与组件 speed 相乘。0 冻结播放。状态速度为负会倒放,但组件速度为负只会使片段停在首帧。
arm.anim.speed = 0.5; // everything at half speed
arm.anim.assignAnimation('pick', pickTrack, undefined, 2); // this state at double speed
状态默认循环。非循环状态结束后保持末帧直到退出。assignAnimation() 最后传 false 可只播放一次。编辑器在状态图设置 Speed、Loop,Web Components 在 <pc-anim-clip> 设置 speed、loop。
组件 playing 设为 false 暂停全部图层,设回 true 继续。各图层也有独立 pause()、play()。
React 通过 <Anim> props 设置,例如 <Anim playing={!paused} speed={0.5} />。每次渲染会重新应用,代码直接设置的引擎值不会长期保留。
关闭 Activate 时,还需启动图层:设置 entity.anim.playing = true,再调用 entity.anim.baseLayer.play()。
判断片段完成
组件不自动发出结束事件,非循环状态保持末帧,可通过以下方式处理:
- 状态图过渡。退出时间为 1 时,首次播放完成后退出。
- **末尾事件。**在片段时长位置添加动画事件。
- **比较时间与时长。**图层
activeStateCurrentTime为片段自身秒数计的已播放时间,activeStateDuration为长度:
const layer = arm.anim.baseLayer;
const finished = layer.activeStateCurrentTime >= layer.activeStateDuration;
适用于正向播放一次。循环时间持续增长,不回到 0;倒放从 0 向下计数。不要使用 activeStateProgress,状态速度不为 1 时它会重复应用速度。
另请参阅
- 动画片段:来源和绑定。
- 状态图:按参数选择片段。
- Anim 组件:编辑器参考。
<Anim/>、<pc-anim>和<pc-anim-clip>:组件参考。- AnimComponent 和 AnimComponentLayer:引擎 API。