《游戏引擎架构》笔记4:游戏所需的三维数学
本文是阅读《游戏引擎架构》第四章"游戏所需的三维数学”进行的归纳和总结,部分内容经过了AI的扩展。
- 点和矢量
- 坐标系和基矢
- 矢量运算:标量乘法、加法、减法、模、点乘、叉乘、插值。
- 矩阵
- 齐次变换
- 齐次坐标
- 基础变换矩阵:平移、旋转、缩放。
- 坐标空间:模型空间、世界空间、观察空间。
- 变换坐标系和变换矢量问题(齐次坐标为列向量,和书不同)
- 变换坐标系:不同坐标系同一点换参考系(点不动):坐标系A→坐标系B
Bp=ABTAp=[ABi0ABj0ABk0ABt1]Ap
其中,ABi 是坐标系A的x轴基矢在坐标系B中的表示,ABj 是坐标系A的y轴基矢在坐标系B中的表示,ABk 是坐标系A的z轴基矢在坐标系B中的表示,ABt 是坐标系A的原点在坐标系B中的表示(平移)。
- 变换矢量:同一个坐标系点A变换到点B,等价于固定点然后把点A的坐标系逆变换到点B的坐标系。对法矢量则不同,需要乘以逆转置矩阵。
-
如何确定使用的引擎是使用行矢量还是列矢量,找变换矩阵中的平移向量是在第4行第1列(行矢量)还是第1行第4列(列矢量)。
-
四元数:q=[qVqS]=[asin2θcos2θ],其中,a 是旋转轴,θ 是旋转角度。
q1q2=[pSqV+qSpV+pV×qVpSqS−pVqV]
q∗=[−qVqS]
q−1=∣q∣2q∗
如果是单位四元数,那么
q−1=q∗
(pq)∗=q∗p∗
(pq)−1=q−1p−1
v′=rotate(q,v)=qvq−1
如果是单元四元数,那么等价于:
v′=rotate(q,v)=qvq∗
- 四元数变换旋转矩阵,若四元数q=[qVqS]=[xyzw],则对应的旋转矩阵为:
R=⎣⎢⎡1−2y2−2z22xy+2zw2xz−2yw2xy−2zw1−2x2−2z22yz+2xw2xz+2yw2yz−2xw1−2x2−2y2⎦⎥⎤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void MatrixToQuaterion( const float R[3][3], float q[] ) { float trace = R[0][0] + R[1][1] + R[2][2]; if (trace >= 0.0f) { float s = sqrt(trace + 1.0f); q[3] = s * 0.5f; float t = 0.5f / s; q[0] = (R[2][1] - R[1][2]) * t; q[1] = (R[0][2] - R[2][0]) * t; q[2] = (R[1][0] - R[0][1]) * t; } else { int i = 0; if (R[1][1] > R[0][0]) i = 1; if (R[2][2] > R[i][i]) i = 2; const int j = (i + 1) % 3; const int k = (i + 2) % 3; float s = sqrt(R[i][i] - R[j][j] - R[k][k] + 1.0f); q[i] = s * 0.5f; float t = 0.5f / s; q[j] = (R[j][i] + R[i][j]) * t; q[k] = (R[k][i] + R[i][k]) * t; q[3] = (R[k][j] - R[j][k]) * t; } }
|
-
四元数插值:
- 线性插值,插值需要归一化,因为不保持矢量长度,旋转动画在两端慢,在中间会快:
qLERP=LERP(qA,qB,t)=∣(1−t)qA+tqB∣(1−t)qA+tqB
- 球面线性插值,动画旋转匀速,但是计算代价昂贵,最好测试一下实现的效率,看是否值得使用:
qSLERP=SLERP(qA,qB,t)=wpqA+wqqB
其中:
wp=sinθsin(1−t)θ,wq=sinθsintθ
θ的计算为:
cosθ=qA⋅qB=qAxqBx+qAyqBy+qAzqBz+qAwqBwθ=cos−1(qA⋅qB)
- 各种旋转表达方式对比
- 欧拉角:简单直观,单轴旋转容易插值,问题在于万向节死锁(旋转后和原来的轴重合失效),轴的旋转次序对结果有影响。
- 旋转矩阵:独一无二地表示旋转,不直观,不容易插值,需要更多的存储空间。
- 轴角:直观,不能简单插值,旋转不能直接施加在点或矢量,需要先转换为旋转矩阵或四元数。
- 四元数:能串接旋转,把旋转直接施加在点和矢量,可以用LERP或SLERP进行旋转插值,存储小。
- SQT变换:包括缩放因子,四元数和平移矢量,容易插值,插值时平移矢量和缩放因子使用LERP,四元数使用LERP或SLERP。
- 对偶四元数:可以表示为两个普通四元数之和,所以有8个元素,分为非偶部和对偶部,q^=qA+εqB,ε2=0,ε=0
- 其他数学对象
- 直线、光线和线段,沿直线方向的矢量参数方程表示
- 球体:中心点坐标和半径四个参数表示
- 平面:参数方程。
- 轴对齐包围盒(AABB).
- 定向包围盒(OBB)。
- 平截头体。
- 凸多面体区域。
- SIMD计算
- 不要把FPU寄存器和SSE寄存器混用传输数据,会导致CPU流水线停顿性能下降。数学库尽可能把数据保存到SSE寄存器。
__m128数据类型,置于SSE寄存器中,用于SIMD计算。
- gcc用
vector float:1
| vector float v = (vector float) {1.0f, 2.0f, 3.0f, 4.0f};
|
- Visual Studio用
_mm_set_ps初始化:1
| __m128 v = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);
|
- 注意
__m128数据类型需要16字节内存对齐。
- SSE内部函数的编码:
- cpp需要
#include <xmmintrin.h>
- 例如
addps可以有内联汇编和内部代码两种实现:
1 2 3 4 5
| __m128 addWithAssembly(const __m128 a, const __m128 b) { __asm addps xmm0, xmm1 }
|
1 2 3 4
| __m128 addWithIntrinsics(const __m128 a, const __m128 b) { return _mm_add_ps(a, b); }
|
- 内联汇编需要依靠编译器的约定知识,编写函数困难,而且编译器不能做出任何假设,限制编译器的优化能力,最后的代码不可移植,内部函数直观清晰可移植,方便编译器优化代码。所以尽量使用内部函数。
float数组载入__m128变量要用__declspec(align(16))对齐,否则可能程序崩溃或性能降低。为了加强理解,可以做一下书上的198-199页的例子,另外书本还给了SSE实现矢量和矩阵相乘的例子,可以复现一下。
- 随机数
- 线性同余产生器(LCG),给定相同的种子,每次生成的随机数都相同,不能产生高质量的伪随机序列。
- 梅森旋转(MT):周期常,高阶均匀分布维度,通过多个随机性测试,速度快,可以看看SIMD实现的SFMT实现。
- Xorshift:所有随机数产生器之母。
- 其他参考: