Smoothly lerping between vectors where speed and distance is inconsistent and/or noisy

303 Views Asked by At

I'm visualising GPS data provided from the cars in races in Formula 1, and am attempting to animate their position on a path. The Formula 1 API provides vector coordinates and timestamps, but the timestamps are varied: they're updated approximately between 100 and 400 milliseconds:

timestamp                   x       y       z
2023-03-19 18:23:39.562     -1396   503     118
2023-03-19 18:23:39.842     -1443   630     118
2023-03-19 18:23:40.142     -1531   868     117
2023-03-19 18:23:40.342     -1589   1028    117
2023-03-19 18:23:40.501     -1636   1157    117
2023-03-19 18:23:40.842     -1772   1527    117
2023-03-19 18:23:41.101     -1813   1640    117
2023-03-19 18:23:41.361     -1932   1964    117
2023-03-19 18:23:41.782     -2015   2190    117
2023-03-19 18:23:42.002     -2080   2368    117
...

When visualised the data points look like this:

vector visualisation

I'm using a coroutine to Lerp between the vectors and updating on the time delta, but because the gaps are varied, it creates quite a jerky animation:

animation

My coroutine looks like this (with thanks to @derHugo for this answer):

private IEnumerator AnimationRoutine()
{
    if (alreadyAnimating) yield break;

    alreadyAnimating = true;

    var lastSample = _samples[0];
    Car.transform.position = lastSample.Position;

    yield return null;
    for (var i = 1; i < _samples.Count; i++)
    {
        var lastPosition = lastSample.Position;
        var currentSample = _samples[i];
        var targetPosition = currentSample.Position;

        var duration = currentSample.TimeDelta;
        var timePassed = 0f;
        while (timePassed < duration)
        {
            var factor = timePassed / duration;

            Car.transform.position = Vector3.Lerp(lastPosition, targetPosition, factor);
            yield return null;
            timePassed += Time.deltaTime;


        }

        Car.transform.position = targetPosition;
        lastSample = currentSample;
    }

    alreadyAnimating = false;

}

Is there a way I can maintain the time data I have but interpolate the transitions between points so they look smoother?

3

There are 3 best solutions below

2
ThatGuy On

You calculate the average speed of the object between two points and move the sphere accordingly but not the acceleration. You'll want to keep track of the sphere's speed in order to apply linear acceleration between two points. Accumulating the acceleration into the sphere's speed and applying that speed into its position over time should make it much less choppy.

To calculate average acceleration, you'll need to divide the velocity at point B subtracted by the velocity at point A by the distance: (v2 - v1) / d. To know the speed of the sphere by the time it reaches point B, it's just distance over time.

0
John Alexiou On

What is going on here is that the set of points only defines the path of the car and not the speed of the car. You are going to need additional information to have the speed of the car along the lap. This requires time telemetry in addition to spatial telemetry.

Ideally, you take the set of (x,y,z) points and corresponding time (t) and define a spatial cubic spline curve that will allow you to interpolate between the nodes.

Vector3.Lerp is not sufficient here because the slopes do not match at the nodes and you are going to have discontinuous speed.

The spline is parametrized with some parameter t = 0 .. t_lap describing where along the lap the car is such that

float[] t_data, float[] x_data, float[] y_data, float[] z_data;
...
Spline spline_x = new Spline(t_data, x_data);
Spline spline_y = new Spline(t_data, y_data);
Spline spline_z = new Spline(t_data, z_data);

for( int i=0; i<spline.Count; i++)
{ 
    float t = (t_end/(spline.Count-1))*i;
    posVector = new Vector(
        spline_x.Value(t),
        spline_y.Value(t),
        spline_z.Value(t));
    // draw position Vector
}

You can find numerous online postings about cubic splines in C# or even my own example in Fortan

If your question is that you do not have time telemetry if you can make it up by assuming some kind of velocity profile along the track (like constant which is not realistic) then it is a different math problem.

You would need the total distance traveled between nodes (the length of the spline curve) which is going to be always greater than the straight line segment given by Vector3.Distance().

This distance is evaluated with a numerical integrator of each distance segment Δs that corresponds to a small time step Δt using the spline slope (speed) function

float speed_x = spline_x.Slope(t);
float speed_y = spline_y.Slope(t);
float speed_z = spline_z.Slope(t);
float speed = Math.SqrtF( speed_x*speed_x + speed_y*speed_y + speed_z*speed_z);

float distInterval = Δt / speed;
    

The full mathematical treatment is rather involved, so I am not sure you want to go down this path, you just a quick estimate suffices here.


I found a similar answer of mine here about smoothly inerpolating between airplane data in 3D.


There is also a [Math.NET] Spline object you can use.

0
derHugo On

Today I would go a different approach and use AnimationCurves and initially when loading your data fill them completely in like e.g.

// extract your key frames - might consider a different way to store them in the first place ;)
var keysX = new Keyframe[_samples.Length];
var keysY = new Keyframe[_samples.Length];
var keysZ = new Keyframe[_samples.Length];

var time = 0f;

for(var i = 0; i < _samples.Length; i++)
{
    var position = _samples.Position;
    
    keysX[i] = new Keyframe[time, position.x];
    keysX[i] = new Keyframe[time, position.y];
    keysX[i] = new Keyframe[time, position.z];

    // would even be easier to track the actual time rather than delta then
    time += _samples.TimeDelta;
}

// From the keyframes construct curves
var animationCurveX = new AnimationCurve(keysX);
var animationCurveY = new AnimationCurve(keysY);
var animationCurveZ = new AnimationCurve(keysZ);

And then later you can simply use the time and AnimationCurve.Evaluate in order to get already correctly interpolated values. See also Keyframe for more advanced constructor options with tangents etc - alternatively let's simply smooth them out via AnimationCurve.SmoothTangents

// smoothing tangents 
for(var i = 0; i < _samples.Length; i++)
{
    animationCurveX.SmoothTangents(i, 0f);
    animationCurveY.SmoothTangents(i, 0f);
    animationCurveZ.SmoothTangents(i, 0f);
}

And then you can simply evaluate the position at any time and have it already interpolated

private IEnumerator AnimationRoutine()
{
    if (alreadyAnimating) yield break;

    alreadyAnimating = true;

    var keys = animationCurveX.keys;

    var endTime = keys[keys.Length - 1].time;

    for(var time = 0f; time < endTime; time += Time.deltaTime /*optionally add a factor here to play slower or faster*/)
    {
        car.transform.position = new Vector3(animationCurveX.Evaluate(time), animationCurveY.Evaluate(time), animationCurveZ.Evaluate(time));

        yield return null;
    }

    car.transform.position = new Vector3(animationCurveX.Evaluate(endTime), animationCurveY.Evaluate(endTime), animationCurveZ.Evaluate(endTime));

    alreadyAnimating = false;
}