简单的ARKit的Demo,网上很多,包括Xcode自建的AR项目也可以直接实现一个飞机效果的Demo,不在赘述基本实现。
本文主要讲述如何一步一步实现ARKit对平面的识别,以及添加模型到该平面的节点上。
基础设置
首先,新建一个类,并且写好基本的代码设置
//RecognitionPlaneViewController
#import "RecognitionPlaneViewController.h"
#import <ARKit/ARKit.h>
#import <SceneKit/SceneKit.h>
@interface RecognitionPlaneViewController ()<ARSCNViewDelegate,ARSessionDelegate>
@property (nonatomic, strong)ARSCNView *arSCNView;
@property (nonatomic, strong)ARSession *arSession;
@property (nonatomic, strong)ARConfiguration *arConfiguration;
@end
@implementation RecognitionPlaneViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self.view addSubview:self.arSCNView];
[self.arSession runWithConfiguration:self.arConfiguration];
}
#pragma mark - ARSCNViewDelegate
#pragma mark - ARSessionDelegate
#pragma mark - lazyLoading
- (ARSCNView *)arSCNView{
if (!_arSCNView) {
_arSCNView = [[ARSCNView alloc] initWithFrame:self.view.bounds];
_arSCNView.delegate = self;
_arSCNView.session = self.arSession;
}
return _arSCNView;
}
- (ARSession *)arSession{
if (!_arSession) {
_arSession = [[ARSession alloc] init];
_arSession.delegate = self;
}
return _arSession;
}
- (ARConfiguration *)arConfiguration{
if (!_arConfiguration) {
ARWorldTrackingConfiguration *arWTConfiguration = [[ARWorldTrackingConfiguration alloc] init];
arWTConfiguration.planeDetection = ARPlaneDetectionHorizontal;
arWTConfiguration.lightEstimationEnabled = YES;
_arConfiguration = arWTConfiguration;
}
return _arConfiguration;
}
@end
识别平面
通过帮助文档查看ARWorldTrackingConfiguration的planeDetection属性可以得知,设置好识别平面之后,识别到的平面会被当做一个ARPlaneAnchor的对象添加到正在运行的session中。利用这点,可以利用ARSCNView的代理方法 -renderer:didAddNode:forAnchor:来监听捕捉到的平面。
/**
当新的节点映射到指定锚点时调用
@param renderer 渲染画面的渲染器
@param node 新添加的节点
@param anchor 节点映射到的锚点
*/
- (void)renderer:(id<SCNSceneRenderer>)renderer didAddNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
//过滤平面锚点
if ([anchor isKindOfClass:[ARPlaneAnchor class]]) {
//平面锚点:anchor
}
}
这样我们就可以获取每次捕捉到的平面锚点了。
画面反馈
很显然我们在手机屏幕上并不能直观的获得这一反馈,我们需要在屏幕上加点什么东西来让使用者能够知道,识别到了平面,和这块平面的范围。
此时,我们需要通过代码构建一个模型,将其添加到一个节点上,再将该节点添加到我们的sence中。
SCNNode有三种实例化方法:
- node
- nodeWithGeometry:
- nodeWithMDLObject:
我们采用第二种方法,nodeWithGeometry:,可以通过实例化一个SCNGeometry对象来产生节点。而我们可以轻易的通过代码实例化一个SCNGeometry对象。
在帮助文档中, 可以看到SCNGeometry具有诸多子类,我们这里识别平面,可以通过实例化一个SCNPlane对象来反馈用户。
实例化SCNPlane时,它的Width与Height可以通过代理方法中获取的锚点来取得。
if ([anchor isKindOfClass:[ARPlaneAnchor class]]) {
//实例化为子类
ARPlaneAnchor *arPlaneAnchor = (ARPlaneAnchor *)anchor;
}
ARPlaneAnchor中有三个属性:
- alignment一个枚举值,用来显示该平面相对于重力的方向,目前只有水平方向这一个枚举值
- center平面的中心点相对于锚点的位置
-
extent在锚的坐标空间中平面的范围
(对于center、extent这两个属性,都为vector_float3类型,文档中解释为(x,y,z)的值,但是实际打印出来是(x,y,z,?),最后一个?的值有时为1有时为0,查阅到vector_float3的类型为simd_float3,而simd_float3的解释为最后一个值是为了与simd_float4的矩阵保持一致而多添加的......使用时应该可以忽略)
有了extent属性,我们就可以初始化SCNPlane了
SCNPlane *scnPlane = [SCNPlane planeWithWidth:arPlaneAnchor.extent.x height:arPlaneAnchor.extent.z];
我们还可以设置这个平面模型的颜色,这里设置为绿色。(查到SCNMaterial类是给SCNGeometry对象定义表面外观属性,规定对象表面如何着色或纹理以及如何反应灯光所用)
//调整颜色
scnPlane.firstMaterial.diffuse.contents = [UIColor greenColor];
//调整透明度
scnPlane.firstMaterial.transparency = 0.5;
接下来我们将这个平面添加到平面锚点的节点上就行了,添加时需要注意的是,SCNPlane的图形是单面的,初始化后是平行于重力方向的,所以我们需要将其反转90°也就是-π/2(如果是π/2的话会看不到,因为是单面的)
SCNNode *planeNode = [SCNNode nodeWithGeometry:scnPlane];
[planeNode setPosition:SCNVector3Make(arPlaneAnchor.center.x, 0, arPlaneAnchor.center.z)];
planeNode.transform = SCNMatrix4MakeRotation(- M_PI_2, 1, 0, 0);
[node addChildNode:planeNode];
这样我们就可以直观的接收到平面识别反馈了:
更新所识别的平面
之前提到了ARWorldTrackingConfiguration的planeDetection这个属性,Xcode的属性注释中提到,当两个平面合并的事件发生时,新的平面将会被移除。这个解释光从字面上看,有些难以理解。
我们建立两个可变字典来保存每次添加的节点和平面,以锚点的标识为key
[self.planeDictionary setObject:scnPlane forKey:arPlaneAnchor.identifier];
[self.nodeDictionary setObject:planeNode forKey:arPlaneAnchor.identifier];
然后实现更新节点的代理方法与移除节点的代理方法
- (void)renderer:(id<SCNSceneRenderer>)renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
if ([self.planeDictionary objectForKey:anchor.identifier] && [self.nodeDictionary objectForKey:anchor.identifier]) {
NSLog(@"更新节点%@",anchor.identifier);
}
- (void)renderer:(id<SCNSceneRenderer>)renderer didRemoveNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
NSLog(@"移除节点%@",anchor.identifier);
[self.nodeDictionary removeObjectForKey:anchor.identifier];
[self.planeDictionary removeObjectForKey:anchor.identifier];
}
我们再次运行程序,首先识别一块地板,然后往没有识别到的区域平移,控制台打印的结果如下(此处将锚点的identifier替换为了锚点A、B、C)
2018-01-24 14:11:51.439594+0800 MHARDemo_2_OC[33797:10928440] 锚点添加至Session
2018-01-24 14:11:51.445644+0800 MHARDemo_2_OC[33797:10928482] center - (-0.017074,0.000000,-0.006556)
2018-01-24 14:11:51.445695+0800 MHARDemo_2_OC[33797:10928482] extent - (0.815687,0.000000,0.418654)
2018-01-24 14:11:51.445748+0800 MHARDemo_2_OC[33797:10928482] 添加平面锚点--------锚点A
2018-01-24 14:11:51.445995+0800 MHARDemo_2_OC[33797:10928482] 更新节点--------锚点A
2018-01-24 14:11:51.839800+0800 MHARDemo_2_OC[33797:10928440] 锚点添加至Session
2018-01-24 14:11:51.845624+0800 MHARDemo_2_OC[33797:10928482] center - (0.058377,0.000000,0.022934)
2018-01-24 14:11:51.845675+0800 MHARDemo_2_OC[33797:10928482] extent - (0.829945,0.000000,0.420610)
2018-01-24 14:11:51.845713+0800 MHARDemo_2_OC[33797:10928482] 添加平面锚点------------锚点B
2018-01-24 14:11:51.845896+0800 MHARDemo_2_OC[33797:10928482] 更新节点------------锚点B
2018-01-24 14:11:51.945646+0800 MHARDemo_2_OC[33797:10928469] 更新节点------------锚点B
2018-01-24 14:11:52.045680+0800 MHARDemo_2_OC[33797:10928482] 更新节点------------锚点B
2018-01-24 14:11:52.145672+0800 MHARDemo_2_OC[33797:10928480] 更新节点------------锚点B
2018-01-24 14:11:52.245644+0800 MHARDemo_2_OC[33797:10928469] 更新节点------------锚点B
2018-01-24 14:11:52.345731+0800 MHARDemo_2_OC[33797:10928482] 更新节点------------锚点B
2018-01-24 14:11:52.445655+0800 MHARDemo_2_OC[33797:10928482] 更新节点------------锚点B
2018-01-24 14:11:52.545712+0800 MHARDemo_2_OC[33797:10928480] 更新节点------------锚点B
2018-01-24 14:11:52.645669+0800 MHARDemo_2_OC[33797:10928469] 更新节点------------锚点B
2018-01-24 14:12:01.646132+0800 MHARDemo_2_OC[33797:10928478] 更新节点------------锚点B
2018-01-24 14:12:01.746214+0800 MHARDemo_2_OC[33797:10928469] 更新节点------------锚点B
2018-01-24 14:12:01.846150+0800 MHARDemo_2_OC[33797:10928478] 更新节点------------锚点B
2018-01-24 14:12:01.946155+0800 MHARDemo_2_OC[33797:10928478] 更新节点------------锚点B
2018-01-24 14:12:02.046124+0800 MHARDemo_2_OC[33797:10928469] 更新节点------------锚点B
2018-01-24 14:12:02.146123+0800 MHARDemo_2_OC[33797:10928469] 更新节点------------锚点B
2018-01-24 14:12:02.246198+0800 MHARDemo_2_OC[33797:10928469] 移除节点------------锚点B
2018-01-24 14:12:02.246509+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:02.346143+0800 MHARDemo_2_OC[33797:10928480] 更新节点--------锚点A
2018-01-24 14:12:04.446334+0800 MHARDemo_2_OC[33797:10928478] 更新节点--------锚点A
2018-01-24 14:12:04.546234+0800 MHARDemo_2_OC[33797:10928480] 更新节点--------锚点A
2018-01-24 14:12:04.646352+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:04.746282+0800 MHARDemo_2_OC[33797:10928480] 更新节点--------锚点A
2018-01-24 14:12:04.846371+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:04.946271+0800 MHARDemo_2_OC[33797:10928478] 更新节点--------锚点A
2018-01-24 14:12:06.440534+0800 MHARDemo_2_OC[33797:10928440] 锚点添加至Session
2018-01-24 14:12:06.446355+0800 MHARDemo_2_OC[33797:10928480] center - (-0.000406,0.000000,0.008070)
2018-01-24 14:12:06.446400+0800 MHARDemo_2_OC[33797:10928480] extent - (0.402418,0.000000,0.396076)
2018-01-24 14:12:06.446433+0800 MHARDemo_2_OC[33797:10928480] 添加平面锚点----------------锚点C
2018-01-24 14:12:06.446558+0800 MHARDemo_2_OC[33797:10928480] 更新节点----------------锚点C
2018-01-24 14:12:06.746361+0800 MHARDemo_2_OC[33797:10928478] 更新节点----------------锚点C
2018-01-24 14:12:09.546507+0800 MHARDemo_2_OC[33797:10928478] 更新节点--------锚点A
2018-01-24 14:12:09.646517+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:09.746525+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:09.846596+0800 MHARDemo_2_OC[33797:10928480] 更新节点--------锚点A
2018-01-24 14:12:09.946547+0800 MHARDemo_2_OC[33797:10928478] 更新节点--------锚点A
2018-01-24 14:12:10.046569+0800 MHARDemo_2_OC[33797:10928469] 移除节点----------------锚点C
2018-01-24 14:12:10.046894+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:10.146545+0800 MHARDemo_2_OC[33797:10928482] 更新节点--------锚点A
2018-01-24 14:12:10.246552+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:10.346560+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:10.446555+0800 MHARDemo_2_OC[33797:10928480] 更新节点--------锚点A
2018-01-24 14:12:11.346620+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:11.446623+0800 MHARDemo_2_OC[33797:10928482] 更新节点--------锚点A
2018-01-24 14:12:11.546624+0800 MHARDemo_2_OC[33797:10928480] 更新节点--------锚点A
2018-01-24 14:12:11.646631+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:11.746665+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:11.846669+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:11.946644+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:12.046644+0800 MHARDemo_2_OC[33797:10928482] 更新节点--------锚点A
2018-01-24 14:12:14.046760+0800 MHARDemo_2_OC[33797:10928469] 更新节点--------锚点A
2018-01-24 14:12:14.146824+0800 MHARDemo_2_OC[33797:10928480] 更新节点--------锚点A
2018-01-24 14:12:14.646875+0800 MHARDemo_2_OC[33797:10928482] 更新节点--------锚点A
我们可以看出来,整个过程如图:
这样子我们在更新锚点的代理方法中,重新设置锚点对应的节点的坐标、节点对应的平面模型的大小,就可以实现更新所识别的平面到手机界面上了
- (void)renderer:(id<SCNSceneRenderer>)renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
if ([self.planeDictionary objectForKey:anchor.identifier] && [self.nodeDictionary objectForKey:anchor.identifier]) {
NSLog(@"更新节点%@",anchor.identifier);
ARPlaneAnchor *planeAnchor = (ARPlaneAnchor *)anchor;
SCNNode *scnNode = [self.nodeDictionary objectForKey:anchor.identifier];
SCNPlane *scnPlane = [self.planeDictionary objectForKey:anchor.identifier];
scnPlane.width = planeAnchor.extent.x;
scnPlane.height = planeAnchor.extent.z;
scnNode.position = SCNVector3Make(planeAnchor.center.x, 0, planeAnchor.center.z);
}
}