C# roguelike, devlog 10: Improved Perlin noise

  1. Introduction
  2. Implementation
  3. Conclusion

Introduction


A more or less essential component needed for procedural generation is a good noise algorithm. In order to start working on terrain generation I’ll first add this to the project. I’ve opted for the Improved Perlin Noise algorithm. But another good alternative that is ready and available for C# is OpenSimplex2

This implementation is based on the following sources:

Implementation


This is a C# adaptation of the 4D Java reference implementation from Ken Perlin (linked in the list above) with methods for octaves added.

The Perlin class contains the following methods for generating noise:

using System.Numerics;

/// <summary>
/// Improved Perlin Noise
/// </summary>
public static class Perlin {    
    //Fields    
    private static readonly int[] p = new int[512];
    private static readonly int[] permutation = { 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
    };

    // Constructor
    static Perlin() { for(int i = 0; i < 512; i++) { p[i] = permutation[i % 256]; } }

    // Linear interpolation between two floats
    public static float Lerp(float x, float a, float b) { return a + x * (b - a); }
    
    // Apply an ease curve to a float value by using the function 6t^5 - 15t^4 + 10t^3
    public static float Fade(float t) { return t * t * t * (t * (t * 6f - 15f) + 10f); }

    // Convert hash to 12 gradient directions
    public static float Grad3(int hash, float x, float y, float z) {
        // Mask lo 4 bits of hash code
        // h = 1 1 1 1
        //     | | | |
        //     | | | bit 0
        //     | | bit 1
        //     | bit 2
        //     bit 3
        int h = (hash & 15);
        
        // u = x if hash bit 3 is 0
        float u = h < 8 ? x : y;
		
        // v = y if hash bit 2 and 3 are both 0
        // else: v = x if hash bits are 1100 or 1110
        // else: v = z
        float v = h < 4 ? y : h == 12 || h == 14 ? x : z;
    	
        // Set u to negative if bit 0 is 1
        // Set v to negative if bit 1 is 1
        // Return u + v
        return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
    }

    // Convert hash to 32 gradient directions
    public static float Grad4(int hash, float x, float y, float z, float w) {
        // Mask lo 5 bits of hash code
        // h = 1 1 1 1 1
        //     | | | | |
        //     | | | | bit 0
        //     | | | bit 1
        //     | | bit 2
        //     | bit 3
        //     bit 4
        int h = (hash & 31);
        // Set default a, b, c to x, y, z
        float a=y, b=z, c=w;
        // Check bit 3 & 4
        switch (h >> 3)
        {
            // Bit 3 is 1 and bit 4 is 0
            case 1: a=w; b=x; c=y; break;
            // Bit 3 is 0 and bit 4 is 1
            case 2: a=z; b=w; c=x; break;
            // Both bit 3 and 4 are 1
            case 3: a=y; b=z; c=w; break;
        }
        // Set a to negative if bit 2 is 0
        // Set b to negative if bit 1 is 0
        // Set c to negative if bit 0 is 0
        // Return a + b + c
        return ((h & 4) == 0 ? -a : a) + ((h & 2) == 0 ? -b : b) + ((h & 1) == 0 ? -c : c);
    }
    
    // Generate 3D noise from a 2D position and seed value
    public static float Noise3(Vector2 pos, float seed = 0f) {
        // Find unit cube that contains point
        int x = (int)MathF.Floor(pos.X) & 255;
        int y = (int)MathF.Floor(pos.Y) & 255;
        int z = (int)MathF.Floor(seed)  & 255;

        // Find relative position of point in cube
        pos.X -= MathF.Floor(pos.X);
        pos.Y -= MathF.Floor(pos.Y);
        seed  -= MathF.Floor(seed);
        
        // Compute fade curves
        float f = Fade(pos.X);
        float g = Fade(pos.Y);
        float h = Fade(seed);
    
        // Hash coordinates of the cube corners
        int a  = p[x    ] + y;
        int b  = p[x + 1] + y;
        int aa = p[a    ] + z;
        int ab = p[a + 1] + z;
        int ba = p[b    ] + z;
        int bb = p[b + 1] + z;
   
        // Return blend result from all cube corners
        return (Lerp(h, Lerp(g, Lerp(f, Grad3(p[aa    ], pos.X     , pos.Y     , seed   ),
                                        Grad3(p[ba    ], pos.X - 1f, pos.Y     , seed   )),
                                Lerp(f, Grad3(p[ab    ], pos.X     , pos.Y - 1f, seed   ),
                                        Grad3(p[bb    ], pos.X - 1f, pos.Y - 1f, seed   ))),

                        Lerp(g, Lerp(f, Grad3(p[aa + 1], pos.X     , pos.Y     , seed - 1f ),
                                        Grad3(p[ba + 1], pos.X - 1f, pos.Y     , seed - 1f )),
                                Lerp(f, Grad3(p[ab + 1], pos.X     , pos.Y - 1f, seed - 1f ),
                                        Grad3(p[bb + 1], pos.X - 1f, pos.Y - 1f, seed - 1f )))) + 1f) / 2f;
    }
    
    // Generate 4D noise from a 3D position and seed value
    public static float Noise4(Vector3 pos, float seed = 0f) {
        // Find unit hypercube that contains point
        int x = (int)MathF.Floor(pos.X) & 255;
        int y = (int)MathF.Floor(pos.Y) & 255;
        int z = (int)MathF.Floor(pos.Z) & 255;
        int w = (int)MathF.Floor(seed)  & 255;

        // Find relative position of point in hypercube
        pos.X -= MathF.Floor(pos.X);
        pos.Y -= MathF.Floor(pos.Y);
        pos.Z -= MathF.Floor(pos.Z);
        seed  -= MathF.Floor(seed);
        
        // Compute fade curves
        float f = Fade(pos.X);
        float g = Fade(pos.Y);
        float h = Fade(pos.Z);
        float i = Fade(seed);
    
        // Hash coordinates of the hypercube corners
        int a   = p[x     ] + y;
        int b   = p[x  + 1] + y;
        int aa  = p[a     ] + z;
        int ab  = p[a  + 1] + z;
        int ba  = p[b     ] + z;
        int bb  = p[b  + 1] + z;
        int aaa = p[aa    ] + w;
        int aab = p[aa + 1] + w;
        int aba = p[ab    ] + w;
        int abb = p[ab + 1] + w;
        int baa = p[ba    ] + w;
        int bab = p[ba + 1] + w;
        int bba = p[bb    ] + w;
        int bbb = p[bb + 1] + w;
        
        // Return blend result from all hypercube corners
        return (Lerp(i, Lerp(h, Lerp(g, Lerp(f, Grad4(p[aaa    ], pos.X     , pos.Y     , pos.Z     , seed),
                                                Grad4(p[baa    ], pos.X - 1f, pos.Y     , pos.Z     , seed)),
                                        Lerp(f, Grad4(p[aba    ], pos.X     , pos.Y - 1f, pos.Z     , seed),
                                                Grad4(p[bba    ], pos.X - 1f, pos.Y - 1f, pos.Z     , seed))),

                                Lerp(g, Lerp(f, Grad4(p[aab    ], pos.X     , pos.Y     , pos.Z - 1f, seed),
                                                Grad4(p[bab    ], pos.X - 1f, pos.Y     , pos.Z - 1f, seed)),
                                        Lerp(f, Grad4(p[abb    ], pos.X     , pos.Y - 1f, pos.Z - 1f, seed),
                                                Grad4(p[bbb    ], pos.X - 1f, pos.Y - 1f, pos.Z - 1f, seed)))),

                        Lerp(h, Lerp(g, Lerp(f, Grad4(p[aaa + 1], pos.X     , pos.Y     , pos.Z     , seed - 1f),
                                                Grad4(p[baa + 1], pos.X - 1f, pos.Y     , pos.Z     , seed - 1f)),
                                        Lerp(f, Grad4(p[aba + 1], pos.X     , pos.Y - 1f, pos.Z     , seed - 1f),
                                                Grad4(p[bba + 1], pos.X - 1f, pos.Y - 1f, pos.Z     , seed - 1f))),

                                Lerp(g, Lerp(f, Grad4(p[aab + 1], pos.X     , pos.Y     , pos.Z - 1f, seed - 1f),
                                                Grad4(p[bab + 1], pos.X - 1f, pos.Y     , pos.Z - 1f, seed - 1f)),
                                        Lerp(f, Grad4(p[abb + 1], pos.X     , pos.Y - 1f, pos.Z - 1f, seed - 1f),
                                                Grad4(p[bbb + 1], pos.X - 1f, pos.Y - 1f, pos.Z - 1f, seed - 1f))))) + 1f) / 2f;
    }

    // Generate combined octaves of 3D noise
    public static float Octave3(Vector2 pos, float seed = 0f, int octaves = 4, float persistence = 0.5f) {
    	float result = 0;
    	float frequency = 1;
    	float amplitude = 1;
    	float amount = 0;
    	for(int i = 0; i < octaves; i++)
        {
    		result += Noise3(pos * frequency, seed) * amplitude;
    		amount += amplitude;
    		amplitude *= persistence;
    		frequency *= 2;
    	}
    	return result / amount;
    }
    
    // Generate combined octaves of 4D noise
    public static float Octave4(Vector3 pos, float seed = 0f, int octaves = 4, float persistence = 0.5f) {
    	float result = 0;
    	float frequency = 1;
    	float amplitude = 1;
    	float amount = 0;
    	for(int i = 0; i < octaves; i++)
        {
    		result += Noise4(pos * frequency, seed) * amplitude;
    		amount += amplitude;
    		amplitude *= persistence;
    		frequency *= 2;
    	}
    	return result / amount;
    }
}

Conclusion


Here is a simple example of generating 3D noise (from a 2D position and a seed value set to 0):

screenshot

Download the demo source code: roguelike-devlog10_demo.zip

Find the project on GitHub: lzzrhx/roguelike