メインコンテンツまでスキップ

アニメーションの再生

Animコンポーネントは、その下にあるエンティティでアニメーションクリップを再生します。このページでは、モデルに付属するクリップを再生します。コンポーネントを追加し、各クリップにステートを与え、クリップを切り替え、その再生を制御します。

Animコンポーネントの追加​

Animコンポーネントは、クリップがアニメーションさせる階層のルート、通常はモデルのルートエンティティに追加します。クリップは、アニメーションさせるノードを、コンポーネントのエンティティの子孫の中から名前で探します。ノードが別のエンティティの下にある場合は、コンポーネントのRoot Bone(rootBone)にそのエンティティを指定します。

GLBをコンテナアセットとして読み込み、そのエンティティを作成して、それらのルートにコンポーネントを追加します。アプリケーションには、Animコンポーネントシステムとコンテナハンドラーを登録しておきます。

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にすると、コンポーネントに再生するクリップができた時点で再生が始まります。

クリップの再生​

コンポーネントは、各クリップをステートに保持します。モデルのクリップを名前で再生するには、クリップごとに専用のステートを与え、そのクリップの名前を付けます。GLBのアニメーションにはファイル内で名前が付けられており、コンポーネントは最初に追加したクリップから再生を始めます。キャラクターの速度のようなパラメーターから、どのクリップをいつ再生するかを決めるには、ステートグラフを使います。

コンテナのアニメーションはアニメーションアセットで、そのリソースがクリップ、つまり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']

クリップの切り替え​

別のステートに切り替えるときは、そのステートに直接切り替えることも、指定した時間をかけてクロスフェードすることもできます。コンマ数秒のクロスフェードを入れると、2つのポーズの間の急な変化を隠せます。

arm.anim.baseLayer.transition('pick', 0.4); // 0.4秒かけてクロスフェードします
arm.anim.baseLayer.play('idle'); // idleの先頭に直接切り替えます

例​

このエンジンアプリは、5つのアニメーションを持つロボットアームを読み込み、それぞれにステートを与え、キャンバスをクリックするたびに次のアニメーションへクロスフェードします。

ライブサンプル
import * as pc from 'playcanvas';

// 使用するコンポーネントシステムとリソースハンドラーを指定してアプリケーションを作成します
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);

// 5つのアニメーションを持つGLBであるロボットアームを読み込みます
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);

// アームのエンティティを作成し、そのルートにAnimコンポーネントを与えます
const arm = asset.resource.instantiateRenderEntity({ castShadows: true });
arm.addComponent('anim', { activate: true });
app.root.addChild(arm);

// GLB内の各アニメーションに、その名前を付けた専用のステートを与えます
const clips = asset.resource.animations.map((animation) => {
arm.anim.assignAnimation(animation.resource.name, animation.resource);
return animation.resource.name;
});

// クリックすると、0.4秒かけて次のクリップへクロスフェードします
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に上げると、2つのクリップがブレンドされる様子がわかります。
  • transition()をplay()に置き換えて時間を削除すると、クロスフェードせずにクリップを直接切り替えます。
  • コンポーネントを作成した後にarm.anim.speed = 0.5;を追加すると、すべてのクリップが半分の速度で再生されます。

速度、一時停止、ループ​

再生速度は2つのレベルで掛け合わされます。各ステートは独自の速度を持ち、コンポーネントのspeedがすべてのステートの速度に掛けられます。速度を0にすると再生が止まります。ステートの速度を負にするとクリップは逆再生されますが、コンポーネントの速度を負にしても逆再生にはならず、すべてのクリップが最初のフレームで止まります。

arm.anim.speed = 0.5; // すべてが半分の速度になります
arm.anim.assignAnimation('pick', pickTrack, undefined, 2); // このステートは2倍の速度になります

ステートはデフォルトでループします。ループしないステートは、そのステートを離れるまで、クリップの最後のフレームを保持します。1回だけ再生するステートにするには、assignAnimation()の最後の引数にfalseを渡します。エディターではステートグラフのステートでSpeedとLoopを設定し、Web Componentsでは<pc-anim-clip>でspeedとloopを設定します。

一時停止するには、コンポーネントのplayingをfalseに設定し、再開するにはtrueに戻します。これで、すべてのレイヤーが一度に一時停止します。各レイヤーにも、独自のpause()メソッドとplay()メソッドがあります。

Reactでは、コンポーネントのspeedとplayingを、<Anim playing={!paused} speed={0.5} />のように<Anim>のpropとして設定します。<Anim>はレンダリングのたびにpropを設定し直すので、コードからエンジンのコンポーネントに設定した値は維持されません。

注意

Activateがオフの場合、どのレイヤーもまだ開始されていないので、playingをtrueに設定するだけでは再生は始まりません。各レイヤーも開始してください。entity.anim.playing = trueの後にentity.anim.baseLayer.play()を呼び出します。

クリップの再生が終わったことを知る​

コンポーネントは、クリップが終わってもイベントを発火しません。ループしないクリップは、最後のフレームを保持したまま、そのステートにとどまります。クリップが終わったときに処理を行う方法は3つあります。

  • ステートグラフで次に進む。 終了時間が1の遷移は、クリップを最後まで再生した時点でステートを離れます。
  • クリップの最後にイベントを追加する。 クリップの長さの時点に置いたアニメーションイベントは、再生がそこに達したときに発火します。
  • ステート内の時間をその長さと比べる。 レイヤーのactiveStateCurrentTimeは、クリップが再生された時間をクリップ自体の秒数で表したもので、activeStateDurationはクリップの長さです。
const layer = arm.anim.baseLayer;
const finished = layer.activeStateCurrentTime >= layer.activeStateDuration;

これは、1回だけ順方向に再生されるクリップで機能します。ループするステートでは、activeStateCurrentTimeは0に戻らずに増え続け、逆再生されるクリップでは0からカウントダウンします。この目的にactiveStateProgressは使わないでください。ステートの速度が1以外の場合、速度が二重に適用されます。

関連情報​