Reputation: 33445
I have code adapted from "Improved" Perlin noise
double improved_noise (double x, double y, double z)
{
// Calculate the "unit cube" that the point asked will be located in
// The left bound is ( |_x_|,|_y_|,|_z_| ) and the right bound is that
// plus 1. Next we calculate the location (from 0.0 to 1.0) in that
// cube. We also fade the location to smooth the result.
int xi = (int)x & 255;
int yi = (int)y & 255;
int zi = (int)z & 255;
double xf = x - (int) x;
double yf = y - (int) y;
double zf = z - (int) z;
double u = fade (xf);
double v = fade (yf);
double w = fade (zf);
int aaa, aba, aab, abb, baa, bba, bab, bbb;
auto & p = permutation;
aaa = p[p[p[ xi ] + yi ] + zi ];
aba = p[p[p[ xi ] + inc(yi)] + zi ];
aab = p[p[p[ xi ] + yi ] + inc(zi)];
abb = p[p[p[ xi ] + inc(yi)] + inc(zi)];
baa = p[p[p[inc(xi)] + yi ] + zi ];
bba = p[p[p[inc(xi)] + inc(yi)] + zi ];
bab = p[p[p[inc(xi)] + yi ] + inc(zi)];
bbb = p[p[p[inc(xi)] + inc(yi)] + inc(zi)];
double x1, x2, y1, y2;
// The gradient function calculates the dot product between a
// pseudorandom gradient vector and the vector from the input
// coordinate to the 8 surrounding points in its unit cube.
// This is all then lerped together as a sort of weighted average
// based on the faded (u,v,w) values we made earlier.
x1 = lerp (
grad (aaa, xf , yf , zf),
grad (baa, xf-1, yf , zf),
u);
x2 = lerp (
grad (aba, xf , yf-1, zf),
grad (bba, xf-1, yf-1, zf),
u);
y1 = lerp (x1, x2, v);
x1 = lerp (
grad (aab, xf , yf , zf-1),
grad (bab, xf-1, yf , zf-1),
u);
x2 = lerp (
grad (abb, xf , yf-1, zf-1),
grad (bbb, xf-1, yf-1, zf-1),
u);
y2 = lerp (x1, x2, v);
return (lerp (y1, y2, w) + 1) / 2;
}
I wanted periodic noise in one direction so I wrapped that direction onto a circle in an extra dimension, calling it like this
improved_noise (sin(x*2*M_PI), cos(x*2*M_PI), y))
I got weird results (large and/or negative). Some experimentation revealed that this happens when the arguments to improved_noise
are negative.
Why doesn't this function handle negative values nicely, and can it be easily adapted so the full number line is a valid argument?
Upvotes: 0
Views: 963
Reputation: 223663
improved_noise
was not designed to handle negative inputs.
A comment in it says:
The left bound is ( |_x_|,|_y_|,|_z_| )…
The |…|
notation suggests the absolute value is intended. However, the code computes:
int xi = (int)x & 255;
In common C implementations (where two’s complement is used), this effectively computes the residue of the integer portion of x
modulo 256. For example, if x is −3.25, its integer portion is −3, and this will set xi
to 253 (which is −3+256).
There are two things wrong with that. First, 253 is not the absolute value of −3, so this code does not match the comment. Second, it is taking the “right” boundary of the unit cube containing the point (the boundary with the greater value), whereas the comments, and the behavior for positive values, suggests the intent is to set xi
, yi
, and zi
to the “left” boundary (the one with lesser value).
Going on from there, the code sets double xf = x - (int) x;
. For non-negative values, this produces the fractional part of x
. For example, if x
were 3.25, xf
would be .25. However, with negative values and the prior & 255
operation, this goes astray. For x
= −3.25, it computes −3.25 − 253 = −256.25. But the code is likely intended merely to interpolate within a unit cube, for fractional portions from 0 to 1. Whatever function is used to perform the interpolation likely does not support −256.25.
Essentially, this code was never designed to support negative values, and fixing it requires redesigning it from first principles of how it is supposed to operate.
The original code you point to is better:
int X = (int)Math.floor(x) & 255
…
x -= Math.floor(x);
The former correctly uses floor
to find the “left” boundary, regardless of whether x
is negative or not. Then it applies & 255
to that. Assuming two’s complement, this will provide the correct coordinate in a periodic tiling. (Assuming two’s complement is not purely portable and should be documented or avoided.)
Then it correctly finds the fraction by subtracting the floor
of x
rather than subtracting the result of & 255
. For example, for x
= −3.25, this will produce integer coordinate −4 and fraction .75.
Modifying improved_noise
to work similarly might help. You might try:
int xi = (int) floor(x) & 255;
int yi = (int) floor(y) & 255;
int zi = (int) floor(z) & 255;
double xf = x - floor(x);
double yf = y - floor(y);
double zf = z - floor(z);
You tagged this question with both C++ and C. In C++, it is preferable to use std::floor
instead of floor
, and there may be other issues with differences between C++ and C.
Upvotes: 3