2

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&lt;(Vector3, Vector3)&gt; returnMe = new List&lt;(Vector3, Vector3)&gt;();

for (int i = 1; i &lt; 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 &lt; 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?

Misys
  • 43
  • It is not going to be easy (if not impossible) to completely get rid of floating point weirdness. I think C# uses the same undelying .NET types as PowerShell: in the latter I tried $([double]1/3)3$ and it gave me $1$, then I tried $([decimal]1/3)3$ and it gave me $0.999...$. So you need to test a couple of things to make sure that the result should be rounded or not, sometimes it benefits, other times it does the opposite. – Dávid Laczkó Jan 17 '21 at 21:33
  • I don't know if this helps, but it mentions the $lerp$ function and the floating-point imprecision and an example to address it. – Dávid Laczkó Jan 17 '21 at 21:43
  • I would be very surprised if the things you see come from floating point errors. Even a $32$ bit float gives better than $7$ digits of precision. It is more likely coming from the rounding used to prepare the graph. You could print out the actual values and the differences between successive values to check. – Ross Millikan Jan 17 '21 at 22:15
  • 1
    The camera is jittery as well. The jitter is what made me graph the points to see if there was something else causing the jitter. – Misys Jan 17 '21 at 23:17
  • What is a "lerp"? – mjw Jan 18 '21 at 00:53
  • Looks smooth to me. – marty cohen Jan 18 '21 at 01:11
  • @RossMillikan It turns out you were half right. The issue was not floating point errors, but it had nothing to do with the way it was graphed. There was a problem with the code. See my answer for details. – Misys Jan 18 '21 at 17:56

2 Answers2

2

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.

poetasis
  • 6,338
  • IBM 709x series? I did a lot on 7090 and 7094. – marty cohen Jan 18 '21 at 01:10
  • 1
    @marty cohen NEC/BULL H9000/94 running GCOS-$8$. It was a virtual machine in that you could run under a different OS for your app in multiple languages but I mostly used COBOL$68$, COBOL$74$, COBOL$85\space $ and a sprinkling of ADA before government gave up trying to force it on people. – poetasis Jan 18 '21 at 01:17
  • I did not mind ADA when I used it at Northrop in the 90's. Had a lot of nice features as a step beyond Fortran. – marty cohen Jan 18 '21 at 05:49
  • 1
    I'm so old, my first languages were BASIC and RPG, I've since done lots of COBOL and a little ADA, C, C++, PERL and JAVA. I hate VAX-OS. I like UNIX, GCOS, MACOS and I'll do WINDOWS if nothing else is available. We should quit now. We're getting off topic. – poetasis Jan 18 '21 at 06:02
  • I started programming with IBM 7090 assembly language. Its assembly language was sophisticated enough that I wrote macros that computed primes at assembly time. Lots of fun. – marty cohen Jan 18 '21 at 06:50
0

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
Misys
  • 43