41-如何仿写一个简易版的 Holokit

1,007 阅读4分钟

说明

ARKit系列文章目录

Holokit 简介

Holokit 是一个利用 ARKit 制作的 app,它先以 AR 形式在地面上放上虚拟物体,然后点击右上角的 MR 按钮,进入到 MR 模式。配合十几块钱的纸盒子和半透半反膜,就能让手机上的虚拟物体出现在现实中。

基本思路

基本思路是利用 ARKit 建立场景,并进行 6Dof 的追踪,这样就能知道 3D 模型在什么位置,相机(即手机)在什么位置。

然后另外再建立一个 SceneKit 场景,背景设置为黑色,这个场景中添加一个同样的 3D 模型。然后再加两个相机,分别对应左视图和右视图。

接下来就是同步的工作了,当 AR 场景中的 3D 物体或者相机位置发生移动时,我们手动对 SceneKit 中的 3D 物体和头部 node 位置进行调整。

需要注意的细节

基本思路非常简单,但还是有一些细节需要处理。

如何让两只眼从不同位置看到同一个场景

这点其实非常简单,SceneKit 提供了 SCNView 与 camera 绑定的操作,我们可以让左右两个视图观看同一个场景,但使用的是不同的 camera

// 左视图看到的场景
leftView.scene = self.sceneForEyes;
// 左视图与左相机关联
leftView.pointOfView = self.leftCameraNode;
leftView.delegate = self;

// 右视图看到的场景
rightView.scene = self.sceneForEyes;
// 右视图与右相机关联
rightView.pointOfView = self.rightCameraNode;
rightView.delegate = self;

如何保证两只眼的相对位置始终正确

SceneKit 中有了代表左右眼的相机 Node,但是它们不能独立旋转,因为人是通过转动头部来让两只眼看另一个方向的。

什么意思呢?就是当朝左看看,我们想要的不是下面的效果 而是下面这样的效果。也就是说两个相机要在同一个父结点上,比如说 headNode。当需要转动时,不是直接转动两个相机,而是转动头部 Node,带动两个相机移动。

// 双目场景中代表头部的 Node
    SCNNode *headNode = [SCNNode node];
    self.headNode = headNode;
    [self.sceneForEyes.rootNode addChildNode:headNode];
    
    // 添加两个相机,对应左右眼视图
    SCNNode *leftCameraNode = [SCNNode node];
    self.leftCameraNode = leftCameraNode;
    leftCameraNode.camera = [SCNCamera camera];
    leftCameraNode.camera.zNear = 0;
    leftCameraNode.camera.zFar = 1000;
    leftCameraNode.camera.fieldOfView = 36;//保证双目视频看到的大小和 AR 中的一样大
    [headNode addChildNode:leftCameraNode];
    leftCameraNode.position = SCNVector3Make(-0.03, 0, 0);//左眼在头部的位置
    
    SCNNode *rightCameraNode = [SCNNode node];
    self.rightCameraNode = rightCameraNode;
    rightCameraNode.camera = [SCNCamera camera];
    rightCameraNode.camera.zNear = 0;
    rightCameraNode.camera.zFar = 1000;
    rightCameraNode.camera.fieldOfView = 36;//保证双目视频看到的大小和 AR 中的一样大
    [headNode addChildNode:rightCameraNode];
    rightCameraNode.position = SCNVector3Make(0.03, 0, 0);//右眼在头部的位置
    
    headNode.position = SCNVector3Make(0, 0, 0);
    

让 SceneKit 中的 3D 模型和 AR 中的移动幅度相同

想必你也看到上面的代码中有两行:

leftCameraNode.camera.fieldOfView = 36;//保证双目视频看到的大小和 AR 中的一样大
rightCameraNode.camera.fieldOfView = 36;//保证双目视频看到的大小和 AR 中的一样大

这是什么意思呢?就是说由于 ARKit 和 SceneKit 视图大小不同,SCNCamera 参数不同,导致它们的 FoV 不同。这就会导致当你抬起头部时,看到的虚拟物体虽然也在向下移动/旋转(头向上旋转,物体相对向下旋转),但是运动的幅度不真实。

调整之后当 AR 中的模型从最上边移动到最下边时,SceneKit 视图中的模型也应该是从视图的最上边移动到最下边。大致如下图(下图中模型的位置并不一致,所以有轻微区别):

手机与人眼相对位置的换算

由于用了纸盒子后,手机实际上是挂在人的额头位置,所以手机镜头和人眼位置并不重合,也需要进行一个简单的换算。但是因为盒子里面有放大镜的原因,这个位置需要根据实际调整:

// 相对结点,代表 AR 中的眼睛相对于手机的位置
SCNNode *relativeCamreaNode = [SCNNode node];
relativeCamreaNode.position = SCNVector3Make(0, 0.08, -0.13);//手机在脑袋顶上,画面经过光学放大,这个位置最好测试获得
[self.sceneView.pointOfView addChildNode:relativeCamreaNode];

潜在崩溃问题

如何让 SceneKit 中的头部 Node 跟随 AR 中的手机位置变化呢?当然是在代理方法中处理,但是直接写代码会有一个警告:在一个 SCNScene 的回调中修改另一个 SCNScene 的参数,可能会导致崩溃。

经过测试,添加dispatch_async(dispatch_get_main_queue(), ^{ });可以消除这个警告,但无法完全避免偶尔的崩溃。

我也试过,既然不能修改另一个 SCNScene,那直接复用同一个呢?让 SceneKit 的视图直接显示 ARKit 的 scene 不就行了?然后可惜的是 ARKit 的 scene 是带有视频背景的,而且尝试过各种方法也无法取消显示。

后来想到:我可以在代理方法中,把 AR 中的模型位置和相机位置保存下来,再在双目的代理中修改自己的 Node 的位置就可以了。记得要让 ViewController 成为 leftViewrightView 的代理噢!不然数据只被记录下来,没有被同步刷新到双目场景中。

代码

本文代码已上传 github:代码地址

想要完整体验视觉效果,需要配合 Holokit 的纸盒子。纸盒子可以在万能的淘宝买到,价格并不贵。