XMunkki/FixPointCS

about precision error

GITHUB243884919 opened this issue · 13 comments

I calcd a value of F64Vec3, i use c#。
i known result is (0.0, 0.0, 1.4)
but F64Vec3 result is (-1.93249434232712E-08, 0, 1.39999999897555)
i write a function convert F64Vec3 result to Unity's Vector3
public Vector3 ToUnityVector3()
{
return new Vector3(X.Float, Y.Float, Z.Float);
}
that is return (0.0, 0.0, 1.4). X is F64Vec3.X, Y is F64Vec3.Y, Z is F64Vec3.Z
i want to use (0.0, 0.0, 1.4), but do not want use ToUnityVector3.
what shall i do ?

And i found Debug.LogError(new F64(1.3d) + new F64(0.1d)); result is 1.39999999967404

Can you provide the calculations that outputted the undesired F64Vec3 you mentioned?

In general, the fixed points (nor floating points in Unity vectors) cannot produce all values exactly correctly. Unity rounds its vectors to only a few decimal places when printing them out, which makes it look like they might actually be exactly accurate (like your example of 1.3 + 0.1 == 1.4), when they're actually not.

The included vector library doesn't do such aggressive rounding, which is the reason why you are seeing the different results. You can modify the ToString() methods to output fewer decimals, if you wish to match Unity's behavior more closely.

That being said, there will always be some differences in results when comparing Unity and FixPointCS, as Unity's vectors use floats. This is by design.

1 step :I write 2 functions
(1)For rotate a vector rotate by y-axle
public static F64Vec3 RotateY(F64Vec3 lhs, F64 angle)
{
F64 rad = F64.DegToRad2(angle);
F64 cos = F64.Cos(rad);
F64 sin = F64.Sin(rad);
F64 x = lhs.X * cos + lhs.Z * sin;
F64 y = lhs.Y;
F64 z = -lhs.X * sin + lhs.Z * cos;

return new F64Vec3(x, y, z);

}

(2)For convert a point's pos in world space to local space.
Local space's o is param o, x-axle is param forward, y-axle is param left.
And forward, left is unit vector, and they are all in world space.
public static F64Vec3 PointToLocalSpace2D(F64Vec3 p, F64Vec3 forward, F64Vec3 left, F64Vec3 o)
{
var p2 = new F64Vec2(p.X, p.Z);
var f2 = new F64Vec2(forward.X, forward.Z);
var l2 = new F64Vec2(left.X, left.Z);
var o2 = new F64Vec2(o.X, o.Z);
var X = F64Vec2.Dot(f2, p2) - F64Vec2.Dot(o2, f2);
var Y = F64Vec2.Dot(l2, p2) - F64Vec2.Dot(o2, l2);
return new F64Vec3(X, p.Y, Y);
}

2 step
A tank's pos (0, 0, 0) forward is F64Vec3(0, 0, 1),
left is F64Vec3.RotateY(forward, new FixMath.F64(-90)).
They are all in world space.
I do 3 loop, tank RotateY 30, and move 5. code is follow
for (int i = 0; i < 3; i++)
{
F64Vec3 newDir = F64Vec3.RotateY(obj.GetDir(), new F64(30));
obj.SetDir(F64Vec3.Normalize(newDir));
obj.SetPos(obj.GetPos() + (newDir * new F64(5)));
tankGo.transform.position = obj.GetPos().ToUnityVector3();
tankGo.transform.forward = obj.GetDir().ToUnityVector3();
GameObject.Instantiate(tankGo);
}
Debug.LogError("Tank " + obj.GetPos() + " " + obj.GetPos().ToUnityVector3());
in this code, tank is obj.

3 step
a sphere pos is (x=obj.x, z=obj.z + 1.4), and convert pos to tank's local space.
GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
F64 radius = F64.Half;
//F64 foffsetX = F64.One;
F64 foffsetX = F64.Zero;
//F64 foffsetZ = obj.moveData.detectionWidth + radius + new F64(-0.1);
F64 foffsetZ = F64.FromFloat(1.4f);
F64Vec3 fcubePos = new F64Vec3(obj.GetPos().X + foffsetX, obj.GetPos().Y, obj.GetPos().Z + foffsetZ);
sphere.transform.position = fcubePos.ToUnityVector3();
F64Vec3 flocalCube = F64Vec3.PointToLocalSpace2D(fcubePos,
obj.forward, obj.left, obj.GetPos());

Debug.LogError("Sphere pos in tank's local space " + flocalCube + " " + flocalCube.ToUnityVector3());
I think Sphere's pos in tank's local space is (0.0, 0.0, 1.4)
but now is (-1.93249434232712E-08, 0, 1.39999999897555)
if i use ToUnityVector3, result is (0.0, 0.0, 1.4)
public Vector3 ToUnityVector3()
{
return new Vector3(X.Float, Y.Float, Z.Float);
}

I know why z is 1.39999999897555 and not 1.4
but i do not know why x = -1.93249434232712E-08 and not 0.
Maybe they are in same reason.
Maybe 1.93249434232712E-08 is approximate 0.
I know unity has a function Mathf.Approximately(float a, float b)
I think maybe F64 should has a function like that.

I have a project in https://github.com/GITHUB243884919/unity_study2019.git
if you need to debug. test scene is local
Assets/UFrame/Scenes/TestFx.unity
The Main Camera in this scene has a script. TestFix.cs
there is a function void TestFCS_P(), and it in TestFix's Start function

And PointCS has
public static F64 DegToRad(F64 a) { return FromRaw(Fixed64.Mul(a.Raw, 74961320)); } // F64.Pi / 180
my code is
public static F64 DegToRad2(F64 a) { return a * Pi / F180; }
public static F64 F180 = new F64(180);
I think maybe DegToRad2 can get batter precision than DegToRad

It looks to me like you're doing some fairly complex arithmetic with multiple rotations and expecting to get precise results. This isn't really the case with either fixed point or floating point, as both of them can only provide approximately correct answers, not exactly precise in the general case, especially when angles and rotations are involved.

If you want to get behavior closer to Unity's, you can add the Approximately() function you mentioned. I'm reluctant to add this to the official version, though, as it's quite application-specific what constitutes close enough to be approximately same. I believe Unity even allows for some amount of error in its Vector3==() operator.

I'd need a fully self-contained test case in order to take a look at this further: a single function demonstrating the problem with no outside arguments or dependencies on external software like Unity.

Also, it may be that your version of DegToRad() is somewhat more precise, perhaps giving you even exactly correct results in this case, but it's also a lot slower due to the full resolution F64 division. But again, if this is the behavior that you prefer, feel free to modify your copy of F64, that's precisely how the library is meant to be used!

Oh, I just realized I had overlooked this sentence: "Maybe 1.93249434232712E-08 is approximate 0."

The E-notation means that x is an extremely small number, 1.93249 * 10^-8, or 0.0000000193429, so the value is indeed very close to zero.

Maybe this answers your question?

Thank you very very very much! Thank for you answer, I got~ Now I know DegToRad2 is not batter than DegToRad. About precision, I think Unity has same problems, so they provide Approximately(a, b)。FixPointCS and my arithmetic are both right。 I review Unity's documents. about Mathf.Approximately. I found it have a value Mathf.Epsilon. I copy it's document. I really like FixPointCS , and I really need a value just like Unity's Epsilon. Because I really need some do some logic code , these code must know some things just like (-1.93249434232712E-08, 0, 1.39999999897555) just is (0, 0, 1.4). Can you help me ~ thank you~ i need Approximately and Epsilon for FixPointCS

Mathf.Approximately
Compares two floating point values and returns true if they are similar.
Floating point imprecision makes comparing floats using the equals operator inaccurate. For example, (1.0 == 10.0 / 10.0) might not return true every time. Approximately() compares two floats and returns true if they are within a small value (Epsilon) of each other.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Mathf.Epsilon 
A tiny floating point value (Read Only).
The smallest value that a float can have different from zero.
With the following rules:
- anyValue + Epsilon = anyValue
- anyValue - Epsilon = anyValue
- 0 + Epsilon = Epsilon
- 0 - Epsilon = -Epsilon
~~~~~~~~~~~~~~~~~~~~~~

 

https://docs.unity3d.com/ScriptReference/Mathf.Approximately.html
https://docs.unity3d.com/ScriptReference/Mathf.Epsilon.html
these are Unity's document for Approximately and Epsilon

using UnityEngine;

public class Example : MonoBehaviour
{
// Compares two floating point numbers and return true if they are the same number.
// See also Mathf.Approximately, which compares floating point numbers so you dont have
// to create a function to compare them.

bool isEqual(float a, float b)
{
    if (a >= b - Mathf.Epsilon && a <= b + Mathf.Epsilon)
    {
        return true;
    }
    else
    {
        return false;
    }
}

}

The concept of Epsilon doesn't behave the same in fixed point, so you can't implement the same semantics as Unity's epsilon has. Any fixed point value other than zero added to another fixed point value will always change it. The reason for this is that fixed point addition is identical to integer addition, the inputs and outputs are just interpreted differently.

You can implement an approximating equality comparison along the lines of:

    public static bool ApproximatelyEqual(F64 a, F64 b)
    {
        return Fixed64.Abs(a.Raw - b.Raw) <= 256;
    }

You can tweak the epsilon value (256) to fit your needs. There's no general formula how it should be set.

I'm closing this issue as it isn't a problem in FixPointCS.

thanks for your help ! I know 256 is a very small number in F64, but I do not know why is 256 ? And I guess 128 is that value in F32. May I think 256 >>32 just is precision in F64 ? I think I need review how to use binary to represent float :) Long time ago , when I was a student, I have learned it:)

No particular reason, it's just a small number. You can try tweaking it to fit your use case (try doubling or halving it until you get the effect you want).

That being said, comparing numbers for rough equality is a bit subject, in my opinion. You'll probably get it to work nicely for most of the time, but, depending on the use case, it can be impossible to get it to work robustly all the time.

ok ~ thanks ~