iOS--AR使用

前言

自从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

image

如果,把用户当前位置当作中心点,如图

image

那么,我们想在屏幕中展示出酒店信息,只需要计算两个值来确定酒店的点:

  1. 用户点到酒店点的距离(distance)

  2. 地球南/北线与用户点到酒店点连线的角度(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)

抬高酒店

抬高的方式,判断两个酒店的显示图层,在屏幕上是否有重合,有重合时按距离依次上抬 判断两个酒店是否有重合的方式:

  1. 使用projectPoint方法,将物体在3D世界的坐标转换到显示区的2D的坐标

  2. 结合2D坐标和酒店信息框的宽高得到两个酒店信息框的CGRect

  3. 使用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)代理方法进行实时判断:

  1. 通过isNodeInsideFrustum方法确定酒店node是否在显示区

  2. 获取酒店的位置

  3. 将屏幕左边和右边的点分别转换到3D世界的坐标

  4. 分别计算酒店点到左边点和右边点的距离

  5. 根据距离确定酒店是在屏幕的左边还是右边

代码示列:

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];
        }
    }
});

雷达的实现

很多时候其实用户拿起手机时屏幕视野中是正好没有酒店的,这个时候如果有个雷达功能告诉用户什么方位有酒店那就太好不过了,不多说,搞起来! 先画张图看看思路是怎么样的:

image

我们以用户位置的经纬度坐标作为圆心,圆半径实际就是所有酒店当中离我们最远的那个酒店离我们的距离(可以取一个稍大于这数值的整数)。 这样就很容易能求出酒店的点应该显示在圆的哪个位置。 再利用CLLocationManager的代理- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading 每当用户设备发生转向的时候可以得到一个角度,让雷达视图做动画效果: CATransform3DRotate(CATransform3DIdentity, 角度/360.0 * 2 * M_PI, 0, 0, -1); 这样我们就实现了一个雷达功能,效果如图:

image

导航的实现

既然是AR找酒店,那导航功能是必不可少的,借鉴了市面上一些已经有的产品(随便走,HotStepper),我们的思路是在自身位置与酒店位置之间用一个个箭头的形式来指示用户的行走路线,首先来看下最终效果图吧:

image

首先获取路线规划: 使用系统原生方法即可:

//创建出发点和目的点信息
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<count; i++) {
    SCNVector3 vector = {(_endNode.position.x-_startVector.x)/count*i + _startVector.x,-2,(_endNode.position.z-_startVector.z)/count*i + _startVector.z};//计算每一个箭头的坐标
    SCNNode *node = [self getArrowNodeWithStartVector:vector];//根据坐标生成箭头
    _lastVector = vector;
    [self addChildNode:node];//加入RootNode
}

导航与地图自动切换

这个功能比较简单,却对用户的体验有帮助 我们要做的就是判断用户的手机是平置还是竖起,在平置时我们切换到地图模式,竖起时切换到AR模式 判断手机朝向使用了加速计CMMotionManager的startAccelerometerUpdatesToQueue方法来进行判断。

实时导航提醒功能

现在万事俱备了,整套操作看起来已经完美衔接,可是如果用户真的按照路线走了,能否像高德地图那样实时提醒呢? 还是先上效果图:

image

这里我们用到了高德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功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容