阅读 86

ARAnchor 到底有什么用?我的一些简单认识

ARAnchor 是 ARKit 中重要类型,也是一个重要概念。但是在使用中却让人有很多问题:ARAnchor 是啥?用起来怎么老出问题?没看出来什么明显好处,不用行不行?

ARAnchor 是什么

按照苹果官方的说法:

ARAnchor:可用于将对象放置在AR场景中的现实世界的位置和方向。

要追踪实体或虚拟对象相对于相机的位置和方向,请创建锚点对象,并使用add(anchor:)方法将其添加到AR会话中。
复制代码

苹果工程师也曾经说过:Anchor 是虚拟和现实之间的连接点。

不过这个说法还是让人不明所以,这个锚点 Anchor 到底是个啥?

我的思考

在开发中我也思考过这个问题:先手动添加 Anchor 再为其绑定 SCNNode 对象,与直接将 Node 添加到 scene.rootNode 下有什么不同么?

恰好,苹果在 WWDC2019 上讲多人 AR 时,也讲到了这个问题。再结合我在开发过程中的一些经验,总算形成了一点粗浅的认识。

首先,要做 AR,必须先观察现实世界,并用数学/计算机视觉方式找到特征点。其次要在正确的位置放上 3D 物体,而 Anchor 就是与周围的特征点相关联的一个虚拟位置,用于正确放置 3D 物体。

那么直接用世界坐标原点呢?也就是 rootNode 的位置不行么?其实是差不多的,但又有一些不同,因为世界坐标的原点是综合了所有视觉特征点以及手机陀螺仪数据来确定并不断调整的。而 ARAnchor 会自动和 附近的 特征点相关联,根据附近的特征点世界坐标原点来决定自己的位置并不断轻微调整。如下图所示(其实 World 可能也在抖动):

ARAnchor 类只与附近的特征点相关联,而 ARImageAnchor、ARPlaneAnchor、ARObjectAnchor 除了距离因素,还要考虑形状和纹理等。

所以理论上,即便世界坐标原点发生了抖动和调整,这些 Anchor 也会因为有自己相关的特征点,表现得更为稳定。

实际效果

那么实际开发中,放置虚拟物体时,谁的稳定性更好呢?经过我的初步实践,我认为稳定性(与现实紧密结合的程度)从高到低是:ARImageAnchor(静态) > ARPlaneAnchor >> ARAnchor ≈ WorldOrigin(rootNode 的原点) >> ARObjectAnchor。其中>>表示高出很多。ARFaceAnchor 因为不好对比,所以没测试过。

在开发中,我发现手动创建 ARAnchor 再为其绑定 Node 物体,与直接添加到 rootNode 中,区别其实很小。当识别稳定时,表现都很好,相当接近;当不稳定时,都不怎么好,ARAnchor 稍微好一点点。

而识别静态的 ARImageAnchor 表现最为稳定,ARPlaneAnchor 在平面有花纹并平整时也非常稳定,ARObjectAnchor 最不稳定,甚至在识别后还会不停扭动或抖动,以至于我们为了更平稳,在识别出 3D 物体后,把物体检测给停止掉,这样更平稳一些。

怎么使用

在实际开发中,我发现使用 ARAnchor 过程中,为其绑定 Node 时,调整 Node 的 scale 和 position 无效,必须要调整 transform 或者 simdTransform 才可以。

// 加载 SCNScene 和 SCNNode
self.sceneView.scene = [SCNScene sceneNamed:@"art.scnassets/ship.scn"];
self.shipNode = [self.sceneView.scene.rootNode childNodeWithName:@"ship" recursively:YES];

//设置位置和缩放,这里注意:提前设置 scale 和 position 是无效的,所以只能设置 transfrom/simdTramsfrom,具体原因不明
simd_float4x4 trans = simd_diagonal_matrix(simd_make_float4(1,1,1,1));//对角线都是 1 其余是 0,说明无缩放(1 倍缩放),无平移(平移 0)
trans.columns[3][2] -= 0.5;//矩阵的第 4 列就是平移向量,第 3 个元素是 z 分量,此处沿 z 轴反方向移动 0.5 米
self.shipNode.simdTransform = trans;

//创建 Anchor 并添加到 session 中
ARAnchor *anchor = [[ARAnchor alloc] initWithName:@"ship" transform:trans];
[self.sceneView.session addAnchor:anchor];
复制代码
// 代理方法中,将 Anchor 与 Node 关联起来
- (SCNNode *)renderer:(id<SCNSceneRenderer>)renderer nodeForAnchor:(ARAnchor *)anchor {
    if ([anchor.name isEqualToString:@"ship"]) {
        // 在这里设置 scale 和 position 也是无效的
        // 注意:这个 node 会被强制添加 rootNode 下面,不管它以前在什么结点下面
        return self.shipNode;
    }
    return nil;
}


-(void)renderer:(id<SCNSceneRenderer>)renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor {
    // 这个方法说明 anchor 的位置相对于世界坐标原点发生了调整,实践中发现并不经常调用,一般也没有太明显的变更
    // 这里其实有两种可能:一是世界坐标发生了抖动,但是 anchor 和周围物体紧密结合,造成了相对移动;二是世界坐标没动,但 anchor 位置发生了调整,也是相对移动。
    if ([anchor.name isEqualToString:@"ship"]) {
        NSLog(@"anchor 发生了调整");
    }
    
    //PS.在这个方法里设置 node 的 scale 和 position 是有效果的
}
复制代码

要获取 ARAnchor 和 SCNNode 对象的对应关系,可以用下面 ARSCNView 的方法:

// 获取 node 对应的 anchor
- (nullable ARAnchor *)anchorForNode:(SCNNode *)node;

// 获取 anchor 对应的 node
- (nullable SCNNode *)nodeForAnchor:(ARAnchor *)anchor;
复制代码

总结

实践了一圈下来,发现最贴切的说法还是:Anchor 是虚拟和现实之间的连接点。

按照苹果的推荐,我们在放置任何虚拟物体时,都应该先创建 Anchor 再为其关联 Node。但是实际开发中,除了 ARImageAnchor 和 ARPlaneAnchor 明显更稳定外,其余感觉没太大区别。

另一个麻烦的是,Node 与 Anchor 关联起来后,会被强制放到 rootNode 下面,也就是直接放在世界坐标系中(即 Node 的 transform == worldTransform),这会造成当我们想要将若干个 Node 作为一个整体进行平移或旋转时,非常困难。因为苹果要求,除非有特殊要求,最好将每个 Node 单独与 Anchor 关联起来,并且不要放得离 Anchor 太远。

我想,这也是苹果在 AR 开发中推荐使用实体组件系统(Entity Component System)的一个原因吧。当然,这只是我的一点猜测,毕竟我还没怎么用过实体组件系统。

关注下面的标签,发现更多相似文章
评论