骨骼动画

上次修改时间:2021-05-29 00:11:76

Oasis Engine 中的骨骼动画通过 glTF 模型(相关教程请参考资源加载)的动画组件(Animation)获得,动画组件中包含多个动画片段(AnimationClip),动画组件可以控制动画片段的播放。

/**
 * @title Skeleton Animation
 * @category Animation
 */
import { Animation, Camera, DirectLight, EnvironmentMapLight, GLTFResource, Vector3, WebGLEngine } from "oasis-engine";
import { OrbitControl } from "@oasis-engine/controls";

const engine = new WebGLEngine("canvas");
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();

// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 3);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);

const lightNode = rootEntity.createChild("light_node");
rootEntity.addComponent(EnvironmentMapLight);
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));

engine.resourceManager
  .load<GLTFResource>("https://gw.alipayobjects.com/os/basement_prod/aa318303-d7c9-4cb8-8c5a-9cf3855fd1e6.gltf")
  .then((asset) => {
    const { animations, defaultSceneRoot } = asset;
    rootEntity.addChild(defaultSceneRoot);

    const animator = defaultSceneRoot.getComponent(Animation);
    animator.playAnimationClip(animations[0].name);
  });

engine.run();

代码示例

// 在脚本组件中使用
class ResourceScript extends Script {
  async onAwake() {
    const {defaultSceneRoot, animations} = await this.engine.resourceManager.load(
      'https://gw.alipayobjects.com/os/basement_prod/aa318303-d7c9-4cb8-8c5a-9cf3855fd1e6.gltf',
    );

    this.entity.addChild(defaultSceneRoot);

    // ps: 此处需import oasis-engine的Animation
    const animator = defaultSceneRoot.getComponent(Animation);

    animator.playAnimationClip(animations[0].name);
  }
}

rootEntity.addComponent(ResourceScript);

属性

播放速度

通过 Animation 组件的 timeScale  属性来控制动画的播放速度。 timeScale 默认值为 1.0 ,值越大播放的越快,越小播放的越慢。

animator.timeScale = 2.0

播放次数

通过设置 WrapMode  来控制动画播放的模式,默认 WrapMode.LOOP

animator.playAnimationClip('walk', {
   wrapMode: WrapMode.LOOP // or WrapMode.ONCE
})

方法

动画融合

实现两个动画之间的平滑切换。例如一个角色从步行动画切换到待机动画,直接切换会有明显的跳帧,使用动画混合可以平滑地从上一个动作切换到下一个动作。 通过 Animation 组件的 crossFade  接口实现动画融合切换。

// 先让角色播放步行(‘walk’)动画
animator.playAnimationClip('walk');  

// 在需要切换的时间点,执行下面的函数。角色将从步行('walk')动画 平滑切换到待机('idle')动画。
animator.crossFade('idle', 600)
/**
 * @title Animation Cross Fade
 * @category Animation
 */
import { OrbitControl } from "@oasis-engine/controls";
import * as dat from "dat.gui";
import {
  AmbientLight,
  Animation,
  Camera,
  Color,
  DirectLight,
  GLTFResource,
  WebGLEngine
} from "oasis-engine";


//-- create engine object
const engine = new WebGLEngine("canvas");
engine.canvas.resizeByClientSize();

const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
const lightEntity = rootEntity.createChild("light");
lightEntity.transform.rotate(0, 180, 0);

const ambient = lightEntity.addComponent(AmbientLight);
ambient.color = new Color(0.2, 0.2, 0.2, 1);
const light = lightEntity.addComponent(DirectLight);
light.color = new Color(0.8, 0.8, 0.8, 1.0);

//-- create camera
const cameraEntity = rootEntity.createChild("camera_entity");
cameraEntity.transform.setPosition(0, 0, -10);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl);

engine.run();

engine.resourceManager
  .load<GLTFResource>("https://gw.alipayobjects.com/os/OasisHub/267000040/494/redPacket.gltf")
  .then((asset) => {
    const { animations, defaultSceneRoot } = asset;
    const animationNameList = animations.map(({ name }) => name);

    rootEntity.addChild(defaultSceneRoot);

    const animator = defaultSceneRoot.getComponent(Animation);
    animator.playAnimationClip(animationNameList[0]);

    const debugInfo = {
      animation: animationNameList[0],
      crossFade: true,
      crossTime: 1000
    };

    const gui = new dat.GUI();

    gui.add(debugInfo, "animation", animationNameList).onChange((v) => {
      const { crossFade, crossTime } = debugInfo;

      if (crossFade) {
        animator.crossFade(v, crossTime);
      } else {
        animator.playAnimationClip(v);
      }
    });

    gui.add(debugInfo, "crossFade");
    gui.add(debugInfo, "crossTime", 0, 5000).name("过渡时间(毫秒)");
  });

动画混合

实现角色的一部分播放一个动画,别的部分播放另外一个动画。例如,一个角色可以上半身播放射击、砍杀等动作,下半身播放站立、走动、跑动等动作。使用这种融合方式可以实现不同动作的自由组合,极大减少美术的工作量。通过 Animation  组件的 mix  接口实现动画组合功能。

animator.playAnimationClip('walk');  //先让角色播放步行('walk')动画
animator.mix('wave', 'upper_body');  //设置角色的上半身的根骨骼('upper_body')播放挥手('wave')动画。

为了实现动画融合功能,需要对导出的动画资源做一些一致性限制。

事件

经常需要在动画播放到特定时间的时候触发事件。目前的骨骼动画支持以下事件:

事件名称解释
AnimationEvent.FINISHED动画播放结束后触发事件,仅在 WrapMode.ONCE 时生效
AnimationEvent.FRAME_EVENT 动画播放到特定时间触发事件,需要配置触发时间
AnimationEvent.LOOP_END 循环播放的动画在一轮结束后触发事件,仅在 WrapMode.LOOP 是生效
//-- 回调函数
let cb = ()=>{
  console.log('FRAME_EVENT')
};

let cb2 = ()=>{
  console.log('LOOP_END')
};

animator.playAnimationClip('walk', {
  wrapMode: WrapMode.LOOP,  // 使用循环播放模式
  events: [
    { type: AnimationEvent.FRAME_EVENT, triggerTime: 0.5, callback: cb }, // 添加FRAME_EVENT事件
    { type: AnimationEvent.LOOP_END, callback: cb2 }                      // 添加LOOP_END事件
  ]
})