ARKit和CoreLocation:第二部分
演示代码
ARKit和CoreLocation:第一部分
ARKit和CoreLocation:第二部分
ARKit和CoreLocation:第三部分
数学与坐标之间的计算
image.png如果您没有机会,请先查看第一部分。
现在我们需要弄清楚如何获得两个坐标之间的方位(角度)。寻找轴承设置我们以创建旋转变换以使我们的节点朝向正确的方向。
image.png定义
弧度:该弧度是定义为使得一个的角角度度量单位弧度从单位圆的中心所对产生具有弧长的弧1.一种弧度等于180 /π度,使从弧度转换为度,乘以180 /π。
image.pngextension Double {
func toRadians() -> Double {
return self * .pi / 180.0
}
func toDegrees() -> Double {
return self * 180.0 / .pi
}
}
半正矢
Haversine公式的一个缺点是它可能在较长距离内变得不太准确。如果我们为商用客机设计可能存在问题的导航系统,但距离的长度不足以对ARKit演示产生影响。
定义
方位角:是球面坐标系的角度测量。
球形三角形通过半导体定律解决image.png如果您有两个不同的纬度 - 地球上两个不同点的经度值,那么在Haversine公式的帮助下,您可以轻松计算大圆距离(球体表面上两点之间的最短距离)。
sin =对边 / 斜边
cos = 邻边 / 斜边
tan = 对边 / 邻边
atan2: 具有两个参数的反正切或反正切函数。
tan 30 = 0.577
意思是:30度的正切是0.577
arctan 0.577 = 30
平均值:切线为0.577的角度为30度。
image.png
按键
' R'是地球的半径
' L'是 经度
'θ'是纬度
' β '正在承受
' Δ '是delta /变化
一般来说,当你沿着一条很大的圆形路径(正统)时,你的当前航向会有所不同; 根据距离和纬度不同,最终航向将与初始航向不同(如果你从35°N,45°E(≈巴格达)到35°N,135°E(≈大阪),你将从60°的航向开始,并以120°的航向结束!)。
该公式用于初始方位(有时称为前方方位角),如果沿着大圆弧沿直线跟随,将从起点到终点
式
β = atan2(X,Y)
where, X and Y are two quantities and can be calculated as:
其中,X和Y是两个数量,可以计算为:
X = cos θb * sin ∆L
Y = cos θa * sin θb — sin θa * cos θb * cos ∆L
extension CLLocationCoordinate2D {
func calculateBearing(to coordinate: CLLocationCoordinate2D) -> Double {
let a = sin(coordinate.longitude.toRadians() - longitude.toRadians()) * cos(coordinate.latitude.toRadians())
let b = cos(latitude.toRadians()) * sin(coordinate.latitude.toRadians()) - sin(latitude.toRadians()) * cos(coordinate.latitude.toRadians()) * cos(coordinate.longitude.toRadians() - longitude.toRadians())
return atan2(a, b)
}
func direction(to coordinate: CLLocationCoordinate2D) -> CLLocationDirection {
return self.calculateBearing(to: coordinate).toDegrees()
}
}
获得距离坐标
image.png虽然MKRoute为我们提供了构建ARKit导航体验的良好框架,但沿着这条路线的步骤可以相距太远而不会破坏体验。为了缓解这种情况,我们需要遍历我们的步骤并生成它们之间的距离间隔的坐标。
给定起点,初始方位和距离,这将计算沿(最短距离)大圆弧行进的目标点和最终方位。
‘d‘ being the distance travelled
‘R’ is the radius of Earth
‘L’ is the longitude
‘φ’ is latitude
‘θ‘ is bearing (clockwise from north)
‘δ‘ is the angular distance d/R
' d ' 是旅行的距离
' R' 是地球的半径
' L' 是经度
'φ' 是纬度
' θ ' 北极(从北向顺时针方向)
' δ ' 是角距离d / R.
公式
φ2 = asin( sin φ1 ⋅ cos δ + cos φ1 ⋅ sin δ ⋅ cos θ )
L2 = L1 + atan2( sin θ ⋅ sin δ ⋅ cos φ1, cos δ − sin φ1 ⋅ sin φ2 )
let metersPerRadianLat: Double = 6373000.0
let metersPerRadianLon: Double = 5602900.0
extension CLLocationCoordinate2D {
// adapted from https://github.com/ProjectDent/ARKit-CoreLocation/blob/master/ARKit%2BCoreLocation/Source/CLLocation%2BExtensions.swift
func coordinate(with bearing: Double, and distance: Double) -> CLLocationCoordinate2D {
let distRadiansLat = distance / metersPerRadianLat // earth radius in meters latitude
let distRadiansLong = distance / metersPerRadianLon // earth radius in meters longitude
let lat1 = self.latitude.toRadians()
let lon1 = self.longitude.toRadians()
let lat2 = asin(sin(lat1) * cos(distRadiansLat) + cos(lat1) * sin(distRadiansLat) * cos(bearing))
let lon2 = lon1 + atan2(sin(bearing) * sin(distRadiansLong) * cos(lat1), cos(distRadiansLong) - sin(lat1) * sin(lat2))
return CLLocationCoordinate2D(latitude: lat2.toDegrees(), longitude: lon2.toDegrees())
}
}
三维变换
matrix × matrix = combined matrix
matrix × coordinate = transformed coordinate
直觉上,三维应该在[3x3]矩阵([x,y,z])中表示似乎是显而易见的。然而,有一个额外的矩阵行,所以三维图形使用[4x4]矩阵:[x,y,z,w]。
真的。W?
Yup W.这个第四维称为“投影空间”,投影空间中的坐标称为“齐次坐标”。当w等于1时,它不影响x,y或z,因为矢量是一个位置空间。当W = 0时,坐标表示无穷远处的点(具有无限长度的矢量),其用于表示方向。
旋转矩阵
为了使我们的对象指向正确的方向,我们需要实现旋转变换。
image.png旋转变换
*(0,0,0)*
使用给定的轴和角度围绕原点旋转矢量
import GLKit.GLKMatrix4
import SceneKit
class MatrixHelper {
// column 0 column 1 column 2 column 3
// cosθ 0 sinθ 0
// 0 1 0 0
// −sinθ 0 cosθ 0
// 0 0 0 1
static func rotateAroundY(with matrix: matrix_float4x4, for degrees: Float) -> matrix_float4x4 {
var matrix : matrix_float4x4 = matrix
matrix.columns.0.x = cos(degrees)
matrix.columns.0.z = -sin(degrees)
matrix.columns.2.x = sin(degrees)
matrix.columns.2.z = cos(degrees)
return matrix.inverse
}
}
3D图形和ARKit的大多数旋转都围绕着相机变换。但是,我们并不关心将我们的物体放在POV上,我们有兴趣将它放在我们当前的位置并根据指南针旋转。
矩阵变换
image.png旋转和缩放变换矩阵仅需要三列。但是,为了进行变换,矩阵需要至少有四列。这就是转换通常是4x4矩阵的原因。然而,由于矩阵乘法的规则,具有四列的矩阵不能与3D矢量相乘。四列矩阵只能乘以四元素向量,这就是我们经常使用齐次4D向量而不是3D向量的原因。
class MatrixHelper {
// column 0 column 1 column 2 column 3
// 1 0 0 X x x + X*w
// 0 1 0 Y x y = y + Y*w
// 0 0 1 Z z z + Z*w
// 0 0 0 1 w w
static func translationMatrix(translation : vector_float4) -> matrix_float4x4 {
var matrix = matrix_identity_float4x4
matrix.columns.3 = translation
return matrix
}
}
把它放在一起
结合矩阵变换
组合转换的顺序非常重要。组合转换时,应按以下顺序进行:
Transform = Scaling * Rotation * Translation
SIMD(单指令多数据)
所以你可能在关于矩阵之前看过simd_mul操作。那是什么?这很简单:simd_mul:单指令多次数据乘法。在iOS 8和OS X Yosemite中,Apple加入了一个名为simd的库,用于为标量,向量和矩阵实现SIMD(单指令,多数据)算法。
输入
*simd.h*
:这个内置库为我们提供了一个标准接口,用于在OS X和iOS上的各种处理器上处理2D,3D和4D矢量和矩阵运算。如果CPU本身不支持给定的操作(例如将4通道向量分成两个双通道操作),它会自动回退到软件例程。它还具有使用Metal在GPU和CPU之间轻松传输数据的好处。
SIMD是一种跨越GPU着色器和老式CPU指令之间差距的技术,允许CPU发出单个指令来并行处理数据块
因此,当您看到正在执行sims_mul时,这意味着什么。您应该注意的一件事是:simd_mul按从右到左的顺序执行操作。
import GLKit.GLKMatrix4
import SceneKit
class MatrixHelper {
// column 0 column 1 column 2 column 3
// 1 0 0 X x x + X*w
// 0 1 0 Y x y = y + Y*w
// 0 0 1 Z z z + Z*w
// 0 0 0 1 w w
static func translationMatrix(with matrix: matrix_float4x4, for translation : vector_float4) -> matrix_float4x4 {
var matrix = matrix
matrix.columns.3 = translation
return matrix
}
// column 0 column 1 column 2 column 3
// cosθ 0 sinθ 0
// 0 1 0 0
// −sinθ 0 cosθ 0
// 0 0 0 1
static func rotateAroundY(with matrix: matrix_float4x4, for degrees: Float) -> matrix_float4x4 {
var matrix : matrix_float4x4 = matrix
matrix.columns.0.x = cos(degrees)
matrix.columns.0.z = -sin(degrees)
matrix.columns.2.x = sin(degrees)
matrix.columns.2.z = cos(degrees)
return matrix.inverse
}
static func transformMatrix(for matrix: simd_float4x4, originLocation: CLLocation, location: CLLocation) -> simd_float4x4 {
let distance = Float(location.distance(from: originLocation))
let bearing = GLKMathDegreesToRadians(Float(originLocation.coordinate.direction(to: location.coordinate)))
let position = vector_float4(0.0, 0.0, -distance, 0.0)
let translationMatrix = MatrixHelper.translationMatrix(with: matrix_identity_float4x4, for: position)
let rotationMatrix = MatrixHelper.rotateAroundY(with: matrix_identity_float4x4, for: bearing)
let transformMatrix = simd_mul(rotationMatrix, translationMatrix)
return simd_mul(matrix, transformMatrix)
}
}
创建我们的SCNNode子类
image.png我们应该做的下一件事是创建我们的节点类。我们将子类化SCNNode并为其赋予title属性,该属性是一个字符串,一个锚属性,它是一个可选的****ARAnchor,在设置时更新位置。最后,我们将为BaseNode类提供一个位置属性,它是一个CLLocation。
import SceneKit
import ARKit
import CoreLocation
class BaseNode: SCNNode {
let title: String
var anchor: ARAnchor? {
didSet {
guard let transform = anchor?.transform else { return }
self.position = positionFromTransform(transform)
}
}
var location: CLLocation!
init(title: String, location: CLLocation) {
self.title = title
super.init()
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = SCNBillboardAxis.Y
constraints = [billboardConstraint]
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
我们需要添加方法来创建球体图形。我们将在第一部分中实现类似于球体的东西,但是针对我们的新条件进行了修改。由于我们只需要MKRouteStep指令中的文本,我们应该创建方法:
import SceneKit
import ARKit
import CoreLocation
class BaseNode: SCNNode {
// Basic sphere graphic
func createSphereNode(with radius: CGFloat, color: UIColor) -> SCNNode {
let geometry = SCNSphere(radius: radius)
geometry.firstMaterial?.diffuse.contents = color
let sphereNode = SCNNode(geometry: geometry)
let trailEmitter = createTrail(color: color, geometry: geometry)
addParticleSystem(trailEmitter)
return sphereNode
}
// Add graphic as child node - basic
func addSphere(with radius: CGFloat, and color: UIColor) {
let sphereNode = createSphereNode(with: radius, color: color)
addChildNode(sphereNode)
}
// Add graphic as child node - with text
func addNode(with radius: CGFloat, and color: UIColor, and text: String) {
let sphereNode = createSphereNode(with: radius, color: color)
let newText = SCNText(string: title, extrusionDepth: 0.05)
newText.font = UIFont (name: "AvenirNext-Medium", size: 1)
newText.firstMaterial?.diffuse.contents = UIColor.red
let _textNode = SCNNode(geometry: newText)
let annotationNode = SCNNode()
annotationNode.addChildNode(_textNode)
annotationNode.position = sphereNode.position
addChildNode(sphereNode)
addChildNode(annotationNode)
}
}
当我们更新位置时,我们采用锚点的矩阵变换并使用最后一列中的x,y和z值,这些值是位置变换的值。
class BaseNode: SCNNode {
var anchor: ARAnchor? {
didSet {
guard let transform = anchor?.transform else { return }
self.position = positionFromTransform(transform)
}
}
// Setup
func positionFromTransform(_ transform: matrix_float4x4) -> SCNVector3 {
// column 0 column 1 column 2 column 3
// 1 0 0 X
// 0 1 0 Y
// 0 0 1 Z
// 0 0 0 1
return SCNVector3Make(transform.columns.3.x, transform.columns.3.y, transform.columns.3.z)
}
}
资料来源:
sites.math.washington.edu/~king/coursedir/m308a01/Projects/m308a01-pdf/yip.pdf
tomdalling.com/blog/modern-opengl/04-cameras-vectors-and-input/
原文:https://medium.com/journey-of-one-thousand-apps/arkit-and-corelocation-part-two-7b045fb1d7a1
Christopher Webb-Orenstein