DevNotes

Алгоритмы · Разработка игр

Магия процедурной генерации: как создаются виртуальные миры

Ни один художник не рисует каждую гору в открытом мире вручную — за холмы, облака и текстуры камня отвечает математика. Разбираемся, как устроен шум Перлина и почему он стал стандартом процедурной генерации.

Редакция DevNotes • 12 мин чтения

2D-шум Перлина, сгенерированный прямо в браузере

Что такое шум Перлина

В 1983 году разработчик Кен Перлин искал способ сделать компьютерную графику менее «пластиковой». Случайные числа давали белый шум — хаотичную рябь без всякой структуры. Ему же нужна была плавная, естественная неровность: такая, какую мы видим в облаках, древесных волокнах или рельефе местности.

Решением стал градиентный шум — функция, которая на каждой точке пространства возвращает не случайное, а согласованное значение: соседние точки получают близкие результаты, а удалённые — почти независимые. Так рождается плавный, органический узор, который можно масштабировать, накладывать слоями и превращать во что угодно — от высоты холма до плотности облака.

Где он используется в играх

Шум Перлина и его производные (симплекс-шум, value noise, worley noise) встречаются почти везде, где нужна «случайная, но правдоподобная» вариативность:

Как это работает изнутри

Упрощённо алгоритм для двумерного шума выглядит так: пространство разбивается на сетку, в каждом узле которой лежит случайный вектор-градиент. Для произвольной точки внутри ячейки вычисляется скалярное произведение между вектором до узла и градиентом в этом узле — отдельно для всех четырёх углов ячейки. Затем результаты сглаживаются функцией плавности (fade) и интерполируются — сначала по оси X, затем по оси Y.

perlin.pseudo
// Двумерный шум Перлина в точке (x, y)
function perlin2D(x, y):
    x0 = floor(x)
    x1 = x0 + 1
    y0 = floor(y)
    y1 = y0 + 1

    // сглаживающая интерполяция, а не линейная
    sx = fade(x - x0)
    sy = fade(y - y0)

    // скалярное произведение градиента и вектора до узла
    n00 = dotGridGradient(x0, y0, x, y)
    n10 = dotGridGradient(x1, y0, x, y)
    ix0 = lerp(n00, n10, sx)

    n01 = dotGridGradient(x0, y1, x, y)
    n11 = dotGridGradient(x1, y1, x, y)
    ix1 = lerp(n01, n11, sx)

    return lerp(ix0, ix1, sy)  // значение в диапазоне [-1, 1]

На практике редко используют один «слой» шума — обычно накладывают несколько октав с уменьшающейся амплитудой и растущей частотой (так называемый fractal Brownian motion). Это добавляет крупные формы рельефа вместе с мелкими деталями — примерно как в примере на изображении выше, сгенерированном прямо в вашем браузере.

Совет практикующим. Для игр в реальном времени чаще берут симплекс-шум (Simplex noise) — тот же принцип Перлина, но с меньшим числом вычислений на точку и без выраженных артефактов по осям сетки.