阅读 250

ARKit 中矩阵的简单再理解

矩阵的数据类型

一般在苹果的 ARKit 和 SceneKit 中,用到的矩阵有三种SCNMatrix4simd_float4x4GLKMatrix4

GLKMatrix4是从 OpenGL 框架 GLKit 中带过来的,各种函数很全面。
SCNMatrix4最初是给 SceneKit 使用的,后来扩展到 ARKit 中,矩阵操作函数不够全面,比如没有转置函数 transpose。
simd_float4x4看名字就知道是 simd 类型的,是后来为了取代SCNMatrix4来推出的,各种函数也很全面。

上面三种类型本质都是结构体,互相转换的方法也在苹果文档中给出了,分别为:

SCNMatrix4 SCNMatrix4FromMat4(simd_float4x4 m) {
    return *(SCNMatrix4 *)&m;
}
simd_float4x4 SCNMatrix4ToMat4(SCNMatrix4 m) {
    return (simd_float4x4){
        .columns[0] = simd_make_float4(m.m11, m.m12, m.m13, m.m14),
        .columns[1] = simd_make_float4(m.m21, m.m22, m.m23, m.m24),
        .columns[2] = simd_make_float4(m.m31, m.m32, m.m33, m.m34),
        .columns[3] = simd_make_float4(m.m41, m.m42, m.m43, m.m44)
    };
}

GLKMatrix4 SCNMatrix4ToGLKMatrix4(SCNMatrix4 mat);//未公开方法实现的代码,不过都是结构体,估计是直接强制转换的
SCNMatrix4 SCNMatrix4FromGLKMatrix4(GLKMatrix4 mat);//未公开方法实现的代码
复制代码

行主序row-order与列主序column-order

一般在苹果的 Demo 和说明中,用到的矩阵SCNMatrix4simd_float4x4GLKMatrix4都是列主序的。

比如在 SCNMatrix4 结构体的定义,及相关平移方法SCNMatrix4MakeTranslation中就能看出,平移向量被赋值.m41 = tx, .m42 = ty, .m43 = tz,就说明被赋值的是第四列的第 1,2,3 个元素:

//m11 指第一列第一个元素
typedef struct SCNMatrix4 {
    float m11, m12, m13, m14;
    float m21, m22, m23, m24;
    float m31, m32, m33, m34;
    float m41, m42, m43, m44;
} SCNMatrix4;

SCNMatrix4 SCNMatrix4MakeTranslation(float tx, float ty, float tz) {
    return (SCNMatrix4){
        .m11 = 1.f, .m12 = 0.f, .m13 = 0.f, .m14 = 0.f,
        .m21 = 0.f, .m22 = 1.f, .m23 = 0.f, .m24 = 0.f,
        .m31 = 0.f, .m32 = 0.f, .m33 = 1.f, .m34 = 0.f,
        .m41 =  tx, .m42 =  ty, .m43 =  tz, .m44 = 1.f
    };
}
复制代码

而 simd_float4x4 更为直接,采用了名为columns[4]的数组,这就告诉了我们,这个矩阵是列主序的


/*! @abstract A matrix with 4 rows and 4 columns.*/
typedef struct { simd_float4 columns[4]; } simd_float4x4;
复制代码

SCNMatrix4ToMat4()函数将 SCNMatrix4 矩阵转换成 simd_float4x4 矩阵,也会看到 .m12 处数值对应的是 columns[0][1] 位置。这也说明了 SCNMatrix4 是列主序的。

转置transpose

如果我们需要对矩阵进行变换,从行主序变为列主序,或从列主序变成行主序,那就需要转置 transpose:

SCNMatrix4 scnMat;
simd_transpose(SCNMatrix4ToMat4(scnMat));
GLKMatrix4Transpose(SCNMatrix4ToGLKMatrix4(scnMat));
复制代码

一般什么时候需要转置呢?
主要看用途:一般线性代数教材中的公式,都是行主序的,所以经常会出现在工具方法中,为了更好的抄公式采用了行主序,最后再转置为列主序矩阵。

还有一种常见的是:利用正交矩阵的特性,用矩阵转置来代替矩阵求逆运算。比如,矩阵 A 代表将一个小球从 a 点变换(只有平移和旋转)到 b 点,矩阵 B 则相反,代表将一个小球从 b 点变换(只有平移和旋转)到 a 点。由于 A 不好求解,那只需要求出 B 矩阵,转置后就得到了 A 矩阵。需要注意的是,在求解 B 的过程中,如果进行平移,应操作.m14 = tx, .m24 = ty, .m34 = tz,即当做行主序矩阵来操作,最后转置后才是正确的结果。

附苹果文档中的转置方法代码:

static simd_float4x4 SIMD_CFUNC simd_transpose(simd_float4x4 __x) {
#if defined __SSE__
    simd_float4 __t0 = _mm_unpacklo_ps(__x.columns[0],__x.columns[2]);
    simd_float4 __t1 = _mm_unpackhi_ps(__x.columns[0],__x.columns[2]);
    simd_float4 __t2 = _mm_unpacklo_ps(__x.columns[1],__x.columns[3]);
    simd_float4 __t3 = _mm_unpackhi_ps(__x.columns[1],__x.columns[3]);
    simd_float4 __r0 = _mm_unpacklo_ps(__t0,__t2);
    simd_float4 __r1 = _mm_unpackhi_ps(__t0,__t2);
    simd_float4 __r2 = _mm_unpacklo_ps(__t1,__t3);
    simd_float4 __r3 = _mm_unpackhi_ps(__t1,__t3);
    return simd_matrix(__r0,__r1,__r2,__r3);
#else
    return simd_matrix((simd_float4){__x.columns[0][0], __x.columns[1][0], __x.columns[2][0], __x.columns[3][0]},
                               (simd_float4){__x.columns[0][1], __x.columns[1][1], __x.columns[2][1], __x.columns[3][1]},
                               (simd_float4){__x.columns[0][2], __x.columns[1][2], __x.columns[2][2], __x.columns[3][2]},
                               (simd_float4){__x.columns[0][3], __x.columns[1][3], __x.columns[2][3], __x.columns[3][3]});
#endif
}

GLKMatrix4 GLKMatrix4Transpose(GLKMatrix4 matrix)
{
#if defined(__ARM_NEON__)
    float32x4x4_t m = vld4q_f32(matrix.m);
    return *(GLKMatrix4 *)&m;
#else
    GLKMatrix4 m = { matrix.m[0], matrix.m[4], matrix.m[8], matrix.m[12],
                     matrix.m[1], matrix.m[5], matrix.m[9], matrix.m[13],
                     matrix.m[2], matrix.m[6], matrix.m[10], matrix.m[14],
                     matrix.m[3], matrix.m[7], matrix.m[11], matrix.m[15] };
    return m;
#endif
}

复制代码

左手系与右手系

SceneKit 和 ARKit 中采用的是右手系。

在项目中不管是对世界坐标原点矩阵还是 SCNNode 的矩阵进行操作,如果反转 x,y,z 任意一根坐标轴,矩阵就会改变手性。

SCNNode如果被放在左手系下,那其中的 3D 模型很可能会显示不正常,表面显示黑色或破损状。这是因为法向量也随着坐标系的变换而改变了,导致的显示不正常。

教材中对此理论进行了说明:

在三维空间中,由3D向量V1,V2和V3组成的坐标系的基B具有偏手性,
当(V1 × V2) ∙ V3 >0时,B称为右手螺旋基。
在右手坐标系中,向量V1和V2外积的方向遵循右手螺旋定律,
与向量V3的方向之间的夹角为锐角。
如果B为右手螺旋正交基,则V1 × V2=V3。
反之,当(V1 × V2) ∙ V3<0时,B称为左手螺旋基。


奇数次反射变换将改变基B的偏手性,偶数次反射变换总是等效于旋转变换,因此一系列任意多的反射变换可以当做一个旋转变换加最多一个反射变换。 

通过计算一个3X3矩阵的行列式可以判断该矩阵是否包含反射变换,
如果一个3X3矩阵M的行列式是负值,则该矩阵包含一个反射变换,则矩阵M将改变它所变换的基向量集的偏手性,
反之,如果矩阵M的行列式是正值,该矩阵将保持变换基向量集的偏手性。

正交矩阵M的行列式的值只能为1或者-1,如果 detM=1, 则矩阵M表示一个纯粹的旋转变换,如果detM=-1, 则矩阵M表示一个旋转变换和一个反射变换。
复制代码

所以,要检查 AR 中世界坐标系的手性,可以直接设置ARSCNDebugOptionShowWorldOrigin显示世界坐标轴,直接目视判断。也可以输出其矩阵,计算 3x3 行列式的值,进行判断。

3D 中的矩阵转换 API

SCNNode 中提供了两个矩阵转换的 API,用来将矩阵变换从一个坐标系下转换到另一个 Node 的坐标系下。

//SCNNode 中的对象方法,作用是将 self(即一个 SCNNode 对象)上的某个矩阵,转换到另一个 Node 的坐标系中。
- (SCNMatrix4)convertTransform:(SCNMatrix4)transform toNode:(nullable SCNNode *)node;

//SCNNode 中的对象方法,作用是将另一个 Node 坐标系下的某个矩阵,转换到 self(即 SCNNode 对象)的坐标系中。
- (SCNMatrix4)convertTransform:(SCNMatrix4)transform fromNode:(nullable SCNNode *)node;
复制代码
关注下面的标签,发现更多相似文章
评论