簡單的ARKit的Demo,網上很多,包括Xcode自建的AR項目也可以直接實現(xiàn)一個飛機效果的Demo,不在贅述基本實現(xiàn)。
本文主要講述如何一步一步實現(xiàn)ARKit對平面的識別,以及添加模型到該平面的節(jié)點上。
基礎設置
首先,新建一個類,并且寫好基本的代碼設置
//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:來監(jiān)聽捕捉到的平面。
/**
當新的節(jié)點映射到指定錨點時調用
@param renderer 渲染畫面的渲染器
@param node 新添加的節(jié)點
@param anchor 節(jié)點映射到的錨點
*/
- (void)renderer:(id<SCNSceneRenderer>)renderer didAddNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
//過濾平面錨點
if ([anchor isKindOfClass:[ARPlaneAnchor class]]) {
//平面錨點:anchor
}
}
這樣我們就可以獲取每次捕捉到的平面錨點了。
畫面反饋
很顯然我們在手機屏幕上并不能直觀的獲得這一反饋,我們需要在屏幕上加點什么東西來讓使用者能夠知道,識別到了平面,和這塊平面的范圍。
此時,我們需要通過代碼構建一個模型,將其添加到一個節(jié)點上,再將該節(jié)點添加到我們的sence中。
SCNNode有三種實例化方法:
- node
- nodeWithGeometry:
- nodeWithMDLObject:
我們采用第二種方法,nodeWithGeometry:,可以通過實例化一個SCNGeometry對象來產生節(jié)點。而我們可以輕易的通過代碼實例化一個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對象定義表面外觀屬性,規(guī)定對象表面如何著色或紋理以及如何反應燈光所用)
//調整顏色
scnPlane.firstMaterial.diffuse.contents = [UIColor greenColor];
//調整透明度
scnPlane.firstMaterial.transparency = 0.5;
接下來我們將這個平面添加到平面錨點的節(jié)點上就行了,添加時需要注意的是,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的屬性注釋中提到,當兩個平面合并的事件發(fā)生時,新的平面將會被移除。這個解釋光從字面上看,有些難以理解。
我們建立兩個可變字典來保存每次添加的節(jié)點和平面,以錨點的標識為key
[self.planeDictionary setObject:scnPlane forKey:arPlaneAnchor.identifier];
[self.nodeDictionary setObject:planeNode forKey:arPlaneAnchor.identifier];
然后實現(xiàn)更新節(jié)點的代理方法與移除節(jié)點的代理方法
- (void)renderer:(id<SCNSceneRenderer>)renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
if ([self.planeDictionary objectForKey:anchor.identifier] && [self.nodeDictionary objectForKey:anchor.identifier]) {
NSLog(@"更新節(jié)點%@",anchor.identifier);
}
- (void)renderer:(id<SCNSceneRenderer>)renderer didRemoveNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
NSLog(@"移除節(jié)點%@",anchor.identifier);
[self.nodeDictionary removeObjectForKey:anchor.identifier];
[self.planeDictionary removeObjectForKey:anchor.identifier];
}
我們再次運行程序,首先識別一塊地板,然后往沒有識別到的區(qū)域平移,控制臺打印的結果如下(此處將錨點的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] 更新節(jié)點--------錨點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] 更新節(jié)點------------錨點B
2018-01-24 14:11:51.945646+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點------------錨點B
2018-01-24 14:11:52.045680+0800 MHARDemo_2_OC[33797:10928482] 更新節(jié)點------------錨點B
2018-01-24 14:11:52.145672+0800 MHARDemo_2_OC[33797:10928480] 更新節(jié)點------------錨點B
2018-01-24 14:11:52.245644+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點------------錨點B
2018-01-24 14:11:52.345731+0800 MHARDemo_2_OC[33797:10928482] 更新節(jié)點------------錨點B
2018-01-24 14:11:52.445655+0800 MHARDemo_2_OC[33797:10928482] 更新節(jié)點------------錨點B
2018-01-24 14:11:52.545712+0800 MHARDemo_2_OC[33797:10928480] 更新節(jié)點------------錨點B
2018-01-24 14:11:52.645669+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點------------錨點B
2018-01-24 14:12:01.646132+0800 MHARDemo_2_OC[33797:10928478] 更新節(jié)點------------錨點B
2018-01-24 14:12:01.746214+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點------------錨點B
2018-01-24 14:12:01.846150+0800 MHARDemo_2_OC[33797:10928478] 更新節(jié)點------------錨點B
2018-01-24 14:12:01.946155+0800 MHARDemo_2_OC[33797:10928478] 更新節(jié)點------------錨點B
2018-01-24 14:12:02.046124+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點------------錨點B
2018-01-24 14:12:02.146123+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點------------錨點B
2018-01-24 14:12:02.246198+0800 MHARDemo_2_OC[33797:10928469] 移除節(jié)點------------錨點B
2018-01-24 14:12:02.246509+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:02.346143+0800 MHARDemo_2_OC[33797:10928480] 更新節(jié)點--------錨點A
2018-01-24 14:12:04.446334+0800 MHARDemo_2_OC[33797:10928478] 更新節(jié)點--------錨點A
2018-01-24 14:12:04.546234+0800 MHARDemo_2_OC[33797:10928480] 更新節(jié)點--------錨點A
2018-01-24 14:12:04.646352+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:04.746282+0800 MHARDemo_2_OC[33797:10928480] 更新節(jié)點--------錨點A
2018-01-24 14:12:04.846371+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:04.946271+0800 MHARDemo_2_OC[33797:10928478] 更新節(jié)點--------錨點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] 更新節(jié)點----------------錨點C
2018-01-24 14:12:06.746361+0800 MHARDemo_2_OC[33797:10928478] 更新節(jié)點----------------錨點C
2018-01-24 14:12:09.546507+0800 MHARDemo_2_OC[33797:10928478] 更新節(jié)點--------錨點A
2018-01-24 14:12:09.646517+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:09.746525+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:09.846596+0800 MHARDemo_2_OC[33797:10928480] 更新節(jié)點--------錨點A
2018-01-24 14:12:09.946547+0800 MHARDemo_2_OC[33797:10928478] 更新節(jié)點--------錨點A
2018-01-24 14:12:10.046569+0800 MHARDemo_2_OC[33797:10928469] 移除節(jié)點----------------錨點C
2018-01-24 14:12:10.046894+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:10.146545+0800 MHARDemo_2_OC[33797:10928482] 更新節(jié)點--------錨點A
2018-01-24 14:12:10.246552+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:10.346560+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:10.446555+0800 MHARDemo_2_OC[33797:10928480] 更新節(jié)點--------錨點A
2018-01-24 14:12:11.346620+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:11.446623+0800 MHARDemo_2_OC[33797:10928482] 更新節(jié)點--------錨點A
2018-01-24 14:12:11.546624+0800 MHARDemo_2_OC[33797:10928480] 更新節(jié)點--------錨點A
2018-01-24 14:12:11.646631+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:11.746665+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:11.846669+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:11.946644+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:12.046644+0800 MHARDemo_2_OC[33797:10928482] 更新節(jié)點--------錨點A
2018-01-24 14:12:14.046760+0800 MHARDemo_2_OC[33797:10928469] 更新節(jié)點--------錨點A
2018-01-24 14:12:14.146824+0800 MHARDemo_2_OC[33797:10928480] 更新節(jié)點--------錨點A
2018-01-24 14:12:14.646875+0800 MHARDemo_2_OC[33797:10928482] 更新節(jié)點--------錨點A
我們可以看出來,整個過程如圖:

這樣子我們在更新錨點的代理方法中,重新設置錨點對應的節(jié)點的坐標、節(jié)點對應的平面模型的大小,就可以實現(xiàn)更新所識別的平面到手機界面上了
- (void)renderer:(id<SCNSceneRenderer>)renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{
if ([self.planeDictionary objectForKey:anchor.identifier] && [self.nodeDictionary objectForKey:anchor.identifier]) {
NSLog(@"更新節(jié)點%@",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);
}
}