Slot

在某些场合,我们希望使用和微信小程序UI组件一致的slot能力来做一些灵活的封装,解决复用。xr-frame同样支持slot,但在具体的用法上用一些限制,下面就让我们通过一个例子教大家使用。

定义包含slot的组件

首先我们需要定义一个能插入slot的组件,这个传统微信小程序区别不大,但注意组件配置的renderer一定要是xr-frame

<xr-scene bind:ready="handleReady">
  <xr-light type="ambient" color="1 1 1" intensity="1" />
  <xr-light type="directional" rotation="40 70 0" color="1 1 1" intensity="3" cast-shadow />

  <slot></slot>

  <xr-node node-id="target"></xr-node>
  <xr-camera node-id="camera" clear-color="0.4 0.8 0.6 1" position="0 0 4" target="target" />
</xr-scene>

定义要插入slot的组件

接下来我们便可以定义要插入的组件,同样需要指定rendererxr-frame,但注意因为是作为slot并且受到一个页面只能存在一个xr-scene的约束,所以我们不能将此组件的根节点定义为xr-scene

<xr-node>
  <xr-mesh geometry="cube" scale="0.5 0.5 0.5" position="1 0 0" />
  <xr-mesh geometry="sphere" scale="0.5 0.5 0.5" position="-1 0 0" />
</xr-node>

这里我定义了两个mesh,准备将其插入到第一个组件定义的场景中。

用一个xr组件使用二者

这一步是和传统微信小程序差别最大的,我们不能直接在page中使用slot,而是需要再新建一个xr-frame组件将二者包装起来。

首先创建一个xr-frame组件,定义其json配置,这里一定要注意组件的名字不能是xr-类型的,否则会走到原生组件。这里命名xrTest为定义了slot的组件,xrSlot是要插入的组件:

{
  "component": true,
  "renderer": "xr-frame",
  "usingComponents": {
    "xrTest": "../../components/xr-test/index",
    "xrSlot": "../../components/xr-slot/index"
  }
}

然后在xml中使用它:

<xrTest>
    <xrSlot />
</xrTest>

当然,你也可以不将要插入到slot中的内容定义为组件而是直接写xr-frame原生组件,比如:

<xrTest>
    <xr-node>
      <xr-mesh geometry="cube" scale="0.5 0.5 0.5" position="1 0 0" />
      <xr-mesh geometry="sphere" scale="0.5 0.5 0.5" position="-1 0 0" />
    </xr-node>
</xrTest>

在页面中使用这个xr组件

最后一步就是使用最顶层的这个组件了,正常引入使用即可(这里命名为xr-test-slot):

<view>
  <xr-test-slot
    disable-scroll
    id="main-frame"
    width="{{renderWidth}}"
    height="{{renderHeight}}"
    style="width:{{width}}px;height:{{height}}px;"
  />
</view>

Shadow元素

有时候我们需要用代码动态创建元素之后添加到场景中,这个需求和wxml写标签这种静态的模板编译方式是冲突的,为了保证DOM树不混乱,我们提供了类似于HTML中的ShadowRoot的XRShadow元素,对应于xml中的xr-shadow标签,来解决这个问题。

一个例子

让我们以一个例子来说明如何使用Shadow元素,首先在xml中定义:

<xr-shadow id="shadow" position="0 1 0" />

之后便可以在代码中使用:

const shadow = scene.getElementById('shadow');
const node = scene.createElement(xrFrameSystem.XRNode);

// 添加创建的节点到`shadow`节点下
shadow.addChild(node);

// 移除创建的节点
shadow.removeChild(node);

特别注意

在实际使用中我们有一些特别要注意的地方:

  1. Shadow元素派生自节点元素,拥有3D变换的能力。
  2. wxml中,Shadow元素下绝对不能有子元素!!!当然如果这么做了框架会报错。
  3. 动态创建的元素仅能添加为Shadow元素的子/孙元素,这个框架不会检查,出了问题后果自负。
  4. Shadow元素以及子元素的release需要自己处理,remove后能够继续复用重新add

可见性与图层

对于节点和派生自节点的元素(也就是挂载了Transform组件的元素),都有visiblelayer两个属性可选。这两个属性用于控制节点自身以及其子孙结点的可见性

当我们需要一个开关直截了当地让某个节点之下的所有模型或者灯光都显示或者不显示,最方便的方法就是使用visible

<xr-node visible="false">
  <xr-mesh visible geometry="cube" />
</xr-node>

比如这个例子,所有节点的visible默认都是true,但当父节点设置为false时,即便子节点显式定义了true,也会被隐藏。

除了这种场景,在另外一些场景我们还会有更复杂的需求,比如只是想针对性地剔除掉某一类模型或是灯光,而非按照层级结构,这时候图层layer就派上用场了,让我们来看个例子:

<xr-node layer="1">
  <xr-mesh geometry="sphere" uniforms="u_baseColorFactor:0.937 0.176 0.368 1" />
  <xr-node layer="2">
    <xr-mesh geometry="cylinder" uniforms="u_baseColorFactor:1 0.776 0.364 1" />
  </xr-node>
</xr-node>
<xr-camera
  position="0 1.6 0" clear-color="0.925 0.925 0.925 1"
  cull-mask="0b01"
/>

对于两个xr-node节点,我们给第一层指定了layer=1,第二层指定了layer=2,然后将xr-camera节点的cull-mask属性(对应于Camera组件的cullMask数据)设置为0b01。如此设置的效果应该是第一个mesh被渲染,第二个不被渲染。

开发者看到这里应该不难发现图层的规则:layer可以设置1~32共32个值,而相机的cullMask是一个32位无符号整数,每一位都代表一个layer。只有当一个节点从顶层到自身路径中的所有节点都通过了这个mask的测试,才会被显示出来。这也适用于灯光Light组件。

场景

场景Scene是一种特殊的元素,对于所有的xr-frame 微信小程序组件,其最外层必须有一个xr-scene标签作为根元素,并且组件内只能有一个,以此作为整个组件的基础。

和一般仅用于组合的元素不同,场景有以下几个特别之处:

  1. 其下挂载的组件都是名为系统System的特殊组件,来驱动逻辑和渲染。
  2. 增加了一些方法,用于创建资源、查询等等。
  3. 每个元素、组件、资源都是属于当前被包在的xr-scene标签对应的元素中的。

在我们编写组件时,常常用到的那个this.scene,其实就是这个元素。

用途

一般我们需要用到的是场景的创建和查询能力。

创建资源可以使用类似:

// 脱离于资源加载工作流,创建属于这个场景的纹理资源
const texture = scene.createTexture(options);

// 直接创建一个元素,但注意要和`Shadow元素`一起使用
const el = scene.createElement(xrFrameSystem.XRNode);

的方式,更多的创建可见API文档。

同样在API文档中我们还可以看到有一些查询接口,比如上一节提到的scene.getElementById,以及scene.width这种获取画布信息的,scene.assets这种获取系统实例引用的,开发者可以按需使用。

获取场景实例

要使用场景需要先获取实例,在组件、加载器等中,我们可以直接用this.scene获取,而在用户微信小程序脚本中,则需要通过事件:

<xr-scene bind:ready="handleReady">
......
</xr-scene>
// 微信小程序脚本中绑定的事件
handleReady({detail}) {
  const scene = detail.value;
}

事件

场景元素提供了以下事件:

事件 参数 立即 wxml 时机
ready 场景第一次解析完毕
tick 数字,delta(ms) 一帧驱动开始
pause 场景暂停,一般是是压后台
resume 场景恢复,一般是从后台唤醒

事件

事件管理器从属于元素,提供给组件的设计者一个向使用者派发事件的手段。

事件有两种使用方式,一种是走传统的脚本逻辑,一种是用微信小程序wxml中的事件绑定。

脚本逻辑

在脚本中使用事件很简单,用一段代码例子来解释:

// 定义监听器,比如这个事件的参数类型是`number`
function handleTest(params: number, sender: XrFrame.Element) {
  console.log('test', params, sender);
}

// 添加事件监听器
el.event.add('test', handleTest);

// 移除事件监听器
el.event.remove('test', handleTest);

// 添加事件监听器,但会在一次触发后自动移除
el.event.addOnce('test', handleTest);

wxml绑定

wxml中进行事件绑定会稍微复杂一些,还是来看个例子:

<xr-scene bind:tick="handleTick">
</xr-scene>

这里我们给xr-scene标签绑定了tick事件,事件的handler被定义在组件的method内,和其他的微信小程序组件一致。

这里要注意的是如此绑定的事件获取事件参数的方法不同:

handleTick: function(event) {
  const {value, el} = event.detail;
}

这里取出的value就是事件的参数,el则是派发事件的那个元素。

派发事件

知道了如何监听事件,我们还需要知道如何派发,依照以上两种监听方式,事件的触发有不同参数:

el.event.trigger(type, event, immediately, toXML, bubbles);

immediately是指是否要立即触发,默认是true,如果为false,则会对当帧同名的事件进行合并,然后在当帧组件生命周期的驱动前派发。

toXML是说这个事件是否要派发到xml上,来允许开发者使用事件绑定,默认是true

bubbles是指事件是否要沿着xr-frame的节点树冒泡,同时也会影响在xml中的冒泡。

注意在Shadow元素的子孙元素中,如果要让事件能够在xml中被绑定,bubbles必须为true

元素

元素Element本身没有逻辑,其主要负责两部分——组件的聚合和属性代理。所以在阅读以下内容之前,请保证先阅读了组件的相关内容。

元素有两种使用方式,在xml中直接对应于标签,也可以在脚本中动态创建,但注意动态创建的时候,必须作为Shadow元素的子孙结点!!!

定制一个元素

让我们从定制一个元素开始,理解它的功能:

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

// 指定默认挂载的组件
const AutoRotateTouchableGLTFDefaultComponents: XrFrame.IEntityComponents = Object.assign({
  'mesh-shape': {},
  'auto-rotate': {}
}, xrFrameSystem.GLTFDefaultComponents);

// 指定一个映射,将`xml`上特定的属性,映射为组件的数据
const AutoRotateTouchableGLTFDataMapping: {[key: string]: string[];} = Object.assign({
  speed: ['auto-rotate', 'speed']
}, xrFrameSystem.GLTFDataMapping);

// 定义元素类
class XRAutoRotateTouchableGLTF extends xrFrameSystem.Element {
  // 指定默认组件集
  public readonly defaultComponents: XrFrame.IEntityComponents = AutoRotateTouchableGLTFDefaultComponents;
  // 指定默认数据映射
  public readonly dataMapping: {[key: string]: string[];} = AutoRotateTouchableGLTFDataMapping;
}

// 注册元素实现`XRAutoRotateTouchableGLTF`为名字`auto-rotate-touchable-gltf`
xrFrameSystem.registerElement('auto-rotate-touchable-gltf', XRAutoRotateTouchableGLTF);

在这个自定义元素中,我们首先指定了它初始化时就要挂载的组件,这其中有两部分,第一部分是xrFrameSystem.GLTFDefaultComponents,这其实就是xr-gltf组件默认挂载的组件集合,之后在其上追加了mesh-shape和上一节定制的auto-rotate组件。

其次我们指定了映射,还是分为了两部分,第一部分是xrFrameSystem.GLTFDataMapping,第二部分是追加的speed。来看看speed的值,数组第一个值为auto-rotate,就是挂载在这个元素上的组件名字,第二个值是speed,就是说要将写在标签上的speed属性直接映射到auto-rotate组件的数据speed上。

注意,所有的内置组件的映射,都是会将如isARCamera这样的驼峰,映射为is-ar-camera这样的小写加中划线分割的形式。 从这里我们可以看出,元素确实是用组合而非继承的方式实现的功能。最后我们继承自Element实现了元素,并用registerElement将其注册到了框架中。

使用这个元素

和组件一样,使用元素的方式同样有两种:

在wxml使用

首先是在wxml中使用,只需要按照注册时的名字使用就好:

<xr-auto-rotate-touchable-gltf id="artg" position="-2 0 0" model="gltf-damageHelmet" speed="1 0 0" bind:drag-shape="handleDrag" bind:touch-shape="handleTouchStart" bind:untouch-shape="handleTouchEnd" />

可以看到,这里确实直接写了speed属性。

组件的属性写法和数据映射是可以共存的,同时数据映射并不先要求组件存在。

手动使用

我们也可以在运行时来代码创建元素和添加到场景,但注意绝对不能将其添加到Shadow元素以外的元素!!!

为了避免开发者失误,创建和添加详见Shadow元素。

const node = scene.createElement(xrFrameSystem.XRNode, {
  position: '1 1 1'
});

node.setAttribute(position, '2 2 2');

这段代码通过createElement方法创建了一个元素,注意第二个初始化参数传入的是属性,其完全对应于标签上的名字,值也是字符串,接下来我们还通过setAttribute方法更新了其属性。

但建议不要使用setAttribute去更新属性,而是先获取组件,然后用组件中的setData方法来设置,省去字符串解析!

查找

有时候我们需要获得元素的实例,来进一步进行其他操作,以下几个方法可以帮助我们拿到它:

// 通过在`wxml`的元素上设置的`id`索引,比如上面例子中的`artg`,`id`是唯一的
scene.getElementById(id);

// 获取元素的第`i`个子元素
el.getChildAtIndex(i);

// 通过一个`filter`函数,获取第一个子元素或者子元素集
el.getChildByFilter(filter);
el.getChildrenByFilter(filter);

// 通过类获取子元素
el.getChildByClass(clz);

// 通过在`wxml`元素上设置的名字`name`获取子元素,`name`可以不唯一
el.getChildName(name);
el.getChildrenName(name);

事件

每个元素上都有一个事件管理器,来管理其下挂载的所有组件的事件派发,这个派发也可以触达到wxml的事件绑定,详见事件。

组件数据解析

在组件一章我们知道需要有一个解析器将xml中组件对应属性的字符串转换为组件需要的数据类型,对此,微信小程序框架提供了一套机制来处理。

数据解析器

数据解析器IDataValueHandler就是用来将属性字符串转换成特定类型的数据的,其具体定义如下:

interface IDataValueHandler<TDataValue> {
  create(value: string, defaultValue: any, scene: Scene): TDataValue;
}

可见主要是一个create方法,其接受一个字符串的value,一个组件schema中定义的默认值defaultValue和场景引用scene,返回解析后的值。一般我们会如此注册一个解析器:

registerDataValue('number', {create: (value: string, defaultValue: any, scene: Scene) => {
  return value === undefined ? defaultValue : parseFloat(value));
}});

这里注册了number类型的解析器,可以看到如果没有传入值则返回默认值,否则用parseFloat转换。

除了registerDataValue方式注册的数据类型外,还有一类特殊类型的数据,它们就是资源

特殊类型-资源

资源数据和number这样的普通数据有些不同,其注册一般不使用registerDataValue,而是在registerAssetLoader时定义的。如何定义可以参考资源加载器的内容,一般来讲资源数据使用时填写的值都是资源ID,比如你要使用纹理资源:

<xr-asset-load type="texture" asset-id="waifu" src="/assets/textures/waifu.jpg" />
<xr-asset-material asset-id="simple-mat" uniforms="u_baseColorMap: waifu" />

我们先加载了一张id为waifu的纹理,然后在下面材质的uniform的u_baseColorMap属性使用了它。

资源类型的数据在schema中描述时,默认值是资源的id 注意资源类型的数据还会影响到组件的onAddonUpdate生命周期,微信小程序框架会在每一次资源数据更新后,先等待引用的资源加载完成,才会进入这两个生命周期

内置数据类型

微信小程序框架内置了一些数据类型:

非资源数据

类型 例子 转换 说明
string henshin: KuugaAgitoRyuki FaziBladeHibikiKabutoDenOkiva DecadeWOOOFourzeWizardGaim DriveGhostExaidbuildGrandZio ‘KuugaAgitoRyuki FaziBladeHibikiKabutoDenOkiva DecadeWOOOFourzeWizardGaim DriveGhostExaidbuildGrandZio’ 字符串,不作任何处理
number truth:42 42 数字,会转成float,支持其他进制如0xff0b11
boolean yiyandingzhen:false false 布尔,当写’false’时为false,否则均为true(包括不写值)
array producer:wowaka neru kurogaki [‘wowaka’,’neru’,’kurogaki’] 字符串数组,用空格分割
number-array idolOffice:7 6 5 [7,6,5] 数字数组,用空格分割
color rem:0.57 0.75 1 1 [0.57,0.75,1,1] 颜色,rgba,也可以使用#fff这种方式
map cat:10,dog:8,fox:6 [[‘cat’,10],[‘dog’,8],[‘fox’,6]] 映射形式是key:value,用,分割
dict camp:瞬光:混乱善良,roam:中立善良,xinyi:绝对中立 {瞬光:’混乱善良’,roam:’中立善良’,xinyi:’绝对中立’} 字典,和map类型写起来一样,转换结果不同
transform target:homo nodeId为homo的Transform组件引用 变换,可以用于索引标记过nodeId的变换组件

资源数据

资源数据可见内置的各种资源:效果,图片,纹理,材质,几何数据,模型,渲染目标,帧动画。

组件

组件Component用于实现xr-frame中所有的逻辑,以生命周期拉驱动。它们在wxml中对应于每个标签上的属性,比如<xr-element transform="position: 1 1 1;" />,就是在xr-element标签上挂在了一个transform组件。组件的聚合构成了后面章节的元素,也就是wxml中对应的标签。

组件的构成主要分为两部分——数据定义和生命周期。

让我们定制一个组件

通过定制一个简单的组件,我们可以对组件的机制有比较清晰的了解:

// 这里只包括类型信息
import XrFrame from 'XrFrame';
// 这里是实例
const xrFrameSystem = wx.getXrFrameSystem();

// 自定义组件的数据接口
export interface IAutoRotateData {
  speed: number[];
}

// 自定义组件的`schema`,详见后面论述
const AutoRotateSchema: XrFrame.IComponentSchema = {
  speed: {type: 'number-array', defaultValue: [1, 1, 1]}
}

// 定义组件
class AutoRotate extends xrFrameSystem.Component<IAutoRotateData> {
  public readonly schema: XrFrame.IComponentSchema = AutoRotateSchema;

  private _speedX: number = 1;
  private _speedY: number = 1;
  private _speedZ: number = 1;

  // 所挂载的`element`被挂载到场景时触发的回调。
  public onAdd(parent: XrFrame.Element, data: IAutoRotateData): void {
    this._processData(data);
  }

  // 数据更新时触发的回调。
  public onUpdate(data: IAutoRotateData, preData: IAutoRotateData): void {
    this._processData(data);
  }

  // 渲染每帧触发的回调。
  public onTick(delta: number, data: IAutoRotateData) {
    const trs = this.el.getComponent(xrFrameSystem.Transform);

    // 如果没有挂载到一个`Node`节点,则不生效。
    if (!trs) {
      return;
    }

    // 其实这里实现有点问题,不过是例子也无伤大雅了(逃
    trs.rotation.x += this._speedX * 0.1;
    trs.rotation.y += this._speedY * 0.1;
    trs.rotation.z += this._speedZ * 0.1;
  }

  // 所挂载的`element`从父节点`parent`被移除时,或者自己从`element`上呗移除时,触发的回调。
  // 一般用于消除功能的运作。
  public onRemove(parent: XrFrame.Element, data: IAutoRotateData): void {

  }

  // 从被挂载的`element`上被移除,或是`element`被销毁时,触发的回调。
  // 一般用于释放持有的资源。
  public onRelease(data: IAutoRotateData) {

  }

  private _processData(data: IAutoRotateData) {
    this._speedX = data.speed?.[0] !== undefined ? data.speed[0] : 1;
    this._speedY = data.speed?.[1] !== undefined ? data.speed[1] : 1;
    this._speedZ = data.speed?.[2] !== undefined ? data.speed[2] : 1;
  }
}

// 最后将组件注册进框架,名为`auto-rotate`
xrFrameSystem.registerComponent('auto-rotate', AutoRotate);

通过以上代码,我们定制了一个为挂载的元素提供旋转功能的组件。

首先可以看到的是IAutoRotateDataAutoRotateSchema,接口是为了通过泛型给定义的组件提供类型推断,而真正作用于运行时逻辑的,则是那个schema,其类型定义为:

interface IComponentSchema {
  [key: string]: {type: string, defaultValue?: any}
}

schema中,我们定义了组件每个数据的类型type和默认值defaultValue。这是由于所有组件在xml中对应的属性的值都是字符串,所以需要告诉微信小程序框架如何将这些字符串转换成最终的值,这一部分详见组件数据解析。

在数据定义后,我们继承了xrFrameSystem.Component派生了一个子类,它有一些生命周期,这就是组件具体逻辑实现的载体。我们在onAddonUpdate的时候去更新旋转速度数据,然后在每帧onTick的时候去修改挂载元素的transform组件中的旋转。注意还有onRemoveonRelease两个生命周期,但由于本组件没有额外资源需要处理,所以不需要执行任何操作。

完成了逻辑的实现后,我们最后一步调用了xrFrameSystem.registerComponent将这个组件注册到了微信小程序框架中,并给它了一个名字auto-rotate,接下来就可以正常使用了。

使用这个组件

使用组件的方式有两种:

在wxml中使用

首先是在wxml中使用,我们只需要按照注册的名字和数据类型定义将其写在元素标签上即可:

<xr-node auto-rotate="speed: 2 3 1;" />

如此这个节点便会自动旋转,速度为{x: 2, y: 3, z: 1}

组件还可以通过代理的方式来简化使用,详见元素中的数据代理部分。

手动使用

我们也可以在运行时来手动添加组件或设置它的数据,但注意一定不要和xml中冲突

要手动使用组件,我们需要先拿到元素的实例引用,详见场景中获取元素引用相关的描述,拿到了引用后便可以执行操作:

// 添加组件,并给一个可选的初始值
el.addComponent(AutoRotate, {speed: [2, 3, 1]});

// 获取组件并更新数据,注意`setData`包括`getData`都是通用方法,但有些组件有自己的实现,比如`transform.position`。
el.getComponent(AutoRotate).setData({speed: [1, 1, 1]});

// 手动移除组件
el.removeComponent(AutoRotate);

架构设计

xr-frame使用的架构属于ECS的一种,对应于xml中,Entity就是元素,Component就是属性,System就是场景的顶层组件。这样的设计在严谨的范式下,给开发者提供了灵活的扩展能力,理论上所有的内置功能都可以由开发者在外部自己实现。

ECS

开发者在xml中写的标签,都对应于框架里的元素Element的子类。元素本身不包含逻辑,它的逻辑全部都是被代理到其拥有的组件Component实现的。

而这个组件,在xml中对应的是元素上的属性,比如<xr-element transform="position: 1 1 1;" />,就是说在名为Element的元素上挂载了名为transform的组件。当然为了简化使用,我们还提供了一种属性代理的策略,详见Element的文档。

注意,虽然开发者会发现我们提供了手动给元素添加子节点的功能,但大部分情况下这是非法的!!!当然,出于某些场景的性能和灵活性考虑,我们提供了Shadow Element来满足开发者的需求。 在所有元素的子类中,有一个是最为特殊的,它就是场景Scene,对应于xml中的xr-scene标签。设计上来讲,每个xr-frame微信小程序组件顶层都必须是一个xr-scene元素,其他的元素都必须被包含在其中。这是因为场景负责整个框架的调度、各个系统的挂载以及所有资源的管理。

系统,就是挂载于场景下的各个顶层组件,它们负责每个模块的管理,按照顺序驱动各个模块的运行。目前而言,所有的内置功能由以下几个系统和对应的组件们构成。

逻辑系统

逻辑系统负责驱动所有元素以及组件的生命周期,保证它们按顺序进行,开发者一般只需要关心组件的生命周期即可,详见Component。

节点系统

节点系统负责管理场景中所有3D节点的可见性,详见Node。

渲染系统

渲染系统负责整个场景的渲染,也是最复杂的一个系统,它将Geometry、Effect、Material、Mesh、Texture、Light和Camera等组织起来,送入管线进行渲染。

渲染管线从设计上是可定制的,未来将会逐步开放给开发者。

资源系统

资源系统管理整个场景的资源(注意资源是每个场景专属的),一般通过AssetsAssetsLoad组件以及对应的元素在xml中使用,也提供手动管理的接口。同时还支持开发者自定义Loader来增加新的资源类型。

动画系统

动画系统管理所有的动画,其主要实现是借由每个元素的Animator组件来管理下面的所有AnimationAnimation本身被设计成可扩展的,框架内置了两种Animation来方便开发者简单使用:帧动画Keyframe和gltf动画。

物理系统

物理系统目前仅管理场景中的轮廓,以及触发轮廓点击事件。未来计划加入真实物理效果模拟,包括碰撞体和刚体等。

AR系统

AR系统将xr-frame微信小程序AI系统关联起来,让AR变得十分简单易用。主系统和ARTracker组件协作,让开发者用几行xml就能实现平面跟踪、2D/3DMarker识别等功能。