在本章中,您将实现一个保存位置、旋转和缩放数据的结构。这种结构是一种转变。变换从一个空间映射到另一个空间。位置、旋转和缩放也可以存储在 4x4 矩阵中,那么为什么要使用显式转换结构而不是矩阵呢?答案是插值。矩阵插值不好,但变换结构可以。
在两个矩阵之间进行插值是困难的,因为旋转和缩放存储在矩阵的相同分量中。正因为如此,在两个矩阵之间进行插值不会得到你所期望的结果。变换通过分别存储位置、旋转和缩放组件来解决这个问题。
在本章中,您将实现一个转换结构以及您需要能够在转换中执行的常见操作。到本章结束时,您应该能够执行以下操作:
-
理解什么是转变
-
了解如何组合变换
-
在变换和矩阵之间转换
-
Understand how to apply transforms to points and vectors
重要信息
在本章中,您将实现一个表示位置、旋转和缩放的变换结构。要了解更多关于变换,它们如何与矩阵相关,以及它们如何适应游戏层次,请查看http://gabormakesgames.com/transforms.html。
转换是简单的结构。变换包含位置、旋转和缩放。位置和比例是向量,旋转是四元数。转换可以分层组合,但是这种父子关系不应该是实际转换结构的一部分。以下步骤将指导您创建转换结构:
-
创建新文件,
Transform.h
。声明转换结构需要这个文件。 -
开始在这个新文件中声明
Transform
结构。从变换的属性开始-position
、rotation
和scale
:struct Transform { vec3 position; quat rotation; vec3 scale;
-
创建一个接受位置、旋转和缩放的构造函数。该构造函数应该将这些值分配给转换结构的适当成员:
Transform(const vec3& p, const quat& r, const vec3& s) : position(p), rotation(r), scale(s) {}
-
空白变换应该没有位置或旋转,比例为 1。默认情况下,
scale
组件将被创建为(0, 0, 0)
。为了解决这个问题,Transform
结构的默认构造函数需要将scale
初始化为正确的值:Transform() : position(vec3(0, 0, 0)), rotation(quat(0, 0, 0, 1)), scale(vec3(1, 1, 1)) {} }; // End of transform struct
Transform
结构相当简单;它的所有成员都是公开的。变换有位置、旋转和缩放。默认构造函数将位置向量设置为 0 ,旋转四元数设置为恒等式,比例向量设置为 1 。默认构造函数创建的转换无效。
在下一节中,您将学习如何以类似于矩阵或四元数的方式组合变换。
以骨骼为例。在每个关节处,可以放置一个变换来描述关节的运动。当你旋转你的肩膀时,附着在那个肩膀上的肘部也会移动。要将肩部变换应用于所有连接的关节,每个关节上的变换必须与其父关节的变换相结合。
变换可以像矩阵和四元数一样组合,两个变换的效果可以组合成一个变换。为了保持一致,组合变换应该保持从右到左的组合顺序。与矩阵和四元数不同,这个combine
函数不会作为乘法函数实现。
组合两个变换的缩放和旋转很简单——将它们相乘。组合位置有点难。组合位置也需要受到rotation
和scale
组件的影响。当找到组合位置时,记住变换的顺序:首先缩放,其次旋转,最后平移。
创建新文件,Transform.cpp
。实现combine
功能,别忘了给Transform.h
添加功能声明:
Transform combine(const Transform& a, const Transform& b) {
Transform out;
out.scale = a.scale * b.scale;
out.rotation = b.rotation * a.rotation;
out.position = a.rotation * (a.scale * b.position);
out.position = a.position + out.position;
return out;
}
在后面的章节中,combine
函数将被用来组织变换成一个层次。在下一节中,您将学习如何反转变换,这同样类似于反转矩阵和四元数。
你已经知道变换从一个空间映射到另一个空间。可以反转映射,将变换映射回原始空间。与矩阵和四元数一样,变换也可以反过来。
反转刻度时,请记住 0 不能反转。标度为 0 的情况需要特殊处理
执行Transform.cpp
中的inverse
变换方法。别忘了在Transform.h
申报方法:
Transform inverse(const Transform& t) {
Transform inv;
inv.rotation = inverse(t.rotation);
inv.scale.x = fabs(t.scale.x) < VEC3_EPSILON ?
0.0f : 1.0f / t.scale.x;
inv.scale.y = fabs(t.scale.y) < VEC3_EPSILON ?
0.0f : 1.0f / t.scale.y;
inv.scale.z = fabs(t.scale.z) < VEC3_EPSILON ?
0.0f : 1.0f / t.scale.z;
vec3 invTrans = t.position * -1.0f;
inv.position = inv.rotation * (inv.scale * invTrans);
return inv;
}
反转变换可以消除一个变换对另一个变换的影响。考虑一个角色通过一个关卡。一旦关卡结束,在开始下一个关卡之前,您可能需要将角色移回原点。你可以用字符的倒数乘以字符的变换。
在下一节中,您将学习如何将两个或多个变换混合在一起。
您有代表两个特定时间点的关节的变换。要使模型看起来有动画效果,需要在这些帧的变换之间进行插值或混合。
可以在向量和四元数之间进行插值,这是变换的基础。所以也可以在变换之间进行插值。该操作通常称为混合或混合,而不是插值。将两个变换混合在一起时,线性插值输入变换的位置、旋转和缩放。
在Transform.cpp
中实现mix
功能。别忘了在Transform.h
声明功能:
Transform mix(const Transform& a,const Transform& b,float t){
quat bRot = b.rotation;
if (dot(a.rotation, bRot) < 0.0f) {
bRot = -bRot;
}
return Transform(
lerp(a.position, b.position, t),
nlerp(a.rotation, bRot, t),
lerp(a.scale, b.scale, t));
}
能够将变换混合在一起对于创建动画之间的平滑过渡非常重要。在这里,您实现了变换之间的线性混合。在下一节中,您将学习如何将transform
转换为mat4
。
着色器程序可以很好地处理矩阵。它们没有转换结构的本地表示。您可以将转换代码移植到 GLSL,但这不是最好的解决方案。相反,你可以将一个变换转换成一个矩阵,然后将它作为一个着色器统一提交。
由于变换对可以存储在矩阵中的数据进行编码,因此可以将变换转换成矩阵。要将一个变换转换成一个矩阵,矩阵需要用向量来表示。
首先,通过将全局基向量的方向乘以变换的旋转来找到基向量。接下来,按变换的比例缩放基向量。这产生了填充上面的 3×3 子矩阵的最终基向量。该位置直接进入矩阵的最后一列。
执行Transform.cpp
中的【从 T0】方法。别忘了给Transform.h
添加功能声明:
mat4 transformToMat4(const Transform& t) {
// First, extract the rotation basis of the transform
vec3 x = t.rotation * vec3(1, 0, 0);
vec3 y = t.rotation * vec3(0, 1, 0);
vec3 z = t.rotation * vec3(0, 0, 1);
// Next, scale the basis vectors
x = x * t.scale.x;
y = y * t.scale.y;
z = z * t.scale.z;
// Extract the position of the transform
vec3 p = t.position;
// Create matrix
return mat4(
x.x, x.y, x.z, 0, // X basis (& Scale)
y.x, y.y, y.z, 0, // Y basis (& scale)
z.x, z.y, z.z, 0, // Z basis (& scale)
p.x, p.y, p.z, 1 // Position
);
}
图形应用编程接口处理矩阵,而不是变换。在后面的章节中,变换将在被发送到着色器之前被转换成矩阵。在下一节中,您将学习如何做相反的事情,即将矩阵转换为变换。
外部文件格式可能将转换数据存储为矩阵。例如,glTF 可以将节点的变换存储为位置、旋转和缩放,或者存储为单个 4x4 矩阵。为了使转换代码健壮,您需要能够将矩阵转换为转换。
将矩阵转换为变换比将变换转换为矩阵更困难。提取矩阵的旋转很简单;您已经实现了一个将 4x4 矩阵转换为四元数的函数。提取位置也很简单;将矩阵的最后一列复制到向量中。提取刻度更加困难。
回想一下,变换的操作顺序是缩放、旋转,然后平移。这意味着,如果您有三个矩阵——分别代表比例、旋转和平移的 S 、 R 和 T ,它们将组合成一个变换矩阵,如下所示:
M = SRT
要求尺度,首先忽略矩阵的平移部分, M (将平移向量清零)。这就剩下 M = SR 了。要去除矩阵的旋转分量,将 M 乘以 R 的倒数。这应该只剩下比例部分。不完全是。结果会留下一个包含比例和一些倾斜信息的矩阵。
我们从这个尺度倾斜矩阵中提取尺度的方法是简单地将主对角线作为尺度倾斜矩阵。虽然这在大多数情况下是可行的,但并不完美。获取的比例应被视为有损比例,因为该值也可能包含倾斜数据,这使得比例不准确。
重要说明
可以将矩阵分解为平移、旋转、缩放、倾斜和行列式的符号。然而,这种分解是昂贵的,并且不太适合实时应用。要了解更多信息,请查看肯·舒梅克和汤姆·达夫在的矩阵动画和极坐标分解。
在Transform.cpp
中实现toTransform
功能。别忘了给Transform.h
添加功能声明:
Transform mat4ToTransform(const mat4& m) {
Transform out;
out.position = vec3(m.v[12], m.v[13], m.v[14]);
out.rotation = mat4ToQuat(m);
mat4 rotScaleMat(
m.v[0], m.v[1], m.v[2], 0,
m.v[4], m.v[5], m.v[6], 0,
m.v[8], m.v[9], m.v[10], 0,
0, 0, 0, 1
);
mat4 invRotMat = quatToMat4(inverse(out.rotation));
mat4 scaleSkewMat = rotScaleMat * invRotMat;
out.scale = vec3(
scaleSkewMat.v[0],
scaleSkewMat.v[5],
scaleSkewMat.v[10]
);
return out;
}
重要的是你能够将矩阵转换成变换,因为你并不总是控制你所处理的数据的格式。例如,模型格式可能存储矩阵而不是变换。
到目前为止,你可能已经注意到变换和矩阵通常可以做同样的事情。在下一节中,您将学习如何使用变换来变换点和向量,类似于如何使用矩阵来完成。
Transform
结构可以用来在空间中移动点和向量。想象一个球上下弹跳。球的弹跳来源于Transform
结构,但是你怎么知道球的各个顶点往哪里移动呢?您需要使用Transform
结构(或矩阵)变换所有顶点,以正确显示球。
使用变换修改点和向量就像组合两个变换。要变换一个点,首先应用缩放,然后应用旋转,最后应用变换的平移。要变换向量,请遵循相同的步骤,但不要添加位置:
-
在
Transform.cpp
中实现transformPoint
功能。别忘了把功能声明添加到Transform.h
:vec3 transformPoint(const Transform& a, const vec3& b) { vec3 out; out = a.rotation * (a.scale * b); out = a.position + out; return out; }
-
在
Transform.cpp
中实现transformVector
功能。别忘了把功能声明添加到Transform.h
:vec3 transformVector(const Transform& a, const vec3& b) { vec3 out; out = a.rotation * (a.scale * b); return out; }
transformPoint
函数的作用与矩阵和点相乘的作用相同,只是一步一个脚印。首先应用scale
,然后应用rotation
,最后应用translation
。当你在处理一个向量而不是一个点时,同样的顺序适用,除了平移被忽略。
在本章中,您学习了如何将转换实现为包含位置、旋转和缩放的离散结构。在许多方面,Transform
类保存的数据与通常存储在矩阵中的数据相同。
您学习了如何在变换之间组合、反转和混合,以及如何使用变换来移动点和旋转向量。变换将是向前发展的关键,因为它们被用来制作游戏模型的骨架动画。
你需要一个明确的Transform
结构的原因是矩阵不能很好地插值。插值变换对动画非常重要。这是您创建中间姿势以显示两个给定关键帧的方式。
在下一章中,您将学习如何在 OpenGL 之上编写一个光抽象层,以使以后章节中的渲染更加容易。