我有一个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;
现在,由于对父转换属性(本地或全局(的更改会影响子转换的全局属性,因此我需要更新子转换的全局属性。特别是,我需要更新子转换的scale
和position
属性(在此特定操作中不影响旋转(。我们可以按如下方式执行此操作:
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)
同样,没有必要查找父转换的世界比例。这可以从子变换的世界比例和局部比例计算得出。
在此示例中,我处理了对父转换规模的更改。相同的原则适用于父项位置和旋转的更改,尽管计算会有所不同。