材质

材质 Material 基于 效果 Effect,提供了修改渲染状态、Uniforms 的接口,真正决定了物体最后的渲染方式,体现为物体表面的外观。

创建一个材质

创建材质有两种方式,一般我们会优先在wxml中进行创建。

在wxml中创建

xml中创建需要用到xr-asset-material标签:

<xr-asset-material asset-id="mat" effect="standard" uniforms:"u_baseColorMap: waifu" states="alphaMode:BLEND" renderQueue="2500" />

其中asset-id是会添加到资源系统中的ideffect是已经注册过的效果,这两个参数是必填的。uniforms类型是dict,用于覆盖效果中定义的那些属性,会根据那些属性的type来进行解析,比如Vector2对应1 1这样的数组;states类型也是dict,用于覆盖效果中定义的那些渲染状态(需要效果中开启useMaterialStates);最后的renderQueue用于覆盖效果中的默认渲染顺序,大于等于2500为透明物体。

用代码创建

有时候我们需要用代码创建材质,也很简单:

// 第一个参数是效果实例的引用,第二个参数是默认`uniforms`
const mat = scene.createMaterial(
  // 使用内置的 Standard 效果
  scene.assets.getAsset('effect', 'standard'),
  {u_baseColorMap: scene.assets.getAsset('texture', 'waifu')}
);

// 可以将其添加到资源系统中备用
scene.assets.addAsset('material', 'test-mat', mat);

创建完,接下来就是使用了。

使用材质

材质和其他资源一样,可以通过在组件schema中设定数据类型为material,然后在xml中通过资源id索引,或者用setData直接使用。目前使用了材质的组件只有网格,详见对应章节。

除了被引用外,材质也提供了一些方法来对其进行修改:

// 一系列修改Uniforms的接口
material.setFloat(key, value);
material.setVector(key, vec);
material.setMaterial(key, mat);
material.setTexture(key, texture);

// 修改渲染状态
material.setRenderState(key, value);

// 修改宏
material.setMacros({key: value});

// 重置图片,同时关闭对应的宏
material.resetTexture(key);

// 重置状态
materia.clearRenderState(key);

材质支持渲染状态列表

状态名称 说明 类型
renderQueue 渲染顺序 number
cullOn 是否开启剔除 bool
depthTestOn 是否开启深度测试 bool
depthTestWrite 是否开启深度写入 bool
alphaMode 透明模式 ‘OPAQUE’ ‘BLEND’ ‘MASK’
alphaCutOff 是否开启透明剔除 bool
depthTestComp 深度测试方法 number, ECompareFunc对应的值
stencilTestOn 是否开启模板测试 boolean
stencilComp 模板测试相关 number, ECompareFunc对应的值
stencilRef 模板测试相关 number
stencilReadMask 模板测试相关 number
stencilWriteMask 模板测试相关 number
stencilPass 模板测试相关 number, EStencilOp对应的值
stencilFail 模板测试相关 number, EStencilOp对应的值
stencilZFail 模板测试相关 number, EStencilOp对应的值
colorWrite 颜色通道写入掩码,基础库v2.31.1开始支持 number, 一个4bits的mask,由高到低为ABGR四个通道,比如0b1001表示只开启RA通道写入

效果

几何数据 提供了渲染的原材料,材质 决定了渲染的方式,但在讨论材质之前,我们要讨论一下其基于的效果 Effect。

效果可以认为是一个材质模板,通过光照模式lightMode宏定义definition来对模板的各个功能进行开关。

内置效果

详见 内置效果资源。

定制一个效果

function createSimpleEffect (scene: XRFrame.Scene) {
  return scene.createEffect({
    name: 'simple',
    properties: [
      {
        key: 'u_baseColorFactor',
        type: xrFrameSystem.EUniformType.FLOAT4,
        default: [1, 1, 1, 1]
      }
    ],
    images: [
      {
        key: 'u_baseColorMap',
        default: 'white',
        macro: 'WX_USE_BASECOLORMAP'
      }
    ],
    // 透明物体需要大于`2500`!
    defaultRenderQueue: 2000,
    passes: [
      {
        renderStates: {
          cullOn: true,
          blendOn: false,
          depthWrite: true,
          cullFace: xrFrameSystem.ECullMode.BACK,
        },
        lightMode: 'ForwardBase',
        useMaterialRenderStates: true,
        shaders: [0, 1]
      }
    ],
    shaders: [`#version 100

    attribute vec3 a_position;
    attribute highp vec2 a_texCoord;

    uniform mat4 u_view;
    uniform mat4 u_projection;
    uniform mat4 u_world;

    varying highp vec2 v_uv;

    void main()
    {
      v_uv = a_texCoord;
      gl_Position = u_projection * u_view * u_world * vec4(a_position, 1.);
    }`,
      `#version 100
    precision mediump float;

    uniform highp vec4 u_baseColorFactor;
    #ifdef WX_USE_BASECOLORMAP
      uniform sampler2D u_baseColorMap;
    #endif

    varying highp vec2 v_uv;

    void main()
    {
  #ifdef WX_USE_BASECOLORMAP
      vec4 baseColor = texture2D(u_baseColorMap, v_Uv) * u_baseColorFactor;
  #else
      vec4 baseColor = u_baseColorFactor;
  #endif

      gl_FragData[0] = baseColor;
    }  
      `],
  });
}

在效果的参数中,首先是其名字name,然后是其能允许的属性propertiesimages

定义属性

属性对应后续生成材质的uniforms的定义。每个property都需要一个键key、类型type和对应于类型的默认值,而每个image则不需要type、默认值也是已经注册到了资源系统的纹理的资源id

这里特别要注意在那个marco参数,它定义了当用户修改设置(通过材质的接口)了这个属性后,将会开启的宏,这个宏会直接应用到着色器中。

注意,所有的外部设置的宏都必须以WX_开头 为了让整个渲染正常运作,微信小程序框架内置了一些属性和宏,可见文章末尾的附录。

渲染队列

defaultRenderQueue定义的渲染队列,决定了渲染顺序,将在下一章材质中详细论述。

透明物体需要大于2500

配置Pass

Pass对应的是使用了基于这个效果的材质的网格的一次渲染,这个和相机组件相关,使用哪个pass渲染取决于相机当次绘制指定的lightMode

lightMode是光照模式,目前建议只能使用一个模式ForwardBase。当存在多个pass都使用了同一个光照模式时,将会按顺序将网格渲染多次。

renderStates顾名思义即渲染状态,渲染状态直接对应底层图形API都会提供的一系列概念,比如混合blend、深度depthTest等,比如示例里就定义了blendOn: false,即表明要关掉混合

更多的渲染状态可见API文档IRenderStates,这里不赘述,这里主要要说明一个规则,即useMaterialRenderStates这个参数的作用。

xr-frame框架的设计中,渲染状态是由效果决定默认值,材质决定最终值的,而useMaterialRenderStates则就是用于决定材质是否能够覆盖效果默认值的,如果为false,则总是使用效果提供的默认值,如果为true,则由下面会讲到材质的渲染状态的规则来决定。

在配置完这些之后,就是要定制shaders了。这是一个有两个数字的数组,分别指定了顶点着色器片段着色器在顶层参数中的shaders的索引。

编写着色器

效果配置的顶层参数中最后一项是shaders,这是一个字符串数组,每个元素都是一个完整的着色器。着色器使用标准的glsl1语法,这个不属于教程范畴不再赘述。

我们提供了一些内置可用的着色器的函数,在文章末尾会给出。

注册到资源系统

将效果注册到资源系统可用:

xrFrameSystem.registerEffect('custom', createSimpleEffect);

注册后在schema中类型指定为effect的数据,便可以使用custom这个资源id引用到创建的资源了。

几何数据

渲染的基础之一是几何数据Geometry资源,它描述了一个模型的顶点信息、索引信息以及顶点的存取结构。

一般来讲,几何数据是通过模型中自动加载,或使用 内置Geometry,但有时候我们需要去定制一些程序化生成的数据比如粒子等等,所以还是要明白如何去定制。

定制一个看看

const geometry = scene.createGeometry(
  vertexLayout, vertexBuffer,
  indexBuffer, indexType
)

可见构造一个Geometry需要提供好几个参数,他们是:

VertexLayout

首先是VertexLayout,用于描述顶点布局,举个例子来说:

const layout = new xrFrameSystem.VertexLayout({
  attributes: [
    {
      name: 'a_position',
      format: xrFrameSystem.EVertexFormat.FLOAT2,
      offset: 0,
      usage: xrFrameSystem.EVertexLayoutUsage.POSITION,
    },
    {
      name: 'a_texCoord',
      offset: 8,
      format: xrFrameSystem.EVertexFormat.FLOAT2,
      usage: xrFrameSystem.EVertexLayoutUsage.UV0,
    },
    {
      name: 'a_color',
      format: xrFrameSystem.EVertexFormat.UBYTE4,
      offset: 16,
      usage: xrFrameSystem.EVertexLayoutUsage.COLOR,
    }
  ],
  stride: 20
});

这里定义了一个自定义的VertexLayout,其中有两个参数:

  1. attributes:描述了顶点的结构,即如何GPU将如何理解传入的顶点数据,比如第一个元素,其表示此顶点属性在shader中名字为a_position,格式是FLOAT2,在顶点Buffer中偏移为0,并且用做POSITION
  2. stride:描述了每个顶点所占带宽的字节数。
  3. usage会影响到渲染这些数据时开启的宏,详见内置效果。

Buffer和Type

后面三个参数分别是顶点数据索引数据索引格式,顶点数据按照布局结构存储着整个Geometry的顶点数据,索引数据存储着图元对顶点的索引,而索引格式则是决定了索引数据的存储格式,其可以为UINT16或者UINT32,如果为UINT16,则索引数据中的顶点索引值不得超过65535

设置参数

在创建完一个新的几何数据后,开发者还需要设置一些参数来让它正确得运作起来,主要是SubMesh和包围球/包围盒。

SubMesh

有了数据和布局后,几何数据还需要提供一些信息去让渲染器知道如何使用这些数据,我们提供了叫做SubMesh的抽象,来将Geometry分割为数个部分:

// 添加一个SubMesh,其索引数据长度为`length`个顶点,第一个索引偏移为`offset`
geometry.addSubMesh(length, offset);

// 修改第`subMeshIndex`位置的SubMesh信息
geometry.modifySubMesh(subMeshIndex, length, offset);

这样分割的原因主要是即便是同一个几何数据,也可能拥有不同的材质来处理不同的部分,但是出于综合考虑,目前渲染时几何数据和材质是一一对应的,所以只支持一个SubMesh

包围球和包围盒

对于每个几何数据来说,在相机的剔除阶段,都需要一个包围球/包围盒来决定其是否在可视范围内,开发者可以通过以下方案来设置包围球:

// `center`为球心相对于模型原点的偏移,`radius`是球的半径
geometry.setBoundBall(center, radius);

或是设置包围盒:

// `center`为球心相对于模型原点的偏移,`size`是盒的三维长度,默认会同步更新包围球
geometry.setBoundBox(center, size);

更新数据

在某些场合,开发者还会需要去动态更新顶点或索引数据,比如粒子系统,我们也提供了一些方案来完成这种需求(注意这种更新操作只能在Mesh已经被提交到GPU后使用):

// 从字节偏移`offset`开始,将`buffer`整个更新到顶点数据。
geometry.uploadVertexBuffer(offset, buffer);

// 从字节偏移`offset`开始,将`buffer`整个更新到索引数据。
geometry.uploadIndexBuffer(offset, buffer);

之后便可以使用这个几何数据了。

注册到资源系统

和其他资源一样,我们提供了一套注册机制来让开发者定制在xml中也可以被引用的几何数据:

xrFrameSystem.registerGeometry('custom', scene => {
  return scene.createGeometry(
    vertexLayout, vertexBuffer,
    indexBuffer, indexType
  );
});

注册后在schema中类型指定为geometry的数据,便可以使用custom这个资源id引用到创建的资源了。

画布透明配置

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

在基础库版本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组件。