3D 矩阵:绝对到相对变换,反之亦然



我有一个OpenGL C#项目,我想提供Unity3D游戏引擎等功能。

介绍:我有 Transform 类,它为着色器提供转换矩阵。每个转换都可以有父转换。计算最终转换矩阵的代码如下所示:

public Vector3 LocalPosition { get; set; }
public Quaternion LocalRotation { get; set; }
public Vector3 LocalScale { get; set; }
public Matrix GetModelMatrix() {
    Matrix result;
    if(HasParent)
        result = Parent.GetModelMatrix();
    else
        result = Matrix.CreateIdentity();
    ApplyLocalTransformations(result);
    return result;    
}
private void ApplyLocalTransform(Matrix matrix)
{
    matrix.Translate(LocalPosition);
    matrix.Rotate(LocalRotation);
    matrix.Scale(LocalScale);
}

如您所见,LocalPosition、LocalScale 和 LocalRotate 是相对于父级的转换。此代码工作正常。

问题:我想再添加 3 个属性(你好 Unity3D(:

public Vector3 AbsolutePosition { get; set; }
public Quaternion AbsoluteRotation { get; set; }
public Vector3 AbsoluteScale { get; set; }

我希望能够获取和设置子转换的绝对转换。在设置绝对值时,应始终更新本地,反之亦然。

示例:我们有父级在位置 (1, 1, 1( 和子项的位置 LocalPosition = (0, 0, 0(,有了这些信息,我们可以计算出子项的绝对位置 = (1, 1, 1(。现在我们设置 child's AbsolutePosition = (0, 0, 0(。它的 LocalPosition 现在将是 = (-1, -1, -1(。这是一个非常简单的例子,在实际场景中,我们必须考虑父级的规模和旋转来计算位置。

如何计算绝对和局部位置 我有一个想法:我可以从转换矩阵中获取最后一列,这将是我的绝对位置。要获得 LocalPosition,我可以从父转换矩阵的 AbsolutePosition 最后一列中减去。但是旋转和规模背后的数学对我来说仍然不清楚。

问题:你能帮我计算局部和绝对位置、旋转和比例的算法吗?

附言:考虑性能会很棒。

我已经处理了这个确切的问题。解决它的方法不止一种,所以我只会给你我想出的解决方案。简而言之,我将位置、旋转和比例存储在局部和世界坐标中。然后,我计算增量,以便可以将在一个坐标空间中所做的更改应用于另一个坐标空间。

最后,我使用事件将增量广播到所有降序游戏对象。事件不是绝对必要的。您可以递归地调用降序游戏对象的转换组件上的一些函数,以便将增量沿游戏对象树向下应用。

这一点上最好举个例子,所以看看这个二传手方法,用于转换的本地位置(我已经从我参与的一个非常小的游戏中提升(:

void Transform::localPosition(const Vector3& localPosition)
{
    const Vector3 delta = localPosition - m_localPosition;
    m_localPosition = localPosition; // Set local position.
    m_position += delta; // Update world position.
    // Now broadcast the delta to all descended game objects.
}

所以这是微不足道的。世界排名的二传手类似:

void Transform::position(const Vector3& position)
{
    const Vector3 delta = position - m_position;
    m_position = position; // Set world position.
    m_localPosition += delta; // Update local position.
    // Now broadcast the delta to all descended game objects.
}

旋转的原理是一样的:

void Transform::localRotation(const Quaternion& localRotation)
{
    const Quaternion delta = m_localRotation.inverse() * localRotation;
    m_localRotation = localRotation; // Set the local orientation.
    m_rotation = delta * m_rotation; // Update the world orientation.
    // Now broadcast the delta to all descended game objects.
}
void Transform::rotation(const Quaternion& rotation)
{
    const Quaternion delta = m_rotation.inverse() * rotation;
    m_rotation = rotation; // Set the world orientation.
    m_localRotation = delta * m_localRotation; // Update the local orientation.
    // Now broadcast the delta to all descended game objects.
}

最后是规模:

void Transform::localScale(const Vector3& scale)
{
    const Vector3 delta = scale - m_localScale;
    m_localScale = scale; // Set the local scale.
    m_scale += delta; // Update the world scale.
    // Now broadcast the delta to all descended game objects.
}
void Transform::scale(const Vector3& scale)
{
    const Vector3 delta = scale - m_scale;
    m_scale = scale; // Set the world scale.
    m_localScale += delta; // Update the local scale.
    // Now broadcast the delta to all descended game objects.
}

我不确定您如何从性能角度改进这一点。计算和应用增量相对便宜(当然比分解转换矩阵便宜得多(。

最后,由于您正在尝试模拟 Unity,因此您可能想看看我的小型 c++ 数学库,该库以 Unity 的数学类为模型。

计算示例

所以我在最初的回答中省略了很多细节,这似乎引起了一些混乱。我在下面提供了一个详细的例子,它遵循使用增量的概念(如上所述(来回应 Xorza 的评论。

我有一个游戏对象,它有一个孩子。我将分别将这些游戏对象称为对象和对象。它们都具有默认比例 (1, 1, 1(,并且位于原点 (0, 0, 0(。

请注意,Unity 的变换类不允许写入 lossyScale(世界尺度(属性。因此,按照 Unity 提供的行为,我将处理对父转换的 localScale 属性的修改。

首先,我称parent.transform.setLocalScale(0.1, 0.1, 0.1).

setLocalScale 函数将新值写入localScale字段,然后按如下方式计算缩放增量:

scalingDelta = newLocalScale / oldLocalScale
             = (0.1, 0.1, 0.1) / (1, 1, 1)
             = (0.1, 0.1, 0.1)

我们使用此缩放增量来更新转换的世界scale属性。

scale = scalingDelta * scale;

现在,由于对父转换属性(本地或全局(的更改会影响子转换的全局属性,因此我需要更新子转换的全局属性。特别是,我需要更新子转换的scaleposition属性(在此特定操作中不影响旋转(。我们可以按如下方式执行此操作:

child.transform.scale = scalingDelta * child.transform.scale
                      = (0.1, 0.1, 0.1) * (1, 1, 1)
                      = (0.1, 0.1, 0.1)
child.transform.position = parent.transform.position + scalingDelta * child.transform.localPosition
                         = (child.transform.position - child.transform.localPosition) + scalingDelta * child.transform.localPosition
                         = ((0, 0, 0) - (0, 0, 0)) + (0.1, 0.1, 0.1) * (0, 0, 0)
                         = (0, 0, 0)

请注意,如果使用事件将增量沿游戏对象树向下传递,则很难访问父转换的位置。但是,由于child.transform.position = parent.transforn.position + child.transform.localPosition,我们可以从子变换的世界位置和局部位置计算父变换的世界位置。

此外,重要的是,请注意子转换的本地属性不会更改。

其次,我打电话给child.transform.setPosition(1, 1, 1).

setPosition函数将新值写入position然后按如下方式计算转换增量:

translationDelta = newPosition - oldPosition
                 = (1, 1, 1) - (0, 0, 0)
                 = (1, 1, 1)

最后,setPosition函数使用计算的增量更新转换的localPosition。但是,请注意,计算的平移增量位于世界空间坐标中。因此,在更新localPosition之前,我们需要做一些工作将其转换为本地空间坐标。特别是,我们需要考虑父转换的世界规模。

localPosition = localPosition + translationDelta / parent.transform.scale
              = localPosition + translationDelta / (scale / localScale)
              = localPosition + translationDelta * (localScale / scale)
              = (0, 0, 0) + (1, 1, 1) * ((1, 1, 1,) / (0.1, 0.1, 0.1))
              = (10, 10, 10)

同样,没有必要查找父转换的世界比例。这可以从子变换的世界比例和局部比例计算得出。

在此示例中,我处理了对父转换规模的更改。相同的原则适用于父项位置和旋转的更改,尽管计算会有所不同。

最新更新