用 C# 实现 Perlin 噪声发生器

问题描述 投票:0回答:1

我一直在尝试根据这篇论文编写自己的 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'.");
    }
}

创建的图像如下所示:

Resultant image

经过多次修改,这是我的最终代码:

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");
    }
}

生成的图像如下:

Perlin Noise Perlin Noise - Detailed Perlin Noise - Smoothed

c# perlin-noise
1个回答
2
投票

您的

DotGridGradient
应始终为相同的网格坐标生成相同的随机数。据我所知,事实并非如此,因为它是使用相同的种子为每个像素重新创建的,这可能解释了您所看到的重复模式。

最简单的解决方案可能是预先生成梯度网格。另一种可能的解决方案是使用网格坐标作为生成器的种子。我从未编写过 perlin 生成器,所以我不确定首选什么解决方案。

其他一些建议

  • 调试时,从最简单的情况开始,例如 2x2 网格和单个八度音阶。这往往会让您更容易地跟随程序并弄清楚发生了什么。
  • 这样的代码实际上需要 Vector2 类型,因此您不必在每个步骤中对 x 和 y 重复相同的计算。有很多可用的矢量库,请参阅 math.netSystem.numerics 作为开始。或者自己写。
© www.soinside.com 2019 - 2024. All rights reserved.