Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementation of a Perlin noise Generator in C#

I've been trying to code my own implementation of the Perlin noise algorithm based on this paper.

I initially struggled a lot with the implementation, but I managed to get it working so I thought that I'd share the code.

Here is my initial attempt:

using System;
using System.Drawing;

class PerlinNoise2D
{
    // Fade function for smoothing transitions
    private static double Fade(double t)
    {
        return t * t * t * (t * (t * 6 - 15) + 10);
    }

    // Linear interpolation function
    private static double Lerp(double t, double a, double b)
    {
        return a + t * (b - a);
    }

    // Dot product of gradient and displacement vectors
    private static double DotGridGradient(int gridX, int gridY, double x, double y, Random rand)
    {
        // Generate a pseudo-random gradient vector at the grid point
        double angle = rand.NextDouble() * Math.PI * 2;
        double gradX = Math.Cos(angle);
        double gradY = Math.Sin(angle);

        // Displacement vector from grid point to input point
        double dx = x - gridX;
        double dy = y - gridY;

        // Return dot product
        return (dx * gradX + dy * gradY);
    }

    // Noise function for a single layer
    public static double Noise(double x, double y, int gridSize, Random rand)
    {
        // Identify the grid cell the point is in
        int x0 = (int)Math.Floor(x) % gridSize;
        int y0 = (int)Math.Floor(y) % gridSize;
        int x1 = (x0 + 1) % gridSize;
        int y1 = (y0 + 1) % gridSize;

        // Local coordinates within the grid cell
        double localX = x - Math.Floor(x);
        double localY = y - Math.Floor(y);

        // Apply fade function to smooth transitions
        double xFade = Fade(localX);
        double yFade = Fade(localY);

        // Compute dot products with gradients at each corner
        double n00 = DotGridGradient(x0, y0, x, y, rand);
        double n10 = DotGridGradient(x1, y0, x, y, rand);
        double n01 = DotGridGradient(x0, y1, x, y, rand);
        double n11 = DotGridGradient(x1, y1, x, y, rand);

        // Interpolate along x for the two rows
        double nx0 = Lerp(xFade, n00, n10);
        double nx1 = Lerp(xFade, n01, n11);

        // Interpolate along y for the final noise value
        return Lerp(yFade, nx0, nx1);
    }

    // Perlin noise with multiple octaves for fractal-like detail
    public static double Perlin(double x, double y, int gridSize, int octaves, double persistence)
    {
        double total = 0;
        double frequency = 1;
        double amplitude = 1;
        double maxValue = 0;

        // Random seed for consistent results (same noise pattern for the same inputs)
        Random rand = new(69);

        // Iterates through octaves (layers of noise)
        for (int i = 0; i < octaves; i++)
        {
            // Generates noise for current octave (scaled by frequency and amplitude)
            total += Noise(x * frequency, y * frequency, gridSize, rand) * amplitude;

            // Amplitude added to max possible value
            maxValue += amplitude;

            // Persistence is a factor between 0 and 1, which dictates how much each successive octave contributes
            amplitude *= persistence;

            // Frequency is doubled as higher octaves have smaller frequencies
            frequency *= 2;
        }

        // Returns noise normalized to a [0, 1] range
        return total / maxValue;
    }

    static void Main()
    {
        // Configuration for the Perlin noise
        int width = 512;           // Width of the output image
        int height = 512;          // Height of the output image
        int gridSize = 16;         // Size of the grid for noise
        int octaves = 4;           // Number of noise octaves
        double persistence = 0.5;  // Persistence factor

        // Create a bitmap to store the Perlin noise
        Bitmap bitmap = new Bitmap(width, height);

        // Generate Perlin noise for each pixel
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // Normalize coordinates for Perlin noise
                double noiseX = (double)x / width * gridSize;
                double noiseY = (double)y / height * gridSize;

                // Compute Perlin noise value using the provided implementation
                double noiseValue = Perlin(noiseX, noiseY, gridSize, octaves, persistence);

                // Map noise value to grayscale (0-255)
                int gray = (int)(noiseValue * 255);
                gray = Math.Max(0, Math.Min(255, gray)); // Ensure it's within valid range

                Color color = Color.FromArgb(gray, gray, gray);

                // Set the pixel color in the bitmap
                bitmap.SetPixel(x, y, color);
            }
        }

        // Save the image to a file
        bitmap.Save("PerlinNoise.png");
        Console.WriteLine("Perlin noise image saved as 'PerlinNoise.png'.");
    }
}

And the image created looks like this:

Resultant image

After much tinkering, this was my final code:

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

public class PerlinNoise
{
    private readonly int[] permutation;

    public PerlinNoise(int seed = 0)
    {
        Random rand = new Random(seed);

        // Create permutation table
        permutation = new int[512];
        for (int i = 0; i < 256; i++)
        {
            permutation[i] = i;
        }

        // Shuffle the array
        for (int i = 255; i > 0; i--)
        {
            int j = rand.Next(i + 1);
            (permutation[i], permutation[j]) = (permutation[j], permutation[i]);
        }

        // Duplicate the permutation table to avoid overflow
        for (int i = 0; i < 256; i++)
        {
            permutation[256 + i] = permutation[i];
        }
    }

    private static double Fade(double t)
    {
        // 6t^5 - 15t^4 + 10t^3 (Improved smoothing function by Ken Perlin)
        return t * t * t * (t * (t * 6 - 15) + 10);
    }

    private static double Lerp(double t, double a, double b)
    {
        return a + t * (b - a);
    }

    private static double Grad(int hash, double x, double y)
    {
        // Convert low 4 bits of hash code into 12 gradient directions
        int h = hash & 15;
        double u = h < 8 ? x : y;
        double v = h < 4 ? y : (h == 12 || h == 14 ? x : 0);
        return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
    }

    public double Noise(double x, double y)
    {
        // Find unit cube that contains point
        int X = (int)Math.Floor(x) & 255;
        int Y = (int)Math.Floor(y) & 255;

        // Find relative x, y of point in cube
        x -= Math.Floor(x);
        y -= Math.Floor(y);

        // Compute fade curves for each of x, y
        double u = Fade(x);
        double v = Fade(y);

        // Hash coordinates of the 4 cube corners
        int A = permutation[X] + Y;
        int AA = permutation[A];
        int AB = permutation[A + 1];
        int B = permutation[X + 1] + Y;
        int BA = permutation[B];
        int BB = permutation[B + 1];

        // Add blended results from 4 corners of cube
        double res = Lerp(v,
            Lerp(u,
                Grad(permutation[AA], x, y),
                Grad(permutation[BA], x - 1, y)
            ),
            Lerp(u,
                Grad(permutation[AB], x, y - 1),
                Grad(permutation[BB], x - 1, y - 1)
            )
        );

        return res;
    }

    public double OctaveNoise(double x, double y, int octaves, double persistence = 0.5)
    {
        double total = 0;
        double frequency = 1;
        double amplitude = 1;
        double maxValue = 0;

        for (int i = 0; i < octaves; i++)
        {
            total += Noise(x * frequency, y * frequency) * amplitude;
            maxValue += amplitude;
            amplitude *= persistence;
            frequency *= 2;
        }

        return total / maxValue;
    }

    public void GenerateNoiseImage(string outputPath, int width, int height, double scale = 10.0, int octaves = 4, double persistence = 0.5)
    {
        using (Bitmap bitmap = new Bitmap(width, height))
        {
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    // Sample the noise at scaled coordinates
                    double nx = x / scale;
                    double ny = y / scale;

                    // Get noise value
                    double value = OctaveNoise(nx, ny, octaves, persistence);

                    // Normalize the value from [-1, 1] to [0, 1]
                    value = (value + 1) * 0.5;

                    // Convert to grayscale color (0-255)
                    int grayscale = (int)(value * 255);
                    grayscale = Math.Max(0, Math.Min(255, grayscale)); // Clamp values

                    Color pixelColor = Color.FromArgb(grayscale, grayscale, grayscale);
                    bitmap.SetPixel(x, y, pixelColor);
                }
            }

            // Save the bitmap
            bitmap.Save(outputPath, ImageFormat.Png);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Create a new Perlin noise generator with a seed
        PerlinNoise perlin = new PerlinNoise(42);

        // Define the output path
        string outputPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "perlin_noise.png");

        // Generate different variations of noise images

        // Standard noise
        perlin.GenerateNoiseImage(
            outputPath,
            width: 800,
            height: 600,
            scale: 50.0,
            octaves: 6,
            persistence: 0.5
        );

        // More detailed noise (higher frequency)
        perlin.GenerateNoiseImage(
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "perlin_noise_detailed.png"),
            width: 800,
            height: 600,
            scale: 25.0,
            octaves: 8,
            persistence: 0.6
        );

        // Smoother noise (lower frequency)
        perlin.GenerateNoiseImage(
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "perlin_noise_smooth.png"),
            width: 800,
            height: 600,
            scale: 100.0,
            octaves: 4,
            persistence: 0.4
        );

        Console.WriteLine($"Images have been generated in: {AppDomain.CurrentDomain.BaseDirectory}");
        Console.WriteLine("1. perlin_noise.png - Standard noise");
        Console.WriteLine("2. perlin_noise_detailed.png - More detailed noise");
        Console.WriteLine("3. perlin_noise_smooth.png - Smoother noise");
    }
}

And the produced images are as follows:

Perlin Noise Perlin Noise - Detailed Perlin Noise - Smoothed

like image 980
Ramennoodles Avatar asked Oct 16 '25 19:10

Ramennoodles


1 Answers

Your DotGridGradient should always produce the same random number for the same grid coordinates. As far as I can see it does not, since it is recreated for each pixel, with the same seed, and that probably explains the repeating pattern you are seeing.

The easiest solution might be to just pre-generate the gradient grid(s). Another possible solution would be to use the grid coordinates as the seed for the generator. I have never written a perlin generator, so I'm not sure what solution is preferred.

Some other suggestions

  • When debugging, start with the simplest possible case, like a 2x2 grid and a single octave. That tend to make it easier to follow along in the program and figure out what is going on.
  • Code like this practically begs for a Vector2-type, so you do not have to repeat the same calculations for x and y in every single step. There are a bunch of vector libraries available, see math.net or System.numerics as a start. Or write your own.
like image 76
JonasH Avatar answered Oct 18 '25 07:10

JonasH