说明
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。