レイヤーとマスク
Animコンポーネントは、レイヤーごとに1つずつ、複数のステートマシンを同時に実行できます。各レイヤーは独自のステート、遷移、デフォルトステートを持ち、すべてのレイヤーがコンポーネントのパラメーターを共有します。レイヤーは、同時に起こり、それぞれ独立して変化する動作に使います。キャラクターの脚が1つのレイヤーで歩き、走り、跳ぶ間に、腕は別のレイヤーで狙いを定めて撃ち、顔は3つ目のレイヤーで微笑んだりまばたきしたりできます。
どのコンポーネントにも最初のレイヤーであるベースレイヤーがあり、レイヤーは順番に、それぞれ前のレイヤーの上に重ねて適用されます。各レイヤーには、どれだけ寄与するかを決めるウェイトと、下のレイヤーとどのように組み合わさるかを決めるブレンドタイプがあります。マスクはレイヤーをモデルのノードの一部に限定し、例えば腕だけをアニメーションさせて、残りは下のレイヤーに任せるようにします。
レイヤーの追加
- Engine
- Editor
- React
- Web Components
addLayer()は、ウェイト、マスク、ブレンドタイプを指定してレイヤーを追加し、そのレイヤーを返します。レイヤー自身のassignAnimation()で、レイヤーにステートを与えます。
const upperBody = character.anim.addLayer('UpperBody', 1, upperBodyMask, pc.ANIM_LAYER_OVERWRITE);
upperBody.assignAnimation('Aim', aimTrack);
upperBody.assignAnimation('Shoot', shootTrack, 1, false);
ステートグラフのデータにも複数のレイヤーを含められます。layersの各エントリーは、ステートと遷移に加えてweightとblendTypeを受け取ります。
layers: [
{ name: 'Base', states: [/* ... */], transitions: [/* ... */] },
{ name: 'UpperBody', weight: 1, blendType: pc.ANIM_LAYER_OVERWRITE, states: [/* ... */], transitions: [/* ... */] }
]
レイヤーを名前で見つけるにはfindAnimationLayer()を使うか、layers配列から取得します。
ステートグラフアセットには最初からベースレイヤーがあり、これは削除できません。グラフエディターのレイヤーパネルで + LAYER をクリックして別のレイヤーを追加し、そこでName、Default State、Blend Type、Blend Weightを設定します。編集するレイヤーは、グラフの右上にあるドロップダウンから選択します。

Animコンポーネントには、レイヤーごとにセクションが表示され、その中にレイヤーの各ステートのスロットがあります。
モデルの<Entity>の中、<Anim playing />の後に配置した独自のコンポーネントから、エンジンのコンポーネントにレイヤーを追加します。
import { useEffect } from 'react';
import { useParent } from '@playcanvas/react/hooks';
function UpperBody({ tracks }) {
const entity = useParent();
useEffect(() => {
const layer = entity.anim.addLayer('UpperBody', 1, upperBodyMask);
layer.assignAnimation('Aim', tracks.aim);
}, [entity, tracks]);
return null;
}
<pc-anim>は、1つのレイヤーでクリップを再生します。複数のレイヤーを使うには、ステートグラフの場合と同じようにコードからモデルのエンティティにAnimコンポーネントを追加し、そこにレイヤーを追加します。
model.entity.addComponent('anim', { activate: true });
model.entity.anim.assignAnimation('Walk', tracks.walk);
const upperBody = model.entity.anim.addLayer('UpperBody', 1, upperBodyMask);
upperBody.assignAnimation('Aim', tracks.aim);
ウェイトとブレンドタイプ
レイヤーのウェイトは、効果がまったくない0から1までの範囲です。その間のウェイトでどのように働くかは、レイヤーのブレンドタイプによって決まります。
- デフォルトのOverwriteレイヤー(
pc.ANIM_LAYER_OVERWRITE)は、ウェイトに応じて、下のレイヤーの結果から自身のポーズへとブレンドします。1では下のレイヤーの結果を置き換え、0.5では結果がその中間になります。ベースレイヤーも同じようにモデルの元のポーズとブレンドされるため、ウェイトが0.5のベースレイヤーはクリップを半分の強さで再生します。 - Additiveレイヤー(
pc.ANIM_LAYER_ADDITIVE)は、クリップとモデルの元のポーズとの差分を、ウェイトでスケーリングして下のレイヤーの結果に加えます。照準の上に反動を加える、どんなポーズの上にも呼吸を加えるなど、ある動きの上に別の動きを重ねるのに使います。
ウェイトは、レイヤーのweightでいつでも変更できます。レイヤーを滑らかにフェードインまたはフェードアウトさせるにはblendToWeight()を使います。これは、秒単位で指定した時間をかけて、ウェイトを指定した値まで動かします。
const upperBody = character.anim.findAnimationLayer('UpperBody');
upperBody.blendToWeight(1, 0.3); // 0.3秒かけてフェードイン
upperBody.blendToWeight(0, 0.3); // そして再びフェードアウト
AnimコンポーネントのNormalize Weights(normalizeWeights)オプションは、レイヤーの組み合わせ方を変えます。これをオンにすると、あるノードをアニメーションさせるレイヤーのウェイトがその合計で割ってスケーリングされるため、元のポーズが透けて見えることはありません。ノード上で単独のレイヤーはウェイトに関係なく最大の強さで再生され、上書きレイヤーは自身がアニメーションさせるノード上で下のすべてのレイヤーを置き換え、加算レイヤーは下のレイヤーに加算されるのではなくブレンドされます。この動作が必要でない限り、オフのままにしてください。エディターは、このオプションが存在する前に作成されたAnimコンポーネントでは、動作を変えないためにこれをオンにします。
マスク
マスクは、レイヤーがアニメーションさせるノードを列挙したものです。レイヤーのマスクに含まれないノードは、そのレイヤーが存在しないかのように下のレイヤーに任されます。マスクのないレイヤーは、そのクリップがアニメーションさせるすべてのノードをアニメーションさせます。
- Engine
- Editor
- React
- Web Components
マスクは、ノードのパスをキーとするオブジェクトです。パスは、Animコンポーネントのエンティティ(Root Boneが設定されている場合はそのボーン)から対象のノードまでのノード名を、/で区切って並べたものです。クリップのカーブのパスと同様に、パスはそのエンティティの名前から始まります。値がtrueならそのノードがマスクに含まれ、{ children: true }ならそのノードとその下にあるすべてのノードがマスクに含まれます。次の例では、コンポーネントはRootという名前のエンティティにあります。
upperBody.mask = {
// 背骨の上部と、腕や頭など、その下にあるすべてのノード
'Root/Hips/Spine/Spine1': { children: true },
// 背骨の下部。その子は含まない
'Root/Hips/Spine': true
};
マスクはaddLayer()の3番目の引数として渡すか、後からレイヤーのmaskで設定します。削除するにはnullを設定します。
マスクはステートグラフアセットではなく各エンティティのAnimコンポーネントに属するため、同じグラフでもモデルごとに異なるマスクをかけられます。
-
エンティティを選択し、Animコンポーネント内のレイヤーのセクションでCREATE MASKをクリックします。

-
マスクインスペクターには、コンポーネントがアニメーションさせる階層がRoot Boneから表示されます。レイヤーにアニメーションさせるノードにチェックを入れます。ノードを右クリックしてAdd hierarchyまたはRemove hierarchyを選ぶと、そのノードとその下にあるすべてのノードのチェックを一度に入れたり外したりできます。

後からマスクを変更するにはEDIT MASKをクリックし、削除するにはその横にある削除ボタンをクリックします。
Engineタブと同じように、独自のコンポーネントからマスクをaddLayer()に渡すか、レイヤーのmaskを設定します。
Engineタブと同じように、コードから追加したコンポーネントで、マスクをaddLayer()に渡すか、レイヤーのmaskを設定します。
例
このエンジンアプリは、ベースレイヤーでロボットアームに待機の動作をさせ、アームの点検のクリップを再生する2つ目のレイヤーを追加します。2つ目のレイヤーのマスクには手首とその下の関節だけが含まれるため、アームの残りの部分は待機を続けます。キャンバスをクリックすると、blendToWeight()でレイヤーがフェードインまたはフェードアウトします。
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);
const tracks = Object.fromEntries(asset.resource.animations.map(animation => [animation.resource.name, animation.resource]));
// ベースレイヤーで、アーム全体に待機の動作をさせます
arm.anim.assignAnimation('Idle', tracks.idle);
// 手首とその下のすべてにマスクした2つ目のレイヤーで、ウェイト0でinspectを再生します
const wrist = arm.anim.addLayer('Wrist', 0, {
'arm_base/arm_column/arm_upper/arm_forearm/arm_wrist_roll': { children: true }
}, pc.ANIM_LAYER_OVERWRITE);
wrist.assignAnimation('Inspect', tracks.inspect);
// クリックすると、0.5秒かけて手首のレイヤーをフェードインまたはフェードアウトします
let on = false;
canvas.addEventListener('click', () => {
on = !on;
wrist.blendToWeight(on ? 1 : 0, 0.5);
});
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 = `Wrist layer weight: ${wrist.weight.toFixed(2)}. Click to blend it in or out.`;
});
次のことを試してみてください。
- マスクをアームの上のほうへ移して
'arm_base/arm_column/arm_upper'にすると、レイヤーが上腕と前腕も受け持つようになります。 - マスクを
nullに置き換えると、レイヤーがフェードインしているときは、アーム全体が点検の動作をします。 pc.ANIM_LAYER_OVERWRITEをpc.ANIM_LAYER_ADDITIVEに変更すると、手首は待機の動作を置き換えるのではなく、それに点検の動きを加えます。
Layer Masksのサンプルは、キャラクターの全身のレイヤーの上に上半身のレイヤーを再生します。サンプルブラウザのコントロールパネルで、各レイヤーのステートを切り替えたり、上半身のレイヤーのブレンドタイプを変えたり、そのマスクのオンとオフを切り替えたり、各レイヤーがどれだけ影響するかを設定したりできます。
関連情報
- ステートグラフ - 各レイヤーのステートと遷移
- Animコンポーネント - コンポーネントとそのレイヤーやマスクについてのエディターリファレンス
- AnimComponentとAnimComponentLayer - エンジンのAPIリファレンス