使用 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 更靈活