[ARKit]9-3D/AR 中的 simd 类型

说明

ARKit系列文章目录objective-c

simd

SIMD全称Single Instruction Multiple Data,单指令多数据流,可以复制多个操做数,读做[洗目~底]swift

以加法指令为例,单指令单数据(SISD)的CPU对加法指令译码后,执行部件先访问内存,取得第一个操做数;以后再一次访问内存,取得第二个操做数;随后才能进行求和运算。而在SIMD型的CPU中,指令译码后几个执行部件同时访问内存,一次性得到全部操做数进行运算。这个特色使SIMD特别适合于多媒体应用等数据密集型运算。数组

iOS中的simd

iOS11开始,正式引入这个运算,用于加速图形图象的处理.安全

SceneKit中保留旧属性名称和类型的同时,在iOS11添加新类型,并以simd_前缀开头好比SCNNode类型的属性中:架构

// 旧的变换矩阵
open var transform: SCNMatrix4

// 新的simd类型矩阵
@available(iOS 11.0, *)
open var simdTransform: simd_float4x4

复制代码

SceneKit中这二者只要改变一个,另外一个也会随之改变,做用是同样的,只是格式不一样.ide

ARKit在iOS11才推出,直接使用了新类型,名称前也没有前缀,好比在ARAnchor类型的属性中:函数

/** The transformation matrix that defines the anchor’s rotation, translation and scale in world coordinates. */
open var transform: matrix_float4x4 { get }
复制代码

下面咱们来仔细看看matrix_float4x4这个结构体,只是个别名,真实类型仍是simd_float4x4:post

public typealias matrix_float4x4 = simd_float4x4
复制代码
/*! @abstract A matrix with 4 rows and 4 columns. */
public struct simd_float4x4 {

    public var columns: (simd_float4, simd_float4, simd_float4, simd_float4)

    public init()

    public init(columns: (simd_float4, simd_float4, simd_float4, simd_float4))
}
复制代码

里面是4个simd_float4向量类型组成的,而这只是个别名,真正类型是float4,它也是结构体:测试

/*! @abstract A vector of four 32-bit floating-point numbers. * @description In C++ and Metal, this type is also available as * simd::float4. The alignment of this type is greater than the alignment * of float; if you need to operate on data buffers that may not be * suitably aligned, you should access them using simd_packed_float4 * instead. 该向量,由4个32-bit浮点数组成.在C++和Metal中,该类型为simd::float4.该类型的对齐大于float类型的对齐;若是你须要直接操做数据缓冲可能会有对齐问题,你应该经过simd_packed_float4来访问. */
public typealias simd_float4 = float4


/// A vector of four `Float`. This corresponds to the C and
/// Obj-C type `vector_float4` and the C++ type `simd::float4`.
/// 四个`Float`组成的向量.在C和ObjC中的对应类型为`vector_float4`,在C++中为`simd::float4`.
public struct float4 {

    public var x: Float

    public var y: Float

    public var z: Float

    public var w: Float

    /// Initialize to the zero vector.
    public init()

    /// Initialize a vector with the specified elements.
    public init(_ x: Float, _ y: Float, _ z: Float, _ w: Float)

    /// Initialize a vector with the specified elements.
    public init(x: Float, y: Float, z: Float, w: Float)

    /// Initialize to a vector with all elements equal to `scalar`.
    public init(_ scalar: Float)

    /// Initialize to a vector with elements taken from `array`.
    ///
    /// - Precondition: `array` must have exactly four elements.
    public init(_ array: [Float])

    /// Access individual elements of the vector via subscript.
    public subscript(index: Int) -> Float
}
复制代码

simd_floatN & floatN & simd_packed_floatN

float4为例,总共有多种类型和它相关:ui

public typealias simd_float4 = float4

public typealias vector_float4 = simd_float4

public typealias simd_packed_float4 = float4

还有一个是已经废除的packed_float4:

public typealias packed_float4 = simd_packed_float4

Xcode文档对此进行了解释:

这些类型是基于clang特性,叫作"扩展向量类型"或"OpenCL向量类型"(可用在C,Objective-C和C++中).还有一些新特性让它比传统的simd类型更方便使用:

  • 重写了基础运算符,能够进行向量-向量运算,向量-标量运算.
  • 能够经过"."操做符及子元素名称"x", "y", "z", "w",像数组那样访问子元素.
  • 还有一些已命名的子向量:.lo和.hi分别是向量的前半部分和后半部分,.even和.odd则分别是偶数位和奇数位向量元素.
  • Clang提供了一些有用的内置操做能够操做这些向量类型:__builtin_shufflevector__builtin_convertvector.
  • <simd/simd.h>头文件中定义了大量的向量和矩阵操做,适用于这些类型.
  • 你也能够根据不一样架构,引用<immintrin.h> and <arm_neon.h>来使用simd类型.

simd向量的类型定义有:

simd_charN   where N is 1, 2, 3, 4, 8, 16, 32, or 64.
 simd_ucharN  where N is 1, 2, 3, 4, 8, 16, 32, or 64.
 simd_shortN  where N is 1, 2, 3, 4, 8, 16, or 32.
 simd_ushortN where N is 1, 2, 3, 4, 8, 16, or 32.
 simd_intN    where N is 1, 2, 3, 4, 8, or 16.
 simd_uintN   where N is 1, 2, 3, 4, 8, or 16.
 simd_floatN  where N is 1, 2, 3, 4, 8, or 16.
 simd_longN   where N is 1, 2, 3, 4, or 8.
 simd_ulongN  where N is 1, 2, 3, 4, or 8.
 simd_doubleN where N is 1, 2, 3, 4, or 8.
复制代码

这些类型相比底层的标量类型有更大的对齐间隔;它们对齐的是:向量[1]的尺寸与目标平台Clang中"最大对齐间隔"[2],这二者中的最小值.

[1]注意,3维向量和4维向量尺寸是同样的,由于3维有一个隐藏的通道.

[2]通常来讲,架构层面的向量宽度是16, 32, or 64 bytes.若是你须要精确控制对齐方式,须要当心一点,由于这个值会根据编译设置的不一样而变化.

对于simd_typeN类型来讲,除了N等于1或3外,都有相应类型的simd_packed_typeN,它只要求对齐方式匹配底层标量类型.若是你须要处理指向标量的指针或包含标量的数组,就使用simd_packed_typeN类型:

//float *pointerToFourFloats是指向四维Float数组的指针
void myFunction(float *pointerToFourFloats) {
   
    // 这样作是个bug,由于`pointerToFourFloats`并不知足`simd_float4`类型要求的对齐方式;强制转换极可能在运行时致使崩溃
    simd_float4 *vecptr = (simd_float4 *)pointerToFourFloats;

  
    // 应该转换为`simd_packed_float4`类型:
    simd_packed_float4 *vecptr = (simd_packed_float4 *)pointerToFourFloats;
  
    // `simd_packed_float4`类型的对齐方式和`float`相同,因此这个转换是安全的,可让咱们成功加载一个向量.
    // `simd_packed_float4`类型能够不用转换就直接赋值给`simd_float4`;它们的类型只有在做为指针或数组时才会有所不一样.
    simd_float4 vector = vecptr[0];
}
 
 
复制代码

全部simd_开头的类型,在C++中都在simd::命名空间中;好比,simd_char4能够用simd::char4. 这些类型大部分都和Metal shader language中的向量类型相匹配,除了那些超过4维的向量,由于Metal中没有超过4的向量.

类型的转化

swift中的转换

在swift中没有指针的转换问题,而simd_floatN & vector_floatN & simd_packed_floatN这些类型本质上都是floatN类型,因此转换就至关简单了,直接init()从新构造一个就能够了:

let floats:[Float] = [1,2,3,4]
// 直接转换为simd_float4类型,本质是调用了float4.init()方法,该init()方法能够接收多种类型,其中就包括了[Float]类型参数
let testVect4:simd_float4 = simd_float4(floats)
// 转换为simd_packed_float4类型,再赋值给simd_float4类型,本质也是调用了float4.init()方法
let result2Vect4:simd_float4 = simd_packed_float4(floats)

// 先转为simd_packed_float4类型,原理也是init()方法,不使用as再次转换也能够
let packedVect4:simd_packed_float4 = simd_packed_float4(floats)
let resultVect4:simd_float4 = packedVect4 as simd_float4

// 直接强制转换是不能够的,好比下面的方法
let result1111Vect4:simd_float4 = (simd_float4)floats
let result2222Vect4:simd_float4 = floats as! simd_float4
复制代码

反过来转换也是同样的道理,调用init()方法

let simdVect4 = simd_float4(1,2,3,4)
// 调用数组的Array.init()方法
let array:[Float] = Array(simdVect4);
复制代码

在测试中发现,swift中的[Float]类型和simd_float4类型即float4在内存中储存结构彻底不一样:

OC中的转换

OC中由于有指针的概念,因此须要注意的是指针的类型.

在OC和C中,直接转换指针类型,数据的结构并无变,而结构体类型在通过一次赋值后,会发生复制,在复制过程当中类型转换就完成了:

float floats[4] = {1,2,3,4};
// 此时packedVect4指针仍然指向的是数组开头,只是指针的类型不一样
simd_packed_float4 *packedVect4 = (simd_packed_float4 *)floats;
// *packedVect4表示取出packedVect4指针指向的数据,而后赋值给resultVect4,C语言中常量和结构体的赋值是值传递,所谓值传递就是复制一份出来,在复制并赋值的同时,类型转换就完成了.
simd_float4 resultVect4 = *packedVect4;


// 由于simd_packed_float4类型和simd_float4本质都是float4类型,理论上用哪一个都无所谓,我测试的过程当中未发现异常.
// 可是苹果给出了示例代码中使用了simd_packed_float4类型作中转,并且特别说明:它们的类型只有在做为指针或数组时才会有所不一样.因此在OC中仍是用simd_packed_float4中转一下更安全.
simd_float4 result2Vect4 = *(simd_packed_float4 *)floats;
simd_float4 result3Vect4 = *(simd_float4*)floats;
复制代码

相反的转换,由于C语言中的数组初始化只能用{},因此限制了类型转换,很是不便:

simd_float4 simdVect4 = simd_make_float4(1, 2, 3, 4);
// 只能一个个数值取出,再初始化C数组
float arr[4] = {simdVect4.x,simdVect4.y,simdVect4.z,simdVect4.w};
float arr2[4] = {simdVect4[0],simdVect4[1],simdVect4[2],simdVect4[3]};
复制代码

测试中发现,不考虑内存对齐,OC中的float [4]类型数组与simd_float4类型在内存中储存结构是同样的:

使用中的优点

在WWDC2018中,也讲到了simd类型的应用. 主要有几点:

  • 用在2,3,4维矩阵中,和2, 3, 4, 8, 16, 32, 或 64维向量中.
  • 向量与向量,向量与标题能够用运算符(+,-,*,/)进行运算.
  • 常见的向量和几何运算(dot, length, clamp).
  • 支持超越函数(如sin,cos).
  • 四元数

举例,之前的作法,常规数组很慢,simd很快:

// 求两个向量的平均值,之前作法
var x:[Float] = [1,2,3,4]
var y:[Float] = [3,3,3,3]
var z = [Float](repeating:0, count:4)
for i in 0..<4 {
    z[i] = (x[i] + y[i]) / 2.0
}



// 如今作法
let x = simd_float4(1,2,3,4)
 let y = simd_float4(3,3,3,3)
let z = 0.5 * (x + y)
复制代码

重点讲一下四元数在旋转中的使用.

// 要旋转的向量(下图中红点)
 let original = simd_float3(0,0,1)
  // 旋转轴和角度
 let quaternion = simd_quatf(angle: .pi / -3,
 axis: simd_float3(1,0,0))
 // 应用旋转(下图中黄点)
  let rotatedVector = simd_act(quaternion, original)
复制代码

开发中,咱们不会只绕一个轴旋转,可能会是两个或或复杂.

// 要旋转的向量(红点)
let original = simd_float3(0,0,1)
// 旋转轴
let quaternion = simd_quatf(angle: .pi / -3,
                            axis: simd_float3(1,0,0))
let quaternion2 = simd_quatf(angle: .pi / 3,
                             axis: simd_float3(0,1,0))
// 组合两个旋转
let quaternion3 = quaternion2 * quaternion
// 应用旋转(黄点)
let rotatedVector = simd_act(quaternion3, original)
复制代码

另外simd还支持四元数的插值运算,Slerp Interpolation(球面线性插值)和Spline Interpolation(样条曲线插值).

// Slerp Interpolation球面线性插值
let blue = simd_quatf(...) //蓝色
let green = simd_quatf(...) //绿色
let red = simd_quatf(...) //红包

for t: Float in stride(from: 0, to: 1, by: 0.001) {
    let q = simd_slerp(blue, green, t) //从蓝色到绿色的插值曲线(最短球面距离)
    // Code to Add Line Segment at `q.act(original)`
}

for t: Float in stride(from: 0, to: 1, by: 0.001) {
    let q = simd_slerp_longest(green, red, t) //从蓝色到绿色的插值曲线(最长球面距离)
    // Code to Add Line Segment at `q.act(original)`
}
复制代码

// Spline Interpolation样条曲线插值
let original = simd_float3(0,0,1)
let rotations: [simd_quatf] = ...
for i in 1 ... rotations.count - 3 {
    for t: Float in stride(from: 0, to: 1, by: 0.001) {
        let q = simd_spline(rotations[i - 1],
                            rotations[i],
                            rotations[i + 1],
                            rotations[i + 2],
                            t)
    }
    // Code to Add Line Segment at `q.act(original)`
}
复制代码

二者在旋转运动中的区别仍是很明显的,以下图顶点轨迹