1
$\begingroup$

I am creating different coloring patterns for my CPU raytracer, such as checker, gradient, ring, stripe pattern etc. The problems started when I tried to create a perturbation pattern (of a subpattern) based on Perlin noise function. The noise seems to work, but not when the subpattern is rotated 45 degrees around Y (up in left-hand coordinate system). For example, 90 or -45 degree rotation works, too. Which is why I am very confused.

The client code is:

plane floor{
        {},
        material{.pattern = stripe_pattern{solid_pattern{1, 1, 1}, solid_pattern{0, 0, 0},
                                           rotation<Axis::Y>(deg_to_rad(45)) * scale(.2, .2, .2)}}};

This defines a plane with a stripe pattern attached. The stripe pattern has two subpatterns (this is designed so that pattern nesting is possible), both return solid colors, white and black in this case. The stripe pattern is scaled down and rotated. The result is:

enter image description here

Great. Now, let's try to apply Perlin noise to it:

    plane floor{{},
                material{.pattern = perturb_pattern{stripe_pattern{
                             solid_pattern{1, 1, 1}, solid_pattern{0, 0, 0},
                             rotation<Axis::Y>(deg_to_rad(45)) * scale(.2, .2, .2)}}}};

I don't see any difference:

Now, let's rotate the stripe pattern 90 degrees instead of 45 and apply the noise:

plane floor{{},
                material{.pattern = perturb_pattern{stripe_pattern{
                             solid_pattern{1, 1, 1}, solid_pattern{0, 0, 0},
                             rotation<Axis::Y>(deg_to_rad(90)) * scale(.2, .2, .2)}}}};

Well, it did something:

enter image description here

Finally, let's try to crank up the Perlin noise coefficient (the default is .2):

plane floor{{},
                material{.pattern = perturb_pattern{
                             stripe_pattern{solid_pattern{1, 1, 1}, solid_pattern{0, 0, 0},
                                            rotation<Axis::Y>(deg_to_rad(90)) * scale(.2, .2, .2)},
                             1}}};

And the result:

So, as you can see, my implementation generally works okay with patterns and transformations. I also tested it with other patterns, and it works too. The only case when it doesn't work is the 45-degree rotation transformation. It just doesn't seem to do anything.

Here is an attempt with 50 degrees and coefficient of 1, which makes the changes slightly visible:

If I try this with .2 instead of 1, the changes are again invisible.

Here is the definition of perturb_pattern:

struct perturb_pattern {
    class pattern pattern; // subpattern to apply Perlin noise to
    float scale; // how much weight Perlin noise has on the resulting jitter
    tform4 tform; // patterns can be transformed
    tform4 inv_tform;

    perturb_pattern(class pattern pattern, float scale = .2f) { /*...*/ }
    perturb_pattern(class pattern pattern, tform4 tform, float scale = .2f) { /*...*/ }
};

The code works as follows. In this particular case, for each world space point that can be traced from the screen pixel grid, its color is determined by calling the main dispatch function:

color pattern_at(pattern const& pattern, shape const& shape, pnt3 const& world_point) noexcept {
    return pattern_at(pattern, inv_tform(pattern) * inv_tform(shape) * world_point);
}

as pattern_at(perturb_pattern, floor, point). This applies both shape and pattern transformations and then delegates to a particular pattern, in our case:

color pattern_at(perturb_pattern const& pattern, pnt3 const& world_point) noexcept {
    shape stub{stub_shape{}};
    float jitter = perlin_noise(world_point.x, world_point.y, world_point.z);
    return pattern_at(pattern.pattern, stub,
                      {world_point.x + pattern.scale * jitter,
                       world_point.y + pattern.scale * jitter,
                       world_point.z + pattern.scale * jitter});
}

Here we calculate get the jitter in range [0;1] from Perlin noise function, scale it by some constant (.2 by default, but we also tried 1 previously) and delegate the resulting point to subpattern's main dispatch function (see above), which this time applies subpattern's transformations. The stub_shape contains just an identity matrix. In our case, the subpattern is the stripe_pattern, and so pattern_at(stripe_pattern, stub_shape, point) calls stripe_pattern's function:

color pattern_at(stripe_pattern const& pattern, pnt3 const& world_point) noexcept {
    shape stub{stub_shape{}};
    if (int(std::floor(world_point.x)) % 2 == 0) {
        return pattern_at(pattern.first, stub, world_point);
    }
    return pattern_at(pattern.second, stub, world_point);
}

Based on whether point.x is odd or even, the first or the second subpattern of stripe_pattern is used in a call to the main dispatch function. In our case, the innermost subpattern is the solid_pattern, so again, pattern_at(solid_subpattern, stub_shape, point) is called to apply transformations, which finally calls

color pattern_at(solid_pattern const& pattern, pnt3 const& /*unused*/) noexcept {
    return pattern.color;
}

and the single color is returned. I would like to stress that excluding Perlin noise this implementation works just fine with other patterns and subpatterns, and was provided for better understanding, there shouldn't be an error here.

Now, here is my Perlin noise implementation. It is based on the original implementation of improved Perlin noise with the grad() replaced by the one from this blog post.

float perlin_noise(float x, float y, float z) noexcept;

namespace {

constexpr int p[] = {
    151, 160, 137, 91,  90,  15,  131, 13,  201, 95,  96,  53,  194, 233, 7,   225, 140, 36,  103,
    30,  69,  142, 8,   99,  37,  240, 21,  10,  23,  190, 6,   148, 247, 120, 234, 75,  0,   26,
    197, 62,  94,  252, 219, 203, 117, 35,  11,  32,  57,  177, 33,  88,  237, 149, 56,  87,  174,
    20,  125, 136, 171, 168, 68,  175, 74,  165, 71,  134, 139, 48,  27,  166, 77,  146, 158, 231,
    83,  111, 229, 122, 60,  211, 133, 230, 220, 105, 92,  41,  55,  46,  245, 40,  244, 102, 143,
    54,  65,  25,  63,  161, 1,   216, 80,  73,  209, 76,  132, 187, 208, 89,  18,  169, 200, 196,
    135, 130, 116, 188, 159, 86,  164, 100, 109, 198, 173, 186, 3,   64,  52,  217, 226, 250, 124,
    123, 5,   202, 38,  147, 118, 126, 255, 82,  85,  212, 207, 206, 59,  227, 47,  16,  58,  17,
    182, 189, 28,  42,  223, 183, 170, 213, 119, 248, 152, 2,   44,  154, 163, 70,  221, 153, 101,
    155, 167, 43,  172, 9,   129, 22,  39,  253, 19,  98,  108, 110, 79,  113, 224, 232, 178, 185,
    112, 104, 218, 246, 97,  228, 251, 34,  242, 193, 238, 210, 144, 12,  191, 179, 162, 241, 81,
    51,  145, 235, 249, 14,  239, 107, 49,  192, 214, 31,  181, 199, 106, 157, 184, 84,  204, 176,
    115, 121, 50,  45,  127, 4,   150, 254, 138, 236, 205, 93,  222, 114, 67,  29,  24,  72,  243,
    141, 128, 195, 78,  66,  215, 61,  156, 180, // and again
    151, 160, 137, 91,  90,  15,  131, 13,  201, 95,  96,  53,  194, 233, 7,   225, 140, 36,  103,
    30,  69,  142, 8,   99,  37,  240, 21,  10,  23,  190, 6,   148, 247, 120, 234, 75,  0,   26,
    197, 62,  94,  252, 219, 203, 117, 35,  11,  32,  57,  177, 33,  88,  237, 149, 56,  87,  174,
    20,  125, 136, 171, 168, 68,  175, 74,  165, 71,  134, 139, 48,  27,  166, 77,  146, 158, 231,
    83,  111, 229, 122, 60,  211, 133, 230, 220, 105, 92,  41,  55,  46,  245, 40,  244, 102, 143,
    54,  65,  25,  63,  161, 1,   216, 80,  73,  209, 76,  132, 187, 208, 89,  18,  169, 200, 196,
    135, 130, 116, 188, 159, 86,  164, 100, 109, 198, 173, 186, 3,   64,  52,  217, 226, 250, 124,
    123, 5,   202, 38,  147, 118, 126, 255, 82,  85,  212, 207, 206, 59,  227, 47,  16,  58,  17,
    182, 189, 28,  42,  223, 183, 170, 213, 119, 248, 152, 2,   44,  154, 163, 70,  221, 153, 101,
    155, 167, 43,  172, 9,   129, 22,  39,  253, 19,  98,  108, 110, 79,  113, 224, 232, 178, 185,
    112, 104, 218, 246, 97,  228, 251, 34,  242, 193, 238, 210, 144, 12,  191, 179, 162, 241, 81,
    51,  145, 235, 249, 14,  239, 107, 49,  192, 214, 31,  181, 199, 106, 157, 184, 84,  204, 176,
    115, 121, 50,  45,  127, 4,   150, 254, 138, 236, 205, 93,  222, 114, 67,  29,  24,  72,  243,
    141, 128, 195, 78,  66,  215, 61,  156, 180};

float fade(float t) { return t * t * t * (t * (t * 6 - 15) + 10); }

// https://riven8192.blogspot.com/2010/08/calculate-perlinnoise-twice-as-fast.html
float grad(int hash, float x, float y, float z) {
    switch (hash & 0xF) {
    case 0x0:
        return x + y;
    case 0x1:
        return -x + y;
    case 0x2:
        return x - y;
    case 0x3:
        return -x - y;
    case 0x4:
        return x + z;
    case 0x5:
        return -x + z;
    case 0x6:
        return x - z;
    case 0x7:
        return -x - z;
    case 0x8:
        return y + z;
    case 0x9:
        return -y + z;
    case 0xA:
        return y - z;
    case 0xB:
        return -y - z;
    case 0xC:
        return y + x;
    case 0xD:
        return -y + z;
    case 0xE:
        return y - x;
    case 0xF:
        return -y - z;
    default:
        return 0; // never happens
    }
}

} // namespace

float perlin_noise(float x, float y, float z) noexcept {
    float const floor_x = std::floor(x);
    float const floor_y = std::floor(y);
    float const floor_z = std::floor(z);

    int const X = static_cast<int>(floor_x) & 255;
    int const Y = static_cast<int>(floor_y) & 255;
    int const Z = static_cast<int>(floor_z) & 255;

    x -= floor_x;
    y -= floor_y;
    z -= floor_z;

    float const u = fade(x);
    float const v = fade(y);
    float const w = fade(z);

    int const A = p[X] + Y;
    int const AA = p[A] + Z;
    int const AB = p[A + 1] + Z;
    int const B = p[X + 1] + Y;
    int const BA = p[B] + Z;
    int const BB = p[B + 1] + Z;

    return std::lerp(
        std::lerp(std::lerp(grad(p[AA], x, y, z), grad(p[BA], x - 1, y, z), u),
                  std::lerp(grad(p[AB], x, y - 1, z), grad(p[BB], x - 1, y - 1, z), u), v),
        std::lerp(
            std::lerp(grad(p[AA + 1], x, y, z - 1), grad(p[BA + 1], x - 1, y, z - 1), u),
            std::lerp(grad(p[AB + 1], x, y - 1, z - 1), grad(p[BB + 1], x - 1, y - 1, z - 1), u),
            v),
        w);
}

The std::lerp(a,b,t) is the usual a+t(b-a).

The whole project can be found on GitHub, but these should be the only relevant parts.

In general, I am very confused why particularly 45 degree rotation is causing trouble. Shouldn't all rotations cause trouble then? Shouldn't -45 degree rotation cause trouble then? Perhaps there is some corner case in the improved Perlin noise algorithm?

For completeness, here is the result with -45 degree rotation and again the "mild" .2 coefficient:

$\endgroup$

0

Your Answer

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

Browse other questions tagged or ask your own question.