材质

材质 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的透明通道。