团结加速

Dan*_*Dan 10 c# interpolation unity-game-engine catmull-rom-curve

我试图在Unity中模拟加速和减速.

我已编写代码以在Unity中生成轨道,并根据时间将对象放置在轨道上的特定位置.结果看起来有点像这样.

立方体通过Catmull-Rom Spline中途

我现在有问题的是,花键的各个部分是不同的长度和不同的在每个部分中的立方体的动作,但均匀,速度.这导致在切片之间转换时立方体的速度突然跳跃.

为了尝试解决这个问题,我试图在方法上使用Robert Penner的缓动方程GetTime(Vector3 p0, Vector3 p1, float alpha).然而,虽然这确实有所帮助,但这还不够.在转换之间仍然存在速度跳跃.

有没有人对如何动态地缓解立方体的位置以使其看起来像加速和减速,没有轨道段之间的速度没有大的跳跃有任何想法?


我编写了一个脚本,显示了我的代码的简单实现.它可以附加到任何游戏对象.为了便于查看代码运行时发生的情况,请附加到多维数据集或球体之类的内容.

using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class InterpolationExample : MonoBehaviour {
    [Header("Time")]
    [SerializeField]
    private float currentTime;
    private float lastTime = 0;
    [SerializeField]
    private float timeModifier = 1;
    [SerializeField]
    private bool running = true;
    private bool runningBuffer = true;

    [Header("Track Settings")]
    [SerializeField]
    [Range(0, 1)]
    private float catmullRomAlpha = 0.5f;
    [SerializeField]
    private List<SimpleWayPoint> wayPoints = new List<SimpleWayPoint>
    {
        new SimpleWayPoint() {pos = new Vector3(-4.07f, 0, 6.5f), time = 0},
        new SimpleWayPoint() {pos = new Vector3(-2.13f, 3.18f, 6.39f), time = 1},
        new SimpleWayPoint() {pos = new Vector3(-1.14f, 0, 4.55f), time = 6},
        new SimpleWayPoint() {pos = new Vector3(0.07f, -1.45f, 6.5f), time = 7},
        new SimpleWayPoint() {pos = new Vector3(1.55f, 0, 3.86f), time = 7.2f},
        new SimpleWayPoint() {pos = new Vector3(4.94f, 2.03f, 6.5f), time = 10}
    };

    [Header("Debug")]
    [Header("WayPoints")]
    [SerializeField]
    private bool debugWayPoints = true;
    [SerializeField]
    private WayPointDebugType debugWayPointType = WayPointDebugType.SOLID;
    [SerializeField]
    private float debugWayPointSize = 0.2f;
    [SerializeField]
    private Color debugWayPointColour = Color.green;
    [Header("Track")]
    [SerializeField]
    private bool debugTrack = true;
    [SerializeField]
    [Range(0, 1)]
    private float debugTrackResolution = 0.04f;
    [SerializeField]
    private Color debugTrackColour = Color.red;

    [System.Serializable]
    private class SimpleWayPoint
    {
        public Vector3 pos;
        public float time;
    }

    [System.Serializable]
    private enum WayPointDebugType
    {
        SOLID,
        WIRE
    }

    private void Start()
    {
        wayPoints.Sort((x, y) => x.time.CompareTo(y.time));
        wayPoints.Insert(0, wayPoints[0]);
        wayPoints.Add(wayPoints[wayPoints.Count - 1]);
    }

    private void LateUpdate()
    {
        //This means that if currentTime is paused, then resumed, there is not a big jump in time
        if(runningBuffer != running)
        {
            runningBuffer = running;
            lastTime = Time.time;
        }

        if(running)
        {
            currentTime += (Time.time - lastTime) * timeModifier;
            lastTime = Time.time;
            if(currentTime > wayPoints[wayPoints.Count - 1].time)
            {
                currentTime = 0;
            }
        }
        transform.position = GetPosition(currentTime);
    }

    #region Catmull-Rom Math
    public Vector3 GetPosition(float time)
    {
        //Check if before first waypoint
        if(time <= wayPoints[0].time)
        {
            return wayPoints[0].pos;
        }
        //Check if after last waypoint
        else if(time >= wayPoints[wayPoints.Count - 1].time)
        {
            return wayPoints[wayPoints.Count - 1].pos;
        }

        //Check time boundaries - Find the nearest WayPoint your object has passed
        float minTime = -1;
        float maxTime = -1;
        int minIndex = -1;
        for(int i = 1; i < wayPoints.Count; i++)
        {
            if(time > wayPoints[i - 1].time && time <= wayPoints[i].time)
            {
                maxTime = wayPoints[i].time;
                int index = i - 1;
                minTime = wayPoints[index].time;
                minIndex = index;
            }
        }

        float timeDiff = maxTime - minTime;
        float percentageThroughSegment = 1 - ((maxTime - time) / timeDiff);

        //Define the 4 points required to make a Catmull-Rom spline
        Vector3 p0 = wayPoints[ClampListPos(minIndex - 1)].pos;
        Vector3 p1 = wayPoints[minIndex].pos;
        Vector3 p2 = wayPoints[ClampListPos(minIndex + 1)].pos;
        Vector3 p3 = wayPoints[ClampListPos(minIndex + 2)].pos;

        return GetCatmullRomPosition(percentageThroughSegment, p0, p1, p2, p3, catmullRomAlpha);
    }

    //Prevent Index Out of Array Bounds
    private int ClampListPos(int pos)
    {
        if(pos < 0)
        {
            pos = wayPoints.Count - 1;
        }

        if(pos > wayPoints.Count)
        {
            pos = 1;
        }
        else if(pos > wayPoints.Count - 1)
        {
            pos = 0;
        }

        return pos;
    }

    //Math behind the Catmull-Rom curve. See here for a good explanation of how it works. https://stackoverflow.com/a/23980479/4601149
    private Vector3 GetCatmullRomPosition(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float alpha)
    {
        float dt0 = GetTime(p0, p1, alpha);
        float dt1 = GetTime(p1, p2, alpha);
        float dt2 = GetTime(p2, p3, alpha);

        Vector3 t1 = ((p1 - p0) / dt0) - ((p2 - p0) / (dt0 + dt1)) + ((p2 - p1) / dt1);
        Vector3 t2 = ((p2 - p1) / dt1) - ((p3 - p1) / (dt1 + dt2)) + ((p3 - p2) / dt2);

        t1 *= dt1;
        t2 *= dt1;

        Vector3 c0 = p1;
        Vector3 c1 = t1;
        Vector3 c2 = (3 * p2) - (3 * p1) - (2 * t1) - t2;
        Vector3 c3 = (2 * p1) - (2 * p2) + t1 + t2;
        Vector3 pos = CalculatePosition(t, c0, c1, c2, c3);

        return pos;
    }

    private float GetTime(Vector3 p0, Vector3 p1, float alpha)
    {
        if(p0 == p1)
            return 1;
        return Mathf.Pow((p1 - p0).sqrMagnitude, 0.5f * alpha);
    }

    private Vector3 CalculatePosition(float t, Vector3 c0, Vector3 c1, Vector3 c2, Vector3 c3)
    {
        float t2 = t * t;
        float t3 = t2 * t;
        return c0 + c1 * t + c2 * t2 + c3 * t3;
    }

    //Utility method for drawing the track
    private void DisplayCatmullRomSpline(int pos, float resolution)
    {
        Vector3 p0 = wayPoints[ClampListPos(pos - 1)].pos;
        Vector3 p1 = wayPoints[pos].pos;
        Vector3 p2 = wayPoints[ClampListPos(pos + 1)].pos;
        Vector3 p3 = wayPoints[ClampListPos(pos + 2)].pos;

        Vector3 lastPos = p1;
        int maxLoopCount = Mathf.FloorToInt(1f / resolution);

        for(int i = 1; i <= maxLoopCount; i++)
        {
            float t = i * resolution;
            Vector3 newPos = GetCatmullRomPosition(t, p0, p1, p2, p3, catmullRomAlpha);
            Gizmos.DrawLine(lastPos, newPos);
            lastPos = newPos;
        }
    }
    #endregion

    private void OnDrawGizmos()
    {
        #if UNITY_EDITOR
        if(EditorApplication.isPlaying)
        {
            if(debugWayPoints)
            {
                Gizmos.color = debugWayPointColour;
                foreach(SimpleWayPoint s in wayPoints)
                {
                    if(debugWayPointType == WayPointDebugType.SOLID)
                    {
                        Gizmos.DrawSphere(s.pos, debugWayPointSize);
                    }
                    else if(debugWayPointType == WayPointDebugType.WIRE)
                    {
                        Gizmos.DrawWireSphere(s.pos, debugWayPointSize);
                    }
                }
            }

            if(debugTrack)
            {
                Gizmos.color = debugTrackColour;
                if(wayPoints.Count >= 2)
                {
                    for(int i = 0; i < wayPoints.Count; i++)
                    {
                        if(i == 0 || i == wayPoints.Count - 2 || i == wayPoints.Count - 1)
                        {
                            continue;
                        }

                        DisplayCatmullRomSpline(i, debugTrackResolution);
                    }
                }
            }
        }
        #endif
    }
}
Run Code Online (Sandbox Code Playgroud)

Rod*_*ues 5

好的,让我们对此进行一些数学运算

我一直并提倡数学在游戏开发中的重要性和实用性,也许我在这个答案上走得太远了,但我真的认为你的问题根本不是关于编码,而是关于建模和解决代数问题. 不管怎样,我们走吧。

参数化

如果你有大学学位,你可能会记得一些关于函数的东西- 接受一个参数并产生结果的操作 - 和图形- 函数相对于其参数的演变的图形表示(或图)。f(x)可能会提醒你一些事情:它说一个名为的函数f取决于 参数x。因此,“参数化”大致意味着用一个或多个参数来表达系统。

您可能不熟悉这些条款,但您​​一直都在这样做。你的Track,例如,是用3个参数的系统:f(x,y,z)

关于参数化的一件有趣的事情是,您可以抓取一个系统并根据其他参数对其进行描述。同样,您已经在这样做了。当您描述轨迹随时间的演变时,您是在说每个坐标都是时间的函数f(x,y,z) = f(x(t),y(t),z(t)) = f(t)。换句话说,您可以使用时间来计算每个坐标,并使用坐标在给定时间内将对象定位在空间中。

对轨道系统建模

最后,我开始回答你的问题。为了完整描述您想要的 Track 系统,您需要做两件事:

  1. 路径;

您实际上已经解决了这一部分。您在场景空间中设置一些点,并使用Catmull-Rom 样条线对这些点进行插值并生成路径。这很聪明,没有什么可做的了。

此外,您time在每个点上添加了一个字段,因此您希望确保移动对象将在这个确切时间通过此检查。稍后我会回到这个话题。

  1. 一个移动的物体。

关于您的 Path 解决方案的一件有趣的事情是您使用一个参数对路径计算进行了percentageThroughSegment参数化 - 一个范围从 0 到 1 的值,表示线段内的相对位置。在您的代码中,您以固定的时间步长进行迭代,并且您percentageThroughSegment将花费的时间与该段的总时间跨度之间的比例。由于每个段都有特定的时间跨度,因此您可以模拟许多恒定速度。

这是非常标准的,但有一个微妙之处。你忽略了描述运动的一个非常重要的部分:行进距离

我建议你采用不同的方法。使用行进的距离来参数化您的路径。然后,对象的运动将是相对于时间参数化的行进距离。这样,您将拥有两个独立且一致的系统。动手干活!

例子:

从现在开始,为了简单起见,我会将所有内容都制作为 2D,但稍后将其更改为 3D 将是微不足道的。

考虑以下路径:

示例路径

其中i是线段的索引,d是行进的距离,x, y是平面中的坐标。这可能是由像您这样的样条曲线或贝塞尔曲线或其他任何东西创建的路径。

使用当前解决方案的对象开发的运动可以描述为这样的distance traveled on the pathvs图表time

运动1图

t在该表是对象必须到达检查时间,d再次前往到这个位置的距离,v是速度,a是加速度。

上图显示了物体如何随时间前进。横轴是时间,纵轴是行驶的距离。我们可以想象,纵轴是在一条平线上“展开”的路径。下图是速度随时间的演变。

在这一点上,我们必须回忆一些物理学,并注意到,在每一段,距离图是一条直线,对应于匀速运动,没有加速度。这样的系统由以下等式描述:d = do + v*t

动作 1 动画

每当对象到达检查点时,它的速度值就会突然改变(因为它的图形没有连续性)并且在场景中产生奇怪的效果。是的,您已经知道这一点,这正是您发布问题的原因。

好的,我们怎样才能做得更好?嗯...如果速度图是连续的,就不会是那么烦人的速度跳跃。对这种运动的最简单描述可能是均匀加速。这样的系统由以下等式描述:d = do + vo*t + a*t^2/2。我们还必须假设初始速度,我将在这里选择零(与静止分开)。

在此处输入图片说明

正如我们预期的那样,速度图是连续的,运动通过路径加速。这可以编码到 Unity 中,改变方法StartGetPosition如下所示:

private List<float> lengths = new List<float>();
private List<float> speeds = new List<float>();
private List<float> accels = new List<float>();
public float spdInit = 0;

private void Start()
{
  wayPoints.Sort((x, y) => x.time.CompareTo(y.time));
  wayPoints.Insert(0, wayPoints[0]);
  wayPoints.Add(wayPoints[wayPoints.Count - 1]);
       for (int seg = 1; seg < wayPoints.Count - 2; seg++)
  {
    Vector3 p0 = wayPoints[seg - 1].pos;
    Vector3 p1 = wayPoints[seg].pos;
    Vector3 p2 = wayPoints[seg + 1].pos;
    Vector3 p3 = wayPoints[seg + 2].pos;
    float len = 0.0f;
    Vector3 prevPos = GetCatmullRomPosition(0.0f, p0, p1, p2, p3, catmullRomAlpha);
    for (int i = 1; i <= Mathf.FloorToInt(1f / debugTrackResolution); i++)
    {
      Vector3 pos = GetCatmullRomPosition(i * debugTrackResolution, p0, p1, p2, p3, catmullRomAlpha);
      len += Vector3.Distance(pos, prevPos);
      prevPos = pos;
    }
    float spd0 = seg == 1 ? spdInit : speeds[seg - 2];
    float lapse = wayPoints[seg + 1].time - wayPoints[seg].time;
    float acc = (len - spd0 * lapse) * 2 / lapse / lapse;
    float speed = spd0 + acc * lapse;
    lengths.Add(len);
    speeds.Add(speed);
    accels.Add(acc);
  }
}

public Vector3 GetPosition(float time)
{
  //Check if before first waypoint
  if (time <= wayPoints[0].time)
  {
    return wayPoints[0].pos;
  }
  //Check if after last waypoint
  else if (time >= wayPoints[wayPoints.Count - 1].time)
  {
    return wayPoints[wayPoints.Count - 1].pos;
  }

  //Check time boundaries - Find the nearest WayPoint your object has passed
  float minTime = -1;
  // float maxTime = -1;
  int minIndex = -1;
  for (int i = 1; i < wayPoints.Count; i++)
  {
    if (time > wayPoints[i - 1].time && time <= wayPoints[i].time)
    {
      // maxTime = wayPoints[i].time;
      int index = i - 1;
      minTime = wayPoints[index].time;
      minIndex = index;
    }
  }

  float spd0 = minIndex == 1 ? spdInit : speeds[minIndex - 2];
  float len = lengths[minIndex - 1];
  float acc = accels[minIndex - 1];
  float t = time - minTime;
  float posThroughSegment = spd0 * t + acc * t * t / 2;
  float percentageThroughSegment = posThroughSegment / len;

  //Define the 4 points required to make a Catmull-Rom spline
  Vector3 p0 = wayPoints[ClampListPos(minIndex - 1)].pos;
  Vector3 p1 = wayPoints[minIndex].pos;
  Vector3 p2 = wayPoints[ClampListPos(minIndex + 1)].pos;
  Vector3 p3 = wayPoints[ClampListPos(minIndex + 2)].pos;

  return GetCatmullRomPosition(percentageThroughSegment, p0, p1, p2, p3, catmullRomAlpha);
}
Run Code Online (Sandbox Code Playgroud)

好吧,让我们看看它是怎么回事......

在此处输入图片说明

呃……呃哦。它看起来几乎不错,只是在某些时候它会向后移动然后再次前进。实际上,如果我们检查我们的图表,就会在那里进行描述。在 12 到 16 秒之间,速度为负。为什么会发生这种情况?因为这种运动功能(恒定加速度)虽然简单,但也有一些局限性。对于一些突然的速度变化,可能没有一个恒定的加速度值可以保证我们的前提(在正确的时间通过检查点)而不会产生类似的副作用。

我们现在干什么?

你有很多选择:

  • 描述一个线性加速度变化的系统并应用边界条件(警告:有很多方程需要求解);
  • 描述一个在一段时间内具有恒定加速度的系统,例如在曲线之前/之后加速或减速,然后在该段的其余部分保持恒定速度(警告:要求 解的方程更多,很难保证正确通过检查点的前提时间);
  • 使用插值方法生成随时间变化的位置图。我已经尝试过 Catmull-Rom 本身,但我不喜欢结果,因为速度看起来不是很流畅。Bezier Curves 似乎是一种更可取的方法,因为您可以直接操纵控制点上的斜率(即速度)并避免向后移动;
  • 我最喜欢的是:AnimationCurve在类上添加一个公共字段,并使用 ts 很棒的内置抽屉在编辑器中自定义您的运动图!您可以使用其AddKey方法轻松添加控制点,并使用其方法获取一段时间的位置EvaluateOnValidate当您在曲线中编辑场景时,您甚至可以使用组件类上的方法自动更新场景中的点,反之亦然。

不要停在那里!在路径的线条 Gizmo 上添加渐变以轻松查看它在哪里变快或变慢,在编辑器模式下添加用于操纵路径的句柄......发挥创意!