后处理

后处理一般指一次渲染的最后环节,它接受一个相机的渲染结果,用图像处理算法对结果增强来达到一些效果,比如模糊、辉光、渐晕等等。

xr-frame内置的后处理系统以相机为基础,将后处理资源组装起来,实现最终的效果。

⚠️ 后处理系统在框架内部涉及到渲染管线,目前暂时不接受定制,未来视情况开放。

后处理资源

单个后处理资源对应后处理中的一个步骤,我们可以用以下几种方式来创建它:

在xml中创建

在XML中创建是最常用的使用方式

<xr-asset-post-process asset-id="blur" type="blur" is-hdr data="radius:10" />

可见我们指定了资源的asset-id、后处理类型type、是否为is-hdr和参数data。如此一来在资源系统中将会创建一个类型为blur、开启HDR、参数为{radius:10}的后处理资源blur

在代码中创建

当然,如果有需要我们也可以使用代码创建:

const xrFrameSystem = wx.getXrFrameSystem();

scene.assets.addAsset('post-process', 'vignette', scene.createPostProcess({
  type: 'vignette',
  isHDR: false,
  data: {
    intensity: 0,
    smoothness: 2,
    color: [0 0 0 1]
  }
}));

这里创建了一个类型为vignette的后处理资源。

在相机中配置

有了资源,只需要将它们配置给相机便可以开启后处理效果了:

<xr-camera
  ......
  post-process="blur vignette"
/>

相机元素的post-process属性接受一个字符串数组,数组的每个元素都是后处理资源的id,它们会按照步骤顺序执行。

修改参数

有时候我们想修改后处理资源的参数来调整效果,有两种方式可以实现:

在代码中修改

一个最常用的方法就是在代码中拿到资源引用后修改:

const blur = scene.assets.getAssets('post-process', 'blur');
blur.data.radius = 20;

使用帧动画

另一种方法是使用帧动画,指定某帧的属性为后处理资源:

{
  "keyframe": {
    "blur": {
      "0": {
        "asset-post-process.assetData.radius": 10
      },
      "100": {
        "asset-post-process.assetData.radius": 64
      }
    }
  },
  "animation": {
    "parent": {
      "keyframe": "blur",
      "duration": 4,
      "ease": "linear",
      "loop": -1
    }
  }
}

内置后处理资源

目前框架内置支持的后处理资源类型和对应效果可见:内置后处理资源。

阴影

在前面网格一章中我们提到过阴影,阴影是灯光和这二者的协作结果,只有主平行光能够产生阴影,以如下场景为例:

<xr-mesh node-id="plane" position="0 -0.4 0" scale="5 0.2 5" geometry="cube"  uniforms="u_baseColorFactor:0.48 0.78 0.64 1" receive-shadow
/>
<xr-gltf model="test-gltf" cast-shadow/>
<xr-camera
  position="3 3 3" target="plane" 
/>
<xr-light type="directional" rotation="60 0 0" color="1 1 1" intensity="2.5" cast-shadow />

可见,我们给plane这个xr-mesh开启了receive-shadow去接收阴影,然后开启了xr-gltfcast-shadow来产生阴影,最后开启了主光源的cast-shadow总开关允许灯光产生阴影。

当然,在实际使用中会有一些需要关注的地方:

  1. 支持自己给自己投影,可以同时开启产生和接收阴影。
  2. 可能需要视情况调整shadow-distance来避免阴影投影过大或者过小。
  3. 适当调整shadow-bias来防止自阴影。

宏和uniforms

以下宏或者uniforms会被传递到着色器中:

类型 说明
开启接受阴影 WX_RECEIVE_SHADOW bool

可以直接通过以下方法来获取当前物体阴影:

方法 说明
float shadowCalculation(vec3 posWorld) 传入世界坐标,返回阴影强度

天空盒

在相机的章节里我们提到过天空盒,当camera.background设置为skybox,将使用环境组件所定义的天空盒纹理,其由两个属性定义:

  1. sky-map:天空盒纹理,默认由环境数据提供,但也可以接受用户使用此属性覆盖。可以使用任何纹理,包括视频纹理。
  2. is-sky2d:天空盒渲染模式是否为2D,开启时会渲染一个平铺自适应的2D背景,此时纹理应当是一张2D图。

uniforms和宏

和渲染相关的环境数据均已全局uniform和全局宏的方式传入着色器,它们是:

类型 说明
开启环境漫反射 WX_USE_IBL_DIFFUSE bool
开启环境高光反射 WX_USE_IBL_SPECULAR bool
高光反射mipmaps数量 WX_USE_SPECULAR_MIPMAPS int
高光反射使用RGBD WX_USE_SPECULAR_RGBD bool
类型 uniforms 说明
漫反射系数 u_diffuseSH vec3[9]
漫反射曝光值 u_diffuseExp texture2d
高光反射纹理 u_specularEnvMap texture2d
高光反射曝光值 u_specularExp v
环境和天空盒旋转 u_envRotation float

图集

图集Atlas是一种资源,可以优化渲染流程和资源,在业界中用于精灵动画和UI比较多。它将一些散碎的小图拼接为一张大图,加之偏移数据uvMatrix或者uvST,能有效减少渲染时的纹理数量和切换次数。

目前图集在微信小程序框架中主要配合粒子系统使用。

创建图集

xr-frame使用的图集是 标准,提供了以下几种方式来创建:

工具离线生成,加载器创建

最推荐的方式是使用工具去离线生成图集,比如Shoebox,输出后将描述文件js后缀改为json即可。生成后的图像和描述文件大致为:

之后可以通过加载器加载,我们可以用标签来加载:

<xr-asset-load type="atlas" asset-id="numbers" src="/assets/numbers.json" />

也可以使用代码加载:

scene.assets.loadAsset({type: 'atlas', assetId: 'numbers', src: '/assets/numbers.png'});

代码创建

代码创建

除了加载器,还可以通过代码创建的方式,这里的options详见IAtlasCreationOptions:

// 通过配置创建
const atlas = xrFrameSystem.Atlas.CREATE_FROM_TEXTURE(scene, texture, options);

当然也有完全通过格子创建空图集的方式,详见CREATE_FROM_TEXTURE。

使用图集

加载完的图集提供了几个方法来供开发者在不同场景使用:

// 获取纹理
const texture = atlas.texture;

// 获取UV矩阵,matrix3
const uvMatrix = atlas.getUVMatrix(frameName);

// 获取UV缩放和偏移,[sx, sy, tx, ty]。
const uvST = atlas.getUVST(frameName);

渲染纹理

相机一章中提到了渲染目标renderTarget可以指定为渲染纹理,纹理也提到了渲染纹理可以作为纹理使用。可见,渲染纹理RenderTexture就是连接多个相机渲染之间的桥梁。

一个典型场景就是用渲染纹理来做镜子之类的效果。

创建渲染纹理

创建渲染纹理有两种方式,一般我们会优先在wxml中进行创建。

在wxml中创建

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

<xr-asset-render-texture asset-id="rt" width="2048" height="1024" />

其中rt是资源id,widthheight是渲染纹理的宽高。

手动代码创建

手动代码创建直接使用scene.createRenderTexture即可:

const rt = scene.createRenderTexture({width: 2048, height: 2048});

// 当然,也可以添加到资源系统中
scene.assets.addAsset('render-texture', 'rt', rt);

使用渲染纹理

创建完渲染纹理后便可以使用它,下面是一个在xml中的简单使用实例,用代码使用也是类似的:

<xr-node layer="1">
  <xr-mesh id="cube" node-id="mesh-cube" position="-1 0.5 1.5" scale="1 1 1" rotation="0 45 0" geometry="cube" uniforms="u_baseColorFactor:0.298 0.764 0.85 1" />
  <xr-mesh node-id="mesh-sphere" position="0 1.25 0" scale="1.25 1.25 1.25" geometry="sphere" uniforms="u_baseColorFactor:0.937 0.176 0.368 1" />
  <xr-mesh node-id="mesh-cylinder" position="1 0.7 1.5" scale="1 0.7 1" geometry="cylinder" uniforms="u_baseColorFactor:1 0.776 0.364 1" />
</xr-node>

<xr-node layer="2">
  <xr-mesh node-id="mesh-plane" position="0 0 -6" rotation="-90 0 0" scale="16 1 12" geometry="plane" uniforms="u_baseColorMap:render-rt" />
</xr-node>

<xr-node node-id="rt-camera-target" position="0 1 0"></xr-node>
<xr-camera
  position="0 1 4" clear-color="0.925 0.925 0.925 1" target="rt-camera-target" render-target="rt" cull-mask="0b011"
/>

<xr-camera
 position="0 4 6" clear-color="0.925 0.925 0.925 1" target="mesh-plane" cull-mask="0b111" camera-orbit-control=""
></xr-camera>

第一个相机的render-target设置为了rt,然后mesh-planeuniforms中的一张纹理也设置为了rt。在绘制中就会用第一个相机先绘制到渲染纹理上,然后第二个相机绘制的物体中的那个平面在使用渲染纹理渲染。

注意这里我们用到了layercullMask,这个在前面的章节提到过。因为我们不想让第一个相机绘制那个平面,第二个相机则会绘制所有东西。

相机

相机Camera是渲染系统最核心的组件之一,和几乎所有的渲染引擎一样,它真正驱动着整个渲染管线的运作。相机组件一般被代理到相机元素XRCamera中使用,其派生自XRNode,对应在xml中的标签为xr-camera

相机组件也承载了一部分AR系统相关的能力,同时我们还提供了配套的相机控制器组件CameraOrbitControl来方便开发者进行相机控制。

相机元素

相机元素的一个典型使用方式如下:

<xr-node node-id="target" position="1 1 1" />
<xr-camera
  position="0 1 4" clear-color="0.4 0.6 0.7 1"
  background="skybox" target="target"
  camera-orbit-control=""
/>

这里面只列出了部分属性,实际上属性要多得多,下面就给大家一一举例。

以下属性均已组件数据驼峰命名给出,在元素中按照框架约定均转成**小写加-**的形式。

投影方式

首先是投影方式以及其相关的参数,投影方式由isPerspective决定,声明是否使用透视投影,目前默认即为透视投影,关于这两者的区分开发者可以自行查阅。

无论是对于哪种投影类型,以下参数都是通用的:

  1. near:近裁剪平面,决定相机最近能看到多远的物体。
  2. far:远裁剪平面,决定相机最远能看到多远的物体。

在选择了透视投影的模式下,有以下参数可供调整:

  1. fov:视场角,角度,默认为60。

在选择了正交投影的情况下则有以下参数:

  1. orthSize:相机可视范围大小,是横向的尺寸,纵向会根据aspect换算。

自定义矩阵

本质上来讲,投影方式和参数方式决定了投影矩阵,而相机所在元素挂载的transform组件则决定了视图矩阵,这里不再赘述这种基本概念,一般来讲都是框架自动计算的。但在某些高级需求,比如水面折射投影的渲染需求中,确实有可能要自己定制投影方式、甚至是定制视图矩阵的计算。为了解决这种需求,框架提供了方案。

// 修改投影矩阵
camera.changeProjectMatrix(manual, matrix);

// 修改视图矩阵
camera.changeViewMatrix(manual, matrix);

这两个方法可以用于修改投影或者视图矩阵,第一个参数manual用于控制开启或者关闭手动模式,第二个参数即为要设置的矩阵值。

渲染目标和视图

知道了要花那些物体、以及如何讲这些物体投影到平面上,还需要知道将这些东西最终画在什么上面,这就是渲染目标renderTarget

renderTarget可选值为空或者渲染纹理资源,如果为空则会渲染到主屏上,渲染纹理详见对应的章节。

在渲染目标渲染前,我们需要先视情况将其清空,这主要由几个参数决定:

  1. isClearColor/clearColor:设置是否要在绘制前清除渲染目标的颜色,以及要清除为怎样的颜色。
  2. isClearDepth/clearDepth:设置是否要在绘制前清除渲染目标的深度,以及要清除为怎样的深度。
  3. isClearStencil/clearStencil:设置是否要在绘制前清除渲染目标的模板值,以及要清除为怎样的模板值。

背景

在清屏后我们可能需要为整个场景绘制一个背景,这时候便可以使用background数据,其可选以下几种类型:

  1. default:默认行为,只清屏,不绘制背景。
  2. skybox:绘制天空盒,依赖于环境数据Env。
  3. ar:当开启了AR系统后,将会绘制设备摄像头的内容,详见AR相关。

跟随目标

框架默认集成了基本的了相机跟随控制能力,只需要设置target即可,在xml中这是一个节点的node-id,本质上就是一个transform组件。

指定了它之后,相机在移动时,的前向将始终朝向target

深度和掩码

除了通过平截体剔除确定哪些物体要被渲染之外,还需要一种直接精确的剔除方案,这就是掩码(cullMask)。这个实际上已经在可见性与图层中论述过。

除了单相机的渲染,很多时候会有很多个相机,它们之间也可能会有一定的顺序,这时候就可以使用depth来调整这个顺序,越大渲染顺序越靠后。在同一depth下,以标签编写的前后顺序为准。

AR相关

在开启了AR系统后,相机便可以使用AR相关的能力。

其一就是background指定为ar后,场景背景会绘制当前设备摄像机的画面。

第二是isARCamera数据,将其开启后在某些AR系统的模式下将会自动设置视图和透视矩阵,来匹配现实中设备摄像机的位置和视角。

非常需要注意,当开启isARCamera并且AR系统为Plane或者Marker模式时,不能同时设置target数据!!!

后处理

数据postProcess用于指定相机需要用到的后处理资源数组,详见后处理。

相机控制器

相机控制器组件提供了一个简单的绕着相机target旋转的控制方式,一般而言只需要在元素上添加camera-orbit-control属性即可。当然相机控制器也提供了丰富的配置参数,如果想要调节可见API文档CameraOrbitControl。

使用纹理

加载或者创建了纹理后,便可以在组件或者材质的uniforms中使用。但通过以上的章节,我们知道了纹理的种类有许多,虽然组件数据可以通过指定具体的类型,比如cube-texture来取得具体的类型的资源,但对于uniforms来说是无法判断的,同时也比较繁琐。

为了解决这个问题,我们遵循约定大于配置的原则,对于所有这些纹理资源,开发者只需要将组件数据类型指定为texture,加上不同的前缀,资源系统会自动匹配获取对应的资源:

  1. 不加前缀,取得普通纹理。
  2. cube-前缀,取得立方体纹理资源。
  3. video-前缀,取得视频纹理。
  4. render-前缀,取得渲染纹理。

如果是应用于uniforms中,开发者不需要自己去关心它们的区别,只需要uniforms="u_baseColorMap:video-vt"这样即可,但如果是自定义组件数据,就需要开发者自己处理一下了:

onUpdate(data: {texture: XrFrame.Texture | XrFrame.ITextureWrapper}) {
  const realTex = xrFrameSystem.isTextureWrapper(data.texture)
    ? data.texture.texture
    : data.texture;
}

视频纹理

有时候我们需要将视频放入场景中进行渲染,使用视频纹理VideoTexture资源就可以实现这个需求。视频纹理本质上是创建一个普通纹理,然后定时用视频解码数据对它进行更新。

通过加载器

视频纹理的创建也可以通过加载的方式创建。用xml加载:

<xr-asset-load
  type="video-texture" asset-id="vt" src="/assets/video.mp4"
  options="autoPlay:true,loop:true,abortAudio:false,placeHolder:/assets/video.jpg"
/>

或是使用代码加载:

scene.assets.loadAsset({
  type: 'video-texture', assetId: 'vt', src: '/assets/video.mp4',
  options: {autoPlay: true}
});

注意到视频纹理的几个选项,autoPlay开启后视频加载成功时会自动播放,loop开启时会循环播放,abortAudio用于指定是否要禁止声音(默认禁止),placeHolder则是作为视频尚未加载成功时的一个占位图,可选。

特别注意,placeHolder的尺寸必须和视频完全一致!!!

代码创建

视频纹理也可以手动在代码中创建:

const vt = await createVideoTexture({src, autoPlay, loop, placeHolder});

注意这是个异步方法,在有placeHolder时会在图片加载完毕时返回,否则将在视频准备好时返回。

控制

视频纹理对于开发者而言主要是视频,所以我们提供了一些用于控制视频播放的方法:

// 开始播放,异步方法
await vt.play();

// 从`pos`秒开始播放,异步方法
await vt.seek(pos);

// 停止播放
vt.stop();

// 释放视频
vt.release();

// 在播放结束并且非loop的情况下,会执行
vt.onEnd = () => {};

// 在基础库`v2.33.0`及以上,提供了暂停/唤醒方法
// 同时可以配合新暴露的播放状态使用
const xrSystem = wx.getXrFrameSystem();
if (vt.state === xrSystem.EVideoState.Playing) {
  vt.pause();
} else if (vt.state === xrSystem.EVideoState.Paused) {
  vt.resume();
}

注意,如果是自己创建的视频资源,请务必自己调用release方法释放!!!

立方体纹理

立方体纹理CubeTexture是一种特殊的纹理,一般用于实现天空盒或者环境贴图,虽然在微信小程序框架中一般使用普通全景纹理,但仍然提供给开发者一个选择。

通过加载器

立方体纹理的创建也可以通过加载的方式创建。用xml加载:

<xr-asset-load
  type="cube-texture" asset-id="sky" src="/assets/sky/"
  options="faces: right.jpg left.jpg top.jpg bottom.jpg front.jpg back.jpg,wrapU:1,wrapV:2"
/>

或是使用代码加载:

scene.assets.loadAsset({
  type: 'cube-texture', assetId: 'sky', src: '/assets/sky/',
  options: {faces: ['right.jpg', 'left.jpg', 'top.jpg', 'bottom.jpg', 'front.jpg', 'back.jpg']}
});

options中除了faces用于配置每个面的纹理地址,其他参数和TextureLoader一致。

代码创建

也可以代码创建,通过注册加入资源系统:

function createGreenCubeTexture(scene: XrFrame.Scene) {
  const buffer = new Uint8Array([0, 1, 0, 1]);
  return scene.createTexture({
    type: xrFrameSystem.ETextureType.Cube,
    slices: 6,
    source: [buffer, buffer, buffer, buffer, buffer, buffer],
    pixelFormat: xrFrameSystem.ETextureFormat.RGBA8,
    width: 1,
    height: 1,
    magFilter: xrFrameSystem.EFilterMode.NEAREST,
    minFilter: xrFrameSystem.EFilterMode.NEAREST,
    wrapU: xrFrameSystem.EWrapMode.CLAMP_TO_EDGE,
    wrapV: xrFrameSystem.EWrapMode.CLAMP_TO_EDGE,
    anisoLevel:1
  });
}

// 可以将其注册到资源系统
xrFrameSystem.registerCubeTexture('green', createGreenCubeTexture);

可见其实它也是一种普通的纹理,只不过类型不一样。

立方体纹理也支持更新,和普通纹理基本一致,只不过必须提供slice参数。

网格

网格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效果的材质。

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

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