前言
自从WWDC2017苹果发布了ARKit,引发了很多关注,因为这个框架的发布,意味着开发者不需要引入庞大的第三方框架到工程中(如:Vuforia)就可以实现AR比较强大的虚拟增强功能,且在官方的维护下会有一个持续完善的框架体系(到目前ARKit已经到了1.5版本)。
关于ARKit这边不做过多的介绍,网上相应的资料已经很是普及。
为了让技术的发展给用户带来更多的便利,我们在酒店的场景中尝试了AR的功能,帮助用户直观的找到自己身边的酒店,让身处陌生环境的用户也能像在本地一样,在现实场景下看到周围有哪些好酒店,我们做了很多尝试,最终完成了我们AR找酒店的第一版。
介绍我们在实现AR找酒店功能的过程:
这一块还是想先介绍一下几个坐标系的概念:
坐标到3D世界的位置
坐标系
本项目中坐标数据的计算主要在 五个坐标系中进行:
-
物体坐标系
物体坐标系是描述自己的坐标系,每个物体都有自己的坐标系,可以描述自己的 状态,如:宽、高等。
-
世界坐标系
世界坐标系是系统的绝对坐标系,在没有建立用户坐标系之前画面上所有点的坐标都是以该坐标系的原点来确定各自的位置的。
-
相机坐标系(观察坐标系)
相机坐标系是以光轴与图像平面的交点为图像坐标系的原点所构成的直角坐标系。
-
投影仪坐标系
直接将3D坐标转换为屏幕坐标是非常复杂的(因为它们不仅维度不同,度量不同(屏幕坐标一般都是像素为单位,3D空间中我们可以现实世界的米,厘米为单位),XY的方向也不同,在2D空间时还要进行坐标系变换),所以先将3D坐标降维到2D坐标系中,这个2D坐标系就是投影坐标系。
-
屏幕坐标系
手机屏幕上的2D坐标系
变换顺序:
物体坐标系->世界坐标系: 将物体自身数据放在世界坐标系中,即赋予GPS坐标。
世界坐标系->相机坐标系: 截取部分世界坐标系作为相机坐标系。将GPS坐标相对位置和差值映射在相机坐标系中。
相机坐标系->投影坐标系: 将形体投射到投影面上,从而获得的一种较为接近视觉效果的单面投影图,有点像皮影戏。将3维坐标降维。
投影坐标系->屏幕坐标系: 将投影屏坐标对应转换为手机屏幕上的坐标数据。
在转换过程中会存在左右手坐标系的区分,坐标系单位的转换和其他数学计算等问题,理清这些问题可以对之后的问题定位、修改和优化方向有很大的裨益。
//相关转换代码
- (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;
}
介绍几点关于坐标转换的定义: Haversine公式(大圆距离)
如果地球上有两个不同的经纬度值,那么在Haversine公式的帮助下,您可以轻松计算出大圆距离(球体表面上两点之间的最短距离)。
-- Movable-Type
如果,把用户当前位置当作中心点,如图
那么,我们想在屏幕中展示出酒店信息,只需要计算两个值来确定酒店的点:
用户点到酒店点的距离(distance)
地球南/北线与用户点到酒店点连线的角度(bearing)
distance可以在AR世界中设置3D模型的距离,通过bearing来做旋转转换 如果是在平面系统上,就可以用三角函数等来处理,但是由于地球不是平面,所以需要使用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世界的坐标转换到显示区的2D的坐标
结合2D坐标和酒店信息框的宽高得到两个酒店信息框的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)]];
}
}
}
}
将会互相重合的进行分组Group, 分组以后就可以根据距离的远近进行依次抬高即设置position Y。
酒店方向提示
在导航模式时,当用户手机的朝向不在酒店方向时,我们做了一个方位的提示 使用renderer(ARSCNViewDelegate)代理方法进行实时判断:
通过isNodeInsideFrustum方法确定酒店node是否在显示区
获取酒店的位置
将屏幕左边和右边的点分别转换到3D世界的坐标
分别计算酒店点到左边点和右边点的距离
根据距离确定酒店是在屏幕的左边还是右边
代码示列:
SCNNode *pointOfView = renderer.pointOfView;
BOOL isVisible = [renderer isNodeInsideFrustum:_currentNode withPointOfView:pointOfView];//当前node是否在屏幕中可见
//将屏幕中node的三维坐标转换成屏幕中我们熟知的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];
}
}
});
雷达的实现
很多时候其实用户拿起手机时屏幕视野中是正好没有酒店的,这个时候如果有个雷达功能告诉用户什么方位有酒店那就太好不过了,不多说,搞起来! 先画张图看看思路是怎么样的:
我们以用户位置的经纬度坐标作为圆心,圆半径实际就是所有酒店当中离我们最远的那个酒店离我们的距离(可以取一个稍大于这数值的整数)。 这样就很容易能求出酒店的点应该显示在圆的哪个位置。 再利用CLLocationManager的代理- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
每当用户设备发生转向的时候可以得到一个角度,让雷达视图做动画效果: CATransform3DRotate(CATransform3DIdentity, 角度/360.0 * 2 * M_PI, 0, 0, -1);
这样我们就实现了一个雷达功能,效果如图:
导航的实现
既然是AR找酒店,那导航功能是必不可少的,借鉴了市面上一些已经有的产品(随便走,HotStepper),我们的思路是在自身位置与酒店位置之间用一个个箭头的形式来指示用户的行走路线,首先来看下最终效果图吧:
首先获取路线规划: 使用系统原生方法即可:
//创建出发点和目的点信息
MKPlacemark *fromPlace = [[MKPlacemark alloc] initWithCoordinate:用户自身经纬度
addressDictionary:nil];
MKPlacemark *toPlace = [[MKPlacemark alloc] initWithCoordinate:酒店经纬度 addressDictionary:nil];
//创建出发节点和目的地节点
MKMapItem *fromItem = [[MKMapItem alloc] initWithPlacemark:fromPlace];
MKMapItem *toItem = [[MKMapItem alloc] initWithPlacemark:toPlace];
//初始化导航搜索请求
MKDirectionsRequest *request = [[MKDirectionsRequest alloc] init];
request.source = fromItem;
request.destination = toItem;
request.requestsAlternateRoutes=NO;
request.transportType = MKDirectionsTransportTypeWalking;
//初始化请求检索
MKDirections *directions = [[MKDirections alloc] initWithRequest:request];
//开始检索,结果会返回在block中
[directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) {}];
在返回的线路规划数组中其实是一个个路径坐标点的信息,我们就可以通过这些坐标点在AR世界里画出一段真实的线路出来,关键代码:
//先计算总距离
CLLocationDistance distance = [_startLocation distanceFromLocation:_endNode.location];
//计算需要绘制多少个箭头(每5米一个)
NSUInteger count = distance/5;
_lastVector = SCNVector3Make(0, 0, 0);//用于记录上一个箭头的坐标
for (int i = 1; i
导航与地图自动切换
这个功能比较简单,却对用户的体验有帮助 我们要做的就是判断用户的手机是平置还是竖起,在平置时我们切换到地图模式,竖起时切换到AR模式 判断手机朝向使用了加速计CMMotionManager的startAccelerometerUpdatesToQueue方法来进行判断。
实时导航提醒功能
现在万事俱备了,整套操作看起来已经完美衔接,可是如果用户真的按照路线走了,能否像高德地图那样实时提醒呢? 还是先上效果图:
这里我们用到了高德SDK中的一个地理围栏功能,也就是AMapGeoFenceManager
这个类。 在请求完路径规划,生成一段一段路线的时候我们将每段路线的终点坐标为圆心,定义15米为半径的一个圆形范围,当用户进入此范围的时候就表示可以进行下一段导航了:
self.fenceManager = [[AMapGeoFenceManager alloc] init];
self.fenceManager.delegate = self;
self.fenceManager.activeAction = AMapGeoFenceActiveActionInside; //设置希望侦测的围栏触发行为,默认是侦测用户进入围栏的行为,即AMapGeoFenceActiveActionInside,这边设置为进入,离开,停留(在围栏内10分钟以上),都触发回调
[weakSelf.fenceManager addCircleRegionForMonitoringWithCenter:location.coordinate radius:15 customID:[NSString stringWithFormat:@"%d",i]];//这边通过一个id表示第几段围栏
只要用户进入此范围就会触发高德的代理方法:- (void)amapGeoFenceManager:(AMapGeoFenceManager *)manager didGeoFencesStatusChangedForRegion:(AMapGeoFenceRegion *)region customID:(NSString *)customID error:(NSError *)error;
我们只要在此方法中去更新实时导航信息就行了。
小结与展望
ARKit的功能已经足够强大,但是目前耗电还是有一些,我们会持续优化以尽量减少一些消耗。此外,我们也在期待Android阵营的ARCore能够支持更多的安卓设备,届时我们也会在安卓平台上实现AR找酒店的功能。同时,我们也会尝试在更多更适合的场景来应用AR功能。