正确转换相对于指定 space 的节点?
Correctly transforming a node relative to a specified space?
我目前正在使用分层场景图中的节点,但我很难正确 translating/rotating 相对于特定转换的节点 space(例如 parent 节点).
如何在场景图中正确地 translate/rotate 节点相对于其 parent 节点?
问题
对于场景节点的parent/child结构,考虑以下水分子图(没有连接线),其中O氧原子是parent节点和2个H氢原子是child节点。
翻译问题
如果你抓住parent O氧原子并翻译结构,你期望H氢children 跟随并保持与 parent 相同的相对位置。如果你取一个 child H 原子并翻译它,那么只有 child 会受到影响。这通常是它目前的工作方式。当平移 O 个原子时,H 个原子会自动随之移动,正如层次图中所预期的那样。
然而,在翻译parent时,children也最终积累了额外的翻译,这实质上导致 children 向同一方向 'translate twice' 并远离它们的 parent 而不是保持相同的相对距离。
旋转问题
如果你抓住 parent O 节点并旋转它,你期望 children H节点也旋转,但在轨道上,因为旋转是由 parent 执行的。这按预期工作。
但是,如果你抓取一个childH节点并告诉它相对于旋转它的 parent,我预计只有 child 最终会以相同的方式围绕其 parent 运行,但这并没有发生。相反,child 在其当前位置以更快的速度绕其自身的轴旋转(例如,相对于其自身局部 space 的旋转速度是其旋转速度的两倍)。
我真的希望这个描述足够公平,但如果不公平请告诉我,我会根据需要进行澄清。
数学
我正在使用 4x4 column-major 矩阵(即 Matrix4
)和列向量(即 Vector3
、Vector4
).
下面的错误逻辑是最接近我得出的正确行为。请注意,我选择使用 Java-like 语法,并使用运算符重载以使此处的数学更易于阅读。当我认为我已经弄明白时,我尝试了不同的方法,但我真的没有。
当前翻译逻辑
translate(Vector3 tv /* translation vector */, TransformSpace relativeTo):
switch (relativeTo):
case LOCAL:
localTranslation = localTranslation * TranslationMatrix4(tv);
break;
case PARENT:
if parentNode != null:
localTranslation = parentNode.worldTranslation * localTranslation * TranslationMatrix4(tv);
else:
localTranslation = localTranslation * TranslationMatrix4(tv);
break;
case WORLD:
localTranslation = localTranslation * TranslationMatrix4(tv);
break;
当前循环逻辑
rotate(Angle angle, Vector3 axis, TransformSpace relativeTo):
switch (relativeTo):
case LOCAL:
localRotation = localRotation * RotationMatrix4(angle, axis);
break;
case PARENT:
if parentNode != null:
localRotation = parentNode.worldRotation * localRotation * RotationMatrix4(angle, axis);
else:
localRotation = localRotation * RotationMatrix4(angle, axis);
break;
case WORLD:
localRotation = localRotation * RotationMatrix4(angle, axis);
break;
正在计算 World-Space 转换
为了完整起见,this
节点的世界变换计算如下:
if parentNode != null:
worldTranslation = parent.worldTranslation * localTranslation;
worldRotation = parent.worldRotation * localRotation;
worldScale = parent.worldScale * localScale;
else:
worldTranslation = localTranslation;
worldRotation = localRotation;
worldScale = localScale;
此外,this
节点的 full/accumulated 转换是:
Matrix4 fullTransform():
Matrix4 localXform = worldTranslation * worldRotation * worldScale;
if parentNode != null:
return parent.fullTransform * localXform;
return localXform;
当请求将节点的变换发送到 OpenGL 着色器统一时,将使用 fullTransform
矩阵。
这个基本问题就是如何解决通勤矩阵问题。
假设您有一个矩阵 X 和一个矩阵乘积 ABC。假设你想乘以找到一个 Y 这样
X*A*B*C = A*B*Y*C
反之亦然。
假设没有矩阵是奇异的,首先消除公共项:
X*A*B = A*B*Y
接下来,隔离。跟踪左右,乘以倒数:
A^-1*X*A*B = A^-1 *A *B *Y
A^-1*X*A*B = B *Y
B^-1*A^-1*X*A*B = Y
或者如果你有 Y 但想要 X:
X*A*B *B^-1 *A^-1 = A*B*Y*B^-1 *A^-1
X = A*B*Y*B^-1 *A^-1
以上只是一般规则的特例:
X*A = A*Y
均值
X=A*Y*A^-1
A^-1*X*A=Y
附注(A*B)^-1 = B^-1 * A^-1
.
此过程让您检查一系列转换并提出 "I want to apply a transformation at a specific spot, but store it by applying it elsewhere.",这是您问题的核心。
您使用的矩阵链应包括 所有 变换——平移、旋转、缩放——而不仅仅是求解 X * B = B * Y
不会为 X * A * B = A * B * Y
.
生成解决方案
worldTranslation = parentNode.worldTranslation * localTranslation;
worldRotation = parentNode.worldRotation * localRotation;
worldScale = parentNode.worldScale * localScale;
这不是连续转换累积的工作原理。如果你仔细想想,为什么不呢。
假设您有两个节点:parent 和 child。 parent 有一个 90 度,counter-clockwise 围绕 Z 轴的局部旋转。 child 在 X 轴上有 +5 偏移量。好吧,counter-clockwise 旋转应该使它在 Y 轴上有一个 +5,是的(假设 right-handed 坐标系)?
但事实并非如此。您的 localTranslation
从未受到任何形式的旋转影响。
您的所有转换都是如此。平移仅受平移影响,不受缩放或旋转影响。旋转不受平移的影响。等等
这就是您的代码要求执行的操作,而不是您应该执行的操作。
分解矩阵的分量是个好主意。也就是说,拥有单独的平移、旋转和缩放 (TRS) 组件是一个好主意。它使得以正确的顺序应用连续的局部转换变得更加容易。
现在,将组件保持为 矩阵 是错误的,因为它确实没有任何意义并且无缘无故地浪费时间&space。翻译只是一个vec3
,存储13个其他组件没有任何好处。当你在本地积累翻译时,你只需添加它们。
但是时刻你需要为一个节点累加最终的矩阵,你需要将每个TRS分解转化为它自己的局部矩阵,然后转化为parent 的 整体转换 ,而不是 parent 的单个 TRS 组件。也就是说,您需要在本地组合单独的变换,然后将它们与 parent 变换矩阵相乘。在 pseudo-code:
function AccumRotation(parentTM)
local localMatrix = TranslationMat(localTranslation) * RotationMat(localRotation) * ScaleMat(localScale)
local fullMatrix = parentTM * localMatrix
for each child
child.AccumRotation(fullMatrix)
end
end
每个 parent 将自己的累积旋转传递给 child。给根节点一个单位矩阵。
现在,TRS 分解非常好,但它 仅 在处理 local 转换时有效。即相对于parent的变换。如果要在其局部 space 中旋转 object,请将四元数应用于其方向。
但是在 non-local space 中执行转换完全是另一回事。例如,如果您想将 world-space 中的翻译应用于应用了一些任意系列转换的 object...那是一个 non-trivial 任务。实际上,这是一项简单的任务:计算 object 的 world-space 矩阵,然后在其左侧应用平移矩阵,然后使用 parent 的逆矩阵 world-space 矩阵计算到 parent.
的相对变换
function TranslateWorld(transVec)
local parentMat = this->parent ? this->parent.ComputeTransform() : IdentityMatrix
local localMat = this->ComputeLocalTransform()
local offsetMat = TranslationMat(localTranslation)
local myMat = parentMat.Inverse() * offsetMat * parentMat * localMat
end
P-1OP这个东西的意思其实就是一个普通的构造。意思是将一般变换O
转化为P
的space。因此,它将世界偏移量转换为 parent 矩阵的 space。然后我们将其应用于本地转换。
myMat
现在包含一个转换矩阵,当它乘以 parent 的转换时,将应用 transVec
就像它在世界 space 中一样。这就是你想要的。
问题是myMat
是一个矩阵,不是TRS分解。你如何回到 TRS 分解?好吧...这确实需要 non-trivial 矩阵数学。它需要做一些叫做 Singular Value Decomposition 的事情。即使在实施了丑陋的数学之后,SVD 也可能 失败 。可以有一个 non-decomposable 矩阵。
在我编写的场景图系统中,我创建了一个特殊的 class,它实际上是 TRS 分解与其表示的矩阵之间的联合。您可以查询它是否被分解,如果是您可以修改 TRS 组件。但是一旦你试图直接给它分配一个 4x4 矩阵值,它就变成了一个组合矩阵,你不能再应用局部分解变换。我什至从未尝试实施 SVD。
哦,你可以把矩阵累加进去。但是 任意 转换的连续累积不会产生与分解组件修改相同的结果。如果你想影响旋转而不影响之前的翻译,你只能在 class 处于分解状态时这样做。
无论如何,你的代码有一些正确的想法,但也有一些非常不正确的想法。您需要确定进行 TRS 分解的重要性与能够应用 non-local 转换的重要性。
我发现 有点帮助,尽管还有一些细节我不是很清楚。但那个回答帮助我看到了我正在处理的问题的重要性质,所以我决定简化事情。
更简单的解决方案 - 始终在父级中 Space
我删除了 Node.TransformSpace
以简化问题。所有转换现在都相对于父 Node
的 space 应用,一切都按预期进行。我打算在开始工作后执行的数据结构更改(例如,将本地 translation/scaling 矩阵替换为简单向量)现在也已到位。
更新后的数学总结如下。
更新翻译
Node
的位置现在由 Vector3
对象表示,Matrix4
是按需构建的(见下文)。
void translate(Vector3 tv /*, TransformSpace relativeTo */):
localPosition += tv;
更新轮换
旋转现在包含在 Matrix3
,即 3x3 矩阵中。
void rotate(Angle angle, Vector3 axis /*, TransformSpace relativeTo */):
localRotation *= RotationMatrix3(angle, axis);
我仍然打算稍后查看四元数,在我可以验证我的四元数 <=> 矩阵转换是正确的之后。
更新缩放比例
像 Node
的位置一样,缩放现在也是一个 Vector3
对象:
void scale(Vector3 sv):
localScale *= sv;
已更新 Local/World 变换计算
以下更新 Node
相对于其父 Node
的世界变换(如果有的话)。此处的一个问题已通过删除对父级完整转换的不必要连接得到解决(参见原文 post)。
void updateTransforms():
if parentNode != null:
worldRotation = parent.worldRotation * localRotation;
worldScale = parent.worldScale * localScale;
worldPosition = parent.worldPosition + parent.worldRotation * (parent.worldScale * localPosition);
else:
derivedPosition = relativePosition;
derivedRotation = relativeRotation;
derivedScale = relativeScale;
Matrix4 t, r, s;
// cache local/world transforms
t = TranslationMatrix4(localPosition);
r = RotationMatrix4(localRotation);
s = ScalingMatrix4(localScale);
localTransform = t * r * s;
t = TranslationMatrix4(worldPosition);
r = RotationMatrix4(worldRotation);
s = ScalingMatrix4(worldScale);
worldTransform = t * r * s;
我目前正在使用分层场景图中的节点,但我很难正确 translating/rotating 相对于特定转换的节点 space(例如 parent 节点).
如何在场景图中正确地 translate/rotate 节点相对于其 parent 节点?
问题
对于场景节点的parent/child结构,考虑以下水分子图(没有连接线),其中O氧原子是parent节点和2个H氢原子是child节点。
翻译问题
如果你抓住parent O氧原子并翻译结构,你期望H氢children 跟随并保持与 parent 相同的相对位置。如果你取一个 child H 原子并翻译它,那么只有 child 会受到影响。这通常是它目前的工作方式。当平移 O 个原子时,H 个原子会自动随之移动,正如层次图中所预期的那样。
然而,在翻译parent时,children也最终积累了额外的翻译,这实质上导致 children 向同一方向 'translate twice' 并远离它们的 parent 而不是保持相同的相对距离。
旋转问题
如果你抓住 parent O 节点并旋转它,你期望 children H节点也旋转,但在轨道上,因为旋转是由 parent 执行的。这按预期工作。
但是,如果你抓取一个childH节点并告诉它相对于旋转它的 parent,我预计只有 child 最终会以相同的方式围绕其 parent 运行,但这并没有发生。相反,child 在其当前位置以更快的速度绕其自身的轴旋转(例如,相对于其自身局部 space 的旋转速度是其旋转速度的两倍)。
我真的希望这个描述足够公平,但如果不公平请告诉我,我会根据需要进行澄清。
数学
我正在使用 4x4 column-major 矩阵(即 Matrix4
)和列向量(即 Vector3
、Vector4
).
下面的错误逻辑是最接近我得出的正确行为。请注意,我选择使用 Java-like 语法,并使用运算符重载以使此处的数学更易于阅读。当我认为我已经弄明白时,我尝试了不同的方法,但我真的没有。
当前翻译逻辑
translate(Vector3 tv /* translation vector */, TransformSpace relativeTo):
switch (relativeTo):
case LOCAL:
localTranslation = localTranslation * TranslationMatrix4(tv);
break;
case PARENT:
if parentNode != null:
localTranslation = parentNode.worldTranslation * localTranslation * TranslationMatrix4(tv);
else:
localTranslation = localTranslation * TranslationMatrix4(tv);
break;
case WORLD:
localTranslation = localTranslation * TranslationMatrix4(tv);
break;
当前循环逻辑
rotate(Angle angle, Vector3 axis, TransformSpace relativeTo):
switch (relativeTo):
case LOCAL:
localRotation = localRotation * RotationMatrix4(angle, axis);
break;
case PARENT:
if parentNode != null:
localRotation = parentNode.worldRotation * localRotation * RotationMatrix4(angle, axis);
else:
localRotation = localRotation * RotationMatrix4(angle, axis);
break;
case WORLD:
localRotation = localRotation * RotationMatrix4(angle, axis);
break;
正在计算 World-Space 转换
为了完整起见,this
节点的世界变换计算如下:
if parentNode != null:
worldTranslation = parent.worldTranslation * localTranslation;
worldRotation = parent.worldRotation * localRotation;
worldScale = parent.worldScale * localScale;
else:
worldTranslation = localTranslation;
worldRotation = localRotation;
worldScale = localScale;
此外,this
节点的 full/accumulated 转换是:
Matrix4 fullTransform():
Matrix4 localXform = worldTranslation * worldRotation * worldScale;
if parentNode != null:
return parent.fullTransform * localXform;
return localXform;
当请求将节点的变换发送到 OpenGL 着色器统一时,将使用 fullTransform
矩阵。
这个基本问题就是如何解决通勤矩阵问题。
假设您有一个矩阵 X 和一个矩阵乘积 ABC。假设你想乘以找到一个 Y 这样
X*A*B*C = A*B*Y*C
反之亦然。
假设没有矩阵是奇异的,首先消除公共项:
X*A*B = A*B*Y
接下来,隔离。跟踪左右,乘以倒数:
A^-1*X*A*B = A^-1 *A *B *Y
A^-1*X*A*B = B *Y
B^-1*A^-1*X*A*B = Y
或者如果你有 Y 但想要 X:
X*A*B *B^-1 *A^-1 = A*B*Y*B^-1 *A^-1
X = A*B*Y*B^-1 *A^-1
以上只是一般规则的特例:
X*A = A*Y
均值
X=A*Y*A^-1
A^-1*X*A=Y
附注(A*B)^-1 = B^-1 * A^-1
.
此过程让您检查一系列转换并提出 "I want to apply a transformation at a specific spot, but store it by applying it elsewhere.",这是您问题的核心。
您使用的矩阵链应包括 所有 变换——平移、旋转、缩放——而不仅仅是求解 X * B = B * Y
不会为 X * A * B = A * B * Y
.
worldTranslation = parentNode.worldTranslation * localTranslation; worldRotation = parentNode.worldRotation * localRotation; worldScale = parentNode.worldScale * localScale;
这不是连续转换累积的工作原理。如果你仔细想想,为什么不呢。
假设您有两个节点:parent 和 child。 parent 有一个 90 度,counter-clockwise 围绕 Z 轴的局部旋转。 child 在 X 轴上有 +5 偏移量。好吧,counter-clockwise 旋转应该使它在 Y 轴上有一个 +5,是的(假设 right-handed 坐标系)?
但事实并非如此。您的 localTranslation
从未受到任何形式的旋转影响。
您的所有转换都是如此。平移仅受平移影响,不受缩放或旋转影响。旋转不受平移的影响。等等
这就是您的代码要求执行的操作,而不是您应该执行的操作。
分解矩阵的分量是个好主意。也就是说,拥有单独的平移、旋转和缩放 (TRS) 组件是一个好主意。它使得以正确的顺序应用连续的局部转换变得更加容易。
现在,将组件保持为 矩阵 是错误的,因为它确实没有任何意义并且无缘无故地浪费时间&space。翻译只是一个vec3
,存储13个其他组件没有任何好处。当你在本地积累翻译时,你只需添加它们。
但是时刻你需要为一个节点累加最终的矩阵,你需要将每个TRS分解转化为它自己的局部矩阵,然后转化为parent 的 整体转换 ,而不是 parent 的单个 TRS 组件。也就是说,您需要在本地组合单独的变换,然后将它们与 parent 变换矩阵相乘。在 pseudo-code:
function AccumRotation(parentTM)
local localMatrix = TranslationMat(localTranslation) * RotationMat(localRotation) * ScaleMat(localScale)
local fullMatrix = parentTM * localMatrix
for each child
child.AccumRotation(fullMatrix)
end
end
每个 parent 将自己的累积旋转传递给 child。给根节点一个单位矩阵。
现在,TRS 分解非常好,但它 仅 在处理 local 转换时有效。即相对于parent的变换。如果要在其局部 space 中旋转 object,请将四元数应用于其方向。
但是在 non-local space 中执行转换完全是另一回事。例如,如果您想将 world-space 中的翻译应用于应用了一些任意系列转换的 object...那是一个 non-trivial 任务。实际上,这是一项简单的任务:计算 object 的 world-space 矩阵,然后在其左侧应用平移矩阵,然后使用 parent 的逆矩阵 world-space 矩阵计算到 parent.
的相对变换function TranslateWorld(transVec)
local parentMat = this->parent ? this->parent.ComputeTransform() : IdentityMatrix
local localMat = this->ComputeLocalTransform()
local offsetMat = TranslationMat(localTranslation)
local myMat = parentMat.Inverse() * offsetMat * parentMat * localMat
end
P-1OP这个东西的意思其实就是一个普通的构造。意思是将一般变换O
转化为P
的space。因此,它将世界偏移量转换为 parent 矩阵的 space。然后我们将其应用于本地转换。
myMat
现在包含一个转换矩阵,当它乘以 parent 的转换时,将应用 transVec
就像它在世界 space 中一样。这就是你想要的。
问题是myMat
是一个矩阵,不是TRS分解。你如何回到 TRS 分解?好吧...这确实需要 non-trivial 矩阵数学。它需要做一些叫做 Singular Value Decomposition 的事情。即使在实施了丑陋的数学之后,SVD 也可能 失败 。可以有一个 non-decomposable 矩阵。
在我编写的场景图系统中,我创建了一个特殊的 class,它实际上是 TRS 分解与其表示的矩阵之间的联合。您可以查询它是否被分解,如果是您可以修改 TRS 组件。但是一旦你试图直接给它分配一个 4x4 矩阵值,它就变成了一个组合矩阵,你不能再应用局部分解变换。我什至从未尝试实施 SVD。
哦,你可以把矩阵累加进去。但是 任意 转换的连续累积不会产生与分解组件修改相同的结果。如果你想影响旋转而不影响之前的翻译,你只能在 class 处于分解状态时这样做。
无论如何,你的代码有一些正确的想法,但也有一些非常不正确的想法。您需要确定进行 TRS 分解的重要性与能够应用 non-local 转换的重要性。
我发现
更简单的解决方案 - 始终在父级中 Space
我删除了 Node.TransformSpace
以简化问题。所有转换现在都相对于父 Node
的 space 应用,一切都按预期进行。我打算在开始工作后执行的数据结构更改(例如,将本地 translation/scaling 矩阵替换为简单向量)现在也已到位。
更新后的数学总结如下。
更新翻译
Node
的位置现在由 Vector3
对象表示,Matrix4
是按需构建的(见下文)。
void translate(Vector3 tv /*, TransformSpace relativeTo */):
localPosition += tv;
更新轮换
旋转现在包含在 Matrix3
,即 3x3 矩阵中。
void rotate(Angle angle, Vector3 axis /*, TransformSpace relativeTo */):
localRotation *= RotationMatrix3(angle, axis);
我仍然打算稍后查看四元数,在我可以验证我的四元数 <=> 矩阵转换是正确的之后。
更新缩放比例
像 Node
的位置一样,缩放现在也是一个 Vector3
对象:
void scale(Vector3 sv):
localScale *= sv;
已更新 Local/World 变换计算
以下更新 Node
相对于其父 Node
的世界变换(如果有的话)。此处的一个问题已通过删除对父级完整转换的不必要连接得到解决(参见原文 post)。
void updateTransforms():
if parentNode != null:
worldRotation = parent.worldRotation * localRotation;
worldScale = parent.worldScale * localScale;
worldPosition = parent.worldPosition + parent.worldRotation * (parent.worldScale * localPosition);
else:
derivedPosition = relativePosition;
derivedRotation = relativeRotation;
derivedScale = relativeScale;
Matrix4 t, r, s;
// cache local/world transforms
t = TranslationMatrix4(localPosition);
r = RotationMatrix4(localRotation);
s = ScalingMatrix4(localScale);
localTransform = t * r * s;
t = TranslationMatrix4(worldPosition);
r = RotationMatrix4(worldRotation);
s = ScalingMatrix4(worldScale);
worldTransform = t * r * s;