正确转换相对于指定空间的节点?
Posted
技术标签:
【中文标题】正确转换相对于指定空间的节点?【英文标题】:Correctly transforming a node relative to a specified space? 【发布时间】:2016-10-22 21:09:52 【问题描述】:我目前正在使用分层场景图中的节点,并且我在相对于特定变换空间(例如父节点)正确平移/旋转节点时遇到了困难。
如何在场景图中相对于其父节点正确平移/旋转节点?
问题
考虑以下场景节点的父/子结构的水分子图(没有连接线),Oxygen原子是父节点,2个H氢原子是子节点。
翻译问题
如果您抓取父 Oxygen 原子并转换结构,您希望 Hydrogen 子代跟随并保持与父代相同的相对位置。如果你抓住一个子 H 原子并翻译它,那么只有孩子会受到影响。这通常是它当前的工作方式。当 O 原子被平移时,H 原子会自动随之移动,正如层次图中所预期的那样。
然而,当翻译父级时,子级最终也会累积一个额外的翻译,这实质上会导致子级在同一方向上“翻译两次”并移动远离父母,而不是保持相同的相对距离。
轮换问题
如果你抓住父 O 节点并旋转它,你希望子 H 节点也旋转,但在一个轨道上,因为旋转是由父母。这按预期工作。
然而,如果你抓住一个子 H 节点并告诉它相对于其父节点旋转,我预计只有子节点会结束以相同的方式围绕其父级运行,但这不会发生。相反,孩子在其当前位置以更快的速度在自己的轴上旋转(例如,相对于自己的局部空间旋转的速度是其旋转速度的两倍)。
我真的希望这个描述足够公平,但如果不是,请告诉我,我会根据需要澄清。
数学
我正在使用 4x4 column-major 矩阵(即Matrix4
)和列向量(即Vector3
、Vector4
)。
下面不正确的逻辑是最接近我得出的正确行为。请注意,我已选择使用类似 Java 的 语法,并带有运算符重载以使此处的数学更易于阅读。当我以为我已经弄明白的时候,我尝试了不同的东西,但我真的没有。
当前翻译逻辑
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;
计算世界空间变换
为了完整起见,this
节点的世界变换计算如下:
if parentNode != null:
worldTranslation = parent.worldTranslation * localTranslation;
worldRotation = parent.worldRotation * localRotation;
worldScale = parent.worldScale * localScale;
else:
worldTranslation = localTranslation;
worldRotation = localRotation;
worldScale = localScale;
此外,节点对this
的完整/累积转换是:
Matrix4 fullTransform():
Matrix4 localXform = worldTranslation * worldRotation * worldScale;
if parentNode != null:
return parent.fullTransform * localXform;
return localXform;
当请求将节点的变换发送到 OpenGL 着色器统一时,使用fullTransform
矩阵。
【问题讨论】:
不是一个答案,但您是否考虑过使用四元数来避免精度的增量损失? 很久以前我做了一个类似的程序(化学图的交互式操作)。在移动原子时,我使用了一个简单的球和弹簧模型(使用动态“虚拟”弹簧来保持显示的角度),以及一个刚体模型(每个原子在 2D 或 3D 体积内都有一个位置,并且使用标准刚体公式,你可以在任何地方找到类似的公式)在移动整个分子时。简而言之:通过单独处理你的原子,你使这变得比它需要的更难。永远不要假设旋转和平移是不同的任务。 @o11c:我想使用四元数来实现平滑插值,尤其是当节点连接了相机并且您想通过节点移动相机时。但我目前正在追查一个似乎在四元数 -> 矩阵转换中的问题,这似乎在相机的视锥体中产生了一个奇怪的剪切平面。我的猜测是转换在某个地方是错误的......即使我已经尝试了很多东西。我想我很快就必须就那个问题发表一个问题。 @Dave:你能说得更具体点吗?这里的分子只是一种直观的方式来解释我在场景图中的父/子节点是如何组织的,我不确定我是否遵循“永远不要假设旋转和平移是不同的任务”的部分。你可以说得更详细点吗?您是在数学中发现问题还是猜到了? 抱歉,我没有查看您的代码。您是否考虑过使用库来为您处理复杂性?大多数 3D 引擎都有用于这些转换任务的例程,这些例程已经过广泛的设计和测试(以及本机使用四元数并为您处理所有这些逻辑)。如果你真的想自己做,我建议你坐下来用笔和纸从头开始(在处理复杂问题时,很容易陷入“特殊情况”/“小调整”当你最好从不同的角度来看待它时的心态)。 【参考方案1】:worldTranslation = parentNode.worldTranslation * localTranslation; worldRotation = parentNode.worldRotation * localRotation; worldScale = parentNode.worldScale * localScale;
这不是连续转换累积的工作原理。如果您考虑一下,那很明显为什么不这样做。
假设您有两个节点:父节点和子节点。父对象围绕 Z 轴进行 90 度逆时针局部旋转。孩子在 X 轴上有一个 +5 的偏移量。嗯,逆时针旋转应该会导致它在 Y 轴上有 +5,是的(假设是右手坐标系)?
但事实并非如此。您的localTranslation
永远不会受到任何形式的轮换影响。
您的所有转换都是如此。平移仅受平移影响,不受缩放或旋转影响。旋转不受平移影响。等等。
这就是你的代码所说的,而不是你应该这样做的。
保持矩阵的组件分解是个好主意。也就是说,具有单独的平移、旋转和缩放 (TRS) 组件是一个好主意。它可以更轻松地以正确的顺序应用连续的局部变换。
现在,将组件保留为 矩阵 是错误的,因为它确实没有任何意义,并且无缘无故地浪费时间和空间。翻译只是一个vec3
,用它存储13 个其他组件没有任何好处。当您在本地积累翻译时,您只需添加它们。
但是时刻你需要为一个节点累加最终的矩阵,你需要将每个TRS分解转换成自己的局部矩阵,然后转换成父级的整体转换,而不是父级的单个 TRS 组件。也就是说,您需要在本地组合单独的变换,然后将它们与父变换矩阵相乘。在伪代码中:
function AccumRotation(parentTM)
local localMatrix = TranslationMat(localTranslation) * RotationMat(localRotation) * ScaleMat(localScale)
local fullMatrix = parentTM * localMatrix
for each child
child.AccumRotation(fullMatrix)
end
end
每个父级将其自己的累积旋转传递给子级。给根节点一个单位矩阵。
现在,TRS 分解非常好,但它仅在处理 本地 转换时有效。也就是说,相对于父级的转换。如果你想在它的局部空间中旋转一个对象,你可以在它的方向上应用一个四元数。
但在非本地空间中执行转换则完全是另一回事。例如,如果您想将世界空间中的平移应用于应用了任意一系列变换的对象……这是一项不平凡的任务。实际上,这是一项简单的任务:计算对象的世界空间矩阵,然后在其左侧应用一个平移矩阵,然后使用父世界空间矩阵的逆矩阵来计算与父对象的相对变换。
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
的空间。因此,它将世界偏移量转换为父矩阵的空间。然后我们将其应用于本地转换。
myMat
现在包含一个变换矩阵,当乘以父级的变换时,将应用 transVec
,就好像它在世界空间中一样。这就是你想要的。
问题在于myMat
是一个矩阵,而不是TRS 分解。你如何回到 TRS 分解?嗯...这需要真的非平凡的矩阵数学。它需要做一些名为Singular Value Decomposition 的事情。即使在实现了丑陋的数学之后,SVD 也可能失败。可能有一个不可分解的矩阵。
在我编写的场景图系统中,我创建了一个特殊的类,它实际上是 TRS 分解和它所表示的矩阵之间的联合。您可以查询它是否已分解,如果已分解,您可以修改 TRS 组件。但是,一旦您尝试直接为其分配一个 4x4 矩阵值,它就会变成一个组合矩阵,并且您不能再应用局部分解变换。我什至从未尝试实施 SVD。
哦,您可以将矩阵累积到其中。但是任意变换的连续累积不会产生与分解的组件修改相同的结果。如果你想在不影响之前的翻译的情况下影响旋转,你只能在类处于分解状态时这样做。
无论如何,您的代码有一些正确的想法,但也有一些非常不正确的想法。您需要确定 TRS 分解的重要性与能够应用非局部转换的重要性。
【讨论】:
我编辑了帖子以澄清一些可能不清楚的地方。在我对fullTransform
的实现中,似乎我已经按照AccumRotation
执行了类似于您的第一个建议的操作,其中连接的本地xform 连接到父级的fullTransform
矩阵(如果存在)。这似乎解决了儿童的翻译问题。 (我错误地连接了世界,而不是本地 TRS 矩阵。)我仍在查看您的其余帖子和轮换问题。
看来我没有完全理解你解释的第二部分,因为我似乎无法让它正常工作。我暂时删除了Node.TransformSpace
,以避免卡住的时间比我已经停留的时间长。我以不同的方式更新了转换,它似乎工作正常。【参考方案2】:
我发现Nicol Bolas' response 有点帮助,尽管还有一些我不太清楚的细节。但这种回应让我看到了我正在处理的问题的重要性,所以我决定简化一些事情。
更简单的解决方案 - 始终在父空间中
为了简化问题,我删除了Node.TransformSpace
。现在,所有转换都相对于父 Node
的空间应用,并且一切都按预期工作。我打算在使事情正常工作后执行的数据结构更改(例如,将本地平移/缩放矩阵替换为简单向量)现在也已到位。
更新后的数学总结如下。
更新翻译
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;
更新了局部/世界变换计算
以下内容会更新 Node
相对于其父 Node
的世界变换(如果有)。此处的问题已通过删除对父级完整转换的不必要串联得到解决(请参阅原始帖子)。
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;
【讨论】:
【参考方案3】:这个基本问题是如何解决交换矩阵问题。
假设您有一个矩阵 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
。
此过程允许您检查一系列转换并询问“我想在特定位置应用转换,但通过在其他地方应用它来存储它。”,这是您问题的核心。
您使用的矩阵链应该包括所有 变换——平移、旋转、缩放——而不仅仅是同类变换,因为求解X * B = B * Y
并不能解决X * A * B = A * B * Y
.
【讨论】:
有些事情我还不清楚;请编辑澄清/可读性。我说我使用的是列主要矩阵/向量,但是您的数学似乎是列/行主要解释的混合,并且缺少运算符,这是不一致的 & diff 遵循。另外,当您说我的方程式缺少逆时:确切地缺少哪里?另外,我知道矩阵运算不可交换,请解释为什么/如何“强制”它;我没有遵循。节点还通过连接Tw * Rw * Sw
(w=world) 返回它们的完整翻译。有一些理由将T*R*S
矩阵分开。
我的意思是说,节点还通过连接Tw * Rw * Sw
矩阵返回其完整的变换(不是翻译)。由于它们是 4x4 列矩阵并且从右到左读取,因此它们的应用顺序等同于 Tw * (Rw * Sw)
。
@ray 想象有人发布了代码,其中充满了通过跳转的手动循环,并与在每个位置手动编写的逐位操作进行添加和比较。它没有用。您可以发现至少有一个位操作有错误,因为它们没有进位逻辑,但无法遵循应有的逻辑。您指出,通过代码使用流控制和标准操作会更好。他们回答说 goto 和 bit 操作有一定的优势,并要求你告诉你他们应该从哪里得到进位,他们正在使用 3s 补码加法。
即使我把所有东西都放在一个矩阵中,并且它突然起作用了,我并没有真正纠正我目前的数学误解。我认为你的评论错过了这一点。还有一些事情似乎变得更加困难 - 例如。不能再通过从向量 b/c 创建一个新的平移矩阵来设置绝对世界位置,我会失去旋转/缩放变换。由于您在数学中的列/行主要组合,我也很难理解您的解释。不幸的是,在目前的情况下,答案对我来说并没有太大用处。
我想今天晚些时候我会看到你的编辑,因为我称之为一个晚上,但请注意,矩阵和向量之间的乘法的写入/读取方式取决于你是否使用列或行主要的。例如,如果使用列优先组织(这是我正在使用的),矩阵向量乘法必须写为v' = M*v
,而写v' = v*M
意味着行优先组织。这是数学课本的标准。希望这有助于消除歧义,并且由于这些操作不是可交换的,因此它确实对数学很重要。以上是关于正确转换相对于指定空间的节点?的主要内容,如果未能解决你的问题,请参考以下文章