super1 发表于 2022-7-25 16:26

Unity3d FootIK Better Hybrid IK(4)

前言:

正如上一节所述,IK问题实际上是可以分成“IK Solver”与“IK Goal Solver”两个问题的。
现在,我们手上的“IK Solver”,有:

[*]Unity Animator 内置的IK系统——Humanoid OnAnimatorIK
[*]AnimationRigging 的两个IK解算器【SolveTwoBoneIK】、【SolveFABRIK】
[*]Final IK的IK Solver,比如Biped IK
我们现有的“IK Goal Solver”解决方案,有:

[*]RayCast + AnimationCurve 的简单实现
[*]Final IK 的基于速度预测
[*]假设我们实现了《刺客信条》的基于路径预测的方案
那么,最终的解决方案,是否可以通过上面的两两排列组合而来呢?答案自然是可以的。
一、 OnAnimatorIK with Velocity Prediction

花活儿就不弄了,我们就简单地依赖于【Unity Animator】的内置IK系统,使用【Final IK】基于速度预测的算法,来实现一版杂交解决方案。
由于算法99%是【Final IK】的,别人又是商业付费插件,以下源码权当学习使用(应该不会侵权吧)。
1. Leg Solver

出于分离【IK Solver】与【IK Goal Solver】的想法,我们定义一个数据交互的接口【IKGoalSolverInterface】便于其他的“IK Solver”快速访问数据。
using UnityEngine;

public interface IKGoalSolverInterface
{
    public Vector3 IKOffset { get; } //计算出的IK Goal 位置偏移量
    public Vector3 IKPosition { get; }//计算出的IK Goal 最终位置
    public Quaternion RotationOffset { get; } //计算出的 IK Goal 旋转偏移
}
当然,现阶段这种写法纯粹是为了统一【Leg Solver】与【Pelvis Solver】的数据访问,存在冗余项。
然后就是定义【Leg Solver】,数据结构与算法基本参考【Final IK】的【RootMotion.FinalIK.Grounding.Leg】
using UnityEngine;

namespace MyHybridIK
{
    public class MyLegSolver : IKGoalSolverInterface
    {
      public Vector3 IKPosition { get; private set; }
      public Vector3 IKOffset => Vector3.zero;
      public Quaternion RotationOffset { get; private set; }

      public Transform footTransform;
      public bool isGrounded;
      public bool invertFootCenter = false;
      public float m_IKOffset { get; private set; }
      //Ray Cast Info
      public RaycastHit heelHit { get; private set; }
      public RaycastHit capsuleHit { get; private set; }

      private MyGrounding grounding;
      private Vector3 up;
      private float lastTime, deltaTime;
      private Vector3 lastPosition, transformPosition;
      private float heightFromGround;
      private Vector3 velocity;

      //IK Calculation Paras
      private Quaternion toHitNormal, r;
      //IK   Infos
      private bool initiated;

      private Vector3 capsuleStart;
      private Vector3 capsuleEnd;

      /// <summary>
      /// 初始化GroundSolver
      /// </summary>
      /// <param name="grounding">Ground参数对象</param>
      /// <param name="transform">目标足部骨骼的Transform</param>
      public void Initiate(MyGrounding grounding, Transform transform)
      {
            this.grounding = grounding;
            this.footTransform = transform;
            up = Vector3.up;
            IKPosition = transform.position;
            RotationOffset = Quaternion.identity;

            initiated = true;

            lastPosition = transform.position;
            lastTime = Time.deltaTime;
      }
      public void Process()
      {
            if (!initiated) return;
            if (grounding.maxStep <= 0) return;

            //Initiate Position;
            transformPosition = footTransform.position;

            //Time Calculate
            deltaTime = Time.time - lastTime;
            lastTime = Time.time;
            if (deltaTime == 0f) return;

            up = grounding.up;
            heightFromGround = Mathf.Infinity;

            // Calculating velocity
            velocity = (transformPosition - lastPosition) / deltaTime;
            lastPosition = transformPosition;

            var prediction = velocity * grounding.prediction;

            //质量区间,暂时不考虑
            //if (grounding.footRadius <= 0) grounding.quality = Grounding.Quality.Fastest;

            isGrounded = false;

            //Best Quality Cast
            {
                heelHit = GetRaycastHit(invertFootCenter ? -grounding.GetFootCenterOffset() : Vector3.zero);
                capsuleHit = GetCapsuleHit(prediction);

                if (heelHit.collider != null || capsuleHit.collider != null) isGrounded = true;

                SetFootToPlane(capsuleHit.normal, capsuleHit.point, heelHit.point);
            }

            float offsetTarget = stepHeightFromGround;
            if (!grounding.rootGrounded) offsetTarget = 0f;

            m_IKOffset = RootMotion.Interp.LerpValue(m_IKOffset, offsetTarget, grounding.footSpeed, grounding.footSpeed);
            m_IKOffset = Mathf.Lerp(m_IKOffset, offsetTarget, deltaTime * grounding.footSpeed);

            float legHeight = grounding.GetVerticalOffset(transformPosition, grounding.root.position);
            float currentMaxOffset = Mathf.Clamp(grounding.maxStep - legHeight, 0f, grounding.maxStep);

            m_IKOffset = Mathf.Clamp(m_IKOffset, -currentMaxOffset, m_IKOffset);

            RotateFoot();

            IKPosition = transformPosition - up * m_IKOffset;
            float rW = grounding.footRotationWeight;
            RotationOffset = rW >= 1 ? r : Quaternion.Slerp(Quaternion.identity, r, rW);
      }
      private RaycastHit GetRaycastHit(Vector3 offsetFromHeel)
      {
            RaycastHit hit = new RaycastHit();
            Vector3 origin = transformPosition + offsetFromHeel;

            if (grounding.overstepFallsDown)
            {
                hit.point = origin - up * grounding.maxStep;
            }
            else
            {
                hit.point = new Vector3(origin.x, grounding.root.position.y, origin.z);
            }
            hit.normal = up;

            if (grounding.maxStep <= 0f) return hit;

            Physics.Raycast(origin + grounding.maxStep * up, -up, out hit, grounding.maxStep * 2, grounding.layerMask, QueryTriggerInteraction.Ignore);

            // Since Unity2017 Raycasts will return Vector3.zero when starting from inside a collider
            if (hit.point == Vector3.zero && hit.normal == Vector3.zero)
            {
                if (grounding.overstepFallsDown)
                {
                  hit.point = origin - up * grounding.maxStep;
                }
                else
                {
                  hit.point = new Vector3(origin.x, grounding.root.position.y, origin.z);
                }
            }

            return hit;
      }
      private RaycastHit GetCapsuleHit(Vector3 offsetFromHeel)
      {
            RaycastHit hit = new RaycastHit();
            Vector3 f = grounding.GetFootCenterOffset();
            if (invertFootCenter) f = -f;
            Vector3 origin = transformPosition + f;

            if (grounding.overstepFallsDown)
            {
                hit.point = origin - up * grounding.maxStep;
            }
            else
            {
                hit.point = new Vector3(origin.x, grounding.root.position.y, origin.z);
            }
            hit.normal = up;

            // Start point of the capsule
            capsuleStart = origin + grounding.maxStep * up;
            // End point of the capsule depending on the foot's velocity.
            capsuleEnd = capsuleStart + offsetFromHeel;

            if (Physics.CapsuleCast(capsuleStart, capsuleEnd, grounding.footRadius, -up, out hit, grounding.maxStep * 2, grounding.layerMask, QueryTriggerInteraction.Ignore))
            {
                // Safeguarding from a CapsuleCast bug in Unity that might cause it to return NaN for hit.point when cast against large colliders.
                if (float.IsNaN(hit.point.x))
                {
                  hit.point = origin - up * grounding.maxStep * 2f;
                  hit.normal = up;
                }
            }

            // Since Unity2017 Raycasts will return Vector3.zero when starting from inside a collider
            if (hit.point == Vector3.zero && hit.normal == Vector3.zero)
            {
                if (grounding.overstepFallsDown)
                {
                  hit.point = origin - up * grounding.maxStep;
                }
                else
                {
                  hit.point = new Vector3(origin.x, grounding.root.position.y, origin.z);
                }
            }

            return hit;
      }
      private void SetFootToPlane(Vector3 planeNormal, Vector3 planePoint, Vector3 heelHitPoint)
      {
            planeNormal = RotateNormal(planeNormal);
            toHitNormal = Quaternion.FromToRotation(up, planeNormal);

            Vector3 pointOnPlane = RootMotion.V3Tools.LineToPlane(transformPosition + up * grounding.maxStep, -up, planeNormal, planePoint);

            // Get the height offset of the point on the plane
            heightFromGround = GetHeightFromGround(pointOnPlane);

            // Making sure the heel doesn't penetrate the ground
            float heelHeight = GetHeightFromGround(heelHitPoint);
            heightFromGround = Mathf.Clamp(heightFromGround, -Mathf.Infinity, heelHeight);
      }
      private Vector3 RotateNormal(Vector3 normal)
      {
            return normal;
            //return Vector3.RotateTowards(up, normal, grounding.maxFootRotationAngle * Mathf.Deg2Rad, deltaTime);
      }
      private float GetHeightFromGround(Vector3 hitPoint)
      {
            return grounding.GetVerticalOffset(transformPosition, hitPoint) - rootYOffset;
      }

      public float stepHeightFromGround
      {
            get
            {
                return Mathf.Clamp(heightFromGround, -grounding.maxStep, grounding.maxStep);
            }
      }
      private float rootYOffset
      {
            get
            {
                return grounding.GetVerticalOffset(transformPosition, grounding.root.position - up * grounding.heightOffset);
            }
      }
      private void RotateFoot()
      {
            // Getting the full target rotation
            Quaternion rotationOffsetTarget = GetRotationOffsetTarget();

            // Slerping the rotation offset
            r = Quaternion.Slerp(r, rotationOffsetTarget, deltaTime * grounding.footRotationSpeed);
      }
      private Quaternion GetRotationOffsetTarget()
      {
            if (grounding.maxFootRotationAngle <= 0f) return Quaternion.identity;
            if (grounding.maxFootRotationAngle >= 180f) return toHitNormal;
            return Quaternion.RotateTowards(Quaternion.identity, toHitNormal, grounding.maxFootRotationAngle);
      }
    }
}
笔者才疏学浅,暂时无法把这段代码的核心算法抽丝剥茧地阐明原理,权当一个“黑匣子”来用吧。
通过【Leg Solver】,我们能基于速度,得到一个脚部的期望平面位置与旋转偏移,他最后的的输出就是IKPosition与 RotationOffset 两个变量。
2. Pelvis Solver

由于Pelvis的计算相对简单,咱就直接Copy-Paste了,代码来自【RootMotion.FinalIK.Grounding.Pelvis】
using UnityEngine;

namespace MyHybridIK
{
    public class MyPelvisSolver : IKGoalSolverInterface
    {
      public Vector3 IKOffset { get; private set; }
      public Vector3 IKPosition => Vector3.zero;
      public Quaternion RotationOffset => Quaternion.identity;

      public float heightOffset { get; private set; }

      private MyGrounding grounding;
      private Vector3 lastRootPosition;
      private float damperF;
      private bool initiated;
      private float lastTime;
      public void Initiate(MyGrounding grounding)
      {
            this.grounding = grounding;

            initiated = true;
            OnEnable();
      }
      public void Reset()
      {
            this.lastRootPosition = grounding.root.transform.position;
            lastTime = Time.deltaTime;
            IKOffset = Vector3.zero;
            heightOffset = 0f;
      }
      public void OnEnable()
      {
            if (!initiated) return;
            this.lastRootPosition = grounding.root.transform.position;
            lastTime = Time.time;
      }
      public void Process(float lowestOffset, float highestOffset, bool isGrounded)
      {
            if (!initiated) return;

            float deltaTime = Time.time - lastTime;
            lastTime = Time.time;
            if (deltaTime <= 0f) return;

            float offsetTarget = lowestOffset + highestOffset;
            if (!grounding.rootGrounded) offsetTarget = 0f;

            // Interpolating the offset
            heightOffset = Mathf.Lerp(heightOffset, offsetTarget, deltaTime * grounding.pelvisSpeed);

            // Damper
            Vector3 rootDelta = (grounding.root.position - lastRootPosition);
            lastRootPosition = grounding.root.position;

            // Fading out damper when ungrounded
            damperF = RootMotion.Interp.LerpValue(damperF, isGrounded ? 1f : 0f, 1f, 10f);

            // Calculating the final damper
            heightOffset -= grounding.GetVerticalOffset(rootDelta, Vector3.zero) * grounding.pelvisDamper * damperF;

            // Update IK value
            IKOffset = grounding.up * heightOffset;
      }
    }
}
他最后的输出是IKOffset ,用于之后对Pelvis进行一定量的偏移。
3. Grounding

大体参考【RootMotion.FinalIK.Grounding】,这个类的作用是指定地面的一系列检测参数,计算Root的一些信息,用于提供给【Leg Solver】与【Pelvis Solver】进行运算。
唯一需要注意的是在使用时,需要手动给面板赋值LeftFootTransform与RightFootTransform,原版是通过【Biped IK】的Reference自动获取的。
using UnityEngine;

namespace MyHybridIK
{
    public class MyGrounding : MonoBehaviour
    {
      
      public Transform root;

      #region Needed Paras
      
      public Transform leftFoot;
      
      public Transform rightFoot;

      public float maxStep = 0.5f;
      public LayerMask layerMask;
      
      public Vector3 up = Vector3.up;
      public float footSpeed = 2.5f;
      
      public float footRadius = 0.15f;
      
      public float prediction = 0.05f;
      public float footCenterOffset;
      public float heightOffset;
      public bool rootGrounded => rootHit.distance < maxStep * 2f;
      
      public float footRotationWeight = 1;
      public float footRotationSpeed = 7f;
      public float maxFootRotationAngle = 45f;
      public float pelvisSpeed = 5f;
      public float pelvisDamper = 0.5f;
      public float rootSphereCastRadius = 0.1f;

      public bool isGrounded;
      public float lowerPelvisWeight = 1f;
      public float liftPelvisWeight;
      public bool overstepFallsDown = true;
      private bool initiated;
      private RaycastHit rootHit;
      #endregion


      public MyLegSolver rightLegSolver;
      public MyLegSolver leftLegSolver;
      public MyPelvisSolver pelvisSolver;

      private MyLegSolver[] legs;

      private void Start()
      {
            root = this.transform;
            Initate();
      }
      private void Initate()
      {
            rootHit = new RaycastHit();
            //构建Leg
            leftLegSolver = new MyLegSolver();
            leftLegSolver.Initiate(this, leftFoot);
            rightLegSolver = new MyLegSolver();
            rightLegSolver.Initiate(this, rightFoot);
            legs = new MyLegSolver;
            legs = leftLegSolver;
            legs = rightLegSolver;

            //构建Pelvis
            //....
            pelvisSolver = new MyPelvisSolver();
            pelvisSolver.Initiate(this);

            initiated = true;
      }
      public void GroundingUpdate()
      {
            BetterGroundingProcessing();
      }
      private void BetterGroundingProcessing()
      {
            if (!initiated) return;
            if (layerMask == 0) Debug.LogWarning("Grounding layers are set to nothing. Please add a ground layer.");

            maxStep = Mathf.Clamp(maxStep, 0f, maxStep);
            footRadius = Mathf.Clamp(footRadius, 0.0001f, maxStep);
            pelvisDamper = Mathf.Clamp(pelvisDamper, 0f, 1f);
            rootSphereCastRadius = Mathf.Clamp(rootSphereCastRadius, 0.0001f, rootSphereCastRadius);
            maxFootRotationAngle = Mathf.Clamp(maxFootRotationAngle, 0f, 90f);
            prediction = Mathf.Clamp(prediction, 0f, prediction);
            footSpeed = Mathf.Clamp(footSpeed, 0f, footSpeed);

            //Root Hit
            rootHit = GetRootHit();

            float lowestOffset = Mathf.NegativeInfinity;
            float highestOffset = Mathf.Infinity;
            isGrounded = false;
            foreach (var leg in legs)
            {
                leg.Process();
                if (leg.m_IKOffset > lowestOffset) lowestOffset = leg.m_IKOffset;
                if (leg.m_IKOffset < highestOffset) highestOffset = leg.m_IKOffset;
                if (leg.isGrounded) isGrounded = true;
            }

            // PelvisMove
            lowestOffset = Mathf.Max(lowestOffset, 0f);
            highestOffset = Mathf.Min(highestOffset, 0f);
            pelvisSolver.Process(-lowestOffset * lowerPelvisWeight, -highestOffset * liftPelvisWeight, isGrounded);
      }
      private RaycastHit GetRootHit(float maxDistanceMlp = 10f)
      {
            RaycastHit h = new RaycastHit();
            Vector3 _up = up;
            Vector3 legsCenter = Vector3.zero;

            legsCenter += leftLegSolver.footTransform.position;
            legsCenter += rightLegSolver.footTransform.position;
            legsCenter /= (float)legs.Length;

            h.point = legsCenter - _up * maxStep * 10f;
            float distMlp = maxDistanceMlp + 1;
            h.distance = maxStep * distMlp;

            if (maxStep <= 0f) return h;

            Physics.SphereCast(legsCenter + _up * maxStep, rootSphereCastRadius, -up, out h, maxStep * distMlp, layerMask, QueryTriggerInteraction.Ignore);

            return h;
      }
      public float GetVerticalOffset(Vector3 p1, Vector3 p2)
      {
            return p1.y - p2.y;
      }
      public Vector3 GetFootCenterOffset()
      {
            return root.forward * footRadius + root.forward * footCenterOffset;
      }
    }
}
方法逻辑就不一一解释了,咱也是一知半解。
4. RootMotionTool

迁移部分原【Final IK】的计算工具类代码。
namespace RootMotion {

    public class Interp
    {
      public static float LerpValue(float value, float target, float increaseSpeed, float decreaseSpeed)
      {
            if (value == target) return target;
          if (value < target) return Mathf.Clamp(value + Time.deltaTime * increaseSpeed, -Mathf.Infinity, target);
          else return Mathf.Clamp(value - Time.deltaTime * decreaseSpeed, target, Mathf.Infinity);
      }
    }

    public static class V3Tools
    {
      public static Vector3 LineToPlane(Vector3 origin, Vector3 direction, Vector3 planeNormal, Vector3 planePoint)
      {
                float dot = Vector3.Dot(planePoint - origin, planeNormal);
                float normalDot = Vector3.Dot(direction, planeNormal);
                       
                if (normalDot == 0.0f) return Vector3.zero;
                       
                float dist = dot / normalDot;
                return origin + direction.normalized * dist;
          }
    }

}
5. OnAnimatorIK

“IK Solver”层就很简单了,由于我们依赖的是OnAnimatorIK,直接使用Unity提供的接口进行IK Goal的权重,坐标的赋予就行了。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MyHybridIK;


public class BetterInnerIK : MonoBehaviour
{
    public bool EnableIK;

    public MyGrounding myGrounding;
    private Animator m_animator;
    public float pelvisWeight = 1;
    public float leftFootPositionWeight = 1;
    public float rightFootPositionWeight = 1;
    public float leftFootRotationWeight = 1;
    public float rightFootRotationWeight = 1;

    public void Start()
    {
      m_animator = GetComponent<Animator>();
    }
    private void OnAnimatorIK(int layerIndex)
    {
      if (!EnableIK)
            return;
      myGrounding.GroundingUpdate();
      PelvisProcess(myGrounding.pelvisSolver);
      LeftIKProcess(myGrounding.leftLegSolver);
      RightIKProcess(myGrounding.rightLegSolver);
    }
    private void PelvisProcess(IKGoalSolverInterface pelvisSolver)
    {
      m_animator.bodyPosition += pelvisSolver.IKOffset * pelvisWeight;
    }
    private void LeftIKProcess(IKGoalSolverInterface leftFootSolver)
    {
      m_animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, leftFootPositionWeight);
      m_animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, leftFootRotationWeight);
      var originalRotation = m_animator.GetIKRotation(AvatarIKGoal.LeftFoot);
      var finalRotation = Quaternion.Slerp(Quaternion.identity, leftFootSolver.RotationOffset, leftFootRotationWeight) * originalRotation;
      m_animator.SetIKRotation(AvatarIKGoal.LeftFoot, finalRotation);
      m_animator.SetIKPosition(AvatarIKGoal.LeftFoot, leftFootSolver.IKPosition);
    }
    private void RightIKProcess(IKGoalSolverInterface rightFootSolver)
    {
      m_animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, rightFootPositionWeight);
      m_animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, rightFootRotationWeight);
      var originalRotation = m_animator.GetIKRotation(AvatarIKGoal.RightFoot);
      var finalRotation = Quaternion.Slerp(Quaternion.identity, rightFootSolver.RotationOffset, leftFootRotationWeight) * originalRotation;
      m_animator.SetIKRotation(AvatarIKGoal.RightFoot, finalRotation);
      m_animator.SetIKPosition(AvatarIKGoal.RightFoot, rightFootSolver.IKPosition);
    }
}
至此,【IK Goal Solver】的问题使用【Final IK】进行解决,【IK Solver】的问题交由【Humanoid Animator IK】解决,我们基本上实现了一版解决方案。
二、表现测试

走一遍Unity内置IK的设置流程:把模型动画设置成Humanoid、打开Animator Layer的IK Pass、挂载脚本等,最后,我们的角色实体大约长这样:



平平无奇小脚本

跑起来试试?



Cool,表现跟Final IK 相差无几

三、性能优化

1. Timeline统计

瞅一瞅Profiler的Timeline:



IK Goal Solver的用时几乎占了整个动画用时的一半



IK Sovler应该就是这个Job,用时反而很低

就如上一节我们所得出的结论,IK Goal Solver的耗时才是最大的那一环,真是恐怖如斯。
那么【Final IK】表现如何呢?配置一个使用【Final IK】的实体,看看表现:



总体0.084ms

SolverManager的处理是包含了ReadTransform,IK Solve,WriteTransform等等流程,一个实体总计0.08ms左右的用时,感觉跟用Unity的内置IK半斤八两。
把实体数弄到10个试试。
OnAnimatorIK的表现:



10个OnAnimatorIK串行执行



算上后面计算,的大约0.35ms消耗

Final IK的表现:



10个SolverManager串行执行



大约0.52ms消耗

这就有意思了,两种方法的耗时差值达到了0.2ms,为什么会出现这种情况?
实际上,由于我们的“IK Goal Solver”的算法基本一致,这一块的耗时两者其实是差不多的。
而“IK Solver”的算法,大家都是用三角函数解析,耗时也是相差无几。
真正出现差距的地方在那儿?还记得OnAnimatorIK的后面的逻辑块吗?



顾名思义,IK Solver Job



写入Transform的Job

非常朴素的猜想就是,OnAnimatorIK只是提供“IK Goal Solver”的接口,决定好了IK Goal的旋转位置后,交由【Animators.IKAndTwistBoneJob】与【Animators.WriteJob】执行多线程统一计算、写入。而事实也正是如此:



看见那个“over 12 threads”了吗



多线程Job也显示了他就是采用了这种策略

而【Final IK】呢,那就是完全的串行计算了,算一个写一个,产生了0.2ms的差距。
2. 并行化

基于上述分析,很明显的一点,如果我们能把OnAnimator的计算使用多线程并行计算(比如JobSystem),可想而知的用时会大幅减少。
换句话说,本来就应该把复数实体的“IK Goal Solver”问题给并行化计算,因为他们的计算完全独立,相互之间不存在任何依赖。
上面那种10个Solver的串行表现看着就很傻啊。
3. 优化Physics检测

当然,这是复数个实体的情况,那么单个实体的性能怎么提升呢?
虽然从现在的表现来看,一个实体的cost顶天0.1ms的消耗,但是我们就是不满意,就是想降低,有没有什么办法呢?
在【Final IK】插件的【Grounder Biped】上,有一个“Quality”选项:



Quality = Best

把他改成“Fast”,看看对比。



使用“Fast”



使用“Best”

提升很明显,而在代码中,Fast使用“RayCast”代替了“SphereCast”,舍弃了最耗时的“CapsuleCast”,牺牲了部分准确度换来了性能提升。
4. 颗粒度细分

这一节纯属笔者臆想。
现阶段实现的FootIK,在所有的动画状态都使用一套算法。有没有一种可能,在idle静止状态时,可以不用Prediction,就简单地检测地面;在Walk/Running时,再使用Prediction算法;而在某些待机动画中,则完全禁止IK(比如《原神》的角色在进行待机动画时,是完全禁止了IK的)。
又比如玩家角色和AI角色使用不同的IK算法,又或者进行视距剔除之类的balabala。
嗯,只能说深究下去,优化方向还是蛮多的hhh。
四、小结

终于把IK的本质问题给弄懂了,也着手实现了各种IK方案(虽然核心算法是抄Final IK)的,只是没有完全弄懂Final IK的预测算法有点遗憾。
如果可以,我真希望IK的研究就此告一段落,呜呜呜。
页: [1]
查看完整版本: Unity3d FootIK Better Hybrid IK(4)