专栏首页iOSDevLogARKit和CoreLocation

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 第0章:什么是机器学习?

    原文:https://medium.com/machine-learning-101/chapter-0-what-is-machine-learning-ad...

    iOSDevLog
  • 聚焦位置-选择您喜欢的位置放置虚拟物体

    在上一个视频中,您学习了如何检测水平曲面并能够透视它。正如我所提到的,它们是放置物体的锚点。但是,在飞机上我们应该添加我们的物体?为此,我们需要在屏幕上选择一个...

    iOSDevLog
  • ARKit+Swift 版本的机器学习算法 k-NN

    在模式识别领域中,最近邻居法(KNN算法,又译K-近邻算法)是一种用于分类和回归的非参数统计方法[1]。在这两种情况下,输入包含特征空间(Feature Spa...

    iOSDevLog
  • 【翻译】200行代码讲透RUST FUTURES (2)

    在我们深入研究 Futures in Rust 的细节之前,让我们快速了解一下处理并发编程的各种方法,以及每种方法的优缺点。

    MikeLoveRust
  • kaggle-2美国人口普查年收入50K分类

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/haluoluo211/article/d...

    bear_fish
  • JavaScript中的函数式编程

    函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看作是对数学函数的评估,避免了状态的变化和数据的可变,与函数式编程相对的是命令式编程。我...

    蒋鹏飞
  • Mysql Limit 调优

    tanoak
  • PHP细节

    var_dump($obj->j); //null var_dump(isset($obj->j));//由于$j没有赋值,为空null,所以返回false /...

    唐成勇
  • TypeScript的类型断言,有点像ABAP的强制类型转换

    通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只...

    Jerry Wang

扫码关注云+社区

领取腾讯云代金券