前言
自從WWDC2017蘋果發(fā)布了ARKit鸯乃,引發(fā)了很多關(guān)注鲸阻,因為這個框架的發(fā)布,意味著開發(fā)者不需要引入龐大的第三方框架到工程中(如:Vuforia)就可以實現(xiàn)AR比較強(qiáng)大的虛擬增強(qiáng)功能,且在官方的維護(hù)下會有一個持續(xù)完善的框架體系(到目前ARKit已經(jīng)到了1.5版本)缨睡。
關(guān)于ARKit這邊不做過多的介紹赘娄,網(wǎng)上相應(yīng)的資料已經(jīng)很是普及。
為了讓技術(shù)的發(fā)展給用戶帶來更多的便利宏蛉,我們在酒店的場景中嘗試了AR的功能遣臼,幫助用戶直觀的找到自己身邊的酒店,讓身處陌生環(huán)境的用戶也能像在本地一樣拾并,在現(xiàn)實場景下看到周圍有哪些好酒店揍堰,我們做了很多嘗試,最終完成了我們AR找酒店的第一版嗅义。
介紹我們在實現(xiàn)AR找酒店功能的過程:
這一塊還是想先介紹一下幾個坐標(biāo)系的概念:
坐標(biāo)到3D世界的位置
坐標(biāo)系
本項目中坐標(biāo)數(shù)據(jù)的計算主要在 五個坐標(biāo)系中進(jìn)行:
-
物體坐標(biāo)系
物體坐標(biāo)系是描述自己的坐標(biāo)系屏歹,每個物體都有自己的坐標(biāo)系,可以描述自己的 狀態(tài)之碗,如:寬蝙眶、高等。
-
世界坐標(biāo)系
世界坐標(biāo)系是系統(tǒng)的絕對坐標(biāo)系褪那,在沒有建立用戶坐標(biāo)系之前畫面上所有點(diǎn)的坐標(biāo)都是以該坐標(biāo)系的原點(diǎn)來確定各自的位置的幽纷。
-
相機(jī)坐標(biāo)系(觀察坐標(biāo)系)
相機(jī)坐標(biāo)系是以光軸與圖像平面的交點(diǎn)為圖像坐標(biāo)系的原點(diǎn)所構(gòu)成的直角坐標(biāo)系。
-
投影儀坐標(biāo)系
直接將3D坐標(biāo)轉(zhuǎn)換為屏幕坐標(biāo)是非常復(fù)雜的(因為它們不僅維度不同,度量不同(屏幕坐標(biāo)一般都是像素為單位,3D空間中我們可以現(xiàn)實世界的米博敬,厘米為單位)友浸,XY的方向也不同,在2D空間時還要進(jìn)行坐標(biāo)系變換),所以先將3D坐標(biāo)降維到2D坐標(biāo)系中偏窝,這個2D坐標(biāo)系就是投影坐標(biāo)系收恢。
-
屏幕坐標(biāo)系
手機(jī)屏幕上的2D坐標(biāo)系
變換順序:
物體坐標(biāo)系->世界坐標(biāo)系: 將物體自身數(shù)據(jù)放在世界坐標(biāo)系中武学,即賦予GPS坐標(biāo)。
世界坐標(biāo)系->相機(jī)坐標(biāo)系: 截取部分世界坐標(biāo)系作為相機(jī)坐標(biāo)系伦意。將GPS坐標(biāo)相對位置和差值映射在相機(jī)坐標(biāo)系中火窒。
相機(jī)坐標(biāo)系->投影坐標(biāo)系: 將形體投射到投影面上,從而獲得的一種較為接近視覺效果的單面投影圖驮肉,有點(diǎn)像皮影戲沛鸵。將3維坐標(biāo)降維。
投影坐標(biāo)系->屏幕坐標(biāo)系: 將投影屏坐標(biāo)對應(yīng)轉(zhuǎn)換為手機(jī)屏幕上的坐標(biāo)數(shù)據(jù)缆八。
在轉(zhuǎn)換過程中會存在左右手坐標(biāo)系的區(qū)分曲掰,坐標(biāo)系單位的轉(zhuǎn)換和其他數(shù)學(xué)計算等問題,理清這些問題可以對之后的問題定位奈辰、修改和優(yōu)化方向有很大的裨益栏妖。
//相關(guān)轉(zhuǎn)換代碼
- (LocationTranslation)translationToLocation:(CLLocation *)location
{
CLLocation *inbetweenLocation = [[CLLocation alloc] initWithLatitude:self.coordinate.latitude longitude:location.coordinate.longitude];
CLLocationDistance distanceLatitude = [location distanceFromLocation:inbetweenLocation];
double latitudeTranslation;
if (location.coordinate.latitude > inbetweenLocation.coordinate.latitude) {
latitudeTranslation = distanceLatitude;
} else {
latitudeTranslation = 0 - distanceLatitude;
}
CLLocationDistance distanceLongitude = [self distanceFromLocation:inbetweenLocation];
double longitudeTranslation;
if (self.coordinate.longitude > inbetweenLocation.coordinate.longitude) {
longitudeTranslation = 0 - distanceLongitude;
} else {
longitudeTranslation = distanceLongitude;
}
CLLocationDistance altitudeTranslation = location.altitude - self.altitude;
return [TCTARCLUtil getLocationWith:latitudeTranslation longitudeTranslation:longitudeTranslation altitudeTranslation:altitudeTranslation];
}
+ (LocationTranslation)getLocationWith:(double)latitudeTranslation longitudeTranslation:(double)longitudeTranslation altitudeTranslation:(double)altitudeTranslation
{
LocationTranslation location = {latitudeTranslation,longitudeTranslation,altitudeTranslation};
return location;
}
介紹幾點(diǎn)關(guān)于坐標(biāo)轉(zhuǎn)換的定義: Haversine公式(大圓距離)
如果地球上有兩個不同的經(jīng)緯度值,那么在Haversine公式的幫助下奖恰,您可以輕松計算出大圓距離(球體表面上兩點(diǎn)之間的最短距離)吊趾。
-- Movable-Type
如果,把用戶當(dāng)前位置當(dāng)作中心點(diǎn)瑟啃,如圖
那么论泛,我們想在屏幕中展示出酒店信息,只需要計算兩個值來確定酒店的點(diǎn):
用戶點(diǎn)到酒店點(diǎn)的距離(distance)
地球南/北線與用戶點(diǎn)到酒店點(diǎn)連線的角度(bearing)
distance可以在AR世界中設(shè)置3D模型的距離蛹屿,通過bearing來做旋轉(zhuǎn)轉(zhuǎn)換 如果是在平面系統(tǒng)上屁奏,就可以用三角函數(shù)等來處理,但是由于地球不是平面错负,所以需要使用Haversine公式來計算坟瓢。 計算bearing方位角值的公式: atan2 ( X, Y )
其中X等于:sin(long2 - long1) * cos(long2)
而Y等于: cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(long2 - long1)
抬高酒店
抬高的方式,判斷兩個酒店的顯示圖層犹撒,在屏幕上是否有重合折联,有重合時按距離依次上抬 判斷兩個酒店是否有重合的方式:
使用projectPoint方法,將物體在3D世界的坐標(biāo)轉(zhuǎn)換到顯示區(qū)的2D的坐標(biāo)
結(jié)合2D坐標(biāo)和酒店信息框的寬高得到兩個酒店信息框的CGRect
使用CGRectIntersectsRect判斷兩個CGRect是否有重合
參考代碼:
NSMutableArray *intersectsArray = [NSMutableArray new];
for (TCTSCNVector3Object *object in _originPositions) {
SCNVector3 thisPoint = [self projectPoint:object.position];
CGRect thisRect = CGRectMake(thisPoint.x, thisPoint.y, 150, 40);
for (TCTSCNVector3Object *nextObject in _originPositions) {
if (![object isEqual:nextObject]) {
SCNVector3 nextPoint = [self projectPoint:nextObject.position];
CGRect nextRect = CGRectMake(nextPoint.x, nextPoint.y, 150, 40);
if (CGRectIntersectsRect(thisRect, nextRect)) {
[intersectsArray addObject:@[@(object.index), @(nextObject.index)]];
}
}
}
}
將會互相重合的進(jìn)行分組Group, 分組以后就可以根據(jù)距離的遠(yuǎn)近進(jìn)行依次抬高即設(shè)置position Y识颊。
酒店方向提示
在導(dǎo)航模式時诚镰,當(dāng)用戶手機(jī)的朝向不在酒店方向時,我們做了一個方位的提示 使用renderer(ARSCNViewDelegate)代理方法進(jìn)行實時判斷:
通過isNodeInsideFrustum方法確定酒店node是否在顯示區(qū)
獲取酒店的位置
將屏幕左邊和右邊的點(diǎn)分別轉(zhuǎn)換到3D世界的坐標(biāo)
分別計算酒店點(diǎn)到左邊點(diǎn)和右邊點(diǎn)的距離
根據(jù)距離確定酒店是在屏幕的左邊還是右邊
代碼示列:
SCNNode *pointOfView = renderer.pointOfView;
BOOL isVisible = [renderer isNodeInsideFrustum:_currentNode withPointOfView:pointOfView];//當(dāng)前node是否在屏幕中可見
//將屏幕中node的三維坐標(biāo)轉(zhuǎn)換成屏幕中我們熟知的CGPoint
SCNVector3 thisPoint = [renderer projectPoint:_currentNode.position];
CGPoint leftPoint = CGPointMake(0, thisPoint.y);
CGPoint rightPoint = CGPointMake(SCREEN_WIDTH, thisPoint.y);
SCNVector3 leftWorldPosition = [renderer unprojectPoint:SCNVector3Make(leftPoint.x, leftPoint.y, 0)];
SCNVector3 rightWorldPosition = [renderer unprojectPoint:SCNVector3Make(rightPoint.x, rightPoint.y, 0)];
CGFloat leftDistance = [TCTARCLUtil distanceToAnotherVector:_currentNode.position anotherVector:leftWorldPosition];
CGFloat rightDistance = [TCTARCLUtil distanceToAnotherVector:_currentNode.position anotherVector:rightWorldPosition];
dispatch_async(dispatch_get_main_queue(), ^{
if (isVisible) {
[_leftRightTipView turnToState:HTARLeftRightTipViewStateHidden positionValue:0];
}
else {
if (leftDistance > rightDistance) {
[_leftRightTipView turnToState:HTARLeftRightTipViewStateRight positionValue:thisPoint.y];
}
else {
[_leftRightTipView turnToState:HTARLeftRightTipViewStateLeft positionValue:thisPoint.y];
}
}
});
雷達(dá)的實現(xiàn)
很多時候其實用戶拿起手機(jī)時屏幕視野中是正好沒有酒店的祥款,這個時候如果有個雷達(dá)功能告訴用戶什么方位有酒店那就太好不過了清笨,不多說,搞起來镰踏! 先畫張圖看看思路是怎么樣的:
我們以用戶位置的經(jīng)緯度坐標(biāo)作為圓心函筋,圓半徑實際就是所有酒店當(dāng)中離我們最遠(yuǎn)的那個酒店離我們的距離(可以取一個稍大于這數(shù)值的整數(shù))。 這樣就很容易能求出酒店的點(diǎn)應(yīng)該顯示在圓的哪個位置奠伪。 再利用CLLocationManager的代理- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
每當(dāng)用戶設(shè)備發(fā)生轉(zhuǎn)向的時候可以得到一個角度跌帐,讓雷達(dá)視圖做動畫效果: CATransform3DRotate(CATransform3DIdentity, 角度/360.0 * 2 * M_PI, 0, 0, -1);
這樣我們就實現(xiàn)了一個雷達(dá)功能,效果如圖:
導(dǎo)航的實現(xiàn)
既然是AR找酒店绊率,那導(dǎo)航功能是必不可少的谨敛,借鑒了市面上一些已經(jīng)有的產(chǎn)品(隨便走,HotStepper)滤否,我們的思路是在自身位置與酒店位置之間用一個個箭頭的形式來指示用戶的行走路線脸狸,首先來看下最終效果圖吧:
首先獲取路線規(guī)劃: 使用系統(tǒng)原生方法即可:
//創(chuàng)建出發(fā)點(diǎn)和目的點(diǎn)信息
MKPlacemark *fromPlace = [[MKPlacemark alloc] initWithCoordinate:用戶自身經(jīng)緯度
addressDictionary:nil];
MKPlacemark *toPlace = [[MKPlacemark alloc] initWithCoordinate:酒店經(jīng)緯度 addressDictionary:nil];
//創(chuàng)建出發(fā)節(jié)點(diǎn)和目的地節(jié)點(diǎn)
MKMapItem *fromItem = [[MKMapItem alloc] initWithPlacemark:fromPlace];
MKMapItem *toItem = [[MKMapItem alloc] initWithPlacemark:toPlace];
//初始化導(dǎo)航搜索請求
MKDirectionsRequest *request = [[MKDirectionsRequest alloc] init];
request.source = fromItem;
request.destination = toItem;
request.requestsAlternateRoutes=NO;
request.transportType = MKDirectionsTransportTypeWalking;
//初始化請求檢索
MKDirections *directions = [[MKDirections alloc] initWithRequest:request];
//開始檢索,結(jié)果會返回在block中
[directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) {}];
在返回的線路規(guī)劃數(shù)組中其實是一個個路徑坐標(biāo)點(diǎn)的信息藐俺,我們就可以通過這些坐標(biāo)點(diǎn)在AR世界里畫出一段真實的線路出來炊甲,關(guān)鍵代碼:
//先計算總距離
CLLocationDistance distance = [_startLocation distanceFromLocation:_endNode.location];
//計算需要繪制多少個箭頭(每5米一個)
NSUInteger count = distance/5;
_lastVector = SCNVector3Make(0, 0, 0);//用于記錄上一個箭頭的坐標(biāo)
for (int i = 1; i<count; i++) {
SCNVector3 vector = {(_endNode.position.x-_startVector.x)/count*i + _startVector.x,-2,(_endNode.position.z-_startVector.z)/count*i + _startVector.z};//計算每一個箭頭的坐標(biāo)
SCNNode *node = [self getArrowNodeWithStartVector:vector];//根據(jù)坐標(biāo)生成箭頭
_lastVector = vector;
[self addChildNode:node];//加入RootNode
}
導(dǎo)航與地圖自動切換
這個功能比較簡單,卻對用戶的體驗有幫助 我們要做的就是判斷用戶的手機(jī)是平置還是豎起欲芹,在平置時我們切換到地圖模式卿啡,豎起時切換到AR模式 判斷手機(jī)朝向使用了加速計CMMotionManager的startAccelerometerUpdatesToQueue方法來進(jìn)行判斷。
實時導(dǎo)航提醒功能
現(xiàn)在萬事俱備了菱父,整套操作看起來已經(jīng)完美銜接颈娜,可是如果用戶真的按照路線走了,能否像高德地圖那樣實時提醒呢浙宜? 還是先上效果圖:
這里我們用到了高德SDK中的一個地理圍欄功能官辽,也就是AMapGeoFenceManager
這個類。 在請求完路徑規(guī)劃粟瞬,生成一段一段路線的時候我們將每段路線的終點(diǎn)坐標(biāo)為圓心同仆,定義15米為半徑的一個圓形范圍,當(dāng)用戶進(jìn)入此范圍的時候就表示可以進(jìn)行下一段導(dǎo)航了:
self.fenceManager = [[AMapGeoFenceManager alloc] init];
self.fenceManager.delegate = self;
self.fenceManager.activeAction = AMapGeoFenceActiveActionInside; //設(shè)置希望偵測的圍欄觸發(fā)行為裙品,默認(rèn)是偵測用戶進(jìn)入圍欄的行為乓梨,即AMapGeoFenceActiveActionInside,這邊設(shè)置為進(jìn)入清酥,離開扶镀,停留(在圍欄內(nèi)10分鐘以上),都觸發(fā)回調(diào)
[weakSelf.fenceManager addCircleRegionForMonitoringWithCenter:location.coordinate radius:15 customID:[NSString stringWithFormat:@"%d",i]];//這邊通過一個id表示第幾段圍欄
只要用戶進(jìn)入此范圍就會觸發(fā)高德的代理方法:- (void)amapGeoFenceManager:(AMapGeoFenceManager *)manager didGeoFencesStatusChangedForRegion:(AMapGeoFenceRegion *)region customID:(NSString *)customID error:(NSError *)error;
我們只要在此方法中去更新實時導(dǎo)航信息就行了焰轻。
小結(jié)與展望
ARKit的功能已經(jīng)足夠強(qiáng)大臭觉,但是目前耗電還是有一些,我們會持續(xù)優(yōu)化以盡量減少一些消耗辱志。此外蝠筑,我們也在期待Android陣營的ARCore能夠支持更多的安卓設(shè)備,屆時我們也會在安卓平臺上實現(xiàn)AR找酒店的功能揩懒。同時什乙,我們也會嘗試在更多更適合的場景來應(yīng)用AR功能。