42-如何仿写一个简易版的3自由度 AR 导航

1,688 阅读5分钟

说明

ARKit系列文章目录

AR 导航简介

AR 导航功能一直是大家期待的,但是直到今天也没有哪个 app 能把 GPS 和 ARKit 完美结合起来。GPS 在短距离内不准和 ARKit 在长距离时不准的问题始终困扰着大家。

不过仍然有很多开发者进行了尝试。比如

  • ARKit-CoreLocation
    ARKit-CoreLocation 是利用 ARKit 的世界追踪和 CoreLocation 的 GPS 定位来综合实现 AR 导航的项目,整体效果非常好。

不过在真正实用中,各家 app 使用的还是 3 自由度的导航比较多,比如百度地图的步行实景导航,美团外卖等等。 下图是百度地图的 AR 导航功能演示(为保护隐私做了遮挡),百度地图的 SDK 中也直接提供了步行 AR 导航功能:

接下来我们就以此为原型,进行分析并完成一个 3 自由度的 AR 导航功能。

实现思路

首先,是地图 sdk 的准备,这里我选用了百度地图,并将显示模式设置为跟随手机方向BMKUserTrackingModeFollowWithHeading。这样当手机旋转时,地图就会旋转,地图上的箭头始终是朝向屏幕前方。

其次,是 ARKit 的使用,就使用朝向追踪配置AROrientationTrackingConfiguration并设置configuration.worldAlignment = ARWorldAlignmentGravityAndHeading;这样 z 轴会自动对齐真实世界的南方。

接着,我们调用地图的步行路线规划并在地图上画线,这里可以参照百度地图的 demo。

最后,在 GPS 位置第一次更新时,我们取出路线上最近的 6 个点,计算其相对于手机的位置,并在 AR 中显示出来。并在点之间连线。

此后,每次位置更新时,都计算手机与附近 6 个点的距离,当发现距离足够近了(如小于 20 米),就把该点及更近的点都删除,重新取新的 6 个点显示在 AR 中。

// 每次位置更新时,调用该方法
- (void)updateTargetNodePositionAndGuideLine {
    if (self.pointCount > 0 && !(self.userLocation.location.coordinate.latitude == 0 && self.userLocation.location.coordinate.longitude == 0)) {
        BMKMapPoint userPoint = BMKMapPointForCoordinate(self.userLocation.location.coordinate);
       // 百度地图上每一点,对应的真实距离,和纬度有关
        double metersPerMapPoint =  BMKMetersPerMapPointAtLatitude(self.userLocation.location.coordinate.latitude);
        
        // 更新 AR 中的终点
        double targetDisplayX = 0,targetDisplayY = 0;//AR显示终点的位置
        double targetDistance = BMKMetersBetweenMapPoints(userPoint, self.polylinePoints[self.pointCount - 1]);//终点的距离
        if (targetDistance > 20) {//超过了 20 米,放在 20 米处
            targetDisplayX = 20.0 / targetDistance * (self.polylinePoints[self.pointCount - 1].x - userPoint.x) * metersPerMapPoint;
            targetDisplayY = 20.0 / targetDistance * (self.polylinePoints[self.pointCount - 1].y - userPoint.y) * metersPerMapPoint;
        } else {
            targetDisplayX = (self.polylinePoints[self.pointCount - 1].x - userPoint.x) * metersPerMapPoint;//东西方向实际距离
            targetDisplayY = (self.polylinePoints[self.pointCount - 1].y - userPoint.y) * metersPerMapPoint;//南北方向实际距离
        }
       // 终点的显示位置
        self.targetNode.simdPosition = simd_make_float3(targetDisplayX, 0, targetDisplayY);
       // 终点的朝向,始终指向(0,0,0)点,即手机处
        [self.targetNode simdLookAt:simd_make_float3(0)];
        
        
        // 更新最近的 6 个点
        NSInteger displayCount = self.pointCount > 5 ? 6 : self.pointCount;//可能少于 6 个,以实际为准
        
        int tmp = 0;//记录已经跨过/绕过的点。用户绕过2,3,4 个点也能正常导航。一次绕过 6 个点则认为偏航,需重新规划路线
        for (int i = 1; i <= displayCount; i++) {
            double distance = BMKMetersBetweenMapPoints(userPoint, self.polylinePoints[i]);
            if (distance < 20 && self.pointCount > 1) {//走到最近 6 个点附近时,跨越太靠近的点,如果只剩最后一个点不再跨越
                tmp = i;
            }
        }
        self.pointCount -= tmp;//跨过太近的若干个点
        self.polylinePoints += tmp;//跨过太近的若干个点
        
        //刷新地图polyline路线显示
        [self.mapView removeOverlays:self.mapView.overlays];
        //根据指定直角坐标点生成一段折线
        BMKPolyline *polyline = [BMKPolyline polylineWithPoints:self.polylinePoints count: self.pointCount];
        [self.mapView addOverlay:polyline];
        //再画一根,从自己位置到最近的点
//        BMKMapPoint *pointsMe = [Tools creatMapPoints:2];
//        pointsMe[0] = userPoint;
//        pointsMe[1] = self.polylinePoints[0];
//        BMKPolyline *polylineMe = [BMKPolyline polylineWithPoints:pointsMe count:2];
//        [self.mapView addOverlay:polylineMe];
        self.mapView.zoomLevel = 19;
        
        // 删除旧AR箭头(和测试用的 6 个轨迹点)
        NSArray *childNodes = self.guideNode.childNodes.copy;
        SCNNode *arrowNode;
        if (childNodes.count) {
            for (SCNNode *node in childNodes) {
                [node removeFromParentNode];
                if ([node.name isEqualToString:@"arrow"]) {
                    arrowNode = node;//重用旧的箭头
                }
            }
        } else if (!arrowNode){
            // Create a new scene
            SCNScene *scene = [SCNScene sceneNamed:@"art.scnassets/arrow.dae"];
            arrowNode = [scene.rootNode childNodeWithName:@"arrow" recursively:YES];
            arrowNode.name = @"arrow";
        }
        
        // 为最近的 6 个点添加 AR 箭头
        BMKMapPoint *closeSix = self.polylinePoints;// 下一次显示的,附近的 6 个点
        displayCount = self.pointCount > 5 ? 6 : self.pointCount;//可能少于 6 个,以实际为准

        // 第一次摆放箭头的起点,在手机下方 1 米处
        simd_float3 beginPosition = simd_make_float3(0,-1, 0);
        for (int i = 0; i < displayCount; i++) {//6个最近点之间连线
            float displayX = (closeSix[i].x - userPoint.x) * metersPerMapPoint * 0.1;//AR 中缩小 10 倍显示
            float displayY = (closeSix[i].y - userPoint.y) * metersPerMapPoint * 0.1;//AR 中缩小 10 倍显示
            simd_float3 endPosition = simd_make_float3(displayX,-1,displayY);//终点
            
            float displayDistance = simd_fast_distance(beginPosition,endPosition);
            for (int j = 1; j < displayDistance; j++) {//1米放一个
                SCNNode *cloneNode = [arrowNode clone];
                // 对 x,z 进行插值
                float x = j / displayDistance * (endPosition.x - beginPosition.x) + beginPosition.x;
                float z = j / displayDistance * (endPosition.z - beginPosition.z) + beginPosition.z;
                
                // 每个箭头的位置
                cloneNode.simdPosition = simd_make_float3(x, -1, z);
                [cloneNode simdLookAt:endPosition];//箭头指向终点方向
                [self.guideNode addChildNode:cloneNode];
            }
            
            // 测试用,显示轨迹点
            SCNNode *tempNode = [SCNNode nodeWithGeometry:[SCNBox boxWithWidth:0.1 height:0.1 length:0.1 chamferRadius:0]];
            [self.guideNode addChildNode:tempNode];
            tempNode.simdPosition = endPosition;
            tempNode.geometry.materials.firstObject.diffuse.contents = [UIColor redColor];
            
            beginPosition = endPosition;//将这个终点做为新的起点,开启下一轮循环添加箭头
        }
    }
}

最终效果如下:

其他补充说明

需要注意的是,百度地图路线规划时,用到了 c++ 数组,但是 ARKit 与 c++ 不兼容:如果用了 ARKit,那文件中不能有 c++ 语句,文件名也不能是.mm。所以我单独创建了一个类来处理 c++ 数组:

@implementation Tools
// 创建一个长度为 count 的数组,用来存放BMKMapPoint结构体。返回数组地址
+(BMKMapPoint *)creatMapPoints:(NSUInteger)count {
    BMKMapPoint * points= new BMKMapPoint[count];
    return points;
}
@end

由于这只是个演示 Demo,因此效果较为粗糙。百度地图中,AR 中的路径显示为较为圆滑的曲线,离手机近的箭头会变为灰色,GPS 刷新时路线平滑移动。这些效果都没有实现。

代码

本 Demo 代码已上传 github:github.com/XanderXu/AR…

由于 ARKit 需要在真机上运行,所以请先更改 Bundle Identifier,并根据自己的 Bundle Identifier 到百度开发平台申请 key。