我一直在尝试根据这篇论文编写自己的 Perlin 噪声算法实现。
我最初在实现过程中遇到了很多困难,但我设法让它工作,所以我想我应该分享代码。
这是我最初的尝试:
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'.");
}
}
创建的图像如下所示:
经过多次修改,这是我的最终代码:
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");
}
}
生成的图像如下:
您的
DotGridGradient
应始终为相同的网格坐标生成相同的随机数。据我所知,事实并非如此,因为它是使用相同的种子为每个像素重新创建的,这可能解释了您所看到的重复模式。
最简单的解决方案可能是预先生成梯度网格。另一种可能的解决方案是使用网格坐标作为生成器的种子。我从未编写过 perlin 生成器,所以我不确定首选什么解决方案。
其他一些建议