跳至内容

使用 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 更灵活
华夏公益教科书