Search code examples
c#unity-game-enginemathfloating-pointformula

Why does this formula break after 31 loops?


I have the following code:

public float SnapValueToCoreBlock(float ValueToSnap)
{
    ValueToSnap = ValueToSnap + 0.5f;
    ValueToSnap = Mathf.Floor(ValueToSnap);
    return ValueToSnap;
}

float floatvar;
int intvar;


    for (int z = 0; z < 100; z++)
    {

        floatvar = z + (Mathf.FloorToInt(0.499999f) + 1) * 0.499999f / (Mathf.FloorToInt(0.499999f) + 1);
        intvar = (int)SnapValueToCoreBlock(floatvar);
    }

I am expecting "intvar" to always be equal to "z" in the loop, however after 31 iterations there is some kind of rounding error and when z=32 intvar = 33, instead of z=32 intvar =32

from then onwards intvar is always off by 1, so for z<32 intvar=z and for z >31 intvar=z+1

my intended result is for z = intvar always, I dont understand why this arbitrarily changes when z reaches 32, I would appreciate it if someone could help me, Thanks in advance.


Solution

  • I took your code and translated them from Unity to the .NET Framework. This involved changing Mathf.FloorToInt(someFloat) to (int)Math.Floor(someFloat) and Mathf.Floor(ValueToSnap) to (float)Math.Floor(ValueToSnap). I believe it does the same thing, but it takes a detour through double.

    I also inserted a WriteLine statement in your loop:

    for (int z = 0; z < 100; z++)
    {
        floatvar = z + ((int)Math.Floor(0.499999f) + 1) * 0.499999f / ((int)Math.Floor(0.499999f) + 1);
        intvar = (int)SnapValueToCoreBlock(floatvar);
        Debug.WriteLine($"FloatVar: {floatvar} Z: {z} IntVar: {intvar}");
    }
    

    I see the same behaviour.

    In particular, I see breaks at z == 9:

    FloatVar: 8.499999 Z: 8 IntVar: 8
    FloatVar: 9.499999 Z: 9 IntVar: 9
    FloatVar: 10.5 Z: 10 IntVar: 10
    FloatVar: 11.5 Z: 11 IntVar: 11
    

    and at z == 32:

    FloatVar: 30.5 Z: 30 IntVar: 30
    FloatVar: 31.5 Z: 31 IntVar: 31
    FloatVar: 32.5 Z: 32 IntVar: 33
    FloatVar: 33.5 Z: 33 IntVar: 34
    

    Even if I extend the precision of the output beyond the precision of the float:

    Debug.WriteLine($"FloatVar: {floatvar:0.0000000000000000000} Z: {z} IntVar: {intvar}");
    

    I see the same behaviour, rounding down below 32 and rounding up above it.

    Then I greatly simplified your calculation:

    floatvar2 = z + 0.499999f;
    intvar2 = (int)SnapValueToCoreBlock(floatvar2);
    

    And I still see the same behaviour.

    So, what it seems is that:

     anInteger + 0.499999f + 0.5f;
    

    is less than anInteger + 1.0f for values of anInteger < 32 and equal to or greater than anInteger + 1.0f for values anInteger >= 32. And, you know what, that doesn't surprise me. You are right at the edge of floating point precision (remember, floats have ~6-9 digits of precision: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types). When you went from 31 to 32, you ended up setting another bit, and that's what likely made the difference.

    Final Comment (as an update)

    You say "the 0.4999999 is a variable that I am using for collisions". You need to read up on how to compare floating point values, and how to properly use an epsilon. Here's one thing I found (https://bitbashing.io/comparing-floats.html). It's C++ focused, but seems to address the issues. As a final comment, if you are ever using measured values (like in a chemical process control system), you need to include the precision of your measurements in the establishment of an epsilon.