使用 XNA/数学物理/逆运动学创建游戏
逆运动学 (IK) 与骨骼动画相关。例如,机械臂的运动或动画角色的运动。 类人骨骼的逆运动学教程 和 维基百科上的逆运动学。
一个例子可能是使用 XNA 框架模拟机械臂。本章应该更多地关注数学背景,而角色动画章节将更多地处理来自 3D 建模师的模型。
如果你想让机械臂或动画角色的胳膊移动到某个方向,这个实体通常被建模成一个刚性多体系统,它由一组称为连杆的刚性物体组成。这些连杆通过关节连接。为了控制这个刚性多体的运动并使其到达目标方向,通常使用逆运动学。
逆运动学的目标是将每个关节放置到其目标位置。为此,需要找到关节角度的正确设置。角度用向量表示[1]。
逆运动学非常具有挑战性,因为角度可能有多种可能的解决方案,或者根本没有解决方案。在存在解决方案的情况下,可能需要进行复杂且代价高昂的计算来找到它[2]。存在许多不同的方法来解决这个问题
- 雅可比转置法
- 伪逆法
- 阻尼最小二乘法 (DLS)
- 选择性阻尼最小二乘法 (SDLS)
- 循环坐标下降法
实现基于雅可比的方法是一项巨大的工作,因为它们需要大量的数学知识和许多先决条件,例如具有 m 列和 n 行的矩阵类或奇异值分解。一个实现示例可以找到 这里。它由 Samuel R. Buss 和 Jin-Su Kim 创建。
除循环坐标下降法之外,上面提到的所有方法都基于雅可比矩阵,它是一个关节角度值的函数,用于确定末端位置。它们讨论了如何选择角度的问题。需要改变角度的值,直到达到与目标值近似相等的值。
更新关节角度的值可以使用两种方式
1) 每一步执行一次角度值的单次更新(使用方程),使关节跟随目标位置。
2) 迭代地更新角度,直到它接近一个解决方案[1]
雅可比只能作为近似值用于某个位置附近。因此,必须在到达所需的末端位置之前,以小步长重复计算雅可比的过程。
伪代码
while (e is too far from g) {
Compute J(e,Φ) for the current pose Φ
Compute J-1 // invert the Jacobian matrix
Δe = β(g - e) // pick approximate step to take
ΔΦ = J-1 • Δe // compute change in joint DOFs
Φ = Φ + ΔΦ // apply change to DOFs
Compute new e vector // apply forward kinematics to see where we ended up
}
以下方法处理选择适当角度值的问题。
雅可比转置法的想法是使用方程更新角度,使用转置而不是逆或伪逆(因为逆并不总是可能的)[1]。使用这种方法,可以通过循环遍历角度直接计算角度的变化。它避免了昂贵的求逆和奇异性问题,但收敛到解的速度非常慢。这种方法的运动与物理运动非常匹配,这与其他可能导致不自然运动的逆运动学解决方案不同[3]。
这种方法将角度值设置为雅可比的伪逆。它试图找到一个矩阵,它有效地反转了一个非方阵。它存在奇异性问题,这些问题倾向于某些方向不可达。问题在于该方法首先循环遍历所有角度,然后需要计算和存储雅可比矩阵,对其进行伪逆,计算角度变化,最后应用这些变化[4]。
这种方法避免了伪逆法中的一些问题。它找到最小化数量的角度值,而不仅仅是找到最小向量。必须仔细选择阻尼常数,以使方程稳定[1]。
这种方法是对 DLS 方法的改进,需要的迭代次数更少。
基于逆雅可比矩阵的算法有时不稳定,无法收敛。因此,存在另一种方法。循环坐标下降法一次调整一个关节角度。它从链条的最后一个连杆开始,迭代地向后遍历所有可调整的角度,直到到达所需的位置,或者循环重复了一定次数。该算法使用两个向量来确定角度,以便将模型旋转到所需的位置。这可以通过点积的反余弦求解。此外,为了定义旋转方向,使用叉积[5]。可以观看该方法的概念演示 这里
这是一个示例实现
首先,我们需要一个表示关节的对象。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace InverseKinematics
{
/// <summary>
/// Represents a chain link of the class BoneChain
/// </summary>
public class Bone
{
/// <summary>
/// the bone's appearance
/// </summary>
private Cuboid cuboid;
/// <summary>
/// the bone's last calculated angle if errors occure like not a number
/// this will be used instead
/// </summary>
public float lastAngle = 0;
private Vector3 worldCoordinate, destination;
/// <summary>
/// where the bone does point at
/// </summary>
public Vector3 Destination
{
get { return destination; }
set { destination = value; }
}
/// <summary>
/// the bone's source position
/// </summary>
public Vector3 WorldCoordinate
{
get { return worldCoordinate; }
set { worldCoordinate = value; }
}
/// <summary>
/// Generates a bone by another bone's end
/// </summary>
/// <param name="lastBone">the bone's end for this bone's source</param>
/// <param name="destination"></param>
public Bone(Bone lastBone, Vector3 destination) : this(lastBone.Effector, destination)
{
}
/// <summary>
/// Generates a bone at a coordinate in
/// </summary>
/// <param name="worldCoordinate"></param>
/// <param name="destination"></param>
public Bone(Vector3 worldCoordinate, Vector3 destination)
{
cuboid = new Cuboid();
this.worldCoordinate = worldCoordinate;
this.destination = destination;
}
这些是骨骼类所需的字段和构造函数。cuboid 字段是表示骨骼的 3D 模型。destination 和 worldCoordinate 描述了关节。worldCoordinate 显示骨骼的位置。destination 是目标位置。第一个构造函数包含两个向量的设置。第二个构造函数采用世界位置和目标位置(也称为末端执行器),并从它们生成新的骨骼的世界位置。
/// <summary>
/// calculate's the bone's appearance appropiate to its world position
/// and its destination
/// </summary>
public void Update()
{
Vector3 direction = new Vector3(destination.Length() / 2, 0, 0);
cuboid.Scale(new Vector3(destination.Length() / 2, 5f, 5f));
cuboid.Translate(direction);
cuboid.Rotate(SphereCoordinateOrientation(destination));
cuboid.Translate(worldCoordinate);
cuboid.Update();
}
update 方法使用 destination 向量的长度、宽度为 5 和深度为 5 来缩放 cuboid。它将 cuboid 沿其一半长度平移以获得旋转枢轴,并通过 destination 向量的球坐标角度旋转它,然后将其平移到其 worldCoordinate。
/// <summary>
/// Draws the bone's appearance
/// </summary>
/// <param name="device">the device to draw the bone's appearance</param>
public void Draw(GraphicsDevice device)
{
cuboid.Draw(device);
}
draw 方法绘制更新后的向量。
/// <summary>
/// generates the bone's rotation by unsing sphere coordinates
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
private Vector3 SphereCoordinateOrientation(Vector3 position)
{
float alpha = 0;
float beta = 0;
if (position.Z != 0.0 || position.X != 0.0)
alpha = (float)Math.Atan2(position.Z, position.X);
if (position.Y != 0.0)
beta = (float)Math.Atan2(position.Y, Math.Sqrt(position.X * position.X + position.Z * position.Z));
return new Vector3(0, -alpha, beta);
}
/// <summary>
/// the bone's destination is local and points to the world's destination
/// so this function just subtract's the bone's world coordinate from the world's destination
/// and gets the bone's local destination vector
/// </summary>
/// <param name="destination">The destination in the world coordinate system</param>
public void SetLocalDestinationbyAWorldDestination(Vector3 destination)
{
this.destination = destination - worldCoordinate;
}
/// <summary>
/// the bone's source plus the bone's destination vector
/// </summary>
/// <returns></returns>
public Vector3 Effector
{
get
{
return worldCoordinate + destination;
}
}
}
}
骨骼类的其余部分是 getter 和 setter。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace InverseKinematics
{
/// <summary>
/// The BoneChain class repressents a list of bones which are always connected once.
/// On the one hand you can add new bones and every bone's source is the last bone's destination
/// on the other hand you can use the cyclic coordinate descent to change the bones' positions.
/// </summary>
public class BoneChain
{
/// <summary>
/// The last bone that were created
/// </summary>
private Bone lastBone;
/// <summary>
/// All the concatenated bones
/// </summary>
private List<Bone> bones;
/// <summary>
/// Creates an empty bone chain
/// Added Bones will be affected by inverse kinematics
/// </summary>
public BoneChain()
{
this.bones = new List<Bone>();
}
BoneChain 类表示一个始终连接在一起的骨骼列表。一方面,您可以添加新的骨骼,每个骨骼的来源是最后一个骨骼的目的地;另一方面,您可以使用循环坐标下降来更改骨骼的位置。该类使用一个包含骨骼及其坐标的列表。该类有两种模式。第一种是创建模式,在这种模式下,一个骨骼在另一个骨骼之后创建,它们保持连接。另一种模式是 CCD(将在下面进一步描述)。
/// <summary>
/// Draws all the bones in this chain
/// </summary>
/// <param name="device"></param>
public void Draw(GraphicsDevice device)
{
foreach (Bone bone in bones) bone.Draw(device);
}
/// <summary>
/// Creates a bone
/// Every bone's destination is the next bone's source
/// </summary>
/// <param name="v">the bone's destination</param>
/// <param name="click">if true it sets the bone with its coordinate and adds the next bone</param>
public void CreateBone(Vector3 v, bool click)
{
if (click)
{
//if it is the first bone it will create the bone's source at the destination point
//so it need not to start at the coordinates(0/0/0)
if (bones.Count == 0)
{
lastBone = new Bone(v, Vector3.Zero);
bones.Add(lastBone);
}
else
{
Bone temp = new Bone(lastBone, v);
bones.Add(temp);
lastBone = temp;
}
}
if (lastBone != null)
{
lastBone.SetLocalDestinationbyAWorldDestination(v);
}
}
这是创建骨骼的方法(创建模式)
/// <summary>
/// The Cyclic Coordinate Descent
/// </summary>
/// <param name="destination">Where the bones should be adjusted</param>
/// <param name="gameTime"></param>
public void CalculateCCD(Vector3 destination, GameTime gameTime)
{
// iterating the bones reverse
int index = bones.Count - 1;
while (index >= 0)
{
//getting the vector between the new destination and the joint's world position
Vector3 jointWorldPositionToDestination = destination - bones.ElementAt(index).WorldCoordinate;
//getting the vector between the end effector and the joint's world position
Vector3 boneWorldToEndEffector = bones.Last().Effector - bones.ElementAt(index).WorldCoordinate;
//calculate the rotation axis which is the cross product of the destination
Vector3 cross = Vector3.Cross(jointWorldPositionToDestination, boneWorldToEndEffector);
//normalizing that rotation axis
cross.Normalize();
//check if there occured divisions by 0
if (float.IsNaN(cross.X) || float.IsNaN(cross.Y) || float.IsNaN(cross.Z))
//take a temporary vector
cross = Vector3.UnitZ;
// calculate the angle between jointWorldPositionToDestination and boneWorldToEndEffector
// in regard of the rotation axis
float angle = CalculateAngle(jointWorldPositionToDestination, boneWorldToEndEffector, cross);
if (float.IsNaN(angle)) angle = 0;
//create a matrix for the roation of this bone's destination
Matrix m = Matrix.CreateFromAxisAngle(cross, angle);
// rotate the destination
bones.ElementAt(index).Destination = Vector3.Transform(bones.ElementAt(index).Destination, m);
// update all bones which are affected by this bone
UpdateBones(index);
index--;
}
}
这是 CCD 算法的一种可能版本。
/// <summary>
/// While CalculateCCD changes the destinations of all the bones,
/// every affected adjacent bone's WorldCoordinate must be updated to keep the bone chain together.
/// </summary>
/// <param name="index">when the bones should updated, because CalculateCCD changed their destinations</param>
private void UpdateBones(int index)
{
for (int j = index; j < bones.Count - 1; j++)
{
bones.ElementAt(j + 1).WorldCoordinate = (bones.ElementAt(j).Effector);
}
}
/// <summary>
/// Updates all the representation parameters for every bone
/// including orienations and positionsin this bonechain
/// </summary>
public void Update()
{
foreach (Bone bone in bones) bone.Update();
}
/// <summary>
/// This function calculates an angle between two vectors
/// the cross product which is orthogonal to the two vectors is the most common orientation vector
/// for specifing the angle's direction.
/// </summary>
/// <param name="v0">the first vector </param>
/// <param name="v1">the second vector </param>
/// <param name="crossProductOfV0andV1">the cross product of the first and second vector </param>
/// <returns>the angle between the two vectors in radians</returns>
private float CalculateAngle(Vector3 v0, Vector3 v1, Vector3 crossProductOfV0andV1)
{
Vector3 n0 = Vector3.Normalize(v0);
Vector3 n1 = Vector3.Normalize(v1);
Vector3 NCross = Vector3.Cross(n1, n0);
NCross.Normalize();
float NDot = Vector3.Dot(n0, n1);
if (float.IsNaN(NDot)) NDot = 0;
if (NDot > 1) NDot = 1;
if (NDot < -1) NDot = -1;
float a = (float)Math.Acos(NDot);
if ((n0 + n1).Length() < 0.01f) return (float)Math.PI;
return Vector3.Dot(NCross, crossProductOfV0andV1) >= 0 ? a : -a;
}
}
}
整个项目可以从 这里 下载
Nexus' Child
- ↑ a b c d Samuel R. Buss: 雅可比转置、伪逆和阻尼最小二乘法的逆运动学入门.
- ↑ a b Steve Rotenberg: 逆运动学 (第一部分)
- ↑ Mike Tabaczynski: 雅可比解逆运动学问题
- ↑ Jeff Rotenberg: 逆运动学 (第二部分)
- ↑ Jeff Lander: 让 Kine 更灵活