跳至內容

使用 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

}

[2]


以下方法處理選擇適當角度值的問題。

雅可比轉置法

[編輯 | 編輯原始碼]

雅可比轉置法的想法是使用方程更新角度,使用轉置而不是逆或偽逆(因為逆並不總是可能的)[1]。使用這種方法,可以透過迴圈遍歷角度直接計算角度的變化。它避免了昂貴的求逆和奇異性問題,但收斂到解的速度非常慢。這種方法的運動與物理運動非常匹配,這與其他可能導致不自然運動的逆運動學解決方案不同[3]

偽逆法

[編輯 | 編輯原始碼]

這種方法將角度值設定為雅可比的偽逆。它試圖找到一個矩陣,它有效地反轉了一個非方陣。它存在奇異性問題,這些問題傾向於某些方向不可達。問題在於該方法首先迴圈遍歷所有角度,然後需要計算和儲存雅可比矩陣,對其進行偽逆,計算角度變化,最後應用這些變化[4]

阻尼最小二乘法 (DLS)

[編輯 | 編輯原始碼]

這種方法避免了偽逆法中的一些問題。它找到最小化數量的角度值,而不僅僅是找到最小向量。必須仔細選擇阻尼常數,以使方程穩定[1]

選擇性阻尼最小二乘法 (SDLS)

[編輯 | 編輯原始碼]

這種方法是對 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

參考文獻

[編輯 | 編輯原始碼]
  1. a b c d Samuel R. Buss: 雅可比轉置、偽逆和阻尼最小二乘法的逆運動學入門.
  2. a b Steve Rotenberg: 逆運動學 (第一部分)
  3. Mike Tabaczynski: 雅可比解逆運動學問題
  4. Jeff Rotenberg: 逆運動學 (第二部分)
  5. Jeff Lander: 讓 Kine 更靈活
華夏公益教科書