如何在C#中实现Perlin噪声生成器?
2D Perlin噪声的C#实现与调试过程
基于马里兰大学的Perlin噪声课程资料,我尝试自行编码实现2D Perlin噪声算法,过程中遇到不少困难,最终完成后决定分享代码。
初始尝试代码
using System; using System.Drawing; class PerlinNoise2D { // 用于平滑过渡的Fade函数 private static double Fade(double t) { 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 DotGridGradient(int gridX, int gridY, double x, double y, Random rand) { // 在网格点生成伪随机梯度向量 double angle = rand.NextDouble() * Math.PI * 2; double gradX = Math.Cos(angle); double gradY = Math.Sin(angle); // 从网格点到输入点的位移向量 double dx = x - gridX; double dy = y - gridY; // 返回点积 return (dx * gradX + dy * gradY); } // 单层级噪声函数 public static double Noise(double x, double y, int gridSize, Random rand) { // 确定点所在的网格单元 int x0 = (int)Math.Floor(x) % gridSize; int y0 = (int)Math.Floor(y) % gridSize; int x1 = (x0 + 1) % gridSize; int y1 = (y0 + 1) % gridSize; // 网格单元内的局部坐标 double localX = x - Math.Floor(x); double localY = y - Math.Floor(y); // 应用Fade函数平滑过渡 double xFade = Fade(localX); double yFade = Fade(localY); // 计算四个角点的梯度点积 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); // 沿X轴插值两行结果 double nx0 = Lerp(xFade, n00, n10); double nx1 = Lerp(xFade, n01, n11); // 沿Y轴插值得到最终噪声值 return Lerp(yFade, nx0, nx1); } // 多八度的Perlin噪声,生成分形细节 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 rand = new(69); // 遍历所有八度层级 for (int i = 0; i < octaves; i++) { // 生成当前八度的噪声并缩放 total += Noise(x * frequency, y * frequency, gridSize, rand) * amplitude; // 累加最大可能值用于归一化 maxValue += amplitude; // 衰减系数控制后续八度的贡献度 amplitude *= persistence; // 每八度频率翻倍 frequency *= 2; } // 归一化到[0,1]范围 return total / maxValue; } static void Main() { // Perlin噪声配置参数 int width = 512; // 输出图像宽度 int height = 512; // 输出图像高度 int gridSize = 16; // 噪声网格尺寸 int octaves = 4; // 八度数量 double persistence = 0.5; // 衰减系数 // 创建存储噪声的位图 Bitmap bitmap = new Bitmap(width, height); // 为每个像素生成Perlin噪声 for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // 归一化坐标以适配噪声网格 double noiseX = (double)x / width * gridSize; double noiseY = (double)y / height * gridSize; // 计算Perlin噪声值 double noiseValue = Perlin(noiseX, noiseY, gridSize, octaves, persistence); // 将噪声值映射到灰度范围(0-255) int gray = (int)(noiseValue * 255); gray = Math.Max(0, Math.Min(255, gray)); // 确保值在有效范围内 Color color = Color.FromArgb(gray, gray, gray); // 设置像素颜色 bitmap.SetPixel(x, y, color); } } // 保存图像到文件 bitmap.Save("PerlinNoise.png"); Console.WriteLine("Perlin噪声图像已保存为'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); // 创建置换表 permutation = new int[512]; for (int i = 0; i < 256; i++) { permutation[i] = i; } // 打乱数组 for (int i = 255; i > 0; i--) { int j = rand.Next(i + 1); (permutation[i], permutation[j]) = (permutation[j], permutation[i]); } // 复制置换表以避免越界 for (int i = 0; i < 256; i++) { permutation[256 + i] = permutation[i]; } } private static double Fade(double t) { // Ken Perlin改进的平滑函数:6t^5 - 15t^4 + 10t^3 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) { // 将哈希值的低4位转换为12种梯度方向 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) { // 找到包含点的单位网格 int X = (int)Math.Floor(x) & 255; int Y = (int)Math.Floor(y) & 255; // 计算点在网格内的相对坐标 x -= Math.Floor(x); y -= Math.Floor(y); // 计算x和y的平滑曲线 double u = Fade(x); double v = Fade(y); // 哈希四个网格角点的坐标 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]; // 混合四个角点的结果 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++) { // 缩放坐标采样噪声 double nx = x / scale; double ny = y / scale; // 获取噪声值 double value = OctaveNoise(nx, ny, octaves, persistence); // 将噪声值从[-1,1]归一化到[0,1] value = (value + 1) * 0.5; // 转换为灰度颜色(0-255) int grayscale = (int)(value * 255); grayscale = Math.Max(0, Math.Min(255, grayscale)); // 限制值范围 Color pixelColor = Color.FromArgb(grayscale, grayscale, grayscale); bitmap.SetPixel(x, y, pixelColor); } } // 保存位图 bitmap.Save(outputPath, ImageFormat.Png); } } } class Program { static void Main(string[] args) { // 创建带种子的Perlin噪声生成器 PerlinNoise perlin = new PerlinNoise(42); // 定义输出路径 string outputPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "perlin_noise.png"); // 生成不同风格的噪声图像 // 标准噪声 perlin.GenerateNoiseImage( outputPath, width: 800, height: 600, scale: 50.0, octaves: 6, persistence: 0.5 ); // 细节丰富的噪声(更高频率) perlin.GenerateNoiseImage( Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "perlin_noise_detailed.png"), width: 800, height: 600, scale: 25.0, octaves: 8, persistence: 0.6 ); // 平滑噪声(更低频率) 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($"图像已生成在:{AppDomain.CurrentDomain.BaseDirectory}"); Console.WriteLine("1. perlin_noise.png - 标准噪声"); Console.WriteLine("2. perlin_noise_detailed.png - 细节丰富版"); Console.WriteLine("3. perlin_noise_smooth.png - 平滑版"); } }
最终生成图像
- 标准噪声:

- 细节丰富版:

- 平滑版:

内容的提问来源于stack exchange,提问作者Ramennoodles
相关产品推荐
相关产品推荐

