支付宝 AR 空中写福技术揭秘

简介: 支付宝 AR 空中写福技术揭秘
🙋🏻‍♀️ 编者按:本文作者为蚂蚁集团前端工程师胡松,为你揭秘支付宝五福项目中,如何利用 AR 技术实现 AR 空中写福,欢迎享用~

在支付宝五福项目中,去年我们推出了手写福,获得了不错的反响,而今年在交互形式上做了一些创新,通过 AR 技术让用户感受到『福临其境』。 我们利用 AR 技术推出了 AR 空中写福,让用户可以在实景之中创作福字,把福字放到真实空间中,也可以 在空间中重现来自亲友的 3D 福字。 👆 上面是 AR 空中写福的示例。

背景

随着互联网产品的交互的深入发展,AR 技术逐渐火热。目前 AR 技术在游戏、医疗、交通、服装和教育等领域发挥作用。但在营销互动领域,却没有出现大量的应用。许多项目跃跃欲试,希望通过技术创新突破现有营销的交互形态。但是在技术侧却有诸多限制,目前主要有:

  1. 硬件限制严格:互动营销主要基于 H5,WebXR 技术尚未成熟,只能基于(ARKit,ARCore)
  2. 开发成本过高:需要前端、客户端、算法联合开发
  3. 性能容易陷入瓶颈:AR 与 AI 算力对 CPU 和 GPU 的占用过高

我们技术基建的现状:

  1. 支付宝小游戏容器:支付宝小游戏容器主要提供渲染能力,封装了底层的 OpenGL,暴露成类 WebGL API。还有把 Native 能力通过 JSAPI 暴露给业务侧。注意:小游戏容器并没有 DOM 的实现,所以 GUI 都主要通过 WebGL API 去绘制。
  2. ARSession:顾名思义是 AR 能力的输出,底层是 ARCore/ARKit/陀螺仪。
  3. Oasis Engine:作为游戏引擎提供了上层的封装,可以轻松地开发 3D 应用,并且通过适配器将部分 H5 API 桥接到小游戏容器层。

通过上面几项技术,我们可以通过前端(小程序)的技术栈和开发模式,提升了 AR 项目开发和迭代效率。接下来我们说下项目的开发流程。

支付宝 AR 的接入

1. 创建前端脚手架

我们采用了 Vite 作为项目工具去做 transpile 和 bundle。Vite 本身十分灵活,编译效率非常高,lib 模式可以直接打包出一个纯 js 文件提供给小游戏容器使用。我们提取出了一套通用的小游戏脚手架,支持了 TypeScript、NPM Packages,更符合现代前端的开发模式,详见开源仓库 create-oasis-app。

2. 适配小游戏容器

小游戏适配器帮助我们抹平 Web 端和小程序/小游戏端 的API 差异,比如 Image 接口,按照 Web 端的写法 new Image()即可,不用管小程序的 canvas.createImage()或者小游戏的 my.createImage()。这意味着你的项目只要能够让 Oasis Engine 在浏览器端正常运行,就可以通过适配器在小游戏等平台运行。详见开源仓库:miniprogram-adapter。

3. 绘制相机背景

虽然适配器能够抹平 API 差异,但是容器内 ARSession  的能力 API 是独有的。ARSession 返回的其实是每帧相机的一些相关数据,我们看到的 AR 背景是将相机数据通过 shader 绘制到屏幕上面的,接下来介绍如何通过 Oasis 创建一个背景,并每帧更新 AR 背景:

3.1. 初始化 AR 背景

我们先创建一个全屏的平面,用来绘制返回的相机数据,因为数据颜色格式是 YUV 格式,所以我们在片元着色器里面主要就干一件事,就是将 YUV 转成 RGB 颜色。

// 顶点着色器的代码就是绘制一个全屏的平面,用来绘制 AR 背景
const yuv_vs = `
attribute vec3 POSITION;
attribute vec2 TEXCOORD_0;
uniform mat4 u_uvMatrix;
varying vec2 v_uv;
void main() {
  vec2 flipUV = TEXCOORD_0;
  flipUV.y = 1.0 - flipUV.y;
  v_uv = (u_uvMatrix * vec4(flipUV, 1.0, 1.0)).xy;
  gl_Position = vec4( POSITION.xy, 1.0, 1.0);
}
`;
// 安卓端 YUV -> RGB 片元着色器代码
const yuv_fs_Android = `
uniform sampler2D u_frameY;
uniform sampler2D u_frameUV;
varying vec2 v_uv;
void main() {
  float y = texture2D(u_frameY, v_uv).a;
  vec4 uvColor = texture2D(u_frameUV, v_uv);
  float u = uvColor.a - 0.5;
  float v = uvColor.r - 0.5;
  float r = y + 1.13983 * v;
  float g = y - 0.39465 * u - 0.58060 * v;
  float b = y + 2.03211 * u;
  gl_FragColor = vec4(r, g, b, 1.0);
}
`;
// iOS 端 YUV -> RGB 片元着色器代码
const yuv_fs_iOS = `
uniform sampler2D u_frameY;
uniform sampler2D u_frameUV;
varying vec2 v_uv;
void main() {
  float y = texture2D(u_frameY, v_uv).a;
  vec4 uvColor = texture2D(u_frameUV, v_uv);
  float u = uvColor.r - 0.5;
  float v = uvColor.a - 0.5;
  float r = y + 1.04 * v;
  float g = y - 0.343 * u - 0.711 * v;
  float b = y + 1.765 * u;
  gl_FragColor = vec4(r, g, b, 1.0);
}
`;
 // 创建一个全屏平面,用来绘制 AR 背景
const bgEntity = rootEntity.createChild("ar-bg");
const bgRenderer = bgEntity.addComponent(MeshRenderer);
bgRenderer.mesh = PrimitiveMesh.createPlane(engine, 2, 2);
// 平面的材质,即上面的 YUV 转 RGB shader 代码
const bgMaterial = new Material(
  engine,
  isAndroid ? Shader.create("ar-bg-android", yuv_vs, yuv_fs_Android) : Shader.create("ar-bg-ios", yuv_vs, yuv_fs_iOS)
);
bgMaterial.renderState.depthState.compareFunction = CompareFunction.LessEqual;
// 先绘制不透明的物体再绘制 AR 背景,可以减少绘制遮挡的像素,提高性能
bgMaterial.renderQueueType = RenderQueueType.AlphaTest + 1;
bgMaterial.shaderData.setMatrix("u_uvMatrix", new Matrix());
// 禁止视椎体裁剪,因为要保证360度都能全屏显示 AR 背景
camera.enableFrustumCulling = false;

3.2. 每帧更新背景

初始化背景后,我们就只需要每帧更新相机数据。ARFrame 不仅包含了相机的视频流信息,也包含了相机的空间位置,通过修改 Transform.worldMatrix 可以让 3D 空间的相机与 AR 空间的相机坐标同步。ARFrame 的数据可以从 onARFrame 的回调函数中每帧获取:

myARSession.onARFrame((arframe) => {
  updateBackgroundScene(arframe);
});


从上面的 shader 代码中可看到,一共有几个uniform变量的值需要每帧更新 u_uvMatrixu_frameYu_frameUV。于是我们封装一个 updateBackgroundScene 函数用来更新 ARFrame 的数据:

function updateBackgroundScene(arframe) {
  const w = arframe.width;
  const h = arframe.height;
  const len = w * h;
  if (len <= 0) {
    return;
  }
  // 更新 u_uvMatrix
  if (arframe.capturedImageMatrix) {
    const matrix = bgMaterial.shaderData.getMatrix("u_uvMatrix");
    matrix.setValueByArray(arframe.capturedImageMatrix);
  }
  if (arframe.capturedImage) {
    const cameraFrame = arframe.capturedImage;
    let textureFrameY = bgMaterial.shaderData.getTexture("u_frameY");
    let textureFrameUV = bgMaterial.shaderData.getTexture("u_frameUV");
    if (!textureFrameY) {
      textureFrameY = new Texture2D(engine, w, h, TextureFormat.Alpha8, false);
      textureFrameY.wrapModeU = textureFrameY.wrapModeV = TextureWrapMode.Clamp;
      bgMaterial.shaderData.setTexture("u_frameY", textureFrameY);
    }
    if (!textureFrameUV) {
      textureFrameUV = new Texture2D(engine, w / 2, h / 2, TextureFormat.LuminanceAlpha, false);
      textureFrameUV.wrapModeU = textureFrameUV.wrapModeV = TextureWrapMode.Clamp;
      bgMaterial.shaderData.setTexture("u_frameUV", textureFrameUV);
    }
    // 更新 u_frameY
    textureFrameY.setPixelBuffer(new Uint8Array(cameraFrame, 0, len));
    // 更新 u_frameUV
    textureFrameUV.setPixelBuffer(new Uint8Array(cameraFrame, len));
  }
}


空中写福的玩法实现

如何实现写福

AR 空中写福分为两条业务链路:写福和福字回放。首先是写福链路,红色方块为关键技术点,绿色部分为关键的客户端 API 的依赖:接下来我们说下红色部分的关键技术实现。

笔刷转矢量

我们使用的是 2D 福字矢量化后挤出几何体的技术方案。为了看效果,我们先在 Blender 中做了初步的尝试,觉得效果还不错,再进一步进行技术调研:调研过程中,我们使用了多个开源项目实现了笔刷的绘制、矢量化和挤出。我们前期通过技术调研 + 迁移库到小游戏 + 技术 Demo 确定了技术可行性,确保了技术产品化的第一步。首先,为了实现笔刷,我们移植开源库 shodo 到小游戏容器中,裁剪不必要的功能,实现不错的书写效果:

然后,我们使用 potrace 开源库把位图数据变成矢量数据。虽然 potrace 有 js 版本,但是因为 iOS 容器中缺少 JIT,js 运算效率太低,最后我们把 potrace 的 c++ 版本植入到客户端中,通过 JSAPI 调用。

最后,挤出方案我们调研了 ThreeJS 的 ExtrudeGeometry 和 geometry-extrude,由于后者和 potrace 都使用标准的 GeoJSON 的数据接口,所以我们采用了第二种方案。

贴纸

完成写福后,用户可以从列表中选择贴纸,还有拖拽删除等功能。每一个贴纸都是一个 glTF 模型,添加贴纸其实就是一个加载模型的过程。在添加 glTF 模型后,可以给 entity 添加一个自定义组件,这里我们起名 DragComponent。在组件的 onPointerDownonPointerDrag生命周期内实现拖拽行为,具体代码可以参考 https://oasisengine.cn/0.6/docs/input-cn。贴纸使用了白色外描边的高亮方式,使对比更加明显自然,且性能最佳。

转场效果

在 2D 写福和贴纸完成之后,开始了 2D 到 3D 的转场:

由上图可看出,福字无缝切换到立体的过程,需要有一个 2D 转 3D 的逆变换,福字本身是在一个 NDC 空间内,需要先计算出在正交相机下的 orthographicSize 和福字的缩放比例,实现福字转到 3D 空间内的大小一致。因为之后的效果需要整个场景放在 3D 空间并且用正交相机展示,所以还需要一个正交到透视的线性插值,使得视角的变换有一定的过度。此时渲染的 3D 福字和贴纸处于旋转中。用户基本察觉不出一点微小的变化。并且在 3D 转场中,播放了一个 Lottie 效果,让整个过程变得炫酷起来。在后续的循环中,相机的变换会根据 AR 相机不断更新位置,使得用户可以 360 度观察特效和 3D 福在空间中的效果,而其余的包括特效,贴纸,3D 福在内的所有东西,都留在原地静待观察即可。

如何实现福字回放

回放就是把原始的笔触数据记录下来,再通过笔刷 + 矢量化 + 挤出的过程去完成。回放包括三个部分:记录数据,上传数据和数据回放。

1. 记录数据

笔迹数据的数据结构是这样的:

{
  char: [
    [[x, y, timestamp], [x, y, timestamp], [x, y, timestamp]],
    [[x, y, timestamp], [x, y, timestamp]],
    [[x, y, timestamp], [x, y, timestamp], [x, y, timestamp], [x, y, timestamp]]
  ],
  brush: { icon, extInfo },
  canvas2D: { width, height }
}


  • char: 笔迹数据,这是一个三层数组,最外层数组代表整个字,其中每一项代表代表一个笔划,比如例子中这个笔迹数据有三个笔划;第二层数组代表一个笔划,其中每一项代表这个笔划的一个点,比如例子中第一笔有三个点;第三层数组就代表一个点数据,其中记录了这个点的位置 xy 以及这个点的时间戳,这样在回放数据的时候可以比较真实地还原整个写字的过程。
  • brush: 笔刷数据,包括笔刷的图片和参数等信息。
  • canvas2D: 屏幕宽高,可以帮助我们在不同比例的屏幕上回放数据。

贴纸数据:

{
  markers: [
    [url,x,y],
    [url,x,y],
    [url,x,y],
  ]
}


因为用户可以添加多个贴纸,所以贴纸数据是一个数组,其中每个贴纸数据包括贴纸模型的链接以及位置。这里有一个真实的数据例子:https://mdn.alipayobjects.com/afts/file/A*1WrWS6pquA8AAAAAAAAAAAAAAQAAAQ?bz=biz_file

2. 数据抽稀

福字的轨迹数据大小是没有上限的,极端情况下可能会达到几M,所以数据要经过抽稀后在上传到 afts。抽稀算法很简单,相邻两个点距离小于10会被判断为冗余点剪裁掉:

for (let i = 0; i < charData.length; i++) {
  const originalStroke = charData[i];
  let last = null;
  charData[i] = [];
  for (let j = 0; j < originalStroke.length; j++) {
    const point = originalStroke[j];
    if (last) {
      const l = length(point[0], point[1], last[0], last[1]);
      if (l < 10 && j !== originalStroke.length - 1) {
        continue;
      }
    }
    charData[i].push(point);
    last = point;
  }
}


经过抽稀后,轨迹数据大小可以稳定在 10k 以下。

内存/性能优化

空中写福对内存的要求相对较高,从进入小游戏容易到项目结束,内存峰值理论应控制在 200M 以下较为安全。而优化手段包含了业务策略优化和容器底层优化两种,先来看一下最初的内存走势图,内存超标,其中 AR 算法侧相关的内存,由于算法需要优化相对困难,我们根据项目上线节奏,合理的选择收益比更大的几个优化点切入:

机型:小米11 pro

适当降低主画布分辨率

在移动端时代,近几年设备的分辨率逐步从 720p 提升到 2K,但 GPU 的渲染能力的提升并没有得到相应比例的提升。而且如果主画布的渲染分辨率采用物理分辨率的话,对于显存的消耗也难以承受。尤其是主画布,显存占用极高,这是因为主画布并非单纯的 RGBA 纹理,为了得到更好的渲染性能,WebGL 内部通常会采用双 Buffer 模式,也就是双画布,并且除了颜色纹理外,还需要一张深度纹理用于完成深度测试。所以我们采取三当配置,分别根据机型能力,按照物理分辨率进行了不同比例的缩放,分别为(0.8,0.6,0.5)。小游戏容器项目初期并不支持分辨率修改,经过优化策略的推动,容器层最终提供了该能力。大幅度降低了画布显存和提升渲染性能。

降低 AR 相机分辨率

同理 AR 相机的分辨率可采用类似的优化策略 - 降低AR 相机分辨率,并且如果主画布渲染分辨率大于 AR 相机分辨率时是一种没有任何受益的内存消耗,因为最终 AR 相机的内容依然要渲染到主画布。

支付宝小游戏 request 优化

我们在当时空中写福的内存分析中找了一个内存异常增长:在贴纸添加阶段内存会异常增幅 150M 左右,而贴纸本身的内存和显存占用小于10M。经过一系列排发现,容器层只要调用 request 就会产生内存大幅增长。原 request 的请求会把二进制数据 (ArrayBuffer) 转化为base64 string,然后通过 JSBridge 传递给前端;后面优化避开了二进制数据转化 base64 的过程,直接在 C++ 层创建 ArrayBuffer 对象,通过JSBinding 提供给前端使用。大幅减少了转换的性能开销和 base64 的缓存占用的内存开销。

福字画布上传优化

福字渲染的流程首先是将笔刷贴图绘制到一张 canvas 上,然后再把 canvas 上传到 GPU 纹理 Texture2D。WebGL 一个非常重要的优化原则就是减少 CPU 数据和 GPU 数据的交互。因此我们做了一个优化,当用户手指在屏幕绘制时更新 canvas,如果用户抬起手指时,我们可利用脏标记避免无效的 canvas 上传到 GPU 纹理。通过该方式,用户在写福字时,性能得到了一定幅度提升和减少发热。

适当降低视频录制分辨率

在对好友分享的链路上,我们有一个视频录制的功能。在视频录制期间内存出现约 50M 增长,这个问题和之前提到的主画布分辨率过高的问题类似。和渲染一样,过高的录制分辨率对用户的收益并不明显,而且内存和性能占用过高。在优化策略的推动,容器层同样开放了录制分辨率的设置,业务根据机型能力将录制分辨率调整为720p或540p,大幅改善了视频录制期间内存增长的情况。

总结

本次空中写福有大量用户参与写福和分享,抖音微博搜索 AR 空中写福能看到不少的案例。我们也会持续优化 AR 工程链路,并探索新的 AR 玩法。后续请关注 oasisengine.cn 我们会推出更多的 AR 开发案例,让人人都可以参与到 AR 业务的开发当中来。可以 star 我们的 github 仓库(链接:https://github.com/oasis-engine/engine,关注我们后续的规划,在issues(链接:https://github.com/oasis-engine/engine/issues 里给我们提需求和问题。

如何联系我们

Oasis 开源社区群 (钉钉):

Oasis 开源社区群 (微信):


*如果微信二维码失效

可添加群管理员微信:zengxinxin2010

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
3月前
|
人工智能 物联网 vr&ar
AR与VR技术的融合:开启全新的现实体验
在当今快速发展的科技领域中,混合现实(AR)和增强现实(VR)技术成为引人注目的热门话题。本文将探讨AR与VR技术的融合应用,以及它们给我们带来的全新现实体验。通过结合虚拟和真实世界的元素,AR与VR技术正逐渐改变着我们对于交互、娱乐、教育和工作的理解。
|
3月前
|
vr&ar 开发者 Python
探索未来的现实世界:混合现实(AR)与增强现实(VR)技术的应用Python异步编程:解放性能的重要利器——异步IO库深入解析
在当今科技飞速发展的时代,混合现实(AR)和增强现实(VR)技术正迅速改变着我们对现实世界的认知和体验。本文将介绍这两种技术的基本原理以及它们在不同领域的广泛应用,包括教育、医疗、旅游、娱乐等。混合现实和增强现实技术为我们带来了全新的沉浸式体验,将人与数字世界融合在一起,为未来的现实世界带来无限可能。 在当今信息爆炸的时代,高效的编程方式成为开发者追求的目标。Python异步编程与其强大的异步IO库(例如asyncio)成为了解放性能的重要利器。本文将深入解析Python异步编程以及异步IO库的原理和使用方法,帮助读者进一步掌握这一技术,提升开发效率。
|
4月前
|
传感器 数据可视化 vr&ar
AR增强现实技术特点、工作原理等简介
AR增强现实技术特点、工作原理等简介
155 0
|
4月前
|
存储 数据处理 vr&ar
实时云渲染技术为何被称为VR和AR领域的加速剂?
实时云渲染技术为何被称为VR和AR领域的加速剂?
|
9月前
|
搜索推荐 机器人 大数据
【AR技术】AR教学机器人
【AR技术】AR教学机器人
|
11月前
|
供应链 定位技术 新制造
【年终特辑】看见科技创新力量 洞见时代创业精神—ARVR—灵犀微光:AR光波导显示技术先行者与领导者
【年终特辑】看见科技创新力量 洞见时代创业精神—ARVR—灵犀微光:AR光波导显示技术先行者与领导者
|
11月前
|
算法 JavaScript 前端开发
支付宝五福 AR 玩法及背后的互动引擎—Paladin
支付宝五福 AR 玩法及背后的互动引擎—Paladin
493 0
|
11月前
|
定位技术 新制造 vr&ar
《「看见新力量」》第六期——No.14专访灵犀微光副总裁陈飞——AR光波导显示技术先行者与领导者(上)
《「看见新力量」》第六期——No.14专访灵犀微光副总裁陈飞——AR光波导显示技术先行者与领导者(上)
171 0
|
11月前
|
供应链 新制造 vr&ar
《「看见新力量」》第六期——No.14专访灵犀微光副总裁陈飞——AR光波导显示技术先行者与领导者(下)
《「看见新力量」》第六期——No.14专访灵犀微光副总裁陈飞——AR光波导显示技术先行者与领导者(下)
181 1
|
6月前
|
设计模式 测试技术 vr&ar
提升你的Android开发技能:从AR/VR沉浸到UI设计和故障排除(三)
提升你的Android开发技能:从AR/VR沉浸到UI设计和故障排除

热门文章

最新文章