积累变换 Accumulating Transforms
整理用 position、rotation、scale 分量累积世界变换,并实现 world space getter/setter。
积累变换 Accumulating Transforms
在之前的章节讨论中,我们知道传统的变换组合做法是逐级相乘 变换矩阵,也就是将每一级节点局部的 Position / Rotation / Scale 转换成 的 TRS 矩阵,然后组合
如果父节点带 非均匀缩放,再叠加了 旋转,那么矩阵乘法会在结果里混入 剪切 skew 成分,子物体会被 “拉歪”
本章讨论另一种做法:直接按照各 分量 累积变换
struct Transform {
Vec3 position; // 平移
Quat rotation; // 四元数
Vec3 scale; // 缩放
};然后我们可以像下面这样合并两个变换 (替代 矩阵相乘)
// a = parent, b = child
out.scale = a.scale * b.scale; // 各分量相乘
out.rotation = b.rotation * a.rotation; // 注意顺序: child * parent
Vec3 p = a.scale * b.position; // 先按父缩放缩放子位置
p = a.rotation * p; // 再用父旋转旋转它
out.position = a.position + p; // 最后加到父位置把这段逻辑 递归 到根节点,就能得到任何节点的 世界变换,最后如果还需要 矩阵,再把总的 position/rotation/scale 组合一次转 矩阵 即可,注意这里使用矩阵乘法顺序
q_total = q_parent * q_child; // column-vector 写法因为 UE4 使用 行向量,所以是 child * parent,如果使用 列向量,则需要反过来
这样做的 优点 是:直到完全展开完层级才把 R 和 S 放到同一个矩阵里,因此不会产生 剪切,解决了 “斜扭曲 (skew artifact)” 问题
但是它当然也有 缺点:由于所有缩放都发生在同一空间 world space,所以 子节点 再怎么 旋转,缩放始终沿 世界坐标轴,而不是沿着 “被旋转后” 的 本地轴,结果是模型不会被 剪切,但看起来会 “缩放” 在世界 X/Y/Z 方向,而不是期望的本地方向
// a = parent transform, b = child (or current) transform
Transform CombineTransforms(Transform a, Transform b) {
Transform out;
out.scale = a.scale * b.scale; // vec3 * vec3, parent scale times child scale
out.rotation = b.rotation * a.rotation; // quat * quat, Quaternions multiply in reverse, this is parent times child
// parent scale times child position, rotated by parent rotation:
out.position = (a.scale * b.position) * a.rotation; // (vec3 * vec3) * quat, quaternions multiply in reverse
out.position = a.position + out.position; // ve3 + vec3, combine positions
return out;
}
Transform GetWorldTransform(Transform transform) {
Transform worldTransform = transform; // This is acopy, not a reference
if (transform.parent != NULL) {
Transform worldParent = GetWorldTransform(transform.parent);
// Accumulate scale, Vector * Vector
worldTransform.scale = worldParent.scale * worldTransform.scale;
// Accumulate rotation, Quaternion * Quaternion
// Remember, quaternions multiply in reverse order! So:
// parent times child is written as: child * parent
worldTransform.rotation = worldTransform.rotation * worldParent.rotation;
// Accumulate position: scale first, Vector * vector
worldTransform.position = worldParent.scale * worldTransform.position;
// Accumulate position: rotate next, vector * Quaternion (quats rotate right to left)
worldTransform.position = worldTransform.position * worldParent.rotation;
// Accumulate position: transform last, Vector + Vector
worldTransform.position = worldParent.position + worldTransform.position;
}
return worldTransform;
}
Matrix GetWorldMatrix(Transform transform) {
Transform worldSpaceTransform = GetWorldTransform(transform);
return ToMatrix(worldSpaceTransform);
}获取世界空间 World Space Getters
当选择 积累变换 而不是 积累矩阵 时,获取 世界空间 下的变换变得容易,只需要像上一章介绍的那样逐变换分量的累积即可
Transform GetWorldTransform(Transform transform) {
Transform worldTransform = transform; // This is acopy, not a reference
if (transform.parent != NULL) {
Transform worldParent = GetWorldTransform(transform.parent);
// Accumulate scale, Vector * Vector
worldTransform.scale = worldParent.scale * worldTransform.scale;
// Accumulate rotation, Quaternion * Quaternion
// Remember, quaternions multiply in reverse order! So:
// parent times child is written as: child * parent
worldTransform.rotation = worldTransform.rotation * worldParent.rotation;
// Accumulate position: scale first, Vector * vector
worldTransform.position = worldParent.scale * worldTransform.position;
// Accumulate position: rotate next, vector * Quaternion (quats rotate right to left)
worldTransform.position = worldTransform.position * worldParent.rotation;
// Accumulate position: transform last, Vector + Vector
worldTransform.position = worldParent.position + worldTransform.position;
}
return worldTransform;
}
Quaternion Transform_GetGlobalRotation(Transform t){
return GetWorldTransform(t).rotation;
}
Vector3 Transform_GetGlobalPosition(Transform t){
return GetWorldTransform(t).position;
}
Vector3 Transform_GetGlobalScale(Transform t){
return GetWorldTransform(t).scale;
}
设置世界空间 World Space Setters
而此时要设置 世界空间 下的变换,我们必须首先将 父变换 的 世界空间 表示求出,然后对其求逆,并施加给我们想要设置的 目标世界空间变换,这样做会将其转换到对应的 局部空间 下
求 父节点 的 世界变换
worldParent = GetWorldTransform(t.parent);把 父节点 的 世界变换 求 逆(LocalInverse)
invScale = 1.0 / worldParent.scale;
invRotation = Conjugate(worldParent.rotation);
invTranslation = invRotation * (invScale * (-worldParent.position));- 逆旋转 = 共轭
- 逆缩放 = 1 / scale
- 逆位移 = 先把 -translation 进行 逆缩放、再进行 逆旋转
最终,把目标世界 SRT 乘到 invParent 上
worldXForm.position = p; // 期望的世界位置
worldXForm.rotation = r; // 期望的世界旋转
worldXForm.scale = s; // 期望的世界缩放
localXForm = CombineTransforms(invParent, worldXForm);
// 把结果写回子节点
t.position = localXForm.position;
t.rotation = localXForm.rotation;
t.scale = localXForm.scale;得到最终结果
Transform LocalInverse(Transform t) {
Quaternion invRotation = Inverse(t.rotation);
Vector3 invScale = Vector3(0, 0, 0);
if (t.scale.x != 0) { // Do epsilon comparison here
invScale.x = 1.0 / t.scale.x
}
if (t.scale.y != 0) { // Do epsilon comparison here
invScale.y = 1.0 / t.scale.y
}
if (t.scale.z != 0) { // Do epsilon comparison here
invScale.z = 1.0 / t.scale.z
}
Vector3 invTranslation = invRotation * (invScale * (-1 * t.translation));
Transform result;
result.position = invTranslation;
result.rotation = invRotation;
result.scale = invScale;
return result;
}
void SetGlobalSRT(Transform t, Vector3 s, Quaternion r, Vector3 p) {
if (t.parent == NULL) {
t.rotation = r;
t.position = p;
t.scale = s;
return;
}
var worldParent = GetWorldTransform(t.parent);
var invParent = LocalInverse(worldParent);
Transform worldXForm;
worldXForm.position = p;
worldXForm.rotation = r;
worldXForm.scale = s;
worldXForm = CombineTransforms(invParent, worldXForm);
t.position = worldXForm.position;
t.rotation = worldXForm.rotation;
t.scale = worldXForm.scale;
}
void SetGlobalRotation (Transform t, Quaternion rotation) {
Transform worldXForm = GetWorldTransform(t);
SetGlobalSRT(t, worldXForm.scale, rotation, worldXForm.position);
}
void SetGlobalPosition (Transform t, Vector3 position) {
Transform worldXForm = GetWorldTransform(t);
SetGlobalSRT(t, worldXForm.scale, worldXForm.rotation, position);
}
void SetGlobalScale(Transform t, Vector3 scale) {
Transform worldXForm = GetWorldTransform(t);
SetGlobalSRT(t, scale, worldXForm.rotation, worldXForm.position);
}