How can I simplify this lerp arithmetic to avoid floating point precision errors?

323 Views Asked by At

I am making a "cinematic camera" that pans around a scene. Each step has a position for the camera and a point that the camera should be looking at. For instance:

1. position = (3, 2, 1), facing = (0, 0, 0)
2. position = (6, 5, 4), facing = (9, 9, 9)

The camera should start at (3, 2, 1) and be looking at the point (0, 0, 0). By the end of the panning the camera should be positioned at (6, 5, 4) and be looking at the point (9, 9, 9).

I've written some C# to do this:

public static IEnumerable<(Vector3, Vector3)> CinematicCamera(params (Vector3 location, Vector3 facing, double time)[] points) {
    if (points.Length < 2) {
        throw new ArgumentException("Need at least 2 points.");
    }
    
    List<(Vector3, Vector3)> returnMe = new List<(Vector3, Vector3)>();

    for (int i = 1; i < points.Length; i++) {
        int numSteps = (int)(points[i].time * 20); // camera runs at 20 frames per second
        double startDistance = Distance(points[i - 1].location, points[i - 1].facing); // distance the camera starts from the point it's looking at
        double endDistance = Distance(points[i].location, points[i].facing); // distance the camera ends from the point it's looking at
        Vector3 startDirection = points[i - 1].location - points[i - 1].facing; // get the start directional vector from the location point and facing point
        Vector3 endDirection = points[i].location - points[i].facing; // get the end directional vector from the location point and facing point

        for (int stepNum = 0; stepNum < numSteps; stepNum++) {
            double progress = (double)stepNum / numSteps; // 0 to 1 based on current step, used for lerp
            double distance = Lerp(startDistance, endDistance, progress); // lerp the distance from the previous point to the next point
            Vector3 currentDirection = Lerp(startDirection, endDirection, progress); // lerp the directional vector
            
            Vector3 lookAt = Lerp(points[i - 1].facing, points[i].facing, progress); // new point to look at
            Vector3 position = lookAt + currentDirection * distance / Length(currentDirection); // new position
            
            returnMe.Add((position, lookAt));
        }
    }

    return returnMe;
}

// helper methods
private static double Distance(Vector3 a, Vector3 b) => Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2) + Math.Pow(a.Z - b.Z, 2));
private static double Length(Vector3 v) => Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);
private static double Lerp(double start, double end, double amount) => start + (end - start) * amount;
private static Vector3 Lerp(Vector3 start, Vector3 end, double amount) => new Vector3(Lerp(start.X, end.X, amount), Lerp(start.Y, end.Y, amount), Lerp(start.Z, end.Z, amount));

Vector3 is a custom data type that uses double to store x, y, and z. This all works, but the camera is a bit jittery due to floating point imprecision. I graphed some of the points in Excel and you can see the jitter in the graph:

enter image description here

I'm considering switching the entire implementation from double to decimal (double in C# uses 8 bytes while decimal uses 16), but I'm not sure that really addresses the root of the problem. How can I simplify this so that I run into less floating point weirdness?

2

There are 2 best solutions below

5
On

In prehistoric times using $36$-bit COBOL, I was able to get around this by two methods.

  1. I took all incoming floating points and multiplied them by a factor that converted them to $36$-bit integers. All calculations were done with integers and the results were then divided by the factor for final output.

  2. For one project, where I began with integers but there would be floating points along the way, I kept numerators and denominators in separate variables and did LCM or GCD calculations as needed to multiply or divide them along the way. This kept the magnitude to $36$ bits and the "division" was used only for the final output.

0
On

I accidentally fixed this. The problem was not floating point inaccuracy, it was actually this line here:

double progress = (double)stepNum / numSteps; // 0 to 1 based on current step, used for lerp

It should be

double progress = (double)stepNum / (numSteps - 1); // 0 to 1 based on current step, used for lerp