正确转换相对于指定 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)和列向量(即 Vector3Vector4 ).

下面的错误逻辑是最接近我得出的正确行为。请注意,我选择使用 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;