Шум Перлина на C#
Шум Перлина — алгоритм генерации текстур, который придумал Кен Перлин в 1983 году. Позже он улучшил свой алгоритм, который теперь так и называется — улучшенный шум Перлина.
Особенность такой генерации в том, что шум Перлина генерирует случайные числа для любой точки пространства, но случайные значения отличаются незначительно для точек, находящихся недалеко друг от друга.
Рассмотрим реализаию для плоскости, т.е. у каждой точки 2 координаты.
Для начала всем точкам плоскости с целыми координатами сопоставляется случайный вектор. В оригинальном алгоритме это был любой вектор единичной длины, в улучшенной версии алгоритма то должен быть 1 из следующих векторов: (1, 0), (-1, 0), (0, 1), (0, -1).
В оригинальной версии алгоритма для этого использовалася хеш от координат, я же для простоты ограничиваю пространство и выбираю случайный вектор для каждой точки. Это неоптимально с точки зрения производительности, но проще в реализации.
Сначала определим класс вектора
private class FloatVector { public double X { get; set; } public double Y { get; set; } public FloatVector () { } public FloatVector (double x, double y) { this.X = x; this.Y = y; } }
и сгенерируем вектора для всех нужных точек. Я это сделал в конструкторе
private FloatVector[, ] grid; private FloatVector[] availableGradients; private Random random = new Random (); public PerlinNoiseGenerator (int width, int height) { availableGradients = new FloatVector[] { new FloatVector(1, 0), new FloatVector(-1, 0), new FloatVector(0, 1), new FloatVector(0, -1) }; grid = new FloatVector[width, height]; for (var i = 0; i < width; i++) { for (var j = 0; j < height; j++) { grid[i, j] = availableGradients[random.Next(3)]; } } }
При генерации значения для точки (x, y) находим ближайшие к ней точки с целочисленными координатами (x0, y0), (x0, y1), (x1, y0), (x1, y1).
Далее для каждой точки считаем скалярное произведение двух векторов: случайного, сгенерированного ранее, и вектора до точки (x, y).
int x0 = (int) x; double dx = x - x0; int y0 = (int) y; double dy = y - y0; var vx0y0 = grid[x0, y0]; var vx0y1 = grid[x0, y0 + 1]; var vx1y0 = grid[x0 + 1, y0]; var vx1y1 = grid[x0 + 1, y0 + 1]; var s = MultiplyVectors (vx0y0, new FloatVector (x - x0, y - y0)); var t = MultiplyVectors (vx1y0, new FloatVector (x - x0 - 1, y - y0)); var u = MultiplyVectors (vx0y1, new FloatVector (x - x0, y - y0 - 1)); var v = MultiplyVectors (vx1y1, new FloatVector (x - x0 - 1, y - y0 - 1));
функция скалярного произведения двух векторов выглядит так:
private double MultiplyVectors (FloatVector v1, FloatVector v2) { return v1.X * v2.X + v1.Y * v2.Y; }
Мы получили случайные числа для каждой из соседних елочисленных точек. Теперь считаем средневзвешенное значение. В качестве веса используем расстояние до точки (x, y) по каждой из осей.
var a = GetWeightedAverage(s, t, dx); var b = GetWeightedAverage(u, v, dx); var c = GetWeightedAverage(a, b, dy); private double GetWeightedAverage(double a, double b, double weight) { return a + Fade(weight) * (b - a); }
Особенность этой реализации средневзвешенного в том, что вес дополнительно прогоняется через функцию Fade
private double Fade (double t) { return 6 * t * t * t * t * t - 15 * t * t * t * t + 10 * t * t * t; }
Эта функция как бы прижимает значение к одному из краев отрезка [0, 1]. Т.е. если X близок к 0, то Fade(X) еще ближе к 0. При этом
Fade(0) == 0
Fade(1) == 1
Fade(0.5) == 0.5
Теперь осталось только вернуть нормализованное значение
return Normalize(c); private double Normalize (double x) { return (x + 1) / 2; }
Функция Normalize переводит отрезок [-1, 1] в [0, 1].
Получившийся код позволяет генерировать вот такие текстуры: