画布透明配置

有时候我们需要让画布有一定的透明度,来和画布后面的背景混合,

在基础库版本v2.30.4以及以上支持。

要实现画布透明非常简单,一个比较典型的用法如下:

<xr-scene render-system="alpha:true">
  <xr-mesh node-id="cube" geometry="cube" uniforms="u_baseColorFactor:0.298 0.764 0.85 0.5" states="alphaMode:BLEND" />
  <xr-camera
    id="camera" node-id="cube" position="0 2 2"
    clear-color="0 0 0 0" target="target"
  />
</xr-scene>

这段代码中我们开启了render-systemalpha属性,并且将xr-camera元素上的clear-color的透明通道设置了一个非零的值,还开启了xr-meshBLEND混合和设置了baseColor的透明通道。

资源加载元素

在资源系统一章中我们简略提到了几个xml中和资源相关的标签xr-assetsxr-asset-load等,这一章就来详细介绍一下它们。

xr-asset-load

xr-asset-load是元素XRAssetLoad在xml中的对应,而这个元素则是组件AssetLoad的一个简单代理。

注意xr-asset-load标签一旦初始化,后续不可动态修改,并且即使移除后再次添加相同asset-id的资源,原先已经使用了此id的组件也不会更新。如果真的有高级需求,请使用脚本的方式。

若要在xml中使用时将其所有的参数写出,大概如下:

<xr-asset-load type="texture" asset-id="waifu" src="/assets/waifu.png" weight="2" options="anisoLevel:2" defer />

其他的属性不用多说,但大家可能会对weightdefer比较好奇。defer是指延迟加载,只有框架发现这个资源被组件应用了才会去真的加载它,而weight,则涉及到下一个要介绍的元素了。

2.28.1版本后,内置资源同样可以使用xr-asset-load配合下面的xr-assets元素获取加载进度,只需要写好类型和资源ID即可:

<xr-asset-load type="env-data" asset-id="xr-frame-team-workspace-day" />

xr-assets

xr-assets对应于元素XRAssets,其是作为组件Assets的一个代理。

这个元素的功能非常简单,一般使用时我们会让它将xr-asset-load元素包裹起来:

<xr-assets bind:progress="handleAssetsProgress" bind:loaded="handleAssetsLoaded">
  <xr-asset-load type="texture" asset-id="waifu" src="/assets/waifu.png" weight="2" />
  <xr-asset-load type="texture" asset-id="waifu2" src="/assets/waifu2.png" weight="1" />
</xr-assets>

可见,其实它本质上就是资源组,是为了在其下资源的加载过程中提供给开发者一些事件,来通知加载进度的。

事件

资源组组件为元素提供了以下事件:

事件 参数 立即 wxml 时机
progress 对象,进度progress,和当前资源描述asset 场景第一次解析完毕
loaded 对象,成功的资源assets,和出的错误errors 场景销毁之前

资源加载器

xr-frame允许开发者定制资源加载器,来添加自己所需的资源类型。所有的资源加载器都需要派生自AssetLoader类,然后使用上一章的方法在xml中或者手动使用。

在基础库版本v2.29.2以上,支持自定义资源加载器。

以一个加载器为例

让我们以内置的纹理加载器为例,

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

// 指定纹理可接受的额外参数
export interface ITextureLoaderOptions {
  anisoLevel?: number;
}

// 加载纹理时会被传入的数据
type ITextureLoadData = XrFrame.IAssetLoadData<ITextureLoaderOptions>;

// 定制加载器
export default class TextureLoader extends xrFrameSystem.AssetLoader<XrFrame.Texture, ITextureLoaderOptions> {
  // 指定加载器参数的`schema`,和**数据解析**一章中的数据类型一致
  public readonly schema: ILoaderOptionsSchema = {
    anisoLevel: {type: 'number', defaultValue: 1},
  };

  // 当纹理资源加载时会调用这个方法
  public load(
    params: ITextureLoadData,
    callbacks: {
      // 开发者需要在加载进度更新时调用的回调
      onLoading(progress: number): void;
      // 开发者需要在加载完成时调用的回调
      onLoaded(value: Kanata.Texture): void;
      // 开发者需要在加载出错时调用的回调
      onError(error: Error): void;
    }
  ): void {
    const {options} = params;

    // 这里可以拿到当前场景`scene`的引用
    const img = this.scene.createImage();

    img.onload = () => {
      const texture = this.scene.createTexture({
        source: [img],
        width: img.width,
        height: img.height,
        anisoLevel: options.anisoLevel
      });

      callbacks.onLoaded(texture);
    }

    img.onerror = (error) => {
      callbacks.onError(error);
    }

    img.src = params.src;
  }

  // 返回一个当前加载器指定类型资源的**默认资源列表**,这些资源可以直接被组件引用,但它们都是`defer`的,只有在用到的时候才会去加载。
  public getBuiltin() {
    return [
      {
        assetId: 'brdf-lut',
        src: 'https://mmbizwxaminiprogram-1258344707.cos.ap-guangzhou.myqcloud.com/xr-frame/brdflut.png',
        options: {}
      }
    ];
  }

  // 某个资源被取消加载时会调用,记得一定要先调用父级的方法
  public cancel(params: ITextureLoadData) {
    super.cancel();
  }

  // 某个资源被释放时会调用,这里可以执行释放操作
  public release(params: ITextureLoadData, value: XrFrame.Texture) {
    value.destroy();
  }
}

// 注册加载器到框架,资源类型为`texture`
xrFrameSystem.registerAssetLoader('texture', TextureLoader);

通过这个纹理加载器我们可以看到,资源系统是通过加载器的loadcancelrelease三个方法来管理整个资源的生命周期的。

最后将加载器注册为某种类型后,这种资源类型将会同时被注册进组件数据解析器,在schema中定义使用。

原始加载器

除了后续会提到的各种类型的资源加载器外,为了最灵活应对需求,框架提供了原始加载器RawLoader来加载最原始的数据,其类型为rawoptions{encoding: 'binary' | 'utl-8'},默认是二进制。

资源系统

资源是一类特殊的对象,其存储着微信小程序框架需要使用的数据,比如纹理、几何数据、材质等等。资源系统管理着整个场景的资源,其包括自身和资源注册、资源加载器、资源元素三部分。

资源系统在运行时可以通过scene.assets获取,可在手动使用中看到具体的用法。

推荐用法

推荐的用法是在xml中用对应的几个标签定义资源,然后在特定的组件中使用资源的asset-id引用:

<xr-assets>
  <xr-asset-load type="texture" asset-id="waifu" src="/assets/textures/waifu.jpg" />
  <xr-asset-material asset-id="standard-mat" effect="standard" />
</xr-assets>

<xr-mesh node-id="waifu" geometry="plane" uniforms="u_baseColorMap: waifu" />

注意这种用法有一定的限制,具体标签和组件的说明,可见资源元素。

需要注意资源加载都是异步的,组件引用资源时会根据资源加载的时机来执行onAdd或者onUpdate周期,所以可能存在资源尚未准备好就开始渲染之类的状况,需要开发者用资源元素中介绍的XRAssets元素的事件回调酌情自行处理。

手动使用

对于高级用户,有时候会想手动加载、获取或者释放资源,资源系统暴露了一系列接口来满足这样的需求:

// 加载资源
const {value: tex1} = await scene.assets.loadAsset({type: 'texture', assetId: 'tex1', src: texUrl1});
scene.assets.loadAsset({type: 'texture', assetId: 'tex2', src: texUrl2});

// 手动获取加载过的资源,最后一个参数是`fallback`资源,作为候补
const tex2 = scene.assets.getAsset('texture', 'tex1', 'white');

// 手动获取加载过的资源和状态
const {value: tex2, state, promise} = scene.assets.getAssetWithState('texture', 'tex1', 'white');

// 手动添加一个资源
scene.assets.addAsset('texture', 'tex3', tex3);

// 取消加载资源
scene.assets.cancelAsset('texture', 'tex2');

// 释放加载过的资源
scene.assets.releaseAsset('texture', 'tex1');

资源注册

除了使用加载机制来添加资源,还有一套额外的机制,就是资源注册。资源注册允许我们为某个id的资源提供创建其实例的一个回调,当有组件引用到这个资源时,便会执行回调去创建它,这个方法只会执行一次:

registerTexture('white', (scene: Scene) => scene.createTexture({
  source: [new Uint8Array([255, 255, 255, 255])],
  width: 1,
  height: 1
}));

这个例子就是注册了一个white的纹理资源。目前微信小程序框架提供的可用资源注册方法会在各个资源的文档(比如纹理)中单独论述。

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的事件绑定,详见事件。