网格

网格Mesh是一种组件,严格来讲应该称为网格渲染器,但出于精简就这么命名了。其作用是组织起几何数据和材质实现渲染的载体。

创建一个网格

网格一般在xml中创建,可以直接以组件模式使用或者是用代理的封装元素xr-mesh,此封装元素派生自节点。

组件的方式:

<xr-node mesh="geometry:cube;material:mat" />

元素的模式:

<xr-mesh geometry="cube" uniforms="u_baseColorMap:waifu" states="cullOn:false" cast-shadow receive-shadow />

以上案例我们可以看到网格的所有数据,其中geometry是必传的,值为注册过的几何数据的资源id。material是创建过的材质的资源id,如果不写则会默认使用一个标准的使用了standard效果的材质。

uniforms和states比较特殊,它们其实是材质的数据,但为了方便开发者使用,不去给每一个网格都创建一个材质,所以我们提供了这样的一种方式。在这种方式下,会自动拷贝一份材质,对材质的修改不会影响到原始材质,同时对原始材质的修改也不会影响到这个网格,这点一定要注意!

最后的castShadow和receiveShadow是阴影相关的数据,详细可见灯光中相关的部分。

材质

材质 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是会添加到资源系统中的id,effect是已经注册过的效果,这两个参数是必填的。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表示只开启R和A通道写入

效果

几何数据 提供了渲染的原材料,材质 决定了渲染的方式,但在讨论材质之前,我们要讨论一下其基于的效果 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,然后是其能允许的属性properties和images。

定义属性

属性对应后续生成材质的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-system的alpha属性,并且将xr-camera元素上的clear-color的透明通道设置了一个非零的值,还开启了xr-mesh的BLEND混合和设置了baseColor的透明通道。

资源加载元素

在资源系统一章中我们简略提到了几个xml中和资源相关的标签xr-assets、xr-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 />

其他的属性不用多说,但大家可能会对weight和defer比较好奇。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);

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

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

原始加载器

除了后续会提到的各种类型的资源加载器外,为了最灵活应对需求,框架提供了原始加载器RawLoader来加载最原始的数据,其类型为raw,options为{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的组件

接下来我们便可以定义要插入的组件,同样需要指定renderer为xr-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!