正确转换相对于指定空间的节点?

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)和列向量(即Vector3Vector4)。

下面不正确的逻辑是最接近我得出的正确行为。请注意,我已选择使用类似 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 意味着行优先组织。这是数学课本的标准。希望这有助于消除歧义,并且由于这些操作不是可交换的,因此它确实对数学很重要。

以上是关于正确转换相对于指定空间的节点?的主要内容,如果未能解决你的问题,请参考以下文章

如何在颤动中相对于前一个路由转换动画

unity坐标转换问题

范围(地址转换)

JS若何将在页面上取得的元素坐标转换成屏幕坐标??

JavaScript的数据类型的隐式转换

转换此节点的正确 xdt:Locator 参数是啥?