跳到主要内容

图层与遮罩

anim 可以同时运行多个状态机,每个图层一个。各层有自己的状态、过渡和默认状态,共享组件参数。适合独立变化的同步动作:腿部行走、奔跑和跳跃,手臂瞄准射击,面部微笑眨眼。

每个组件有首个基础图层,后续按顺序叠在前面的结果上。权重决定贡献程度,混合类型决定组合方式,遮罩限制作用节点,例如仅驱动手臂,其余由下层处理。

添加图层​

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: [/* ... */] }
]

用 findLayer() 按名称查找,或访问 layers 数组。

权重与混合类型​

权重范围为 0 到 1,0 表示无影响,中间值的行为取决于混合类型:

  • Overwrite(pc.ANIM_LAYER_OVERWRITE)为默认,按权重从下层结果混合到本层姿态。1 完全替换,0.5 各占一半。基础层与模型原始姿态也这样混合,因此基础层权重 0.5 表示半强度播放。
  • Additive(pc.ANIM_LAYER_ADDITIVE)将片段相对于模型原始姿态的差值,乘以权重后加到下层结果。适合在瞄准上加后坐力,或在任意姿态上加呼吸。

可随时修改 weight。平滑淡入淡出使用 blendToWeight(),在指定秒数内变化到目标权重:

const upperBody = character.anim.findLayer('UpperBody');
upperBody.blendToWeight(1, 0.3); // fade in over 0.3 seconds
upperBody.blendToWeight(0, 0.3); // and out again

Normalize Weights(normalizeWeights)改变组合方式。启用后,作用于同一节点的层按权重总和归一化,使原始姿态不再透出。独占节点的层不论权重都全强度播放,覆盖层会替换节点上的全部下层,叠加层则与下层混合而非相加。除非需要这些行为,建议关闭。编辑器会为此选项出现前创建的旧组件启用,以保持兼容。

遮罩​

遮罩列出本层驱动的节点。未列出的节点交给下层,仿佛此层不存在;没有遮罩则驱动片段中的全部目标。

遮罩是以节点路径为键的对象。路径从 anim 实体,或设置的 Root Bone 开始,按名称用 / 分隔,包含起始实体名,与片段曲线路径一致。值 true 包含该节点,{ children: true } 包含节点及全部后代。下面组件位于名为 Root 的实体:

upperBody.mask = {
// the upper spine and everything below it, such as the arms and head
'Root/Hips/Spine/Spine1': { children: true },
// the lower spine, without its children
'Root/Hips/Spine': true
};

将遮罩作为 addLayer() 第三个参数,或之后设置图层 mask。设为 null 移除。

示例​

机械臂基础层待机,第二层播放 inspect。第二层遮罩只包含手腕和下级关节,其余继续待机。点击画布通过 blendToWeight() 淡入淡出。

实时示例
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);

const tracks = Object.fromEntries(asset.resource.animations.map(animation => [animation.resource.name, animation.resource]));

// the base layer idles the whole arm
arm.anim.assignAnimation('Idle', tracks.idle);

// a second layer, masked to the wrist and everything below it, plays inspect at a weight of 0
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);

// click to blend the wrist layer in or out over half a second
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 替换遮罩,整条机械臂参与 inspect。
  • 将 pc.ANIM_LAYER_OVERWRITE 改为 pc.ANIM_LAYER_ADDITIVE,在待机上叠加检查动作,而非替换。

Layer Masks 示例在全身层上播放上半身层。示例浏览器控制面板可切换各层状态、上半身混合类型、遮罩和贡献权重:

另请参阅​