6DoF-平面 AR能力

方法定义

6DoF-平面AR能力,提供基础AR功能,提供旋转和平移6自由度的定位功能。

包括 V1V2 两种适用不同场景的算法,两种平面AR能力各有优劣,用户根据适用场景及产品需求,自行判断调用接口类型,具体介绍如下:

  1. V1平面接口,适用于用户在平面场景下,例如桌面,地面,泛平面场景,放置虚拟物体,不提供真实世界距离。用户放置物体时,手机相机倾斜向下对着目标平面点击即可,具有广泛的机型支持。
  2. V2平面接口,提供真实物理距离的AR定位功能,提供平面识别功能,用户在平面范围点击放置虚拟物体的功能,具有有限的支持机型。

能力扩展

V2 平面基础上,可以通过配置开启多种扩展能力,比如:

  1. marker 识别能力,即平面空间下多个不同识别目标的识别。
  2. 虚实遮挡的能力,即虚拟物体和真实世界的交互遮挡能力。

更多使用效果与开关配置细节,可以参考 平面AR能力扩展。

如何开启 V1 或 V2

初始化时,通过 VKSession 配置 version 确定。

const session = wx.createVKSession({
  track: {
    plane: {
        mode: 1
    },
  },
  version: 'v2' // 在满足设备条件时开启,否则会使用 v1
  // version: "v1" 强制开启v1
})

有关完整的VKSession配置,详见 VKSession配置参考

V1 与 V2 对比

接口类型 平面检测 真实距离 初始化速度 机型覆盖率 功耗 精度 多物体放置效果 多种扩展能力
V1


V2




应用场景示例

V1平面演示

planev1_gif

V2平面演示

planev2_gif

V2平面+虚实遮挡演示

planev2_deepocc_gif

程序示例

  1. 可以在 V1平面 页面查看示例代码。
  2. 可以在 V2平面 页面查看示例代码。
  3. 可以在 V2平面+虚实遮挡 页面查看示例代码,在微信小程序示例中的接口-VisionKit视觉能力-水平面AR-v2-虚实遮挡中体验。

附录

V1平面接口系统要求

  1. iOS机型要求: iPhone 6s及以上机型
  2. Android机型要求:Android 7.0, Android SDK 24及以上

V2平面AR接口支持列表

  1. iOS机型要求: iPhone 7及以上机型

  2. Android机型支持会逐步增加,如测试机型效果有误可论坛反馈,目前支持机型包括:

手机厂商 手机型号
Hi Hi nova 9
OPPO OPPO A32
OPPO OPPO A53
OPPO OPPO A57
OPPO OPPO A72 5G
OPPO OPPO A92s
OPPO OPPO A93s
OPPO OPPO A93
OPPO OPPO A95
OPPO OPPO Ace2
OPPO OPPO Find X2
OPPO OPPO Find X3 Pro
OPPO OPPO Find X3
OPPO OPPO Find X5 Pro
OPPO OPPO Find X5
OPPO OPPO K10 Pro
OPPO OPPO K10 Pro
OPPO OPPO K10x
OPPO OPPO K10
OPPO OPPO K3
OPPO OPPO K5
OPPO OPPO K9 Pro
OPPO OPPO Pad
OPPO OPPO R15
OPPO OPPO R17
OPPO OPPO Reno 10倍变焦版
OPPO OPPO Reno Ace
OPPO OPPO Reno Z
OPPO OPPO Reno2
OPPO OPPO Reno3 Pro 5G
OPPO OPPO Reno3 元气版
OPPO OPPO Reno3
OPPO OPPO Reno4
OPPO OPPO Reno5 K
OPPO OPPO Reno5 Pro+
OPPO OPPO Reno5
OPPO OPPO Reno6 Pro+
OPPO OPPO Reno6 Pro
OPPO OPPO Reno6
OPPO OPPO Reno7 Pro
OPPO OPPO Reno7 SE
OPPO OPPO Reno7
OPPO OPPO Reno8 Pro+
OPPO OPPO Reno8 Pro
OPPO OPPO Reno8
OPPO OPPO Reno9 Pro+
OPPO OPPO Reno
OPPO Oppo A11
OPPO Oppo K7x
OPPO Oppo Reno2 Z
OPPO Oppo Reno5 Pro
ROG ROG游戏手机2精英版
Realme Realme C11 (2021)
Realme Realme GT Neo 5
Realme Realme X7 Pro
Samsung Samsung Galaxy Note10+(855)
Samsung Samsung Galaxy S10+
Samsung Samsung Galaxy S10
VIVO VIVO IQOO
VIVO VIVO S7
VIVO Vivo X90
VIVO Vivo Z5x
VIVO Vivo iQOO 11 Pro
VIVO Vivo iQOO Neo7 Racing
VIVO vivo IQOO NEO6 SE
VIVO vivo NEX 3
VIVO vivo NEX
VIVO vivo Pad
VIVO vivo S10
VIVO vivo S12 Pro
VIVO vivo S12
VIVO vivo S15 Pro
VIVO vivo S15e
VIVO vivo S15
VIVO vivo S16e
VIVO vivo S16
VIVO vivo S5
VIVO vivo S6
VIVO vivo S9e
VIVO vivo S9
VIVO vivo T1
VIVO vivo X27
VIVO vivo X30 Pro
VIVO vivo X30
VIVO vivo X50 Pro
VIVO vivo X50
VIVO vivo X60 Pro
VIVO vivo X60
VIVO vivo X70 Pro
VIVO vivo X70
VIVO vivo X80
VIVO vivo Y52s
VIVO vivo Y53s
VIVO vivo Y70s
VIVO vivo Y73s
VIVO vivo Z5
VIVO vivo iQOO 3 5G
VIVO vivo iQOO 5 5G
VIVO vivo iQOO Neo
VIVO vivo iQOO Pro
iQOO iQOO 10 Pro
iQOO iQOO 10
iQOO iQOO 11
iQOO iQOO 7
iQOO iQOO 8 Pro
iQOO iQOO 8
iQOO iQOO 9 Pro
iQOO iQOO 9
iQOO iQOO Neo 855
iQOO iQOO Neo3
iQOO iQOO Neo5 SE
iQOO iQOO Neo5S
iQOO iQOO Neo5
iQOO iQOO Neo6
iQOO iQOO Neo7 SE
iQOO iQOO Z1x
iQOO iQOO Z1
iQOO iQOO Z3
iQOO iQOO Z5x
iQOO iQOO Z5
iQOO iQOO Z6
realme realme GT Neo2T
realme realme GT Neo2
realme realme GT Neo3
realme realme GT Neo
realme realme GT
realme realme Q3 Pro
realme realme Q3
realme realme V15
realme realme X2 Pro
一加 OnePlus 11
一加 OnePlus Ace 2V
一加 OnePlus Ace 2
一加 一加10 Pro
一加 一加7 Pro
一加 一加7T Pro
一加 一加7T
一加 一加7
一加 一加8 Pro
一加 一加8T
一加 一加8
一加 一加9 Pro
一加 一加9RT
一加 一加9R
一加 一加9
一加 一加Ace Pro
一加 一加Ace 竞速版
一加 一加Ace
努比亚 努比亚红魔3
努比亚 努比亚红魔6
华为 HUAWEI Mate50 Pro
华为 HUAWEI Mate50
华为 HUAWEI MatePad Pro 11英寸
华为 HUAWEI nova 10 SE
华为 Huawei Mate 30 5G
华为 Huawei Mate 30 RS Porsche Design
华为 Huawei Mate 30E Pro 5G
华为 Huawei Mate 40 Pro+
华为 Huawei nova 6 5G
华为 华为Mate 20 Pro
华为 华为Mate 9
华为 华为Mate20X
华为 华为Mate20
华为 华为Mate30 Pro
华为 华为Mate30
华为 华为Mate40 Pro
华为 华为Mate40E
华为 华为Mate40
华为 华为MatePad 10.8
华为 华为MatePad 11
华为 华为MatePad Pro
华为 华为MatePad Pro
华为 华为P20
华为 华为P30 Pro
华为 华为P30
华为 华为P40 Pro+
华为 华为P40 Pro
华为 华为P40
华为 华为P50 Pro
华为 华为P50 Pro
华为 华为nova 10 Pro
华为 华为nova 10
华为 华为nova 4e
华为 华为nova 4
华为 华为nova 5 Pro
华为 华为nova 5i Pro
华为 华为nova 6
华为 华为nova 7 5G
华为 华为nova 7 Pro 5G
华为 华为nova 7 Pro
华为 华为nova 7
华为 华为nova 8 Pro
华为 华为nova 8
华为 华为nova 9 Pro
华为 华为nova 9
华为 华为平板 M6 8.4英寸
华为 华为畅享10 Plus
坚果 坚果Pro 3
小米 Redmi K20 Pro
小米 Redmi K30 5G
小米 Redmi K30S
小米 Redmi K40 Pro
小米 Redmi K40S
小米 Redmi K40
小米 Redmi K50 Pro
小米 Redmi K50 Ultra
小米 Redmi K60 Pro
小米 Redmi K60
小米 Redmi Note 10 Pro
小米 Redmi Note 11T Pro+
小米 Redmi Note 11
小米 Redmi Note 12 Pro
小米 Redmi Note 9 5G
小米 Xiaomi 13 Pro
小米 Xiaomi 13
小米 Xiaomi Mi 10 Ultra
小米 Xiaomi Redmi K30 Pro
小米 Xiaomi Redmi K30
小米 Xiaomi Redmi Note 9 Pro 5G
小米 小米 9
小米 小米10 Pro
小米 小米10S
小米 小米10
小米 小米10青春版
小米 小米11 Pro
小米 小米11 Ultra
小米 小米11
小米 小米11青春版
小米 小米12 Pro
小米 小米12S Pro
小米 小米12S Ultra
小米 小米12X
小米 小米12
小米 小米8
小米 小米Civi 1S
小米 小米MIX4
小米 小米平板5 pro
小米 小米平板5
小米 红米K30 Ultra
小米 红米Note 11T Pro
联想 联想小新 Pad
联想 联想拯救者 Y700
联想 联想拯救者电竞手机Pro
荣耀 Honor80 Pro
荣耀 Honor80
荣耀 荣耀20S
荣耀 荣耀20
荣耀 荣耀30 Pro+
荣耀 荣耀30S
荣耀 荣耀30
荣耀 荣耀50 Pro
荣耀 荣耀50
荣耀 荣耀60 Pro
荣耀 荣耀60
荣耀 荣耀70 Pro
荣耀 荣耀70
荣耀 荣耀9X
荣耀 荣耀Magic3
荣耀 荣耀Magic4 Pro
荣耀 荣耀Magic4
荣耀 荣耀V20
荣耀 荣耀V30 PRO
荣耀 荣耀V30
荣耀 荣耀V40
荣耀 荣耀X10
荣耀 荣耀X30
荣耀 荣耀X40 GT
荣耀 荣耀X40
荣耀 荣耀平板V6
荣耀 荣耀平板V7 Pro
黑鲨 黑鲨5 RS
黑鲨 黑鲨游戏手机2 Pro
黑鲨 黑鲨游戏手机2
黑鲨 黑鲨游戏手机3
黑鲨 黑鲨游戏手机4

VisionKit

微信小程序也在基础库 2.20.0 版本开始提供了开发 AR 功能的能力,即 VisionKit。VisionKit 包含了 AR 在内的视觉算法,要想开发微信小程序的 AR 功能,我们需要先了解 VisionKit。

VKSession

VisionKit 的核心就是 VKSession,即 VisionKit 会话对象。我们可以通过 wx.createVKSession 来创建 VKSession 的实例,此实例在页面上是单例,和页面的生命周期强相关,且页面间的 VKSeesion 实例运行周期互斥,这就确保了一个微信小程序在一个确定的时刻最多只会有一个 VKSession 实例,下面的 demo 以 v2 版本为例。

const session = wx.createVKSession({
  track: {
    plane: {mode: 3},
  },
  version: 'v2', 
})

调 VKSession 实例的 start 方法可以启动 VKSession 实例:

session.start(err => {
  if (err) return console.error('VK error: ', err)

  // do something
})

接下来,我们要构建 3D 世界和渲染了。

渲染

AR 本意即为增强现实,通俗来讲就是可以在现实世界融入虚拟的东西,比如在现实世界的桌面上放一个虚拟的机器人。

那么要在微信小程序中看到这个效果,我们首先要能将现实画面画到屏幕上,这就依赖我们的摄像头了。当然,画面不是静止不动的,所以我们还得连续的将摄像头拍到的画面上屏,这就和我们使用 WebGL 绘制 3D 世界类似,逐帧渲染:

session.start(err => {
  if (err) return console.error('VK error: ', err)

  const onFrame = timestamp => {
    const frame = session.getVKFrame(canvas.width, canvas.height)
    if (frame) {
      renderFrame(frame)
    }

    session.requestAnimationFrame(onFrame)
  }
  session.requestAnimationFrame(onFrame)
})

在大家熟知的 requestAnimationFrame 内,通过 VKSession 实例的 getVKFrame 方法可以获取到帧对象,帧对象中即包含了我们需要上屏的画面。此处我们在调 getVKFrame 时传入了画布的宽高,是因为我们此处就准备将其用 WebGL 渲染出来,之后我们就来看看 renderFrame 里是如何做的:

function renderFrame(frame) {
  renderGL(frame)

  // do something
}

function renderGL(frame) {
  const { yTexture, uvTexture } = frame.getCameraTexture(gl, 'yuv')
  const displayTransform = frame.getDisplayTransform()

  // 上屏
}

通过 getCameraTexture 我们可以拿到 yuv 纹理,而此纹理是未经裁剪调整的纹理,所以还需要通过 getDisplayTransform 获取到纹理调整矩阵,然后在上屏时可以使用此矩阵对纹理进行裁剪调整。此处代码中的 gl 即是 WebGLRenderingContext 实例。

WebGL & three.js

那么上屏需要如何操作呢?这里需要我们拥有一定的 WebGL 知识,在此 demo 中我们自己编写着色器来将画面渲染到画布上,用 three.js 来渲染 3D 模型。

首先是初始化 three.js 部分:

import { createScopedThreejs } from 'threejs-miniprogram'
import { registerGLTFLoader } from './loaders/gltf-loader'

const THREE = createScopedThreejs(canvas)
registerGLTFLoader(THREE)

// 相机
const camera = new THREE.Camera()

// 场景
const scene = new THREE.Scene()

// 光源
const light1 = new THREE.HemisphereLight(0xffffff, 0x444444) // 半球光
light1.position.set(0, 0.2, 0)
scene.add(light1)
const light2 = new THREE.DirectionalLight(0xffffff) // 平行光
light2.position.set(0, 0.2, 0.1)
scene.add(light2)

// 渲染层
const renderer = new THREE.WebGLRenderer({antialias: true, alpha: true})
renderer.gammaOutput = true
renderer.gammaFactor = 2.2

// 机器人模型
const loader = new THREE.GLTFLoader()
let model
loader.load('https://dldir1.qq.com/weixin/miniprogram/RobotExpressive_aa2603d917384b68bb4a086f32dabe83.glb', gltf => {
  model = {
    scene: gltf.scene,
    animations: gltf.animations,
  }
})
const clock = new THREE.Clock()

此处使用 threejs-miniprogram 包,这是经过特殊封装以兼容微信小程序环境的 three.js 包,当然开发者们也可以替换成任意其它可以在微信小程序中跑的 WebGL 引擎,此处仅仅是以 three.js 来举例。registerGLTFLoader 则是用来加载 3D 模型。关于 three.js 的使用,这里只是给出了一个简单的 demo,有兴趣者可以查阅官方文档进行了解。

接下来是初始化 WebGL:

const gl = renderer.getContext()

// 编写着色器
const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM)
const vs = `
  attribute vec2 a_position;
  attribute vec2 a_texCoord;
  uniform mat3 displayTransform;
  varying vec2 v_texCoord;
  void main() {
    vec3 p = displayTransform * vec3(a_position, 0);
    gl_Position = vec4(p, 1);
    v_texCoord = a_texCoord;
  }
`
const fs = `
  precision highp float;

  uniform sampler2D y_texture;
  uniform sampler2D uv_texture;
  varying vec2 v_texCoord;
  void main() {
    vec4 y_color = texture2D(y_texture, v_texCoord);
    vec4 uv_color = texture2D(uv_texture, v_texCoord);

    float Y, U, V;
    float R ,G, B;
    Y = y_color.r;
    U = uv_color.r - 0.5;
    V = uv_color.a - 0.5;

    R = Y + 1.402 * V;
    G = Y - 0.344 * U - 0.714 * V;
    B = Y + 1.772 * U;

    gl_FragColor = vec4(R, G, B, 1.0);
  }
`
const vertShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertShader, vs)
gl.compileShader(vertShader)

const fragShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragShader, fs)
gl.compileShader(fragShader)

const program = gl.createProgram()
gl.attachShader(program, vertShader)
gl.attachShader(program, fragShader)
gl.deleteShader(vertShader)
gl.deleteShader(fragShader)
gl.linkProgram(program)
gl.useProgram(program)

const uniformYTexture = gl.getUniformLocation(program, 'y_texture')
gl.uniform1i(uniformYTexture, 5)
const uniformUVTexture = gl.getUniformLocation(program, 'uv_texture')
gl.uniform1i(uniformUVTexture, 6)

const dt = gl.getUniformLocation(program, 'displayTransform')
gl.useProgram(currentProgram)

// 初始化 VAO
const ext = gl.getExtension('OES_vertex_array_object')
const currentVAO = gl.getParameter(gl.VERTEX_ARRAY_BINDING)
const vao = ext.createVertexArrayOES()

ext.bindVertexArrayOES(vao)

const posAttr = gl.getAttribLocation(program, 'a_position')
const pos = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, pos)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW)
gl.vertexAttribPointer(posAttr, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(posAttr)
vao.posBuffer = pos

const texcoordAttr = gl.getAttribLocation(program, 'a_texCoord')
const texcoord = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, texcoord)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, 0, 1, 1, 0, 0, 0]), gl.STATIC_DRAW)
gl.vertexAttribPointer(texcoordAttr, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(texcoordAttr)
vao.texcoordBuffer = texcoord

ext.bindVertexArrayOES(currentVAO)

这一块属于 WebGL 的知识,这里就不再做过多赘述,有兴趣者可以借助搜索引擎查阅相关资料了解。之后我们就可以完善前面的 renderGL 方法,完成上屏代码的编写:

function renderGL(frame) {
  const gl = renderer.getContext()
  gl.disable(gl.DEPTH_TEST)

  // 获取纹理和调整矩阵
  const {yTexture, uvTexture} = frame.getCameraTexture(gl, 'yuv')
  const displayTransform = frame.getDisplayTransform()

  if (yTexture && uvTexture) {
    const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM)
    const currentActiveTexture = gl.getParameter(gl.ACTIVE_TEXTURE)
    const currentVAO = gl.getParameter(gl.VERTEX_ARRAY_BINDING)

    gl.useProgram(program)
    ext.bindVertexArrayOES(vao)

    // 传入调整矩阵
    gl.uniformMatrix3fv(dt, false, displayTransform)
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1)

    // 传入 y 通道纹理
    gl.activeTexture(gl.TEXTURE0 + 5)
    const bindingTexture5 = gl.getParameter(gl.TEXTURE_BINDING_2D)
    gl.bindTexture(gl.TEXTURE_2D, yTexture)

    // 传入 uv 通道纹理
    gl.activeTexture(gl.TEXTURE0 + 6)
    const bindingTexture6 = gl.getParameter(gl.TEXTURE_BINDING_2D)
    gl.bindTexture(gl.TEXTURE_2D, uvTexture)

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)

    gl.bindTexture(gl.TEXTURE_2D, bindingTexture6)
    gl.activeTexture(gl.TEXTURE0 + 5)
    gl.bindTexture(gl.TEXTURE_2D, bindingTexture5)

    gl.useProgram(currentProgram)
    gl.activeTexture(currentActiveTexture)
    ext.bindVertexArrayOES(currentVAO)
  }
}

至此,基础背景画面就画到屏幕上了,在手机上看到的效果就如同开着摄像头一样。

放置 3D 模型

诚然,仅效果来看,到此为止还不能将其称其为 AR,接下来我们要实现这么一个功能:点击屏幕,然后在画面上对应的 3D 世界位置放置一个机器人模型;比如点击画面中的桌子,就在桌子上放一个机器人模型。

前面我们引入的 three.js 就是为了做这个效果,现在流行的 WebGL 引擎基本上都封装了大量方便我们快速使用的接口,比如光照渲染、模型加载等,前面的代码已经做过演示,这里就不再重复说明。

这里我们需要了解的是 VKSession 的 hitTest 接口。这个接口的主要是为了将 2D 坐标转成 3D 世界坐标,即 (x, y) 转成 (x, y, z)。通俗来说就是画面上显示的桌子,在屏幕上它是 2D 的,当我们手指触摸屏幕时拿到的坐标是 2D 坐标,也就是 (x, y);hitTest 接口可以将其转换成 3D 世界坐标 (x, y, z),而 3D 世界坐标系的原点则是相机打开瞬间其所在的点:

function onTouchEnd(evt) {
  const touches = evt.changedTouches.length ? evt.changedTouches : evt.touches

  // 在点击位置放一个机器人模型
  if (touches.length === 1) {
    const touch = touches[0]
    if (session && scene && model) {
      // 调用 hitTest
      const hitTestRes = session.hitTest(touch.x / width, touch.y / height)
      if (hitTestRes.length) {
        model.scene.scale.set(0.05, 0.05, 0.05)

        // 动画混合器
        const mixer = new THREE.AnimationMixer(scene)
        for (let i = 0; i < model.animations.length; i++) {
          const clip = model.animations[i]
          if (clip.name === 'Dance') {
            const action = mixer.clipAction(clip)
            action.play()
          }
        }

        // 把模型放到对应的位置上
        const cnt = new THREE.Object3D()
        cnt.add(model.scene)
        model.matrixAutoUpdate = false
        model.matrix.fromArray(hitTestRes[0].transform)
        scene.add(model)
      }
    }
  }
}

可以看到 hitTest 传入的两个参数并不是标准的坐标值,而是将其除以画布宽高后得到的值再传入。这里接受的参数其实是相对于画布视窗的坐标,取值范围为 [0, 1],0 为左/上边缘,1 为右/下边缘。而 hitTest 返回的结果则是矩阵,里面包含了 3D 世界坐标的位置、旋转和放缩信息。可以看到这矩阵可以直接为 three.js 所用,这也是此次 demo 选用 three.js 的原因之一,它封装了很多繁杂的实现细节,简化了大量代码。

之后就是调 three.js 相关的渲染接口,把机器人模型也画到画面上,这里我们可以继续完善前面的 renderFrame 方法:

function renderFrame(frame) {
  renderGL(frame)

  const frameCamera = frame.camera

  // 更新动画
  const dt = clock.getDelta()
  mixer.update(dt)

  // 相机
  if (camera) {
    camera.matrixAutoUpdate = false
    camera.matrixWorldInverse.fromArray(frameCamera.viewMatrix)
    camera.matrixWorld.getInverse(camera.matrixWorldInverse)

    const projectionMatrix = frameCamera.getProjectionMatrix(NEAR, FAR)
    camera.projectionMatrix.fromArray(projectionMatrix)
    camera.projectionMatrixInverse.getInverse(camera.projectionMatrix)
  }

  renderer.autoClearColor = false
  renderer.render(scene, camera)
  renderer.state.setCullFace(THREE.CullFaceNone)
}

这里通过帧对象的 camera 属性拿到了帧相机,然后通过帧相机的 viewMatrix 拿到了视图矩阵,通过 getProjectionMatrix 方法拿到了投影矩阵,统统传给 three.js 的相机对象,以确保 three.js 的相机位置、角度正确,同时确保 3D 世界渲染出来的效果符合我们人眼所看到的景象。

至此,前面那个点屏幕点击位置对应的 3D 世界放置一个机器人模型的效果得以完成。

平面检测

在对如何在微信小程序中实现一个 AR 功能有所了解后,我们可能需要扩展一些场景:比如需要检测出 3D 世界的平面。

VisionKit 识别到的平面会以 anchor 对象的方式提供给我们,这里 VKSession 提供了很便利的事件:addAnchors/updateAnchors/removeAnchors,通过这三个事件我们可以监听 anchor 列表的变化:

session.on('addAnchors', anchors => {
  // anchor.id - anchor 唯一标识
  // anchor.type - anchor 类型,0 表示是平面 anchor
  // anchor.transform - 包含位置、旋转、放缩信息的矩阵,以列为主序
  // anchor.size - 尺寸
  // anchor.alignment - 方向

  // do something
})
session.on('updateAnchors', anchors => {
  // do something
})
session.on('removeAnchors', anchors => {
  // do something
})
  • 程序示例
    可以在 VisionKit基础能力使用参考 页面查看示例代码。

8bit量化使用指南

开始

微信小程序AI通用接口是由官方提供的通用AI模型推理方案,支持Int8模型量化推理。可显著提升模型推理性能并减小模型的存储和计算开销。

本指南将展示如何通过该技术优化浮点分类Demo

1. 准备

  • 请下载模型量化工具,并安装依赖。
git clone https://github.com/wechat-miniprogram/xnet-miniprogram.git && cd xnet-miniprogram/nncs && pip install -r requirements.txt
  • 请下载ImageNet数据集,或者ImageNet-mini
  • 请下载预训练模型Mobilenetv2。目录
ImageNet
|---train
|     |---n01440764
|     |---n01443537
|     |---...
|     |---n15075141
|---val
|     |---n01440764
|     |---n01443537
|     |---...
|     |---n15075141
nncs
|---nncs
|---demo
|     |---imagenet_classification
|---requirements.txt
|---README.md
mobilenet-v2-71dot82.onnx

2. 量化训练示例

  • 参考代码: demo/imagenet_classification/train_imagenet_onnx.py
  • 修改数据来源和ONNX路径:
    ...
    args.train_data = "/data/yangkang/datasets/ImageNet"
    args.val_data = "/data/yangkang/datasets/ImageNet"
    ...
    model = "mobilenet-v2-71dot82.onnx"
  • 运行量化训练
cd demo/imagenet_classification && python train_imagenet_onnx.py
  • 日志样例: demo/imagenet_classification/nncs_onnx_lr1e-5.logfile,浮点模型精度71.82,QAT微调之后精度71.52。
  • 量化模型导出: mobilenetv2_qat.onnx
python deploy.py
  • 量化方案支持: 量化感知训练(QAT)和后训练量化(PTQ)

3. 微信小程序Demo

量化分类的Demo借鉴了浮点分类Demo。需注意的区别是:

this.session = wx.createInferenceSession({
    model: modelPath,
    precisionLevel : 0,
    allowNPU : false,    
    allowQuantize: true, // 需设置为true,激活量化推理
    });

4. 运行效果

扫描下方二维码,点击接口-通用AI推理能力-mobileNetInt8, 可以查看运行效果。



运行 demo,可以看到摄像头在采集同时,将会实时地将分类结果写回到页面下方。



完整 demo 请参考官方github微信小程序示例

5.开启耗时测试

  data: {
    predClass: "None",
    classifier: null,
    enableSpeedTest: true,  // 设置true
    avgTime: 110.0,
    minTime: 110.0
  },

iphone13ProMax,浮点分类Demo的耗时约10ms,量化分类Demo耗时约5ms。

算子支持列表

微信小程序AI推理负责以最优化的方式运行您的模型,并在可用时利用特定设备的硬件加速。此页面提供关于目前各设备支持哪些Op的信息。

注:
1.以下算子若无特殊备注说明,一般符合ONNX 算子定义,参考:https://github.com/onnx/onnx/blob/main/docs/Operators.md
2.目前GPU推理暂未对外开放,敬请期待。

Operator CPU IOS NPU IOS GPU Android GPU 备注
Activation 详细支持的Activation种类参考下方Activation列表
ArgMax
ArgMin
BatchNorm
Bias
Binary 详细支持的Binary操作种类参考下方Binary列表
Bucketize
Cast
Concat
Const
ConstOfShape
Conv1D
Conv1DTranspose
Conv2D
Conv2DTranspose
Conv3D
Conv3DTranspose
Crop
CropAndResize
CumSum
DepthToSpace
Dropout
ElementWise
Expand
FakeQuantize
Flatten
FullyConnected
Gather
GatherND
Gemm
GlobalPooling
GroupNorm
Gru
InstanceNorm
LayerNorm
LpNorm
Lrn
Lstm
MatMul
NMS
Normalize
OneHot
Pad
Permute
Pooling1D
Pooling2D
Pooling3D
PriorBox
Range
Reduce
Reshape
Resize2D
Rnn
Scale
ScatterND
Shape
ShuffleChannel
SpaceToDepth
Split
Slice
Softmax
Squeeze
Tile
TopK
Unary 详细支持的Unary种类参考下方Unary列表
Unsqueeze
Where


Activation列表:

名称 描述
None f(x) = x
Abs f(x) = [x]
Clip f(x) = min(max(x, constA), constB)
HardSigmoid f(x) = min(max(x * constA + constB, 0), 1)
HardSwish f(x) = min(max(x * constA + constB, 0), 1) * x
HSigmoid f(x) = (ReLU6(x + 3) / 6)
HSwish f(x) = (ReLU6(x + 3) / 6) * x
LeakyReLU f(x) = min(x, 0) * constA + max(x, 0)
Linear f(x) = x * constA + constB
PReLU f(x) = min(x, 0) * weight + max(x, 0) (Caffe1’s)
ReLU f(x) = max(x, 0)
ReLUN f(x) = min(x, 0) * constA + min(max(x, 0), constB)
SELU f(x) = (x >= 0 ? x : (exp(x)-1) * constA) * constB
Sigmoid f(x) = 1 / (1 + exp(-x)), 也叫做 Logistic
SoftPlus f(x) = log(1 + exp(x * constB)) * constA
SoftSign f(x) = x / (1 + |x|)
Swish f(x) = x / (1 + exp(-x * constA))
Tanh f(x) = tanh(x * constB) * constA
Threshold f(x) = (x > constA ? 1 : 0)
ThrReLU f(x) = (x > constA ? x : 0) (阈值化 ReLU)


Binary列表:

名称 描述
Add f(x, y) = x + y
Sub f(x, y) = x – y
Mul f(x, y) = x * y
Div f(x, y) = x / y
Pow f(x, y) = pow(x, y)
Max f(x, y) = max(x, y)
Min f(x, y) = min(x, y)
Mean f(x, y) = (x + y) / 2
And f(x, y) = x & y
Or f(x, y) = x | y
Xor f(x, y) = x ^ y
BitShiftLeft f(x, y) = x << y
BitShiftRight f(x, y) = x >> y
Equal f(x, y) = (x == y)
NotEqual f(x, y) = (x != y)
Greater f(x, y) = (x > y)
GreaterEqual f(x, y) = (x >= y)
Less f(x, y) = (x < y)
LessEqual f(x, y) = (x <= y)


Unary列表:

名称 描述
Abs f(x) = [x]
Neg f(x) = -x
Celi f(x) = ceil(x)
Floor f(x) = floor(x)
Reciprocal f(x) = 1 / x
Sqrt f(x) = sqrt(x)
Exp f(x) = exp(x)
Log f(x) = log(x)
Erf f(x) = erf(x)
Acos f(x) = acos(x)
Acosh f(x) = acosh(x)
Cos f(x) = cos(x)
Cosh f(x) = cosh(x)
Sin f(x) = sin(x)
Sinh f(x) = sinh(x)
Atan f(x) = atan(x)
Atanh f(x) = atanh(x)
Tan f(x) = tan(x)
Tanh f(x) = tanh(x)
ExpM1 f(x) = expm1(x)
Log1P f(x) = log1p(x)

微信小程序AI使用指南

开始

微信小程序AI通用接口是一套微信小程序官方提供的通用AI模型推理解决方案,内部使用充分优化的自研推理引擎,支持 CPU、GPU、NPU 推理。微信小程序开发者无需关注内部实现和模型转换,只需提供训练好的 ONNX 模型,微信小程序内部会将用户的 ONNX 模型自动转换为自研推理引擎可以识别的模型格式并完成推理。

本指南将展示如何从头开始使用微信小程序AI推理能力完成一个分类任务。我们将摄像头实时采集到的数据经过简单的前处理转换为AI推理的输入,完成推理后,再对模型运行输出进行简单的后处理,获取最终分类结果并展示在页面上。

示例使用 ONNX 官方 modelzoo 所提供的 mobileNetV2 模型。相关模型可以从官方github 获取;示例中使用的前后处理方式也与 ONNX 官方给出的imagenet_validation一致。

1 创建session

首先我们需要创建一个 session 用于推理。 这里我们使用从官方github下载的浮点模型,选择 precisionLevel 为0,这样 session 运行时,将会自动选择 fp16 存储中间 tensor 的结果,也会用 fp16 进行计算,并且会开启 fp16 计算的 Winograd,同时开启近似 math 计算。我们选择不使用量化推理,不使用 NPU。 创建 session 时除了必须提供参数 model 来指定 ONNX 模型路径外,其他设置都不是必须的。

一般来说,使用的 precisionLevel 等级越低,推理速度越快,但可能带来精度损失。因此推荐开发者在使用时,在效果满足需求时优先使用更低精度以提高推理速度,节约能耗。

除了使用wx.createInferenceSession()接口创建 session 外,此处我们还为 session 添加了2个事件,以监听 error 或者创建完成。onLoad()函数中我们通过设置一个 isReady 变量,记录 session 完成了初始化,可以用来进行推理。

// 这里 modelPath 为需要的 ONNX 模型,注意 model 目前只能识别后缀为.onnx 的文件作为参数。
const modelPath = `${wx.env.USER_DATA_PATH}/mobilenetv2-12.onnx`;

this.session = wx.createInferenceSession({
    model: modelPath,
    /* 0: 最低精度  使用 fp16 存储浮点,fp16 计算,Winograd 算法也采取 fp16 计算,开启近似 math 计算
       1: 较低精度  使用 fp16 存储浮点,fp16 计算,禁用 Winograd 算法,开启近似 math 计算
       2: 中等精度  使用 fp16 存储浮点,fp32 计算,开启 Winograd,开启近似 math 计算
       3: 较高精度  使用 fp32 存储浮点,fp32 计算,开启 Winograd,开启近似 math 计算
       4: 最高精度  使用 fp32 存储浮点,fp32 计算,开启 Winograd,关闭近似 math 计算

       通常更高的精度需要更长的时间完成推理
    */
    precisionLevel : 0,
    allowNPU : false,     // 是否使用 NPU 推理,仅针对 IOS 有效
    allowQuantize: false, // 是否产生量化模型
    });

// 监听error事件
session.onError((error) => {
  console.error(error);
});

// 监听模型加载完成事件
session.onLoad(() => {
  console.log('session load')
}); 

2 session推理

2.1 处理摄像头采集数据

首先我们创建一个 camera context, 并调用 onCameraFrame 采集帧。此处的 classifier 封装了推理 session 相关的调用,具体完整代码可以参考 demo 示例。onCameraFrame 会持续采集摄像头图像,如果我们的 session 初始化成功的并且完成了上一帧的推理任务,就会传递摄像头采集的数据,执行新一次的推理任务。

    const context = wx.createCameraContext(this); 

    const listener = context.onCameraFrame(frame => {

        const fps = this.fpsHelper.getAverageFps();
        console.log(`fps=${fps}`);

        if (this.classifier && this.classifier.isReady() && !this.predicting) {
           this.executeClassify(frame);
        }
    });

2.2 对摄像头的采集数据进行前处理

OnCameraFrame 返回的 frame 包含属性 width、height 和 data,分别表示二维图像数据的宽度,高度,以及图像像素点数据。其中 data 为一个 ArrayBuffer,存储数据类型为 Uint8,存储的数据 format 为 rgba,即每连续的4个值表示一个像素点的 rgba。 详细关于 onCameraFrame 的内容可以参考CameraContext.onCameraFrame.

对于 frame 内容,我们首先进行前处理操作以转化为模型输入。

用 Netron 打开 ONNX 文件,我们可以看到 mobileNet 输入输出的描述信息。 可以看到,本模型的输入大小为[1, 3, 224, 224],数据类型为 float32。


为了将摄像头采集到的frame转换为模型需要的数据,我们需要丢弃 alpha 通道信息,将数据由 nhwc 变换成 nchw, 将 frame 的 width 和 height resize 成 224 x 224, 并且完成 normallize 操作。

以下代码通过 js 完成了所有前处理过程,将摄像头采集到 frame 转化为模型输入 dstInput。 其中 frame 为摄像头采集的数据, var dstInput = new Float32Array(3 * 224 * 224);

  /* 原始输入为 rgba uint8 数据, 目标为 nchw float32 数据

     将camera 采集数据缩放到模型的 input 大小, 将 uint8 数据转换成 float32,
     并且从 NHWC 转换到 NCHW
  */
  preProcess(frame, dstInput) {

    return new Promise((resolve, reject) =>
    {
      const origData = new Uint8Array(frame.data);

      const hRatio = frame.height / modelHeight;

      const wRatio = frame.width / modelWidth;

      const origHStride = frame.width * 4;
      const origWStride = 4;

      const mean = [0.485, 0.456, 0.406]

      // Reverse of std = [0.229, 0.224, 0.225]
      const reverse_div = [4.367, 4.464, 4.444]
      const ratio = 1 / 255.0

      const normalized_div = [ratio / reverse_div[0], ratio * reverse_div[1], ratio * reverse_div[2]];

      const normalized_mean = [mean[0] * reverse_div[0], mean[1] * reverse_div[1], mean[2] * reverse_div[2]];

      var idx = 0;
      for (var c = 0; c < modelChannel; ++c)
      {
        for (var h = 0; h < modelHeight; ++h)
        {
          const origH = Math.round(h * hRatio);

          const origHOffset = origH * origHStride;

          for (var w = 0; w < modelWidth; ++w)
          {
            const origW = Math.round(w * wRatio);

            const origIndex = origHOffset + origW * origWStride + c;

            const val = origData[origIndex] * (normalized_div[c]) - normalized_mean[c];

            dstInput[idx] = val;

            idx++;
          }
        }
      } 

      resolve();
    });
  }

2.3 模型推理

对摄像头采集到的数据进行简单前处理后,我们就可以用来设置 input,进行模型推理了。 我们用前处理得到的 dstInput 来构造一个 xInput,将这个 xInput 作为模型推理的输入,传递给 session.run 即可。

const xinput = {
    shape: [1, 3, 224, 224],  // 输入形状 NCHW 值
    data: dstInput.buffer,    // 为一个 ArrayBuffer
    type: 'float32',          // 输入数据类型
};

this.session.run({
    // 这里 "input" 必须与 ONNX 模型文件中的模型输入名保持严格一致
    "input": xinput,
})
.then((res) => {

    // 这里使用 res.outputname.data
    // 其中 outputname 需要严格与 ONNX 模型文件中的模型输出名保持一致
    let num = new Float32Array(res.output.data)

运行之后的结果我们通过 res.output 获取。 需要注意的是, 这里的 input/output 不是所有模型固定的,需要严格与具体 ONNX 文件中的输入,输出名字对应。 回到之前 Netron 看到的 mobilenet 模型描述信息:我们可以看到本模型有一个输入,名字叫做 “input”,有一个输出,名字就叫做 “output”。 因此我们设置输入时, session.run({"input": xinput}), “input” 即为 ONNX 模型中的 input name. 当有多个输入时,我们通过session.run({"input1": xxx, "input2": xxx}) 来分别对模型中名字为 “input1” 和 “input2” 的输入设置数据。 同样的,我们获取模型的输出时,res.output 指获取名字为 “output” 的输出。

不管是模型的输入还是输出 Tensor, 都是一个 Object,包含了 shape,type 和 data 三个属性。其中 data 是一块ArrayBuffer。

2.4 后处理

这个示例的后处理过程比较简单。拿到模型输出后,我们通过一个 argMax 操作,计算出分数最高的分类索引,然后将这个索引转换成对应的类别名称。

let num = new Float32Array(res.output.data)

var maxVar = num[0];

var index = 0;

for (var i = 1; i < num.length; ++i)
{
if (maxVar < num[i])
{
    maxVar = num[i]   
    index = i     
}
}

this.getClass(index);

3 运行效果

扫描下方二维码,点击接口-通用AI推理能力-mobileNet,可以查看运行效果。



运行 demo,可以看到摄像头在采集的同时,会实时将分类结果显示在页面下方。



完整的 demo 请参考官方 GitHub 上的微信小程序示例

算子支持列表

详细的算子支持情况,请参考算子支持列表。

HarmonyOS 适配指南

基础库从 3.7.0 起正式支持 HarmonyOS 平台,后续与其它平台一致,通过后台灰度更新基础库,开发者工具可在详情 – 本地设置 – 调试基础库切到 3.7.0 版本进行开发调试。

架构概览

微信小程序在 HarmonyOS 平台的运行环境与安卓类似,即逻辑层的 JavaScript 代码运行在 v8 中,视图层是基于 HarmonyOS 原生的 ArkWeb 引擎来渲染,而 Skyline 渲染引擎在支持中,暂未提供。

此外,微信小程序的运行机制、更新机制、组件框架等均保持一致,但在一些特性支持度上会有区别。

适配方式

目前微信小程序在 HarmonyOS 平台与其它平台的区别主要是 HarmonyOSebView 引擎及涉及原生能力的特性上。

前者在 HarmonyOS 上使用的是 ArkWeb 引擎,可能存在一些依赖 WebView 的特性上的差异,如 CSS 样式相关,这类问题需按实际情况兼容;

后者大多是与组件/接口相关,可通过 wx.canIUse 接口或者通过 wx.getDeviceInfo().platform === 'ohos' 判断,对业务逻辑做必要的兼容。

注意:如在微信开发者工具中模拟鸿蒙,则需判断 wx.getDeviceInfo().system==’HarmonyOS’ (工具中 platform 为 devtools)

调试方式

  • 通过开发者工具调试
  1. 下载最新的nightly版开发者工具,通过最新开发者工具调试
  2. 调试基础库版本选择 3.7.0+
  3. 选择「微信小程序模式」,并选择华为鸿蒙机型
  4. 支持使用 wx.canIUse 判断接口是否可使用
  • 通过真机调试

在 HarmonyOS 的应用商店下载,安装后即可正常打开微信小程序进行调试。

支持情况

以下罗列出暂未支持的特性,对使用到未支持的特性需做好兼容。其中组件/接口具体的支持情况可跳转至对应文档查看,部分支持的一般代表少数高阶功能不支持

框架

特性 支持情况
Skyline 渲染引擎 支持中
初始渲染缓存 不支持
暗黑模式 不支持
周期性更新 不支持
数据预拉取 不支持
无障碍访问 不支持
分享朋友圈 不支持

组件

组件 支持情况
无障碍访问 不支持
keyboard-accessory 不支持
channel-live 不支持
channel-video 不支持
voip-room 不支持
map 部分支持
canvas 部分支持
ad/ad-custom 不支持
official-account 不支持
xr-frame 不支持
web-view 部分支持

接口

模块 接口 支持情况
基础-生命周期 wx.onApiCategoryChange / wx.offApiCategoryChange / wx.getApiCategory 不支持
基础-应用级事件 wx.onThemeChange / wx.offThemeChange / wx.onAudioInterruptionEnd / wx.onAudioInterruptionBegin / wx.offAudioInterruptionEnd / wx.offAudioInterruptionBegin 不支持
基础-性能 wx.preloadWebview / wx.preloadSkylineView 不支持
路由-自定义路由 支持中
跳转 wx.openEmbeddedMiniProgram / wx.onEmbeddedMiniProgramHeightChange / wx.offEmbeddedMiniProgramHeightChange 不支持
转发 wx.showShareImageMenu / wx.onCopyUrl / wx.offCopyUrl 不支持
界面-交互 wx.enableAlertBeforeUnload / wx.disableAlertBeforeUnload 支持中
界面-滚动 ScrollViewContext 不支持
界面-置顶 wx.setTopBarText 不支持
界面-窗口 不支持
界面-worklet动画 支持中
支付 wx.requestCommonPayment / wx.requestVirtualPayment / wx.openHKOfflinePayView 不支持
数据缓存-数据预拉取和周期性更新 wx.getBackgroundFetchData / wx.onBackgroundFetchData / wx.setBackgroundFetchToken / wx.getBackgroundFetchToken 不支持
数据缓存-缓存管理器 不支持
画布 部分支持
媒体-视频 wx.openVideoEditor 不支持
媒体-音频 只支持 WebAudio 不支持
媒体-音视频合成 不支持
媒体-画面录制器 不支持
媒体-视频解码器 不支持
开放接口-卡券 不支持
开放接口-发票 不支持
开放接口-生物认证 不支持
开放接口-车牌 不支持
开放接口-视频号 wx.openChannelsEvent 不支持
开放接口-微信客服 不支持
设备-联系人 wx.addPhoneContact 不支持
设备-无障碍 不支持
设备-电量 wx.onBatteryInfoChange / wx.offBatteryInfoChange 不支持
设备-网络 wx.onNetworkWeakChange / wx.offNetworkWeakChange / wx.offNetworkStatusChange 不支持
设备-屏幕 wx.onScreenRecordingStateChanged / wx.offScreenRecordingStateChanged / wx.getScreenRecordingState 不支持
设备-内存 不支持
AI 不支持
Worker 部分支持
广告 不支持
Skyline 支持中
XR-FRAME 不支持

大屏适配指南

目前市面上的用户设备大致可分为小屏的手机端、中屏的平板、大屏的 PC 端三类,而在这三类设备中又会有细小的尺寸差别,也称作屏幕碎片化。

随着微信小程序能够在越来越多的设备终端上运行,开发者也应该针对不同的屏幕尺寸进行相应的适配。

关于如何在设计、用户体验上实现更好的多端适配微信小程序,可参考微信小程序适配设计指南

1. 适配场景

按照一般的适配原则,结合微信小程序特点,通常在以下三种情况中需要进行适配:

同一类设备下,尺寸有细微差别

使用微信小程序提供的 rpx 单位,在尺寸差别不大的情况下对页面布局进行等比缩放。

在允许屏幕旋转的情况下,可分为横屏与竖屏

手机端设置 "pageOrientation": "auto" 或 iPad 上设置 "resizable": true 时会允许屏幕旋转,此时使用 Page 的 onResize 事件或者 wx.onWindowResize 方法可对该操作进行监听,进而判断是使用横屏还是竖屏布局。

不同类设备或者能够自由拖拽窗口的 PC 微信小程序

微信小程序目前是基于 Webview 实现,利用 CSS 媒体查询可实时监听屏幕尺寸大小,在不同的屏幕下展现不同的 UI 布局,结合 Flex 弹性布局、Grid 网格布局便能实现更加响应式的适配方案。

2. matchMedia – 抽象式媒体查询

微信小程序基础库基于 window.matchMedia API 新增了一组过程式与定义式接口 match-media 。开发者可以通过 <match-media>wx.createMediaQueryObserver 来显式地使用媒体查询能力,对于多端适配来说,它有以下优势:

  1. 开发者能够更方便、显式地使用 Media Query 能力,而不是耦合在 CSS 文件中,难以复用。
  2. 能够在 WXML 中结合数据绑定动态地使用,不仅能做到组件的显示或隐藏,在过程式 API 中可塑性更高,例如能够根据尺寸变化动态地添加 class 类名,改变样式。
  3. 能够嵌套式地使用 Media Query 组件,即能够满足局部组件布局样式的改变。
  4. 组件化之后,封装性更强,能够隔离样式、模版以及绑定在模版上的交互事件,还能够提供更高的可复用性。
  5. 浏览器内置 API ,能够在所有基于 Webview 的微信小程序上使用,兼容性良好。

match-media 具体使用方法可参考相关 API 文档

3. 自适应布局

为了让开发者更好的自适应大屏,微信小程序提供了 row/col 组件 供开发者使用。

自适应的主要特性是:

  • 整行最多只有 24 份,多余的排列会自动向下换行
  • 每个尺寸设置并不会影响到其它尺寸的布局

设计指引与代码示例

同时我们也提供了多端适配示例,体验路径:「扩展能力」 -> 「多端适配(需在PC端体验)」

示例微信小程序中,基于 row/col 组件 简单实现了常见的适配场景,例如:

  • 屏幕越大,布局不变,模块左右伸缩

  • 屏幕越大,内容越多,模块内容换行排列

  • 屏幕越大,布局改变,模块内容可折叠 / 展现

4. 分栏模式

在 PC 等能够以较大屏幕显示微信小程序的环境下,微信小程序支持以分栏模式展示。分栏模式可以将微信窗口分为左右两半,各展示一个页面。

frameset-screenshot

DarkMode 适配指南

微信从 iOS 客户端 7.0.12、Android 客户端 7.0.13 开始正式支持 DarkMode,微信小程序也从基础库 v2.11.0、开发者工具 1.03.2004271 开始,为开发者提供微信小程序内的 DarkMode 适配能力。

开启 DarkMode

app.json中配置darkmodetrue,即表示当前微信小程序已适配 DarkMode,所有基础组件都会根据系统主题展示不同的默认样式,navigation bar 和 tab bar 也会根据下面的配置自动切换。

相关配置

app.json中配置darkmodetrue时,微信小程序的部分配置项可以通过变量的形式来配置。在变量配置文件中定义不同主题下的颜色或图标,方法如下:

  1. app.json中配置themeLocation,指定变量配置文件 theme.json 的路径,例如:在根目录下新增theme.json,需要配置"themeLocation":"theme.json"
  2. theme.json中定义相关变量;
  3. app.json中以@开头引用变量。

支持通过变量配置的属性:

  • 全局配置的 window 属性与页面配置下的属性
    • navigationBarBackgroundColor
    • navigationBarTextStyle
    • backgroundColor
    • backgroundTextStyle
    • backgroundColorTop
    • backgroundColorBottom
  • 全局配置 window.tabBar 的属性
    • color
    • selectedColor
    • backgroundColor
    • borderStyle
    • list
      • iconPath
      • selectedIconPath

数据预拉取

预拉取能够在微信小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升微信小程序的打开速度 。

使用流程

1. 配置数据下载地址

数据来源为开发者服务器时支持配置灰度比例,灰度数据下载地址可以区别于数据下载地址,灰度比例不可回退,且 100% 灰度视为更新数据地址为灰度数据地址, 如需进行测试,可将灰度比例改为百分之 0,即只对开发者体验者进行灰度。

  1. 登录微信小程序 MP 管理后台,进入开发管理 -> 开发设置 -> 数据预加载,点击开启
  2. 个人主体微信小程序仅支持配置云开发环境
  3. 非个人主体微信小程序支持配置HTTPS数据下载地址、 云开发环境

2. 设置 TOKEN

用户登录微信小程序后,微信小程序可以调用 wx.setBackgroundFetchToken() 设置一个自定义 TOKEN 字符串,可以跟用户态相关,TOKEN 会在下一次预拉取或周期性更新,向开发者服务器发起请求时带上,便于服务器校验请求合法性。

Tips: wx.setBackgroundFetchToken 是可选接口,不是必须调用的。

示例:

App({
  onLaunch() {
    // 用户登录后
    wx.setBackgroundFetchToken({
      token: 'xxx'
    })
  }
})

3. 微信客户端提前拉取数据

当用户打开微信小程序时,微信服务器将向开发者服务器(上面配置的数据下载地址)发起一个 HTTP GET 请求,其中包含的 query 参数如下,数据获取到后会将整个 HTTP body 缓存到本地。

参数中 token 和 code 只会存在一个,用于标识用户身份。注意:如果选择使用 code,触发数据预拉取时可能会刷新用户登录态,详见 checkSessionKey

参数 类型 必填 说明
appid String 微信小程序标识。
token String 前面设置的 TOKEN。
code String 用户登录凭证,未设置TOKEN时由微信侧预生成,可在开发者后台调用 auth.code2Session,换取 openid 等信息。
timestamp Number 时间戳,微信客户端发起请求的时间
path String 打开微信小程序的路径。
query String 打开微信小程序的query。
scene Number 打开微信小程序的场景值。
customMiniprogramVersion String 微信小程序版本号

query 参数会使用 urlencode 处理

开发者服务器接口返回的数据类型应为字符串,且大小应不超过 256KB,否则将无法缓存数据

4. 读取数据

用户启动微信小程序时,调用 wx.getBackgroundFetchData 和 wx.onBackgroundFetchData 获取已缓存到本地的数据。

示例:

App({
  onLaunch() {
    wx.onBackgroundFetchData(() => {
      console.log(res.fetchedData) // 缓存数据
      console.log(res.timeStamp) // 客户端拿到缓存数据的时间戳
    })
    wx.getBackgroundFetchData({
      fetchType: 'pre',
      success(res) {
        console.log(res.fetchedData) // 缓存数据
        console.log(res.timeStamp) // 客户端拿到缓存数据的时间戳
        console.log(res.path) // 页面路径
        console.log(res.query) // query 参数
        console.log(res.scene) // 场景值
      }
    })
  }
})

调试方法

为了方便调试数据预拉取,工具提供了下面的调试能力给到开发者,具体可查看预拉取数据调试。

周期性更新

基础库 2.8.0 开始支持,低版本需做兼容处理。

生效条件:用户七天内使用过的微信小程序

周期性更新能够在用户未打开微信小程序的情况下,也能从服务器提前拉取数据,当用户打开微信小程序时可以更快地渲染页面,减少用户等待时间,增强在弱网条件下的可用性。

使用流程

1. 配置数据下载地址

数据来源为开发者服务器时支持配置灰度比例,灰度数据下载地址可以区别于数据下载地址,灰度比例不可回退,且 100% 灰度视为更新数据地址为灰度数据地址, 如需进行测试,可将灰度比例改为百分之 0,即只对开发者体验者进行灰度。

  1. 登录微信小程序 MP 管理后台,进入开发管理 -> 开发设置 -> 数据周期性更新,点击开启
  2. 个人主体微信小程序仅支持配置云开发环境
  3. 非个人主体微信小程序支持配置HTTPS数据下载地址、 云开发环境

2. 设置 TOKEN

用户登录微信小程序后,微信小程序可以调用 wx.setBackgroundFetchToken() 设置一个自定义 TOKEN 字符串,可以跟用户态相关,TOKEN 会在下一次预拉取或周期性更新,向开发者服务器发起请求时带上,便于服务器校验请求合法性。

Tips: wx.setBackgroundFetchToken 是可选接口,不是必须调用的。

示例:

App({
  onLaunch() {
    // 用户登录后
    wx.setBackgroundFetchToken({
      token: 'xxx'
    })
  }
})

3. 微信客户端定期拉取数据

微信客户端会在一定的网络条件下,每隔 12 小时(以上一次成功更新的时间为准)向配置的数据下载地址发起一个 HTTP GET 请求,其中包含的 query 参数如下,数据获取到后会将整个 HTTP body 缓存到本地。

参数 类型 说明
appid String 微信小程序标识
token String 前面设置的 TOKEN
timestamp Number 时间戳,微信客户端发起请求的时间

query 参数会使用 urlencode 处理

开发者服务器接口返回的数据类型应为字符串,且大小应不超过 256KB,否则将无法缓存数据

4. 读取数据

用户启动微信小程序时,调用 wx.getBackgroundFetchData() 获取已缓存到本地的数据。

示例:

App({
  onLaunch() {
    wx.getBackgroundFetchData({
      fetchType: 'periodic',
      success(res) {
        console.log(res.fetchedData) // 缓存数据
        console.log(res.timeStamp) // 客户端拿到缓存数据的时间戳
      }
    })
  }
})

调试方法

由于微信客户端每隔 12 个小时才会发起一次请求,调试周期性更新功能会显得不太方便。 因此为了方便调试周期性数据,工具提供了下面的调试能力给到开发者,具体可查看周期性数据调试。