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:
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:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With