5
$\begingroup$

I suppose this applies to Geometry nodes as well, but the problem came up while I was creating a procedural material. I'm trying to animate a material that mimics the rotating line on a radar. For this material, I need the point slope form of a line, but the points derive from polar coordinates. The problem in this instance is that the equation I need takes the form $$ y = x \frac{sin(\theta)}{cos(\theta)} $$

In the animation, $\theta$ is a function of frame and, of course, $cos(\pi/2)$ is 0, leading to division by 0. This is not a good thing.

Other than trying to arrange drivers to avoid multiples of $\pi/2$, how do I deal with potential domain, Not-A-Number (NAN), or infinity situations in shader math?

EDIT: The solution suggested by John MC in the comments and given in detail by Markus von Broady in his answer does work for this specific case.

But it's not a general solution of NANs. I now suspect that there simply isn't a general solution and one simply has to know the potential domain errors that can come come up and code around them.

EDIT 2: Markus von Broady pointed out that NaN isn't the correct term for the problem I'm trying to solve so I've expanded the phrase to include Domain errors and infinities.

This becomes a bit difficult to follow because Domain errors occur in the mathematics, but NAN and infinites are artifacts of floating point implementations.

$\endgroup$
4
  • 2
    $\begingroup$ depends on what you want to do then. You could catch the 0 with a compare node and then give back a very low number e.g. 0.000000001 $\endgroup$
    – John MC
    Sep 10 at 6:36
  • $\begingroup$ Usually I just don't deal with the problem and see the result. Considering in Blender dividing by 0 doesn't crash and just results in 0, typically nothing bad happens. Also keep in mind there's enough float precision to make the numbers evaluating exactly to 0 a very narrow range of your possibilities - so if you don't use snapping, even if it results in a color quite different than surrounding colors, it's probably going to be irrelevant with high enough sampling... $\endgroup$ Sep 10 at 10:01
  • $\begingroup$ @MarkusvonBroady In the case at hand, the exact 0 happens because cos() is optimized to recognize pi/2 and return exactly 0. The result is that the animated line briefly flashes as exactly horizontal before returning to the sweep position. It's very obvious in an animation run. $\endgroup$ Sep 10 at 13:50
  • $\begingroup$ It would, indeed be. I was just trying to find an example to start a discussion about math errors and exceptions in math nodes. $\endgroup$ Sep 10 at 21:26
7
$\begingroup$

Sorry if this is not an answer to your more general question, but, following up on your commentary, this sort of arrangement will rotate a line of constant thickness about the origin of your texture space:

enter image description here

... and contains no divisions.

$\endgroup$
1
  • 1
    $\begingroup$ It does to the extent that it shows that finding an alternative formulation is a useful way of avoiding domain errors. Besides, it deserves an upvote just because it's a good solution to the specific example. $\endgroup$ Sep 10 at 21:49
5
$\begingroup$

I think I reproduced the gist of your problem:

This node setup uses division of sine by cosine, where the cosine sometimes outputs 0, to calculate a slope (or rather a point on that slope, using current X and calculating Y as in question), then using that to draw a line. When dividing, the smaller the divisor gets, the higher the result is; as the divisor approaches 0, the result gets really damn big (trying to avoid mathematical controversy here). But then, dividing by 0 is undefined, and it can be anything [checked and in Blender the result is 0, see my other answer]: my tests resulted with 0 for 90° and 270°, but something else for 630° (270°+360°) [that's probably the cosine not resulting with exactly 0 due to float unreliability]. The effect is you can get a sudden change from a very big value to 0 which generates something that breaks the pattern, whatever the pattern is (here: a rotating line):

If instead of rendering 360 frames for a 1 second animation, you rendered 360 million frames and used video software to condense that back to 360 frames, the effect wouldn't be noticeable. Likewise, if you're not basing the angle on the time, which almost always has low resolution in software, but use coordinates instead, you don't get this effect:

You need either a terrible luck to hit the sweet spot or force the situation by e.g. rounding the coordinates:

Even then increasing the sampling rate from 1 (above) to default 16 for Eevee viewport (below) will mostly hide it (well at least if the lines breaking the pattern are mathematically thinner than 1 px...):

Going back to the frame driven angle setup, the solution is to check if the value is 0, if so, add a very small value to make it non-zero, or otherwise handle this special case:

Perhaps you want to make the Epsilon slightly larger than 0 as well, though a very small value e.g. 0.00001 would still display as 0 on the screenshot.

Finally, the 90° doesn't break the pattern, but the line still disappears - which is the pattern because it was getting thinner and thinner, which is the flaw of this setup, not sure about yours... To draw a rotating line I suggest a setup like this:

$\endgroup$
3
  • 1
    $\begingroup$ You have, indeed, nailed the example as I described it, and a solution that works for the specific example. (The actual material is a bit more complex to deal with the thickness of the line. It actually checks to see if a point lies in the box bounded by two lines parallel to the radial line in order to keep the thickness even.) I was hoping for a solution to the more general problem of generated NANs but I suspect there isn't one. $\endgroup$ Sep 10 at 17:37
  • 4
    $\begingroup$ @MartyFouts I think the only general solution there is to not divide by 0, therefore making it a mathematical problem. Though a general mathematical solution is to write x ≠ 0, which brings it back to what is the general algorithmic solution, which is probably if x == 0 ... else ... for which the most general shader equivalent is a comparison node as above, controlling a Mix RGB shader, which adds a special case for 0. $\endgroup$ Sep 10 at 17:43
  • $\begingroup$ In this case I agree, one has to either code to avoid cos(k*pi/2+-epsilon) or code around the division by 0. $\endgroup$ Sep 10 at 17:46
4
$\begingroup$

General solution to NaN

Since there is no "NaN" value in Blender shaders (unlike in some programming languages like Javascript), you can't check if a value is "NaN" (like .isNaN() in Javascript). In fact, if you look into Blender's source, the Math node uses safe_divide function, which returns 0.0 when you try to divide by 0:

float safe_divide(float a, float b)
{
  return (b != 0.0) ? a / b : 0.0;
}

So the problem isn't really "how to deal with NaN" - it is "How to deal with special cases". And since special cases are arbitrary, you also have to specifically deal with them on a case by case basis. Not only that, as there is no interface supplied to allow you to deal with them (e.g. the safe_divide function doesn't return a bool + float struct, where the boolean informs you if the float is a special case) you have to investigate what can cause this special behavior and "branch" the logic based on those inputs generating special values or not; therefore the most general solution I can come up with is this:

(to be perfectly clear, you perhaps want to add 0 to that color value so it's a single scalar value again)

Again, it's not really "NaN", just sticking to OP's convention.

$\endgroup$
4
  • $\begingroup$ If I were to call out in detail what was meant to be covered by the question, it's actually function domain errors, NaNs, and infinites. In retrospect, NaN was probably the wrong shorthand. (By the way, that "safe_divide" may still generate IEEE 754 infinities because of the exact comparison to 0.) $\endgroup$ Sep 10 at 19:33
  • $\begingroup$ @MartyFouts The problem you presented is neither of the mentioned. It's the result of division by 0 being 0, which breaks the "pattern" for results of division on a previous and next frames (which have very small divisors, resulting in large numbers). The way I see it, the problem is that you don't get the highest workable value (be it infinity or float max, depending on nodes support for infinity), which would produce a continuous pattern. $\endgroup$ Sep 10 at 19:56
  • $\begingroup$ Yes, it turns out that the example I described exhibits the behavior it does because of how "safe_divide" is implemented in math nodes and thanks for pointing the implementation out. The problem in the original formulation is a domain error in mathematics, as division by 0 is undefined. I tried to impose too much burden on the example. $\endgroup$ Sep 10 at 21:32
  • 1
    $\begingroup$ @MartyFouts I think we finally converged our opinions here. The only more general solution I can think of is having some add-on traverse the node tree and apply "fixes" (like changing output of division by 0 to maximum float instead of 0) automatically. $\endgroup$ Sep 10 at 22:19
2
$\begingroup$

In summary, the answer appears to be:

  1. If the problem originated as a mathematical domain error, such as division by zero being undefined, look for a different formulation of the math that avoids the problem, as demonstrated by Robin Betts' answer where he substituted a dot product method.

  2. If the problem originates as a floating point issue, code around it, as suggested by John MC in a comment and fleshed out by Marcus von Broady in his first answer.

  3. Fudge your math to avoid running into the issue as suggested by me in the original question.

  4. or simply cope, as the math functions are coded to return (sometimes bad) substitution values rather than fail, and sometimes you can get away with ignoring the problem

I've upvoted the two answers that contributed to the solution, but will be accepting this one because it contains all of the (so far known) approaches.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.