外滩大会安全 AR 沙盘首秀--背后的前端技术

简介: 外滩大会安全 AR 沙盘首秀--背后的前端技术

本文来源:支付宝体验科技


🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师墨输,介绍了外滩大会安全 AR 沙盘首秀--背后的前端技术,AR 沙盘是大安全继『AR 打怪兽』后在 AR 方面又一次尝试,不同于之前打怪兽项目是使用 AR 团队集成好的 3Dof 算法,本次采用了算法团队自研的 3D marker 算法,此外模型更多,场景、渲染逻辑、动画的组织关系也都更为复杂。



  1. 前言


1.1 我们要做什么


蚂蚁安全有九大实验室,涉及多个领域,比如专注 AI 安全和智能风控的天筭(suan)、致力于智能安全检测能力的天玑、注重数据安全保护的天堑等,九大实验室一起协作来防范各种黑灰产的恶意进攻。而 9.7-9.9 在上海举办的外滩大会,是一次很好的对外展示九大实验室的机会,我们希望借助这次机会,让参会人员可以了解九大实验的功能,并借助 AR 的玩法,让用户对探索实验室产生更大的兴趣。


我们希望有一个实体的物理沙盘,物理沙盘通过 3D 打印出九大实验室的基本模型。用户通过打开支付宝的活动小程序,可以使用 AR 功能,利用 AR 扫一扫,可以看到真实的物理模型和虚拟模型进行虚拟结合的展示,并通过点击虚拟模型,进行更进一步的详情获取。


1.2 我们是谁


本项目是外滩大会中蚂蚁安全实验室的一次统一展示,因此由「蚂蚁大安全前端技术部」(Ant Security Frontend,ASF)负责前端的内容展示。


我们隶属于蚂蚁大安全技术部,为整个大安全提供前端及体验技术支撑。我们肩负了安全能力产品化的重任,致力于打造有温度的安全体验。


  2. 实现流程


2.1 框架


AR 游戏不同于普通的互动渲染游戏,不仅需要在虚拟空间中渲染出虚拟模型,还得借助摄像头传输视频流,展示现实真实场景。目前适合的形态是小程序,通过在小程序中引入适配 Galacean 的 AR 框架,集成了 AR 的相关能力。


2.2 物体识别


在小程序 AR 框架中,目前已经集成了 6Dof、3Dof 以及 3Dof(重力)的运动追踪方法。如下 Demo 视频所示,视频中是在 iOS 设备中用到了 6Dof 方法实现的,视频中几个物体都可以稳定的悬浮在真实模型的上方。

image.png

:28

但是目前模板中预设的 6Dof、3Dof 皆无法提供物体的识别定位能力,假如应用刚开始启动创建的空间坐标系,与真实的沙盘没有对应上,则无法达到虚实结合的效果。此外目前 6Dof 仅可在 iOS 中使用。


这里就要使用到视觉算法团队提供的 3D marker 算法,通过 3D marker 算法,可以识别到真实物体的空间坐标,然后通过不断调整虚拟物体的坐标,即可实现 AR 中虚实结合的效果,让虚拟物体包裹住实体沙盘进行展示。


2.3 模型


本次项目有外部合作方光线云,由光线云提供模型,但光线云一直以来使用的是自研的引擎,模型并不是 Galacean 所支持的 gltf 格式,因此需要将其转为 gltf 格式,此外由于我们依赖于支付宝,这类超级 app 对应用性能有着极高的要求,在 gltf 资源时,也要求我们不能超过 5M,因此对虚拟模型的面数也有要求。


2.4 功能及链路


针对整体的交互链路,设计了引导入口页及游戏体验页。


在引导入口页,需要借助 lottie 动画,告知用户游戏体验流程,并且在过程中,做一些预处理,比如判断用户设备是否满足 AR 要求、判断客户端版本是否满足要求、引导开启摄像头权限、预下载算法的动态 bundle、预加载 gltf 资源。


在游戏体验页,则需要初始化 Galacean 场景、初始化 3D marker 算法、渲染 gltf 资源、动画控制等操作。


2.4.1 算法集成


本次使用视觉算法团队研发的 3D marker 算法,需要将其算法集成到整个 AR 项目中,这里是通过创建一个类 Detecting,这个类会获取算法提供过来的位置信息,并记录下来,然后通过一个类 PointCloud 将位置信息以点云的形式渲染到空间中。


效果如视频中所示的蓝点

image.png

后续将点云置为透明,然后将点云视为父 Entity,将实验室模型、二级面板模型挂在到点云上即可实现沙盘识别定位的效果。


2.4.2 实验室模型展示逻辑


在最早的设计方案中,实验室模型渲染的效果是 1-9 的关系,比如当镜头拉近,某个实验室在屏幕中心位置,则该实验室播放模型动画,其他 8 个实验室处于静止状态,当镜头拉远,9 个实验室全部进入屏幕范围后,则 9 个实验室全部启动动画播放。


但由于 3D marker 算法是将整个沙盘当作为一个整体去识别的,因此最终返回的也是一个整体的坐标,该坐标是整个沙盘模型的基础坐标,有该坐标,代表识别到了整个沙盘模型,无该坐标,代表没有识别到沙盘模型。也正是因为一个坐标,导致9个实验室必须作为沙盘模型的一个整体去控制渲染,无法单独针对某一个实验室做单独的控制。


此外在开发过程中发现,在 iOS 设备中,当 9 个实验室模型同时存在且播放动画时,整个应用极为卡顿。经过咨询,Galacean 引擎为 Web 优先、移动优先的渲染引擎,主要内容都是使用js来编写的,目前模型动画计算、模型列表和组织渲染数据都是在 js层,计算后在放到 GPU 中进行渲染。


而苹果出于安全的考虑对 JIT 进行了限制,iOS 上自行创建的 js 上下文运行效率较差。详细内容可见:开挂了:iOS 14.2 开启 JIT 支持,大幅提升 JS 性能!https://forum.cocos.org/t/ios-14-2-jit-js/99999)。简单理解可以当做假如在安卓设备下,js 可发挥 60% 的性能,而 iOS 可能只能发挥 30% 的性能。

image.png

而在多模型、多动画的情况下,由于所有的内容都是在 js 进行计算,就必然会导致 iOS 的卡顿现象。为了改善 iOS 的效果,不会因为卡顿导致体感较差,因此在 iOS 下会将实验室模型动画进行暂停。在这种情况下,也不适合实现 1-9 的实验室动画控制。


2.4.3 实验室模型的位置


关于模型数量,我们最为希望的是一个模型,模型里内置了 9 个实验室,这样使用起来最为简单,也可以统一控制沙盘的动画,但这样对 gltf 资源的制作要求较高,一个 gltf 素材包含这么多的内容,必然会增加素材的大小,而超过 5M 的素材是无法渲染的,因此最终采用的方案是拆开成了 9 个模型,每个模型是一个实验室。这里首先要明确一下 Galacean 的空间坐标系

image.png

上图所示分别是左手坐标系和右手坐标系。

  • Unity 的坐标系是左手坐标系,默认旋转方向为顺时针;
  • Galacean 的坐标系是右手坐标系,默认旋转方向为逆时针。

在 Galacean 的具体使用中,我们默认摄像头面对的方向为 -z。实验室模型需要分别放置在对应的实体沙盘位置上,这种情况下是有两种方案的:

  • 方案一:每个模型的位置都是基于自身的原点,每个模型在渲染到空间中后,需要对模型的位置进行微调,将其挪动至对应位置。

image.png

  • 方案二:将沙盘虚拟模型作为一个整体提供过来,即每个模型的位置的原点(0,0,0)都是基于沙盘原点的,而不是以自身作为原点,这样在将模型渲染到空间中后,不用针对每个模型进行位置调整,就会自动形成整齐的 9 大实验室组合关系。

image.png

通过上述比较可以看出,方案二使用成本和调试成本都较低,因此最终选择了方案二。


2.4.4 二级面板的展示


2.4.4.1 展示方案


我们希望在点击虚拟实验室后,展开二级面板,用来介绍每个实验室的功能。关于这种展示,有两种展示方案

  • 方案一:每个实验室对应的二级面板在实验室上方展示,用户点击后可取消展示,类似如下效果

image.png

  • 方案二:每个实验室对应的二级面板在固定位置展示,用户同样点击后可取消展示,但因为在固定位置展示,因此同时只能存在一个二级面板,类似这种效果

image.png

方案一很明显会存在多个二级面板同时打开的情况,整个可视范围就会变得极为拥挤,且多个二级面板同时打开也会造成设备性能问题,综合考虑用户体验和设备性能,最终决定在方案二的基础上,当二级面板展示后,实验室模型也消失,可以减少可视范围上的冗余信息,更好的使用户的注意力聚焦,如下 Demo 效果。

image.png

2.4.4.2 交互方案


若想关闭二级面板,需要点击对应位置上的实验室模型,而实验室模型由于此时已经隐藏,是没有明显的可点位置的,且碰撞器也是不可视状态,在实际渲染的时候,是没有颜色的。这里综合考虑用户的操作习惯,采用类似蒙层点击的方案,当二级面板展示时,用户也可以通过点击屏幕中任何位置,来实现二级面板的隐藏。


关于此处的功能,借助了操作区域的全局 view 标签注册的 onTouchEnd 来监听,并调用 AR 页面暴露出的 hideWin 实例来实现。这样即不用全局添加碰撞器,也可以实现这种功能。

<canvas
    disable-scroll="true"
    onReady="onCanvasReady"
    class="canvas"
    id="canvas"
    type="webgl"
  />
<view
  disable-scroll="true"
  class="full"
  onTouchStart="onTouchStart"
  onTouchMove="onTouchMove"
  onTouchEnd="onTouchEnd"
  onTouchCancel="onTouchCancel"
>
<!-- index.ts -->
<script>
  // ...other code
  onTouchEnd(e) {
    dispatchPointerUp(e);
    dispatchPointerLeave(e);
    GameCtrl.ins.hideWin();
  }
</script>
<!-- GameCtrl.ts -->
<script>
  // ...other code
  /**
   * 点击屏幕隐藏二级面板
   */
  hideWin() {
    // 仅在实验室模型不展示时可隐藏二级面板
    if (!shapanRoot.isActive) {
      let activeWin;
      // 找到当前正在展示的二级面板
      for (const key in winList) {
        if (winList[key].root.isActive) {
          activeWin = winList[key];
        }
      }
      // 播放二级面板消失动画
      const animator = activeWin.root.getComponent(Animator);
      const animatorName = activeWin.animations![0].name;
      const state = animator.findAnimatorState(animatorName);
      activeWin.isPlayLoop = false;
      animator.speed = -1;
      state.wrapMode = 0;
      state.clipStartTime = 0;
      state.clipEndTime = 0.46;
      animator.play(animatorName);
    }
  }
</script>


2.4.5 碰撞器的使用


在上一节中,有提到碰撞器的概念。在 Galacean 中,因为是在 canvas 中创建虚拟场景,点击事件不同于普通的 dom 中添加的 click 事件,需要针对可被触发的物体增加碰撞器才可实现,通过 onPointClick 即可实现点击事件的监听。


但碰撞器的添加,依赖于 Galacean 的物理引擎,否则会报错

image.png


因此在初始化引擎时需要添加上物理引擎相关的依赖,AR 项目可通过 LitePhysics 实现。

MiniXREngine.create({
  canvas,
  XRDelegate: AntAR,
  physics: new LitePhysics(),
})

此处的 LitePhysics 依赖于@galacean/engine-physics-lite,在该项目实现时,本项目仅能使用 1.0.0-beta.14 版本。


虽然整个游戏中,可点击的内容只有 9 大实验室和全屏位置,但是因为碰撞器是要挂载在 Entity 中的,所以整个游戏的 Entity 需要组织一下。

整个游戏的交互链路是:

  1. 点击某个实验室,展示对应的二级面板,9 大实验室模型消失;
  2. 再点击某个实验室,二级面板消失,9 大实验室模型展示;


想要实现 9 大实验室一同展示或消失的逻辑,可以通过遍历 9 个实验室来实现,但更为简单的方法是为 9 个实验室添加一个统一的父 Entity,通过直接控制父 Entity 来实现 9 个实验室一起展示或隐藏的操作。

但这要求碰撞器不能添加到与 9 大实验室共同的父 Entity 中,因为一旦该 Entity 隐藏,则碰撞器也会被一同隐藏,而碰撞器需要一直存在。这里使用了 Galacean 中自带的 clone 功能,用来将碰撞器挂载到上面。


此外上文有介绍,实验室模型可以直接添加到虚拟空间中,即可实现 9 个实验室在对应位置分开展示的效果,但是碰撞器创建都是默认在原点的,因此需要对每个碰撞器移动对应的距离,将实验室模型包裹住。

image.png

图中绿色的立方体就是设置的碰撞器,在实际使用中,会把材质取消,让其不展示。

// 碰撞器
const cubeSize = 0.26;
// init cube + 将碰撞器加入到新节点
const cubeEntity = collRoot.createChild('cube');
// 将cube位移到对应实验室模型的位置
switch (String(i)) {
  case '0':
    cubeEntity.transform.setPosition(-0.65, 0.7, 0.5);
    break;
  case '1':
    cubeEntity.transform.setPosition(-0.65, 0.7, 0);
    break;
  case '2':
    cubeEntity.transform.setPosition(-0.65, 0.7, -0.5);
    break;
  case '3':
    cubeEntity.transform.setPosition(0, 0.7, 0.5);
    break;
  case '4':
    cubeEntity.transform.setPosition(0, 0.7, 0);
    break;
  case '5':
    cubeEntity.transform.setPosition(0, 0.7, -0.5);
    break;
  case '6':
    cubeEntity.transform.setPosition(0.65, 0.7, 0.5);
    break;
  case '7':
    cubeEntity.transform.setPosition(0.65, 0.7, 0);
    break;
  case '8':
    cubeEntity.transform.setPosition(0.65, 0.7, -0.5);
    break;
  default:
    break;
}
cubeEntity.transform.setScale(cubeSize, cubeSize, cubeSize);
// 渲染cube,便于查看位置
// const renderer = cubeEntity.addComponent(MeshRenderer);
// const mtl = new BlinnPhongMaterial(collRoot.engine);
// const color = mtl.baseColor;
// color.r = 0.9;
// color.g = 0.0;
// color.b = 0.5;
// color.a = 0.5;
// mtl.isTransparent = true;
// renderer.mesh = PrimitiveMesh.createCuboid(collRoot.engine);
// renderer.setMaterial(mtl);
// 给cube添加碰撞器
const colliderSize = 1;
const boxColliderShape = new BoxColliderShape();
boxColliderShape.size.set(colliderSize, colliderSize, colliderSize);
const boxCollider = cubeEntity.addComponent(StaticCollider);
boxCollider.addShape(boxColliderShape);


2.4.6 模型渲染


2.4.6.1 模型制作要求


由于本次模型的制作是外部合作方制作的,因此不可避免会出现一些模型不规范的问题,比如模型载入后提示 Worker 报错,这是由于 gltf 中带有KHR_draco_mesh_compression扩展,galacean 在加载时会创建 Worker,小程序上暂时不支持 worker,engine.resourceManager.load不会有回调。

2.4.6.2 添加光照

当调试时,发现当把模型渲染出来后,有些模型会发生闪烁

image.png

怀疑是由于光照导致的。后通过引入一些外部的光照资源 HDRI • Poly Haven 解决该问题,该平台提供了很多光照资源

image.png



将其转为 gltf 后 Galacean 技术点记录,即可引入场景中。

// 环境光
engine.resourceManager
  .load<AmbientLight>({
    type: AssetType.Env,
    url: 'https://mdn.alipayobjects.com/portal_x4occ0/afts/file/A*34_0RLzSf2AAAAAAAAAAAAAAAQAAAQ/light.env'
  })
  .then((ambientLight) => {
    scene.ambientLight = ambientLight;
  });
// 添加方向光
const lightEntity = root.createChild('light');
const directLight = lightEntity.addComponent(DirectLight);
directLight.color.set(1, 1, 1, 1);
lightEntity.transform.setRotation(-45, -45, 0);

2.4.6.3 模型动画

九大实验室需要通过点击控制展示隐藏,在展示和隐藏的过程中,都伴随着动画的播放,分为 3 个阶段,分别是渐入、轮循、渐出。理想的情况是该模型上存在 3 个动画,当处于不同阶段时,分别播放对应动画。但由于动画提供方是外部合作方,他们并不能将其拆开为 3 个动画,仅能提供一个动画,在一个动画中包含 3 个阶段,通过自行控制动画播放,实现 3 种效果。

image.png

整个动画时间轴类似如上所示,在整个动画的归一化时间轴中,A 阶段「渐入」占了 0.46,「轮循」为剩下的阶段,如果需要实现渐出的效果,只需要将 A 阶段倒放即可。


这里就需要借助 Galacean 的 Animator 模块,通过将控制 Animator 的 state,进行控制动画的播放/暂停、状态控制、顺序控制等。


Galacean 中的动画系统可以将动画拆分开,进行单独控制,也可以整体控制动画的一些参数,项目中实际控制逻辑可见下方代码。

let isClicked = false;
// 给碰撞器cube增加脚本控制点击事件
cubeEntity.addComponent(Script).onPointerClick = () => {
  if (isClicked) return;
  isClicked = true;
  // 初始化活跃面板、动画名、动画变量、动画状态、可操作的面板
  let activeWin;
  let animatorName;
  let animator;
  let state;
  let operaWin;
  for (let key in winList) {
    if (winList[key].root.isActive) {
      // 获取到当前处于激活状态的面板
      activeWin = winList[key];
    }
  }
  
  if (activeWin !== undefined) {
    // 若当前存在处于激活状态的面板,表示已经二级面板展示,将当前面板的属性赋值给变量
    operaWin = activeWin;
    animatorName = activeWin.animations![0].name;
    animator = operaWin.root.getComponent(Animator);
    state = animator.findAnimatorState(animatorName);
  } else {
    // 若当前不存在处于激活状态的面板,表示没有二级面板展示,将实验室对应的二级面板属性赋值给变量
    operaWin = winList[`win${i}`];
    animatorName = winList[`win${i}`].animations![0].name;
    animator = operaWin.root.getComponent(Animator);
    state = animator.findAnimatorState(animatorName);
  }
  
  // 给动画添加状态机脚本
  state.addStateMachineScript(
    class extends StateMachineScript {
      onStateExit(
        winanimator: Animator,
        animatorState: AnimatorState,
        layerIndex: number
      ): void {
        // 根据二级面板展示情况,设置播放次数、起止时间即循序,并控制沙盘即二级面板的显隐
        if (operaWin.isPlayLoop) {
          animatorState.wrapMode = 1;
          animatorState.clipStartTime = 0.46;
          animatorState.clipEndTime = 1;
          winanimator.speed = 1;
          winanimator.play(winList[`win${i}`].animations![0].name);
        } else {
          winanimator.speed = 1;
          shapanRoot.isActive = true;
          operaWin.root.isActive = false;
        }
      }
    }
  );
  if (root.isActive) {
    // 沙盘展示:沙盘消失,二级面板展示
    operaWin.root.isActive = true;
    root.isActive = false;
    operaWin.isPlayLoop = true;
    state.wrapMode = 0;
    state.clipStartTime = 0;
    state.clipEndTime = 0.46;
    animator.speed = 1;
    animator.play(animatorName);
    isClicked = false;
  } else {
    // 沙盘消失:沙盘展示,二级面板消失
    operaWin.isPlayLoop = false;
    animator.speed = -1;
    state.wrapMode = 0;
    state.clipStartTime = 0;
    state.clipEndTime = 0.46;
    animator.play(animatorName);
    isClicked = false;
  }
};


2.4.6.4 遮挡效果


由于我们要实现的效果是将虚拟的物体套在实体沙盘上,因此必然会存在虚实结合,而虚实结合如果要自然,就必须存在真实的遮挡效果,否则会出现虚拟物体和实体物体强行耦合在一起,看起来极其违和。

image.png

要想模拟真实的遮挡效果,这里需要将实体沙盘模型做出一个模型

image.png

并将其值为透明即可,利用模型之间的关系实现遮挡效果。

engine.resourceManager
.load<GLTFResource>({
  url: 'https://mdn.alipayobjects.com/portal_x4occ0/afts/file/A*xN9wQbbbcZIAAAAAAAAAAAAAAQAAAQ/sp.glb',
  type: AssetType.GLTF,
})
.then((asset: GLTFResource) => {
  const { defaultSceneRoot, materials } = asset;
  // 初始化沙盘白模位置
  defaultSceneRoot.transform.setPosition(0, -0.45, 0);
  defaultSceneRoot.transform.setRotation(90, 0, 0);
  defaultSceneRoot.transform.setScale(1, 1, 1);
  spRoot.addChild(defaultSceneRoot);
  const meshRenderers = [];
  defaultSceneRoot.getComponentsIncludeChildren(
    MeshRenderer,
    meshRenderers
  );
  // 修改所有材质的colorWriteMask
  materials.forEach((material: Material) => {
    material.renderState.blendState.targetBlendState.colorWriteMask = ColorWriteMask.None;
  });
  // 将渲染优先级置为最小
  meshRenderers.forEach((meshRenderer: MeshRenderer) => {
    // 修改所有render的priority
    meshRenderer.priority = -999;
  });
});


  3. 总结


经过与算法团队 佐啰、高识同学 ,AR 团队常行同学以及外部支撑方的通力协作,最终在本届外滩大会前完成了最终效果的上线。

image.png

AR 沙盘是大安全继『AR 打怪兽』后在 AR 方面又一次尝试,不同于之前打怪兽项目是使用 AR 团队集成好的 3Dof 算法,本次采用了算法团队自研的 3D marker 算法,此外模型更多,场景、渲染逻辑、动画的组织关系也都更为复杂。


经过本项目,也积累了一些在互动渲染领域的经验,为后续更好的开展互动渲染类项目打下了基础。


相关文章
|
8天前
|
前端开发 JavaScript 持续交付
前端技术趋势:2024年值得关注的几个方面
【10月更文挑战第9天】前端技术趋势:2024年值得关注的几个方面
|
10天前
|
存储 前端开发 JavaScript
前端技术趋势:在动态变化中寻求稳定
【10月更文挑战第7天】前端技术趋势:在动态变化中寻求稳定
24 0
|
10天前
|
前端开发 数据可视化 JavaScript
现代前端开发:掌握关键技术与趋势
【10月更文挑战第7天】现代前端开发:掌握关键技术与趋势
29 0
|
22小时前
|
前端开发 JavaScript 安全
JavaScript前端开发技术
JavaScript(简称JS)是一种广泛使用的脚本语言,特别在前端开发领域,它几乎成为了网页开发的标配。从简单的表单验证到复杂的单页应用(SPA),JavaScript都扮演着不可或缺的角色。
10 3
|
2天前
|
人工智能 前端开发
2024 川渝 Web 前端开发技术交流会「互联」:等你来报名!
2024 川渝 Web 前端开发技术交流会「互联」:等你来报名!
2024 川渝 Web 前端开发技术交流会「互联」:等你来报名!
|
8天前
|
前端开发 JavaScript 开发者
探索现代Web前端技术:React框架入门
【10月更文挑战第9天】 探索现代Web前端技术:React框架入门
|
11天前
|
存储 缓存 监控
|
13天前
|
前端开发 JavaScript API
前端技术新趋势:PWA与Jamstack的融合探索
【10月更文挑战第4天】前端技术新趋势:PWA与Jamstack的融合探索
24 4
|
14天前
|
前端开发 搜索推荐 JavaScript
前沿技术:前端开发者的利器
【10月更文挑战第3天】前沿技术:前端开发者的利器
37 0
|
14天前
|
前端开发 JavaScript 编译器
前端开发新视界:2024年的五大技术趋势
【10月更文挑战第3天】前端开发新视界:2024年的五大技术趋势
31 0