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

レイヤーとマスク

Animコンポーネントは、レイヤーごとに1つずつ、複数のステートマシンを同時に実行できます。各レイヤーは独自のステート、遷移、デフォルトステートを持ち、すべてのレイヤーがコンポーネントのパラメーターを共有します。レイヤーは、同時に起こり、それぞれ独立して変化する動作に使います。キャラクターの脚が1つのレイヤーで歩き、走り、跳ぶ間に、腕は別のレイヤーで狙いを定めて撃ち、顔は3つ目のレイヤーで微笑んだりまばたきしたりできます。

どのコンポーネントにも最初のレイヤーであるベースレイヤーがあり、レイヤーは順番に、それぞれ前のレイヤーの上に重ねて適用されます。各レイヤーには、どれだけ寄与するかを決めるウェイトと、下のレイヤーとどのように組み合わさるかを決めるブレンドタイプがあります。マスクはレイヤーをモデルのノードの一部に限定し、例えば腕だけをアニメーションさせて、残りは下のレイヤーに任せるようにします。

レイヤーの追加​

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配列から取得します。

ウェイトとブレンドタイプ​

レイヤーのウェイトは、効果がまったくない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コンポーネントでは、動作を変えないためにこれをオンにします。

マスク​

マスクは、レイヤーがアニメーションさせるノードを列挙したものです。レイヤーのマスクに含まれないノードは、そのレイヤーが存在しないかのように下のレイヤーに任されます。マスクのないレイヤーは、そのクリップがアニメーションさせるすべてのノードをアニメーションさせます。

マスクは、ノードのパスをキーとするオブジェクトです。パスは、Animコンポーネントのエンティティ(Root Boneが設定されている場合はそのボーン)から対象のノードまでのノード名を、/で区切って並べたものです。クリップのカーブのパスと同様に、パスはそのエンティティの名前から始まります。値がtrueならそのノードがマスクに含まれ、{ children: true }ならそのノードとその下にあるすべてのノードがマスクに含まれます。次の例では、コンポーネントはRootという名前のエンティティにあります。

upperBody.mask = {
// 背骨の上部と、腕や頭など、その下にあるすべてのノード
'Root/Hips/Spine/Spine1': { children: true },
// 背骨の下部。その子は含まない
'Root/Hips/Spine': true
};

マスクはaddLayer()の3番目の引数として渡すか、後からレイヤーのmaskで設定します。削除するにはnullを設定します。

例​

このエンジンアプリは、ベースレイヤーでロボットアームに待機の動作をさせ、アームの点検のクリップを再生する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のサンプルは、キャラクターの全身のレイヤーの上に上半身のレイヤーを再生します。サンプルブラウザのコントロールパネルで、各レイヤーのステートを切り替えたり、上半身のレイヤーのブレンドタイプを変えたり、そのマスクのオンとオフを切り替えたり、各レイヤーがどれだけ影響するかを設定したりできます。

関連情報​