代码之家  ›  专栏  ›  技术社区  ›  Dan

统一加速度

  •  10
  • Dan  · 技术社区  · 6 年前

    我试图在统一中模拟加速和减速。

    我已经编写了代码来生成统一的轨迹,并根据时间将对象放置在轨迹上的特定位置。结果看起来有点像这样。

    Cube mid way through Catmull-Rom Spline

    我现在遇到的问题是,样条曲线的每个部分都有不同的长度,立方体在每个部分以不同但均匀的速度移动。这将导致在截面之间过渡时立方体速度的变化出现突然跳跃。

    为了解决这个问题,我尝试使用 Robert Penner's easing equations 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
        }
    }
    
    4 回复  |  直到 6 年前
        1
  •  4
  •   Rodrigo Rodrigues    6 年前

    好吧,我们放一些 数学 对此。

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

    参数化

    如果你有大学学位,你可能会记得 功能 -获取参数并产生结果的操作-以及 -一个函数对其参数演化的图形表示(或绘图)。 f(x) 可能会提醒你:它说 f 取决于血压计 x . 所以,“到” parameterize “大致意思是用一个或多个参数来表示一个系统。

    你可能不熟悉这些术语,但你总是这样做。你的 Track 例如,是一个具有3个参数的系统: f(x,y,z) .

    参数化的一个有趣之处是,您可以获取一个系统并用其他参数来描述它。再说一遍,你已经在做了。当你描述你的轨迹随时间的演变时,你是说每个坐标都是时间的函数, f(x,y,z) = f(x(t),y(t),z(t)) = f(t) . 换言之,您可以使用时间来计算每个坐标,并使用坐标在给定时间内将对象定位在空间中。

    轨道系统建模

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

    1. 一条小路;

    你实际上已经解决了这个问题。在场景空间中设置一些点并使用 Catmull'rom样条曲线 插值点并生成路径。这很聪明,而且没什么可做的了。

    另外,您还添加了一个字段 time 在每个点上,所以你想测量移动的物体将通过这个检查在这个准确的时间。我晚点再来。

    1. 移动的物体

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

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

    我建议你换一种方法。使用行驶距离参数化路径。然后,对象的移动将是相对于时间参数化的移动距离。这样,您将拥有两个独立且一致的系统。动手!

    例子:

    从现在开始,为了简单起见,我将把所有东西都变成二维的,但是以后把它改成三维的就很简单了。

    考虑以下路径:

    Example path

    在哪里? i 是段的索引, d 是行驶的距离 x, y 是飞机上的座舱。这可能是一条由像你这样的样条曲线创建的路径,或者用bzier曲线或其他什么东西创建的路径。

    一个物体用你当前的解决方案所产生的运动可以描述为 distance traveled on the path VS 时间 这样地:

    Movement 1 graph

    在哪里? t 表中是物体必须到达检查点的时间, D 又是到达这个位置的距离, v 是速度和 a 是加速度。

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

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

    Movement 1 animation

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

    好吧,我们怎么才能做得更好呢?隐马尔可夫模型。。。如果速度图是连续的,那就不是那么烦人的速度跳跃。对这样的运动最简单的描述可能是一个统一的无耐力运动。这样的系统由以下方程式描述: d = do + vo*t + a*t^2/2 . 我们还需要假设一个初始速度,我在这里选择0(从静止状态分离)。

    enter image description here

    和我们预期的一样,速度图是连续的,运动通过路径加速。这可以被编码成unity来改变methids Start GetPosition 这样地:

    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);
    }
    

    好吧,让我们看看进展如何…

    enter image description here

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

    我们现在该怎么办?

    你有很多选择:

    • 描述具有线性加速度变化的系统并应用边界条件(警告: 太多了 要解的方程);
    • 描述一个在一段时间内具有恒定加速度的系统,例如在曲线前/后加速或减速,然后在该段的其余部分保持恒定速度(警告: 甚至更多 方程求解,难以保证在正确的时间通过检查站的前提下);
    • 使用插值方法生成位置随时间变化的图。我试过卡特穆尔罗姆本身,但我不喜欢结果,因为速度不是很顺利。贝塞尔曲线似乎是一个更好的方法,因为你可以操纵斜坡(又名速度)上的控制点直接和避免向后移动;
    • 我最喜欢的是:添加一个public AnimationCurve 字段的类和自定义您的运动图在编辑器与ts可怕的内置抽屉!您可以使用 AddKey 方法和获取位置一段时间 Evaluate 方法。 你甚至可以用 OnValidate 方法,以便在曲线中编辑场景中的点时自动更新场景中的点,反之亦然。

    别停在那儿!在路径的线gizmo上添加渐变以方便查看它的速度或速度,添加用于在编辑器模式下操作路径的句柄…发挥创造力!

        2
  •  0
  •   Brice V.    6 年前

    据我所知,大多数解决方案都已经存在,只是初始化不正确。

    本地速度取决于样条曲线的长度,因此您应该通过 段长度的倒数 (你只需几步就可以很容易地接近它)。

    当然,在您的情况下,您不能控制速度,只能控制输入时间,所以您需要正确地分配 SimpleWayPoint.time 根据前面样条线段的顺序和长度,而不是在字段声明中手动初始化 . 这种方式 percentageThroughSegment 应均匀分布。

    正如在评论中提到的那样,有些数学可以用 Lerp() :)

        3
  •  0
  •   Lincoln Cheng    6 年前

    我们先定义一些术语:

    1. t :每条样条曲线的插值变量,范围从 0 1 .
    2. s :每条样条曲线的长度。根据使用的样条曲线类型(catmull rom、bezier等),可以使用公式计算估计的总长度。
    3. dt :中的更改 T 每帧。在您的例子中,如果在所有不同的样条曲线上这是常数,您将看到样条曲线端点处的速度突然变化,因为每个样条曲线有不同的长度 S .

    缓解每个关节速度变化的最简单方法是:

    void Update() {
        float dt = 0.05f; //this is currently your "global" interpolation speed, for all splines
        float v0 = s0/dt; //estimated linear speed in the first spline.
        float v1 = s1/dt; //estimated linear speed in the second spline.
        float dt0 = interpSpeed(t0, v0, v1) / s0; //t0 is the current interpolation variable where the object is at, in the first spline
        transform.position = GetCatmullRomPosition(t0 + dt0*Time.deltaTime, ...); //update your new position in first spline
    }
    

    哪里:

    float interpSpeed(float t, float v0, float v1, float tEaseStart=0.5f) {
        float u = (t - tEaseStart)/(1f - tEaseStart);
        return Mathf.Lerp(v0, v1, u);
    }
    

    上面的直觉是,当我到达第一条样条曲线的末端时,我预测下一条样条曲线的预期速度,并将当前速度减缓到那里。

    最后,为了让宽松政策看起来更好:

    • 考虑在中使用非线性插值函数 interpSpeed() .
    • 考虑在第二条样条曲线的开始处实现“ease into”
        4
  •  0
  •   Doh09    6 年前

    您可以尝试使用他们的车轮系统的车轮碰撞器教程。

    它有一些变量,你可以调整与刚体变量,以实现模拟驾驶。

    就像他们写的

    一辆车上最多可以有20个车轮,每个车轮都施加转向、电机或制动扭矩。

    免责声明:我只有很少的经验与车轮碰撞。但在我看来他们就像你要找的一样。

    https://docs.unity3d.com/Manual/WheelColliderTutorial.html

    enter image description here