算法—插值计算(二)——球形插值

上一篇博客介绍了线性插值 https://blog.51cto.com/myselfdream/2551546
本篇介绍球形差值:
所为球形插值就是物体运动的轨迹为弧线运动,最常见的就会射击类中子弹的运行轨迹。
算法—插值计算(二)——球形插值
例如:t1到t2的运动过程,Unity实现的传统方式

第一种方式

    using UnityEngine;  
    using System.Collections;  

    public class MoveCurve : MonoBehaviour {  

        public GameObject t1;    //开始位置  
        public GameObject t2;     //结束位置  
      // Update is called once per frame  
        void Update () {      
            //两者中心点  
            Vector3 center = (t1.transform .position + t2.transform.position) * 0.5f;     
            center -= new Vector3(0, 1, 0);   
         Vector3 start = t1.transform.position - center;  
            Vector3 end = t2.transform.position - center;     
            //弧形插值  
            transform.position = Vector3.Slerp(start,end,Time.time);  
            transform.position += center;          
       }  
    }  

第二种方式

    using UnityEngine;  
    using System.Collections;  

    public class ProjectileTest : MonoBehaviour  
    {  
        public GameObject target; //要到达的目标  

        public float speed = 10;  //速度  

        private float distanceToTarget;   //两者之间的距离  
        private bool move = true;         
        void Start()  
        {  
            //计算两者之间的距离  
            distanceToTarget = Vector3.Distance(this.transform.position, target.transform.position);    
            StartCoroutine(StartShoot());  
        }  

        IEnumerator StartShoot()  
        {  

            while (move)  
            {  
                Vector3 targetPos = target.transform.position;  

                //让始终它朝着目标  
                this.transform.LookAt(targetPos);  

                //计算弧线中的夹角  
                float angle = Mathf.Min(1, Vector3.Distance(this.transform.position,    targetPos) / distanceToTarget) * 45;  
                            this.transform.rotation = this.transform.rotation *     Quaternion.Euler(Mathf.Clamp(-angle, -42, 42), 0, 0);     
                            float currentDist = Vector3.Distance(this.transform.position, target.transform.position);  
                            if (currentDist < 0.5f)  
                                move = false;  

                         this.transform.Translate(Vector3.forward * Mathf.Min(speed * Time.deltaTime, currentDist));  
                         yield return null;  
             }  
        }     
    } 

球形插值的函数

球形插值的原理解释
static function Vector3 Slerp (Vector3 from, Vector3to, float t)
描述球形插值在两个向量之间。我感觉叫“弧线插值”更直观一些。
通过t数值在from和to之间插值。返回的向量的长度将被插值到from到to的长度之间。
API例子:

using UnityEngine;
using System.Collections;

public class example : MonoBehaviour
{
        public Transform sunrise;
        public Transform sunset;
        void Update()
        {
                //弧线的中心
                Vector3 center = (sunrise.position + sunset.position) * 0.5f;
                //向下移动中心,垂直于弧线
                center -= new Vector3(0, 1, 0);
                //相对于中心在弧线上插值
                Vector3 riseRelCenter = sunrise.position - center;
                Vector3 setRelCenter = sunset.position - center;
                transform.position = Vector3.Slerp(riseRelCenter,setRelCenter, Time.time);
                transform.position += center;
        }
}

对于Slerp函数详细解释:
初识这个函数的同学们对这个函数都不是很理解,既然是球形插值了,那么为什么用这个函数的时候却这么复杂呢,又要找中心点,又要中心点偏移的弄了半天。其实这是从这个函数的实现方法所决定的。
首先定义两个向量 a(2,1,0); b(-2,1,0); 然后咱们以这两个坐标点和原点来构建一个三角形,如下图
算法—插值计算(二)——球形插值

咱们以a和b两个向量来做插值,假设分10等份,代码比较简单,如下代码

for (int i = 1; i < 10; ++i)
{
      Vector3 drawVec = Vector3.Slerp(a, b, 0.1f * i);
      Debug.DrawLine(Vector3.zero, drawVec, Color.yellow);
}

结果:
算法—插值计算(二)——球形插值
但是,不能被表象所欺骗,这样的效果虽然可以,但是却无法控制插值的曲线,也就是那个弧度,虽然你可以调整向量a和向量b的值来调节弧度,比方说a(1,1,0),b(-1,1,0)效果如下,可以看到弧度已经明显变平很多。
算法—插值计算(二)——球形插值
但是在实际运用这个函数的时候往往向量a和向量b是固定的,我们想要的是控制这个弧度,那么怎么办呢,其实也就是改变画这个弧度的中心点位置。上面两个示意图上面中心点我们都是用的坐标原点,我们现在想要在不改变a和a的情况下来改变插值的弧度,就只能自己找出一个中心点,这也就是官方实例中求中心点的由来了。

       //弧线的中心
        Vector3 center = (a + b) * 0.5f;
        //我们把中心点向下移动中心,垂直于弧线
        center -= new Vector3(0, 0.5f, 0);
        // 求出新的中心点到向量a和向量b的
        Vector3 vecA = a - center;
        Vector3 vecB = b - center;
        for (int i = 0; i <= 10; ++i)
        {
            Vector3 drawVec = Vector3.Slerp(vecA, vecB, 0.1f * i);
            Debug.DrawLine(center, drawVec, Color.yellow);
        }

求出中心点后我们再来画一个示意图看看
算法—插值计算(二)——球形插值


(至于为什么要 求出新的中心点到向量a和向量b的vecA和vecB是因为,我们在球形插值的时候要的是两个vector3,而这个vector3是要向量a和向量b到中心点的向量,如果我们不求出vecA和vecB的话不论你怎么插值,其实都是从坐标原点进行的插值,你是控制不了插值的弧度的。)


从上面的效果图我们可以看到插值出来的弧度开始和结束点并不是a、b两点,而是这两个点向下的偏移量,而这个偏移量正好是向量conter的负值,所以我们在求出drawVec之后需要对其做修正处理。

在求出drawVec之后加上下面的代码

drawVec += center;

然后再看效果图
算法—插值计算(二)——球形插值
现在的效果图就是我们想要的插值效果了,要想控制弧度,只用调节centor的偏移量就可以了。

比如:加上这样一条center -= new Vector3(0, 2f, 0)的效果如下
算法—插值计算(二)——球形插值

重点来了,上面咱们介绍的都是有局限性的,比方说向量a和向量b对于Y轴可是左右对称的,并且X,Y轴的值也是相等的,这在实际运用中可是非常不常见的,现在咱们对向量a做一个比较小的改动看看,把向量a改为(2,1,0),那么效果如下所示
算法—插值计算(二)——球形插值
居然缓释好好的球形差值,把向量a的Y轴也调整一下看看,把a改为(2,4,0)效果如下图所示
算法—插值计算(二)——球形插值
这个球形插值和核心其实就是center点的位置,只要我们求的这个center点的位置在a和b连线中心点的垂线上面,那么就是一个完整的左右对称的插值了。
加入如下代码:

     Vector3 centorProject = Vector3.Project(centor, mStart - mEnd); // 中心点在两点之间的投影
     centor = Vector3.MoveTowards(centor, centorProject, 1f); // 沿着投影方向移动移动距离(距离越大弧度越小)

效果如下所示(中心的垂线和起始两条线用蓝色标示出来了)
算法—插值计算(二)——球形插值

所以综上所述,完整的运用Vector3.Slerp的代码应该是:

using UnityEngine;
using System.Collections;
public class example : MonoBehaviour
{
        public Transform sunrise;
        public Transform sunset;
        void Update()
        {
                //弧线的中心
                Vector3 center = sunrise.position + sunset.position * 0.5f;
                Vector3 centorProject = Vector3.Project(centor, sunrise.position - sunset.position); // 中心点在两点之间的投影
                centor = Vector3.MoveTowards(centor, centorProject, 1f); // 沿着投影方向移动移动距离(距离越大弧度越小)               
                //相对于中心在弧线上插值
                Vector3 riseRelCenter = sunrise.position - center;
                Vector3 setRelCenter = sunset.position - center;
                transform.position = Vector3.Slerp(riseRelCenter, setRelCenter, Time.time);
                transform.position += center;
        }
}

会发现上面的实例Vector3的Z轴是0,在对a和b的Z轴做修改。
a((2, 4, -1)),b((-1, 1, 2)),看下效果图:

算法—插值计算(二)——球形插值

这是因为我们锁定成2D视角方向来看的,那么既然Z轴有值了我们就不能已平面视角来看了,等我们把2D锁定给关闭了转换个视角看看。
算法—插值计算(二)——球形插值

移动代码

    centor = Vector3.MoveTowards(centor, centorProject, 1f); // 沿着投影方向移动移动距离(距离越大弧度越小)

在这句代码中我写了个注释(距离越大弧度越小),这个距离的小是相对的,尽量是不能小于0.01的,至于为啥是0.01呢,我就试了下0.01和0.001。如果这个距离小于0.01的话插值的方向就是不可控了,至于为什么呢?因为过于小的话就是a到b两点之间的一条线了,垂直于一条线的平面可是海里去了,谁知道在哪里呢。所以大家在用的时候切记这点啊,不能过于追求极限导致结果不可控。还有上面我说的是距离啊,是距离,不是值,这个值是可以为负值的啊,负值的话插值的弧线就在这边了,转个向而已。

测试源码:

private Vector3 mStart = new Vector3(2, 4, -1);
    private Vector3 mEnd = new Vector3(-1, 1, 2);
    // Update is called once per frame
    private void Update()
    {
        Debug.DrawLine(new Vector3(-100, 0, 0), new Vector3(100, 0, 0), Color.green);
        Debug.DrawLine(new Vector3(0, -100, 0), new Vector3(0, 100, 0), Color.green);
        Debug.DrawLine(Vector3.zero, mStart, Color.red);
        Debug.DrawLine(Vector3.zero, mEnd, Color.red);
        Debug.DrawLine(mStart, mEnd, Color.red);
        Vector3 centor = (mStart + mEnd) * 0.5f;
        Vector3 centorProject = Vector3.Project(centor, mStart - mEnd); // 中心点在两点之间的投影
        centor = Vector3.MoveTowards(centor, centorProject, 1f);        // 沿着投影方向移动移动距离(距离越大弧度越小)
        Debug.DrawLine(centor, mStart, Color.blue);
        Debug.DrawLine(centor, mEnd, Color.blue); 
        Debug.Log(string.Format("{0} : {1}", Vector3.Distance(centor, mStart), Vector3.Distance(centor, mEnd)));
        for (int i = 1; i < 10; ++i)
        {
            Vector3 drawVec = Vector3.Slerp(mEnd - centor, mStart - centor, 0.1f * i);
            drawVec += centor;
            Debug.DrawLine(centor, drawVec, 5 == i ? Color.blue : Color.yellow);
         }
    }

猜你喜欢

转载自blog.51cto.com/myselfdream/2552795