ARKit和CoreLocation:第二部分

image.png

演示代码
ARKit和CoreLocation:第一部分
ARKit和CoreLocation:第二部分
ARKit和CoreLocation:第三部分

数学与坐标之间的计算

image.png

如果您没有机会,请先查看第一部分

现在我们需要弄清楚如何获得两个坐标之间的方位(角度)。寻找轴承设置我们以创建旋转变换以使我们的节点朝向正确的方向。

image.png

定义

弧度:弧度是定义为使得一个的角角度度量单位弧度从单位圆的中心所对产生具有弧长的弧1.一种弧度等于180 /π度,使从弧度转换为度,乘以180 /π。

image.png
extension Double {
    func toRadians() -> Double {
        return self * .pi / 180.0
    }
    
    func toDegrees() -> Double {
        return self * 180.0 / .pi
    }
}

半正矢

Haversine公式的一个缺点是它可能在较长距离内变得不太准确。如果我们为商用客机设计可能存在问题的导航系统,但距离的长度不足以对ARKit演示产生影响。

定义

方位角:是球面坐标系的角度测量。

球形三角形通过半导体定律解决

如果您有两个不同的纬度 - 地球上两个不同点的经度值,那么在Haversine公式的帮助下,您可以轻松计算大圆距离(球体表面上两点之间的最短距离)。

image.png
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时,坐标表示无穷远处的点(具有无限长度的矢量),其用于表示方向。

旋转矩阵

为了使我们的对象指向正确的方向,我们需要实现旋转变换。

旋转变换*(0,0,0)*使用给定的角度围绕原点旋转矢量

image.png
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上,我们有兴趣将它放在我们当前的位置并根据指南针旋转。

矩阵变换

旋转和缩放变换矩阵仅需要三列。但是,为了进行变换,矩阵需要至少有四列。这就是转换通常是4x4矩阵的原因。然而,由于矩阵乘法的规则,具有四列的矩阵不能与3D矢量相乘。四列矩阵只能乘以四元素向量,这就是我们经常使用齐次4D向量而不是3D向量的原因。

image.png
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

khanacademy.org/math/linear-algebra/matrix-transformations/linear-transformations/a/visualizing-linear-transformations

edwilliams.org/avform.htm#LL

tomdalling.com/blog/modern-opengl/04-cameras-vectors-and-input/

movable-type.co.uk

tomdalling.com

中:Yat Choi

opengl-tutorial.org

open.gl/transformations

原文:https://medium.com/journey-of-one-thousand-apps/arkit-and-corelocation-part-two-7b045fb1d7a1
Christopher Webb-Orenstein

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