说明
本文来自于我在 Let’s VisionOS 2024 大会上的演讲,原演讲内容分为两个方面:向量与矩阵基础知识、手势匹配框架 HandVector。为了便于阅读,我对原演讲内容进行了调整与简化,改写为了两篇文章《只需三板斧!带你入门 visionOS 空间计算的数学与几何基础》和《我开源了个手势匹配框架,让你在模拟器调试 visionOS 手部追踪功能!》。
可在 此处 获得 Keynotes 的 PDF 版本。
正文
看不懂苹果 visionOS Demo 中的数学与几何运算?这是很多 iOS 开发者试图学习 visionOS 开发时遇到的最大困难。我周围也有很多人也试图去学习计算几何相关知识,但是却被相关书籍和视频教程陡峭的入门曲线吓退。
本质上,这是因为普通开发者缺少“敲门砖”与“三板斧”,导致难以入门。我自己作为一名 iOS 开发者,也曾面临这样的问题,所以我将 3D/AR 中基础的数学与几何运算做了整理,选出了向量与矩阵最常见的三种运算,做为入门学习的 “敲门砖” 与 “三板斧”,带领大家顺利入门。为了形象说明运算的作用,我们将各种运算用图形化方式来呈现准确说明它们的几何意义,并对苹果 Demo 中的运算作用进行讲解,帮助大家更好入门。
坐标系,点与向量
在 RealityKit 中,我们使用的是右手系,也就是与右手的三根手指完美对应。
点与向量的定义
在这样一个坐标系中,一个变量 let a = simd_float3(1, 2, 3)
,它可以看作是一个点的位置,也可以当做一个向量,但是一但按规则进行运算后不可随意更改几何含义。
运算规定: 点与向量的运算规律如下:
- vector + vector = vector
- point – point = vector
- point + vector = point
- point + point = ?? => (point + point)/2 = middle point
从数学运算角度,为了统一进行运算会将其补成 4 维,点后面补 1,向量后面补 0:
3D point = (x, y, z, 1)
3D vector = (x, y, z, 0)
同样,在 RealityKit 中,点与向量的转换函数也是不同的:
常用向量运算
常见向量运算三板斧:
- 向量点乘:dot(a, b) => a b
- 向量叉乘:cross(a, b) => a b
- 向量归一化:normalize(a) => a/||a||
点乘的几何意义
- 一个向量在另一个向量上的投影长
- 点乘的大小也反映了两个向量有多“平行”,一般配合归一化(normalize)
- 交换律:a b = b a
- 分配律:a (b + c) = a b + a c
比如,在苹果的 Demo SwiftSplash 中的吸附对齐功能,就是用点乘和归一化实现的: 具体的代码如下,先用位置坐标相减得到两个向量,然后将它们归一化之后再点乘,如果大于 0.95 或小于 -0.95 就会自动吸附。
叉乘的几何意义
- 结果是一个新的向量,方向垂直于运算的两个向量,右手螺旋法则
- 叉乘的大小也反映了两个向量有多“垂直”, 长度等于围成的平行四边形的面积
- 反交换律:a b = – b a
- 分配律:a (b + c) = a b + a c
叉乘在实际开发中一般有两个用途:1.用来求平面的法线,2.求向量围成的面积。比如在苹果 Demo Happy Beam 中,手势比心后,光柱的方向就是用叉乘来确定的。 其中,将大拇指根部连线作为 x 轴方向,将大拇指指尖到食指指尖方向作为 y 轴,叉乘后的负方向就是光柱的方向。
矩阵
- 包含了坐标系的位置、朝向和大小信息
- 具体有 xyz 轴的向量,及原点坐标
- RealityKit 中 Entity 包含了矩阵信息
开发中一般使用 4×4 矩阵,它其实就是将坐标系的位置和坐标轴依次写下来:前三列分别是 XYZ 轴的向量,最后一列是原点的位置。其中向量后面补 0,而位置后面补 1,这样可以统一运算。
在苹果 Demo Happy Beam 中,也用到了从手部矩阵中获取位置的方法:matrix.colunms.3.xyz
就得到了矩阵的位置信息。
在对位置信息进行一系列运算后,重新得到了新的 XYZ 轴向量,以及中心点坐标,就可以重新合成得到比心的手势的矩阵。
常用矩阵运算
- 矩阵乘法:m1 * m2
- 矩阵的逆:m.inverse
- 矩阵的反转(镜像):entity.scale *= .init(x: -1, y: 1, z: 1)
矩阵乘法 multiply
- 类似盒子的嵌套
- 用在不同层级物体之间,将子级坐标转换为父级坐标
假如在 Cube_1 中有一个点 a: (1, 2, 3) 那么它在 Cube_0 中的坐标为:matrix_cube_1 * (1, 2, 3, 1) 在 Root 中的坐标为:matrix_cube_0 * matrix_cube_1* (1, 2, 3, 1)
矩阵的逆 inverse
- 一般用在同层级的物体之间
- 将兄弟关系变成父子关系
比如,下面的例子,将橙色盒子中的坐标转换到紫色盒子中,就需要用到矩阵的逆: 假如在 Cube_1 中有一个点 a: (1, 2, 3) 那么它在 Cube_0 中的坐标为:matrix_cube_1 * (1, 2, 3, 1) 在 Cube_2 中的坐标为:matrix_cube_2.inverse * matrix_cube_1* (1, 2, 3, 1)
矩阵反转/镜像 reflection/mirror
- 会将右手系变成左手系,同时造成内外表面反转
- 除非明确了解矩阵反转的含义,否则不要轻易进行反转操作
- 反转任意两根坐标轴,等同于绕不变的轴旋转 180 度
//左侧 Enity,反转 x 轴,造成了内外表面反转
entityLeft.scale *= .init(x: -1, y: 1, z: 1)
//右侧 Enity,反转 x 轴 和 y 轴,等同于绕 z 轴旋转 180 度
entityRight.scale *= .init(x: -1, y: -1, z: 1)
在苹果 Demo Hello World 中,星空背景就是用这种方式实现的。 相关代码如下,关键之处在于倒数第二行:
总结
掌握了向量运算三板斧+矩阵运算三板斧,无需再害怕空间计算中的数学公式。 更多关于 HandVector 的效果演示与计算讲解,请查看《我开源了个手势匹配框架,让你在模拟器调试 visionOS 手部追踪功能!》文章。
感谢主办方的精心组织,希望 Let’s VisionOS 活动未来更上一层楼。