Floating-Point Weirdness

The entire field of 3D graphics is inherently connected with calculations on floating-point numbers. You should be aware, that it can be a bit tricky to work with such values (or rather variables of such type). In this article I’ll begin with some Cinema 4D SDK basics, before showing typical floating-point pitfalls in the end.

Special care needs to be taken, when

  • converting Float datatypes into integer datatypes
  • comparing Float datatypes
  • calculating with Float datatypes

Before diving into these oddities, I want to note the basics and convenience functions.

Cinema 4D Datatypes

Actually this note is not limited to floating-point. Yet, it should be noted here, that you are advised to Cinema 4D’s datatypes, unless you absolutely need other types to communicate with an absolutely needed external library.

Limits

In case you need to have the minimum or maximum values of a Float datatype you should use this syntax:
LIMIT<datatype>::Min()
LIMIT<datatype>::Max()

Like so:

const Float32 FMAX32 = LIMIT<Float32>::Max();
const Float64 FMAX64 = LIMIT<Float64>::Max();

This is different for integer datatypes! There the syntax looks like this: LIMIT<datatype>::MIN and LIMIT<datatype>::MAX

Inverse

Beginning with Cinema 4D R15 there’s a convenience function Inverse(v) to get the inverse of a Float datatype, which already cares for division by zero. In which case it will return zero.

Safely Convert Float-Point into Integer

When assigning Float values to integer variables, simply use the following macros to be on the safe side:

Cinema 4D < R15 Cinema 4D ≥ R15
SAFELONG() SAFEINT32(), SAFEINT64(), SAFEINT()

Like so:

Float fValue = (Float)5.3;
Int32 iValue;

iValue = SAFEINT32(fValue); // use explicit cast macros to convert from Float to int
// of course the other way round is simple and perfectly fine
fValue = (Float)iValue;

Note that it is good practice to explicitly mark Float constants with a cast. By this you can be sure, the constant has the desired precision and it prevents falling into pitfalls like compiler oddities or redundant conversion code generated by typos.

While using the above cast macros is perfectly safe and fine, you should never use a pointer to convert a Float into an Int or vice versa.
Never ever!
You never know, if the variable is currently stored within an register of your CPU and in this case such code fails big time. The search for such a bug can easily cost you a day.

void DontConvertFloatLikeThis(const UInt32* const pIntValue, const Float* const pFloatValue)
{
  // NEITHER DON'T DO SOMETHING LIKE THIS
  Float fValue = *(Float*)pIntValue; // DO NOT TRY TO CONVERT AN INT INTO A FLOAT BY CASTING A POINTER!!!
  // PLEASE DON'T!

  // NOR DO IT THE OTHER WAY ROUND
  Int32 iValue = *(Int32*)pFloatValue; // DO NOT TRY TO CONVERT A FLOAT INTO AN INT BY CASTING A POINTER!!!
  // NEVER EVER!
}

Safely Compare Floating-Point on Equality

Always use tolerant comparisons! Use of less (<), less equal (<=), greater (>) and greater equal (>=) is fine. But for any comparison on equality use CompareFloatTolerant(a, b) instead of a simple equality operator (==).

Like so:

const Float FVALUE= (Float)0.2 + (Float)0.1;
if (CompareFloatTolerant(FVALUE, (Float)0.3))
{
  GePrint("Working fine, the values are equal.");
}

And never like so:

// DON'T COMPARE FLOATS ON EQUALITY WITH EQUALITY OPERATOR
const Float FVALUE = (Float)0.2 + (Float)0.1;
if (FVALUE == (Float)0.3)
{
  GePrint("There are cases it may work...");
}
else
{
  GePrint("...but, ooops, most likely it won't. The values are different!");
}

Floating-Point Weirdness

Always be aware, that math known from school might not hold with floating-point calculations.
Some examples, I just saw neatly collected on a German computer magazine. This is no buggy behavior, just completely normal implications of the floating-point number format:

const Float64 A = (Float64)1000.0;
const Float64 B = (Float64)1e-14;  // this value is still very well in the range of Float64

if ((A + B) == A)
{
  GePrint("Ooops, probably not, what you expected, A equals (A+B)"); // THIS GET'S PRINTED
}
else
{
  GePrint("Yeah, sure, A and (A+B) are different.");
}
// ok, then let's try to add a thousand B's to A
Float64 c = A;
for (Int32 idx = 0; idx < 1000; ++idx)
{
  c += B;
}
if (c == A)
{
  GePrint("Ooops, still not, what you expected, A equals (A+1000*B)"); // THIS GET'S PRINTED
}
else
{
  GePrint("Now that we added b a thousand times, these had to be different.");
}
// now, let's change the order of calculation
Float64 d = (Float64)0.0;
for (Int32 idx = 0; idx < 1000; ++idx)
{
  d += B;
}
d += A;
if (d == A)
{
  GePrint("Got it, same calculation as above, same result.");
}
else
{
  GePrint("Ooops, now they are different? A is not equal (1000*B+A)"); // THIS GET'S PRINTED
}

Let’s try another one, this time subtraction. Again, no bugs shown, just the day to day floating-point madness:

const Float32 A = (Float32)2.0;
const Float32 B = (Float32)2.0000001;

GePrint("B - A = " + String::FloatToString(B - A, 1, 9, true));
// prints: B - A = 0.000000000E+000

const Float32 C = (Float32)0.2;
const Float32 D = (Float32)0.2000001;

GePrint("D - C = " + String::FloatToString(D - C, 1, 9, true));
// prints: D - C = 1.043081284E-007

const Float32 E = (Float32)0.0000001;
const Float32 F = (Float32)5.0;

GePrint("F - E = " + String::FloatToString(F - E, 1, 9, true));
// prints: F - E = 5.000000000E+000

You may think, oh my god, the old Intel bug is back. Yet, it is not, your machine is working fine. Just keep in the back of your head, that calculating with Float datatypes can get tricky and especially, when comparing them, you should remember the above mentioned rule.
And yes, in the second example I cheated a bit, using Float32, but trust me, you can have the same effect with Float64.

Floating-Point Exceptions

By default certain floating-point exceptions, like division by zero, are disabled in Cinema 4D, for good reason I may add. Yet, you may want to check for these in your code. On a per thread basis you can use GeDebugSetFloatingPointChecks(flag) to temporarily(!) enable floating-point exceptions. Needless to say, that you should do so only in debug builds, your release builds have to be free of floating-point exceptions anyway!

Once more:
Do not enable floating-point exceptions in a plugin release. For user this is equivalent to any other crash and his data/work will be lost. Get your exceptions fixed before you release.

When Importing Floating-Point Data

Suppose you are reading floating-point data from external files. There are cases you may not know, that this data is valid. You can use CheckFloat(v) to check the read values or even RepairFloat(v) to restore the integrity of such values. In the later case you should be aware, that RepairFloat(v) will return a valid floating-point value in regards to IEEE-754 standard, only. RepairFloat(v) is not able to regain any data lost due to for example file corruption.

Have Fun

I hope nobody got scared and that at least one of you may have learned something for her or his plugin development while reading this article.

Cheers,
Andreas

Avatar

Andreas Block

Fomer SDK Support Lead and Developer Over ten years of development in industry automation provided me with a good foundation of low level and FPGA programming knowledge as well as with insight into various operating systems. Cinema 4D has been a hobby of mine since it started to emit its first rays of light back in Amiga times (still known as Fastray back then).