游戏引擎数学规则
在接触一个新的数学引擎的时候,或者替换引擎数学库的时候,需要明确基本的数学规则。采用什么样的单位制?长度单位是米还是厘米?角度制还是弧度制?顺时针还是逆时针为正?左手坐标系还是右手坐标系?X/Y/Z轴朝哪个方向?欧拉角旋转的顺序?……如果稍不注意,在导入导出资源的时候,可能会遇到站立的模型躺平了,导入3D建模软件后的模型非常大,看不见了等一系列问题。以 Unreal 引擎为例,其数学库在路径 <UnrealEngine>/Engine/Source/Runtime/Core/Public/Math/。我们来分析上面提到的各种规则。
1. 单位制
旋转的角是用的角度制还是弧度制,这个很容易分辨。浮点数指令、STL 库里的三角函数都采用弧度制。角度是方便用户看的,弧度是给机器计算的。为了避免程序员犯错,角度(angle)参数名称里一般会有 degree / radian 单词,以示区别。比如 Quat.h 的构造函数 FQuat::FQuat(FVector Axis, float AngleRad);。程序员调用的时候,可以用 C++11 的用户自定义的后缀语法表达。比如,旋转30°的时候,写成 30_deg 一目了然。
/*
* @see C++11 feature about user literal, http://en.cppreference.com/w/cpp/language/user_literal
* note that float or double type are not allowed on literal operators
*/
constexpr long double PI = 3.141592653589793238462643383279502884197L;
constexpr long double operator &#34;&#34; _deg(long double degree)
{
return degree * PI / 180;
}
constexpr long double operator &#34;&#34; _deg(long long int degree)
{
return degree * PI / 180;
}
虚幻引擎的旋转采用的角度制,在 Rotation.h 文件里,欧拉角的 pitch/yaw/roll 属性是以角度存储的。仅通过重载的加减运算符是看不出的,因为两者的运算效果一样。在欧拉角转矩阵或四元数的时候,需要计算 sin、cos 值,这个时候就能分辨出。
// Rotator.h
FQuat FRotator::Quaternion() const
{
DiagnosticCheckNaN();
#if PLATFORM_ENABLE_VECTORINTRINSICS
...
#else
const float DEG_TO_RAD = PI/(180.f);
const float RADS_DIVIDED_BY_2 = DEG_TO_RAD/2.f;
float SP, SY, SR;
float CP, CY, CR;
const float PitchNoWinding = FMath::Fmod(Pitch, 360.0f);
const float YawNoWinding = FMath::Fmod(Yaw, 360.0f);
const float RollNoWinding = FMath::Fmod(Roll, 360.0f);
FMath::SinCos(&SP, &CP, PitchNoWinding * RADS_DIVIDED_BY_2);
FMath::SinCos(&SY, &CY, YawNoWinding * RADS_DIVIDED_BY_2);
FMath::SinCos(&SR, &CR, RollNoWinding * RADS_DIVIDED_BY_2);
FQuat RotationQuat;
RotationQuat.X =CR*SP*SY - SR*CP*CY;
RotationQuat.Y = -CR*SP*CY - SR*CP*SY;
RotationQuat.Z =CR*CP*SY - SR*SP*CY;
RotationQuat.W =CR*CP*CY + SR*SP*SY;
#endif // PLATFORM_ENABLE_VECTORINTRINSICS
...
return RotationQuat;
}
// ScaleRotationTranslationMatrix.h
FMath::SinCos(&S, &C, FMath::DegreesToRadians(Degrees));
我们也会看到有的引擎会自己去实现一些三角函数,比如 sin、cos 用少量的 Taylor 级数展开,配上 Horner 法则,牺牲精确度换时间。
2. 左、右手坐标系
在三维空间直角坐标系里,向量定义了点乘(dot product)和叉乘(cross product)运算规则。对于两个向量 \vec a = (a_1, a_2, a_3) 和 \vec b = (b_1, b_2, b_3) ,其点乘的结果为标量 \vec a \cdot \vec b =a_1 b_1 + a_2 b_2 + a_3 b_3,叉乘的结果为矢量,其朝向遵循右手法则。
\vec a \times \vec b ={\begin{vmatrix}\vec {i} &\vec {j} &\vec {k} \\a_{1}&a_{2}&a_{3}\\b_{1}&b_{2}&b_{3}\\\end{vmatrix}} = (a_{2}b_{3}-a_{3}b_{2})\vec {i} +(a_{3}b_{1}-a_{1}b_{3})\vec {j} +(a_{1}b_{2}-a_{2}b_{1})\vec {k}
各种引擎里数学库提供的 vec3 类型都会这么实现,不会因为游戏引擎是左手坐标系而弃用右手法则。Unreal 引擎重载了 operator ^ 作为向量的叉乘运算,operator | 作为向量的点乘运算。这个我不建议使用,因为 ^ 和 | 的运算符优先级低于 + 和 -,A+B^C 会被编译器解释成 (A+B)^C,而不是应该看上去的 A+(B^C)。
// Vector.h
FORCEINLINE FVector FVector::operator^(const FVector& V) const
{
return FVector
(
Y * V.Z - Z * V.Y,
Z * V.X - X * V.Z,
X * V.Y - Y * V.X
);
}
FORCEINLINE FVector FVector::CrossProduct(const FVector& A, const FVector& B)
{
return A ^ B;
}
那么,哪里能看出引擎用的是左手坐标系还是右手坐标系呢?DirectX 提供了两个函数 D3DXMatrixLookAtLH和 D3DXMatrixLookAtRH,计算公式如下。其差别在于一开始 Z 轴的选取,LH (Left Hand)采用 Z 轴朝前,RH(Right Hand)采用 Z 轴朝后。两者的 X 轴都是向右,Y 轴都是向上。Z 轴上镜像后坐标 z 变 - z,任意一个坐标轴的镜像,都可以在左、右手坐标系之间切换。行列式有一条性质:交换任意两行(或列),行列式的结果变号。对左、右手坐标系而言,交换任意两个轴等效于镜像第三个轴。叉乘的结果标示第三个轴,用向量叉乘的话来讲就是 \vec A \times \vec B = - \vec B \times \vec A 。
// vec3 类型的
// D3DXMatrixLookAtLH
zaxis = normal(At - Eye)
xaxis = normal(cross(Up, zaxis))
yaxis = cross(zaxis, xaxis)
xaxis.x yaxis.x zaxis.x 0
xaxis.y yaxis.y zaxis.y 0
xaxis.z yaxis.z zaxis.z 0
-dot(xaxis, eye)-dot(yaxis, eye)-dot(zaxis, eye)1
// D3DXMatrixLookAtRH
zaxis = normal(Eye - At)
xaxis = normal(cross(Up, zaxis))
yaxis = cross(zaxis, xaxis)
xaxis.x yaxis.x zaxis.x 0
xaxis.y yaxis.y zaxis.y 0
xaxis.z yaxis.z zaxis.z 0
-dot(xaxis, eye) -dot(yaxis, eye) -dot(zaxis, eye)1
虚幻引擎的 Matrix.h 类里,注释里提到了自家的 LookAt 功能等同于 D3DXMatrixLookAtLH,用的左手坐标系。看过函数实现后,也确实如此。还有提供镜像的函数 FMatrix::Mirror(EAxis::Type MirrorAxis, EAxis::Type FlipAxis),在对应的轴上取相反数。
struct FLookAtMatrix : FLookFromMatrix
{
/**
* Creates a view matrix given an eye position, a position to look at, and an up vector.
* Equivalent of FLookFromMatrix(EyePosition, LookAtPosition - EyePosition, UpVector)
* The up vector need not be normalized.
* This does the same thing as D3DXMatrixLookAtLH.
*/
FLookAtMatrix(const FVector& EyePosition, const FVector& LookAtPosition, const FVector& UpVector);
};
3. 行、列主序
矩阵是按行存储,还是按列存储?换种说法就是,矩阵是行主序(row major),还是列主序(column major)?Fortran、MATLAB、GNU Octave、线性代数库 BLAS 用的列主序。C 语言用行主序。C 语言申明一个二维数组 int a;,第一维代表行,所以是行优先。
根据矩阵的运算规则 C_{m \times n} = A_{m \times s} B_{s \times n} ,在矩阵A的列等于矩阵B的行时,乘法才有定义。向量是行或列为1的矩阵。如果不知道矩阵是行主序或列主序,自然也不知道向量是行向量还是列向量。可以通过矩阵与向量的乘法来辨别矩阵元素的存储顺序。行列主序跟左右手没有任何关系,可以参考我下面给出的回答。
图形学APIDirect3DOpenGL主序行主序(row major)列主序(column major)向量行向量列向量矩阵 M 与向量 v 的乘法后乘 v*M(post-multiply)前乘 M*v (pre-multiply)MVP 矩阵级联变换v * M * V * PP * V * M * v 对于行主序矩阵,M 访问第r行,第c列元素。对于列主序矩阵,M 访问第r行,第c列元素。如果没有参数名的暗示,仅通过 operator [] 的重载,是看不出是行还是列主序的。
我们来看看虚幻引擎里矩阵与向量的乘积代码,TransformPosition 函数是变换顶点坐标。
// Matrix.inl
/** Transform a location - will take into account translation part of the FMatrix. */
FORCEINLINE FVector4 FMatrix::TransformPosition(const FVector &V) const
{
return TransformFVector4(FVector4(V.X,V.Y,V.Z,1.0f));
}
// Homogeneous transform.
FORCEINLINE FVector4 FMatrix::TransformFVector4(const FVector4 &P) const
{
FVector4 Result;
VectorRegister VecP = VectorLoadAligned(&P);
VectorRegister VecR = VectorTransformVector(VecP, this);
VectorStoreAligned(VecR, &Result);
return Result;
}
接着来到 VectorTransformVector 函数,不同的硬件平台有不同的实现。x86 对应文件 UnrealMathSSE.h,不懂 _mm_shuffle_ps 等 SSE 指令的,可以看文件 UnrealMathFPU.h。插播一则用 SSE 指令转置矩阵的回答:
// UnrealMathFPU.h
/**
* Calculate Homogeneous transform.
*
* @param VecP VectorRegister
* @param MatrixM FMatrix pointer to the Matrix to apply transform
* @return VectorRegister = VecP*MatrixM
*/
FORCEINLINE VectorRegister VectorTransformVector(const VectorRegister&VecP,const void* MatrixM )
{
typedef float Float4x4;
union { VectorRegister v; float f; } Tmp, Result;
Tmp.v = VecP;
const Float4x4& M = *((const Float4x4*)MatrixM);
Result.f = Tmp.f * M + Tmp.f * M + Tmp.f * M + Tmp.f * M;
Result.f = Tmp.f * M + Tmp.f * M + Tmp.f * M + Tmp.f * M;
Result.f = Tmp.f * M + Tmp.f * M + Tmp.f * M + Tmp.f * M;
Result.f = Tmp.f * M + Tmp.f * M + Tmp.f * M + Tmp.f * M;
return Result.v;
}
我们看到注释里写的 v * M。结合代码可以确认,Unreal 的向量是行主序的,与 Direct3D 一致。
4. 欧拉角
4.1 pitch、yaw、roll
各个图形 API 的 X、Y、Z 轴的定义不一,但绕着三个轴的旋转操作 pitch、 yaw 和 roll 是脱离坐标轴而定义的。想象自己在开飞机,俯仰之间叫 pitch,拐弯偏航叫 yaw,沿行驶方向翻滚叫 roll。
如何记忆三个单词?pitch 的意思比较多。在图像里是一行的占用的字节数,因为对齐可能会多出几个字节,也叫 stride。在音乐里是音高。投掷铅球,初速度一定,与地面成45°夹角方向,投掷(pitch)得最远,手在俯仰之间定位45°的动作,就是 pitch 操作。yaw 发音/ j /,首字母Y,遇到分叉口向左或向右拐弯前行,就是 yaw 操作。烤羊肉串翻滚的动作,就是 roll 操作。
pitch, yaw, roll
pitch, yaw, roll 信息记录在引擎的 Rotator.h 类,注释里写明了 pitch 绕 Y 轴,yaw 绕 Z 轴,roll 绕 X 轴。转动的方向跟数学一致,逆时针(clockwise)为正,顺时针(counter-clockwise,常常缩写成 CCW)为负。数值代表角度,而不是弧度,即采用的是角度制。
struct FRotator
{
public:
/** Rotation around the right axis (around Y axis), Looking up and down (0=Straight Ahead, +Up, -Down) */
float Pitch;
/** Rotation around the up axis (around Z axis), Running in circles 0=East, +North, -South. */
float Yaw;
/** Rotation around the forward axis (around X axis), Tilting your head, 0=Straight, +Clockwise, -CCW. */
float Roll;
虚幻引擎采用 X 轴朝前,Y轴朝右,Z轴朝上。这在 Vector.h 头文件定义的常量有所说明,ForwardVector 是(1, 0, 0),RightVector 是(0, 1, 0),UpVector 是(0, 0, 1)。这有点颠覆我们的世界观,X轴不一般都是从左到右没有争议吗?且 Direct3D 与 OpenGL 在 X 轴上达成一致,都是朝右的。pitch 在 Direct3D 和 OpenGL 里都是绕 X 轴旋转。虚幻引擎交换了数学坐标系里的 X 轴和 Y 轴,导致 pitch 是绕 Y 轴旋转。注意下面代码的pitch/yaw/roll 和 X/Y/Z 顺序,想想是不是这样写。如果虚幻引擎不提供这样的代码,程序员在传参时很容易犯错。
FVector FRotator::Euler() const
{
return FVector( Roll, Pitch, Yaw );
}
FRotator FRotator::MakeFromEuler(const FVector& Euler)
{
return FRotator(Euler.Y, Euler.Z, Euler.X);
}
4.2 矩阵、欧拉角、四元数之间的比较
几何物体的旋转、平移操作,称之为刚体变换(rigid body transformation)。旋转可以用3x3的矩阵、欧拉角、四元数表示,平移只能在4x4的矩阵中表示。任何关于刚体旋转的3x3矩阵是由绕三个坐标轴旋转的基本旋转矩阵复合而成的,以欧拉角表示;也可以是绕一个向量旋转而成的,以单位四元数表示。
欧拉角相比其他两种方式而言,数据表达直观,只需要三个角度值,缺点是存在万向节死锁(Gimbal lock),两个旋转轴(面)重叠后,丢失了一个方向上的旋转能力。
四元数以 (x, y, z, w) 表示,从单位向量 \vec {u}=(u_{x},u_{y},u_{z})=u_{x}\mathbf {i} +u_{y}\mathbf {j} +u_{z}\mathbf {k} 和旋转角度 θ 构建单位四元数 q = e^{\frac \theta 2 \vec u} = \cos {\frac \theta 2} + \vec u \sin{\frac \theta 2},其中 w = \cos \frac \theta 2 , (x, y, z) =\vec u\sin \frac \theta 2。从正面看物体顺时针旋转 θ,从反面看物体就是逆时针旋转θ。即绕向量 \vec u 旋转 θ,效果上等于绕向量 -\vec u 旋转 -θ。用四元数也可以证明: \cos \frac \theta 2 + \vec u \cdot \sin \frac \theta 2 与 \cos (-\frac \theta 2) + (-\vec u) \cdot\sin (-\frac \theta 2) 确实相等。四元数很适合球面平滑插值(spherical linear interpolation,简称 slerp),但限制在180°范围内。对于芭蕾运动员踮起脚尖绕地旋转3周,用欧拉角表示就是 yaw 从0° 变到 1080°,如果用上四元数,起始和终止数据一致,插值会给出同样的值。
既然各自有长处和短处,只好结合他们的长处来做这个事。动画数据存储欧拉角,将欧拉角转换为四元数,对四元数进行 slerp 操作,再将这一系列四元数转换成对应的欧拉角,最终用矩阵乘法来组合变换。
旋转用矩阵或四元数表示唯一,用欧拉角则存在多种表示,即使限定了三个轴的旋转顺序。接下来我们通过公式和代码分析数学库采用哪种旋转顺序。
4.3 旋转矩阵
绕 X 轴旋转 α 记作R_x(\alpha)、绕 Y 轴旋转 β 角记作 T_y(\beta)、绕 Z 轴旋转 γ 角记作 T_z(\gamma) ,变换坐标为列向量,这三种基本的变换用矩阵表示如下(行向量的话,需要转置这三个矩阵),这个不难推导。
{\begin{pmatrix} 1& 0 & 0 \\ 0 & \cos \alpha & -\sin \alpha \\ 0 & \sin \alpha & \cos \alpha \end{pmatrix}} 、 {\begin{pmatrix} \cos \beta & 0 &\sin \beta \\ 0 & 1& 0 \\ -\sin \beta & 0 &\cos \beta \end{pmatrix}} 、 {\begin{pmatrix} \cos \gamma & -\sin \gamma & 0 \\\sin \gamma & \cos \gamma & 0\\ 0 & 0 & 1 \end{pmatrix}}
对于列向量,矩阵的乘法是前乘,所以上面按 XYZ 顺序旋转后的旋转矩阵为 T_z(\gamma) \cdot T_y(\beta) \cdot T_x(\alpha) =
{\begin{pmatrix} \cos \beta \cos \gamma & \sin \alpha \sin \beta \cos \gamma - \cos \alpha \sin \gamma & \cos \alpha \sin \beta \cos \gamma + \sin \alpha \sin \gamma \\\cos \beta \sin \gamma & \sin \alpha \sin \beta \sin \gamma + \cos \alpha \cos \gamma & \cos \alpha \sin \beta \sin \gamma - \sin \alpha \cos \gamma\\ -\sin \beta & \sin \alpha \cos \beta & \cos \alpha \cos \beta \end{pmatrix}}
可以按矩阵乘法自行推导,也可以借助 SymPy 这样的符号运算工具完成运算。SymPy 的 Matrix 是行主序。
from sympy import *
α = Symbol(&#39;α&#39;)
β = Symbol(&#39;β&#39;)
γ = Symbol(&#39;γ&#39;)
# or you can create three symbols in one go.
# α, β, γ = symbols(&#39;α β γ&#39;)
Rx = Matrix([
,
,
])
Ry = Matrix([
[ cos(β), 0, sin(β)],
[ 0, 1, 0],
[-sin(β), 0, cos(β)]
])
Rz = Matrix([
,
,
[ 0, 0, 1]
])
print(Rz * Ry * Rx)
&#39;&#39;&#39;
Matrix([
,
,
[ -sin(β), sin(α)*cos(β), cos(α)*cos(β)]])
&#39;&#39;&#39;4.4 虚幻引擎的数学函数实现
虚幻引擎提供矩阵、欧拉角、四元数之间转换的函数如下:
矩阵转欧拉角FRotator FMatrix::Rotator() const;矩阵转四元数FQuat FMatrix::ToQuat() const;
explicit FQuat(const FMatrix& M);欧拉角转矩阵FRotationMatrix::FRotationMatrix(const FRotator& Rot);欧拉角转四元数explicit FQuat::FQuat(const FRotator& R);四元数转矩阵FMatrix FRotationMatrix::Make(FQuat const& Rot);
FRotationTranslationMatrix::FRotationTranslationMatrix(const FRotator& Rot, const FVector& Origin);四元数转欧拉角FRotator::FRotator(const FQuat& Quat);
FRotator FQuat::Rotator() const; 从虚幻引擎的欧拉角转矩阵的实现中,难以辨认 yaw、pitch、roll 的顺序。代码没放任何链接和注释,也抹去了计算过程中的脚手架,直接给出了矩阵每个元素的值。
FORCEINLINE FRotationTranslationMatrix::FRotationTranslationMatrix(const FRotator& Rot, const FVector& Origin)
{
float SP, SY, SR;
float CP, CY, CR;
FMath::SinCos(&SP, &CP, FMath::DegreesToRadians(Rot.Pitch));
FMath::SinCos(&SY, &CY, FMath::DegreesToRadians(Rot.Yaw));
FMath::SinCos(&SR, &CR, FMath::DegreesToRadians(Rot.Roll));
M = CP * CY;
M = CP * SY;
M = SP;
M = 0.f;
M = SR * SP * CY - CR * SY;
M = SR * SP * SY + CR * CY;
M = - SR * CP;
M = 0.f;
M = -( CR * SP * CY + SR * SY );
M = CY * SR - CR * SP * SY;
M = CR * CP;
M = 0.f;
M = Origin.X;
M = Origin.Y;
M = Origin.Z;
M = 1.f;
}
我们前面已经分析出虚幻引擎的矩阵是 row_major 的,写成 column_major 3x3 的旋转矩阵形式就是
{\begin{pmatrix} CP \cdot CY& CP \cdot SY & SP \\ SR \cdot SP \cdot CY - CR \cdot SY & SR\cdot SP\cdot SY + CR\cdot CY & -SR\cdot CP \\ -(CR\cdot SP\cdot CY + SR\cdot SY) & CY\cdot SR - CR\cdot SP\cdot SY & CR\cdot CP \end{pmatrix}}
其中 C 是 cos,S 是 sin,P 是 pitch,Y 是 yaw,R 是 roll。X、Y、Z 三者的顺序总共有 A_3^3 = 3! = 6 种排列方式。用上面的 Python 代码,打印出各种组合的运算结果。观察发现,组合结果的每个矩阵有且仅有一个单独的项,不含乘法和加减运算(例如上面的 SP 项),且这一项的位置均不相同。由此现象可以定位出旋转顺序,发现跟 Rx * Ry * Rz 的结果很像,只是符号上有些差异。
>>> Rx * Ry * Rz
Matrix([
[ cos(β)*cos(γ), -sin(γ)*cos(β), sin(β)],
,
]) 从右上角得知,P=β。接着推敲第一行,有 Y=-γ。最后一列,有 R=α。带入至左下角四个元素进行核对,刚好能对上。由 R_x(\alpha) * R_y(\beta) * R_z(\gamma) 的顺序得知,先绕 Z 轴 γ 角、接着 Y 轴 β 角、后 X 轴 α 角旋转的。带入推断的 pitch 为 β,yaw 为 γ,roll 为 α 后,得到虚幻引擎的旋转顺序为先 yaw、再 pitch、后 roll。至此,完成了我们的推测。
4.5 多出的负号
但是,为什么会有个负号?聪明的你一定想到了,但还不敢确定。是的,左右手坐标系在这里捣蛋。我在前面也提到过,正面看旋转了 θ 角,背面看就是旋转了 -θ 角,这正是翻转了单个轴导致的。我在前面不难推导地放出了三个基本矩阵 R_x(\alpha) 、 T_y(\beta) 、 T_z(\gamma) ,不知道有没有人认真推导过。
在平面上,单位圆上一点(x, y) 绕垂直于平面上的向量逆时针旋转 β 角后的坐标,如何求解?(以下说到的旋转角度,以逆时针方向为正)设初始角为 α,则有 cos(α) = x, sin(α) = y。最终的角为 α + β,所以最终的坐标为 (cos(α + β), sin(α + β)),利用三角函数和角公式展开有以下关系。
x&#39; = cos(α + β) = cosα cosβ - sinα sinβ = x cosβ - y sinβ
y&#39; = sin(α + β) = sinα cosβ + cosα sinβ = x sinβ + y cosβ
忘记三角函数公式的可以用复数的乘法来推导,殊途同归。复数 x + yi 旋转 β 角,等于乘以单位长度的复数 cosβ + sinβ i,而 x&#39; + y&#39;i = (x + yi)(cosβ + sinβ i) =(x cosβ - y sinβ) + (x sinβ + y cosβ)i。
两种方法的答案,写成矩阵形式就是 {\begin{pmatrix} x&#39; \\ y&#39; \end{pmatrix}} = {\begin{pmatrix} \cos \beta & -\sin \beta \\ \sin \beta&\cos \beta \end{pmatrix}}{\begin{pmatrix} x \\ y \end{pmatrix}}。
从二维平面来到三维空间,对某个轴旋转,则该轴上的坐标不会变动,其他两个轴参考上面的公式。例如,对 X 轴旋转 α,X 轴的坐标不变,Y 和 Z 轴带入上面的式子,可得
{\begin{pmatrix} x&#39; \\ y&#39; \\ z&#39; \end{pmatrix}} = {\begin{pmatrix} 1& 0 & 0 \\ 0 & \cos \alpha & -\sin \alpha \\ 0 & \sin \alpha & \cos \alpha \end{pmatrix}} {\begin{pmatrix} x \\ y \\ z \end{pmatrix}}
前面提到过,虚幻引擎的 X 轴朝前,Y 轴朝右,Z 轴朝上。导致在面向 X 轴旋转 α 时,等于顺着 X 轴看去,也就是在 YOZ 平面上顺时针旋转 -α 角,也就是上面的 roll,所以应该调整为
R_x(roll) = {\begin{pmatrix} 1& 0 & 0 \\ 0 & \cos (-\alpha) & -\sin (-\alpha) \\ 0 & \sin (-\alpha) & \cos (-\alpha) \end{pmatrix}} = {\begin{pmatrix} 1& 0 & 0 \\ 0 & \cos \alpha & \sin \alpha \\ 0 & -\sin \alpha & \cos \alpha \end{pmatrix}} ,即 {\begin{pmatrix} 1& 0 & 0 \\ 0 & CR & SR \\ 0 & -SR & CR \end{pmatrix}}
同理,面向 Y 轴旋转 β 角,是在 XOZ 平面上旋转 β 角,对应 pitch,旋转矩阵为R_y(pitch) = {\begin{pmatrix} \cos \beta & 0 &-\sin \beta \\ 0 & 1& 0 \\ \sin \beta & 0 &\cos \beta \end{pmatrix}} = {\begin{pmatrix} CP & 0 &-SP \\ 0 & 1& 0 \\ SP & 0 &CP \end{pmatrix}}
面向 Z 轴旋转 γ 角,是在 YOX 平面旋转 γ 角,或 XOY 平面上旋转 -γ 角,对应 yaw,旋转矩阵为
R_z(yaw) = {\begin{pmatrix} \cos (-\gamma) & -\sin (-\gamma) & 0 \\\sin (-\gamma) & \cos (-\gamma) & 0\\ 0 & 0 & 1 \end{pmatrix}} = {\begin{pmatrix} \cos \gamma & \sin \gamma & 0 \\ -\sin \gamma & \cos \gamma & 0\\ 0 & 0 & 1 \end{pmatrix}} = {\begin{pmatrix} CY & SY & 0 \\ -SY & CY & 0\\ 0 & 0 & 1 \end{pmatrix}}
先 yaw,再 pitch,后 roll 连乘的结果带入, R_x(roll) * R_y(pitch) * R_z(yaw) 即为结果。
最后,附一张各个3D软件采用的坐标系、Y/Z 轴朝向,看看虚幻引擎如何标新立异,独树一帜。
参考
[*]^User-defined literals (since C++11)https://en.cppreference.com/w/cpp/language/user_literal
[*]^C++ Operator Precedencehttps://en.cppreference.com/w/cpp/language/operator_precedence
[*]^Stride is also called pitchhttps://docs.microsoft.com/en-us/windows/win32/medfound/image-stride
[*]^Slerphttps://en.wikipedia.org/wiki/Slerp
大佬牛逼
页:
[1]