帧动画

帧动画是一种内置的动画实现,提供给开发者类似于 CSS 动画的能力,去控制一个元素下所有组件的数据。

我们提供了一种资源来定义帧动画,之后可以用对应的加载器加载,在组件中使用。

帧动画资源

首先我们要准备一个帧动画资源,它是被存储为json格式,大致如下:

{
  "keyframe": {
    "parent": {
      "0": {
        "rotation": [0, 0, 0],
      },
      "100": {
        "rotation": [0, 6.28, 0]
      }
    },
    "child": {
      "0": {
        "position.y": -0.5,
        "material.u_baseColorFactor": [0.48, 0.78, 0.64, 1]
      },
      "100": {
        "position.y": 1.5,
        "material.u_baseColorFactor": [0.176, 0.368, 0.937, 1]
      }
    }
  },
  "animation": {
    "parent": {
      "keyframe": "parent",
      "duration": 8,
      "ease": "linear",
      "loop": -1
    },
    "child": {
      "keyframe": "child",
      "duration": 4,
      "ease": "ease-in-out",
      "direction": "both",
      "loop": -1
    }
  }
}

可以看到其中有两个部分:keyframe和animation。

keyframe中定义了帧动画具体的行为,本质上类似于css3中的keyframes,其中进度的范围是0~100,每个进度下是动画具体影响的键和值。其中键的规则是[组件].[属性].[属性2].[属性...],比如transform.postion.x,就是对应于使用了这个帧动画的元素下的transform组件的position属性的x属性变化。

当然我们不难看到,如果仅仅是按照这样的规则,一来是开发者必须要实现对应组件下每个属性的访问器,其次是对于一些常见的属性写起来会繁琐,再者对于材质动画会无从下手。为了解决这个问题,我做了一些实现上的优化:

  1. 如果整个属性路径最终是[组件].[属性]的形式,则不需要写访问器,会自动设置。
  2. position、rotation、scale会被自动代理到transform组件下。
  3. material是一种特殊的定义,会被自动代理到mesh组件的材质的uniform下。

要注意,属性的值仅仅支持number、number-array和color类型的数据。 有了keyframe,再者就是animation了。animation中定义了真的暴露给animator的片段,其中第一级的每个键都是片段的名字,它对应的值就是其作为动画播放时的一些参数。keyframe定义这个片段对应于上面定义的哪个keyframe,duration是以秒为单位的时长,loop是默认循环次数(-1为永远循环),delay是播放延迟,direction是播放方向,默认forwards,可选backwards和both(在循环时交替前向反向),ease则是时间函数,决定动画的插值曲线。

支持的时间函数可见 API 文档:noneParamsEaseFuncs 和 useParamsEaseFuncs。

使用

有了资源我们便可以加载使用它,和其他资源一样,keyframe资源遵循标准加载流程:

<xr-asset-load type="keyframe" asset-id="anim" src="/assets/keyframes/test.json" />

然后便可以引用使用:

<xr-node anim-keyframe="anim" anim-autoplay="clip:parent">
  <xr-node position="0 -0.5 3" anim-keyframe="anim" anim-autoplay="clip:child">
  </xr-node>
</xr-node>

注意这里使用了anim-keyframe字段来指定animator组件要默认使用哪个keyframe动画。

动画组件

动画组件Animator用于管理所挂载元素下所有的动画。

和其他组件一样,它也提供了在xml中和脚本控制两种方法。我们先看看脚本控制是怎么做的:

const animator = el.getComponent(xrFrameSystem.Animator);

// 添加一个动画,`clipMap`可选
animator.addAnimation(new XrTeamCameraAnimation(data), clipMap);

// 通过动画类直接创建一个动画,`clipMap`可选
const anim = animator.createAnimation(XrTeamCameraAnimation, data, clipMap);

// 移除一个动画
animator.removeAnimation(anim);

// 播放名为`name`的动画,可以同时播放多个
animator.play(name, options);

// 暂停名为`name`的动画,不填`name`则暂停所有
animator.pause(name);

// 唤醒名为`name`的动画,不填`name`则唤醒所有
animator.resume(name);

// 停止名为`name`的动画,不填`name`则停止所有
animator.stop(name);

// 将名为`name`的片段定格到某个进度
animator.pauseToFrame(name, progress);

这里要特别注意的有几个点:

  1. clipMap本质上是给开发者提供了一个从动画组件片段的名字到动画实例片段名字的映射,这一般在动画组件有多个动画实例、而动画实例中的片段有重名的情况下会很有用。如果不填,则会默认使用动画实例的片段名来索引。
  2. options是播放时的参数,具体的定义可以参照API文档,要说明的是在play时提供的这个参数,会覆盖掉动画实例onPlay时返回的参数。

相较于脚本控制,大部分开发者在xml中用动画组件会比较常见。所有派生自XRElement元素的元素,都会拥有默认的动画属性映射:

<xr-node
  anim-keyframe="basic-anim"
  anim-clipmap="default:cube"
  anim-autoplay="clip:cube, speed:2"
></xr-node>

这三个属性对应于动画组件的三个数据,最后一个anim-autoplay指定了是否要默认播放以及默认播放的片段和参数,如果不写clip数据则会播放所有的片段。anim-clipmap则是给默认加载的动画指定一个片段映射,而这个默认的动画就是anim-keyframe指定的,它就是前面提到的内置的帧动画,这个可以在后面的章节看到详细说明。

除了anim-keyframe,其他两个参数可以作用于前面提到的gltf动画,也可以在相关章节查看。

事件

动画组件为元素提供了以下事件:

事件 参数 立即 wxml 时机
anim-stop 对象,其中name是片段名字 是 是 某个片段播放停止时

动画实现

动画实现的基础是基类Animation,所有的动画都必须派生于它去实现必要的方法。让我们以一个例子来看看:

import XrFrame from 'XrFrame';
const xrFrameSystem = wx.getXrFrameSystem();

// 定制动画接受的初始化数据接口
interface IXrTeamCameraAnimtionData {
  targets: {
    hikari: XrFrame.Vector3;
    roam: XrFrame.Vector3;
    xinyi: XrFrame.Vector3;
    final: XrFrame.Vector3;
  },
}

// 定制动画接受的播放额外配置接口
interface IXrTeamCameraAnimationOptions {

}

// 定制动画的实现
class XrTeamCameraAnimation extends xrFrameSystem.Animation<
  IXrTeamCameraAnimtionData,
  IXrTeamCameraAnimationOptions
> {
  private _camera: XrFrame.Transform | undefined;
  private _target: XrFrame.Transform | undefined;
  private _targets: IXrTeamCameraAnimtionData['targets'] | undefined;
  private _startC: XrFrame.Vector3 = new xrFrameSystem.Vector3();
  private _endC: XrFrame.Vector3 = new xrFrameSystem.Vector3();
  private _startT: XrFrame.Vector3 = new xrFrameSystem.Vector3();
  private _endT: XrFrame.Vector3 = new xrFrameSystem.Vector3();

  // 动画初始化时会被执行,传入初始数据
  // 必须设置`this.clipNames`,提供给动画组件必要的信息
  public onInit(data: IXrTeamCameraAnimtionData) {
    this._targets = data.targets;
    this.clipNames = ['hikari', 'roam', 'xinyi'];
  }

  // 动画被播放时会被执行,必须返回片段时长`duration`
  // 剩下三个返回参数是可选的,详见API文档
  public onPlay(el: XrFrame.Element, clipName: string, options: IXrTeamCameraAnimationOptions): {
    duration: number,
    loop?: number,
    delay?: number,
    direction?: XrFrame.TDirection
  } {
    this._camera = this._camera || el.getComponent(xrFrameSystem.Transform);
    this._target = el.getComponent(xrFrameSystem.Camera).target;
    this._startT.set(this._target.position);
    this._endT.setValue(this._targets![clipName].x, this._targets![clipName].y, this._targets![clipName].z);
    this._startC.set(this._camera.position);
    this._endC.set(this._endT);
    this._endC.z += 2;

    return {duration: 3};
  }

  // 动画播放进度更新是会被执行,`progress`的范围是`0~1`
  // `el`参数是指这个动画目前作用于哪个元素,因为动画和元素、组件并非总是一一对应的
  public onUpdate(el: XrFrame.Element, progress: number, reverse: boolean) {
    progress = xrFrameSystem.noneParamsEaseFuncs['ease-in-out'](progress);
    this._startT?.lerp(this._endT, progress, this._target?.position);
    this._startC?.lerp(this._endC, progress, this._camera?.position);
  }

  // 动画播放暂停时会被执行,暂停本身的逻辑是自动的
  public onPause(el: XrFrame.Element) {

  }

  // 动画从暂停中唤醒时会被执行
  public onResume(el: XrFrame.Element) {

  }

  // 动画停止时会被执行,包括播放结束和手动停止
  public onStop(el: XrFrame.Element) {

  }
}

这段代码中,我们定制了一个动画,它的几个生命周期来定义其是如何运作的。框架内置了两种动画帧动画和gltf动画,但这里我们先不讨论它们,先看看在实现了一个动画后,如何去创建和操纵它,这也就引入了动画组件。

获取追踪状态

在追踪器使用过程中,开发者往往会希望实时监测追踪状态,包括追踪中、追踪到、出错等,并进行相应的处理。微信小程序提供了两个事件来实现这个需求,其中ar-tracker-switch比较简单,仅仅在追踪到/追踪中切换,而ar-tracker-state则提供了更加详尽的信息:

ar-tracker-state事件从基础库2.29.1开始支持。

<xr-ar-tracker id="ar-tracker" mode="Marker" src="{{markerImg}}" bind:ar-tracker-state="handleARTrackerState">
  <xr-gltf model="gltf" />
</xr-ar-tracker>

绑定后编写逻辑:

handleARTrackerState({detail}) {
  // 事件的值即为`ARTracker`实例
  const tracker = detail.value;
  // 获取当前状态和错误信息
  const {state, errorMessage} = tracker;
}

以上state的类型为EARTrackerState,当状态为EARTrackerState.Error时,可以从errorMessage获取详细错误信息。

但这里也要注意,由于某些使用时序的问题,很多场景下开发者需要自己去确定ARTracker的初始状态,比如在ARSystem的ar-ready事件中通过id获取ARTracker的引用,然后判定初始状态:

handleARReady({detail}) {
  const xrFrameSystem = wx.getXrFrameSystem();
  const tracker = this.scene.getElementById('ar-tracker').getComponent(xrFrameSystem.ARTracker);
  // 初始状态
  const {state, errorMessage} = tracker;
  // 绑定事件
  tracker.el.event.add('ar-tracker-state', tracker => {
    const {state, errorMessage} = tracker;
  });
}

Hand

从基础库2.28.1开始支持。

手部识别模式,会通过图像算法识别出人手部的特征点,然后变换到3D空间,可用于一些手势等场景。与Face模式用法一致,但多出了两个参数:

// 获取手势姿态
const gesture = tracker.gesture;
// 获取总体置信度
const score = tracker.score;

特征点定义如下:

手势姿态(0~18,-1为无效):

Body

从基础库2.28.1开始支持。

肢体识别模式,会通过图像算法识别出人躯干的特征点,然后变换到3D空间,可用于一些换装、游戏等场景,与Face模式用法一致:

特征点定义如下:

Face

从基础库2.28.1开始支持。
前置相机依赖于客户端版本8.0.31

人脸识别模式,会通过图像算法识别出人面部的特征点,然后变换到3D空间,可用于一些换装应用等场景,比起上面几种模式,它多出了一个参数和一些方法:

<xr-ar-tracker mode="Face" auto-sync="-1 105 104 45 98">
  <xr-mesh name="face" geometry="cube" scale="0.7 0.8 0.1" uniforms="u_baseColorFactor:1 1 1 0.5" states="renderQueue:2500,alphaMode:BLEND"/>
  <xr-mesh name="eyeL" geometry="cube" scale="0.1 0.1 0.1" uniforms="u_baseColorFactor:0 1 0 1" />
  <xr-mesh name="eyeR" geometry="cube" scale="0.1 0.1 0.1" uniforms="u_baseColorFactor:0 1 0 1" />
  <xr-mesh name="nose" geometry="cube" scale="0.1 0.1 0.1" uniforms="u_baseColorFactor:0 0 1 1" />
  <xr-mesh name="mouth" geometry="cube" scale="0.1 0.1 0.1" uniforms="u_baseColorFactor:1 0 0 1" />
</xr-ar-tracker>

auto-sync属性是一个数字数组,用于将对应顺序的子节点绑定到某个特征点上,其中-1表示忽略该节点,在运行过程中会自动同步变换信息,如示例:

与此同时,开发者还可以按自己的需求更加灵活地控制:

const tracker = el.getComponent(xrFrameSystem.ARTracker);

// 视情况需要自己同步`tracker`的`scale`和`rotation`特定节点。
// 第一个参数是特征点编好,第二个是可选的复用结果,第三个是可选的是否相对于`ARTracker`。
// 为`false`为世界空间的位置,需要配合`scale`自己使用
const position = tracker.getPosition(98, new xrSystem.Vector3(), false);

特征点定义如下:

threeDof

从基础库 2.30.4 开始支持。

由于Plane模式在一些安卓机下不稳定,针对不需要用户移动而只需要旋转设备的应用,我们提供了threeDof模式。这种模式下只需要修改ARSystem的modes即可,不需要添加ARTracker。

微信小程序 PlaneMarker

从基础库 v2.33.1 开始支持。

ARsystem 识别模式 需支持 Plane Marker 模式,且开启使用。

平面识别模式下,使用 2D Marker 识别。

使用方法,在开启对应模式后,直接使用 2d Marker Tracker 的使用方式即可。

ar-tracker识别到目标后,会同步算法侧得到的,marker识别出来的,转化为相对于 平面模式世界空间的矩阵信息,到对应的 Tracker 节点。

该模式下,允许 同时识别多个 2D Marker。

目前版本,该模式仅适用于静态物体,识别物体更新频率相对较慢。每 3s,未明显移动会更新一下位置,每 7s 会进行重新检测。

案例代码片段 链接

<xr-ar-tracker mode="Marker" src="https://mmbizwxaminiprogram-1258344707.cos.ap-guangzhou.myqcloud.com/xr-frame/demo/marker/2dmarker-test.jpg">
  <xr-gltf model="butterfly" anim-autoplay position="0.2 0 -0.2" scale="0.6 0.6 0.6" rotation="0 -50 0" />
  <xr-gltf model="butterfly" anim-autoplay position="0.4 0 0.3" scale="0.5 0.5 0.5" rotation="0 -50 0" />
  <xr-gltf model="butterfly" anim-autoplay position="-0.3 0 0.3" scale="0.4 0.4 0.4" rotation="0 -50 0" />
</xr-ar-tracker>

飞机

平面识别模式,不依靠于src和image。

微信小程序的AR系统会标定出一个平面,相机组件开启 isARCamera 之后,相机的三维变换会与AR系统同步。

AR系统每帧会去计算这个平面的交点,之后同步到AR追踪器组件所在元素上,其所有的子节点继而受到影响。

由于这种模式的灵活度比较高,所以我们还给AR系统提供了几个接口供开发者使用:

// 传入某个节点的`nodeId`
scene.ar.placeHere(nodeId);

// 也可以传入元素引用,关闭`switchVisible`
scene.ar.placeHere(element, false);

// resetPlane
scene.ar.resetPlane();

placeHere方法接受一个节点的node-id或者元素引用,将当前和平面的焦点(即追踪器的3D变换)同步到这个节点上。第二个参数switchVisible则会在执行同步后自动将这个节点的visible设置为true。

resetPlane方法则是重置平面的标定。