Шум Перлина — это мост между математикой и природой. Он дает нам инструмент для создания бесконечно разнообразных миров и реалистичных природных эффектов. Он перевернул представления о компьютерной графике и принёс своему создателю Оскар. Давайте разбираться, что это такое и как устроен.
Что такое шум Перлина и зачем он нужен?
Представьте, что вы разрабатываете игру и вам нужно создать:
- Реалистичный горный ландшафт
- Плавно движущиеся облака
- Текстуру дерева или мрамора
- Эффект пламени костра
- И т.д.
Если использовать обычный случайный шум (как «снег» на старом телевизоре), получится резкая, неестественная картина. Именно эту проблему решил Кен Перлин в 1983 году, создав алгоритм, который генерирует «естественную случайность».
Шум Перлина — это математический способ создать цифровую природу, где хаос выглядит гармонично и реалистично.
Как сам Перлин описывает своё изобретение:
«Природа полна шумов, но не случайных — в ней есть структура. Вода течёт по руслам, горные хребты формируются тектоникой… Мой шум — это попытка уловить эту скрытую математику природы»
— Ken Perlin, ACM Turing Award Lecture (2021)
История создания

Кен Перлин — создатель алгоритма
Интересный факт: шум Перлина родился в голливудских спецэффектах. Компьютерные изображения того времени не выдерживали никакой критики. Неестественные и ресурсоёмкие картинки выбесили режиссёра фильма «Трон» (1982 год) настолько, что он был вынужден отыскать Кена Перлина и поставить перед ним задачу:
- Создать реалистичные текстуры поверхностей
- Избежать ручной прорисовки каждого кадра
- Сэкономить вычислительные ресурсы
И Кен Перлин решил её! За это революционное решение Перлин получил технического «Оскара» в 1997 году. Его алгоритм стал фундаментом компьютерной графики.
Алгоритм генерации шума Перлина
Шаг 1: Сетка векторов (градиентов)
Представьте лист миллиметровки, наложенный на изображение. В каждой точке пересечения линий (узле сетки) мы размещаем cлучайный вектор (градиент) — «стрелочку» с направлением, длина которого всегда должна быть равна 1.

Сетка с градиентами в каждой точке
Генерация сетки случайных градиентов
|
1 2 3 4 5 6 7 8 |
for J := 0 to FSizeY - 1 do for I := 0 to FSizeX - 1 do begin Angle := Random*2*PI; SinCos(Angle, SinA, CosA); FGrid[I, J].X := CosA; FGrid[I, J].Y := SinA; end; |
Векторы направлены куда угодно, но их длина равна 1 (sin2α + cos2α = 1).
В улучшенном алгоритме Перлина 2002 года длина векторов может быть больше единицы, направление векторов либо по линиям сетки, либо по диагонали ячейки. Но сути это не меняет.
Шаг 2: Точка в ячейке сетки

Точка P в окружении четырех узлов сетки
Для любой точки P с координатами (x,y):
- Находим 4 ближайших узла сетки (G00, G10, G01, G11)
- Вычисляем вектора от каждого узла до точки P
Шаг 3: Скалярные произведения
Для каждого узла ячейки:
- Берем градиент узла
- Умножаем на вектор до точки P
- Получаем значение влияния узла на точку
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
X0 := Trunc(X); Y0 := Trunc(Y); X1 := X0 + 1; Y1 := Y0 + 1; Dx := X - X0; Dy := Y - Y0; // Скалярные произведения DotTL := FGrid[X0, Y0].X * Dx + FGrid[X0, Y0].Y * Dy; DotTR := FGrid[X1, Y0].X *(Dx-1) + FGrid[X1, Y0].Y * Dy; DotBL := FGrid[X0, Y1].X * Dx + FGrid[X0, Y1].Y *(Dy-1); DotBR := FGrid[X1, Y1].X *(Dx-1) + FGrid[X1, Y1].Y *(Dy-1); |
Во избежание вопросов, откуда взялось (Dx-1), вот простой вывод:
X1=X0+1 => X-X1 = X-X0-1 = Dx-1
Шаг 4: Плавное смешивание
Теперь нужно интерполировать эти четыре значения, чтобы получить некое средневзвешенное значение. Используем простую линейную интерполяцию:
|
1 2 3 4 |
class function TPerlinNoise.Lerp(A, B, X: Double): Double; begin Result := A + X * (B - A); end; |
Однако, если просто интерполировать указанным выше способом, конечный результат будет выглядеть плохо, неестественно (это Кен недоволен, не я). Необходим более плавный переход между градиентами.

Для этих целей используем функцию плавного затухания (fade):
f(t) = 6t⁵ - 15t⁴ + 10t³
|
1 2 3 4 |
class function TPerlinNoise.Fade(T: Double): Double; begin Result := T * T * T * (T * (T * 6 - 15) + 10); end; |
Эту функцию предложил Кен в своей статье по поводу улучшения алгоритма, поэтому нет смысла оспаривать мастера.
Таким образом, интерполяция в своём окончательном виде, будет выглядеть так:
|
1 2 3 4 5 6 7 8 9 10 |
// Применение функции затухания Fx := Fade(Dx); Fy := Fade(Dy); // Интерполяция Result := Lerp( Lerp(DotTL, DotTR, Fx), Lerp(DotBL, DotBR, Fx), Fy ); |
Сетка векторов: Пояснения
Представьте, что вы художник и хотите нарисовать холмистый пейзаж. Вам нужно, чтобы холмы плавно переходили друг в друга, без резких скачков. Как этого добиться математически?
Сетка как основа
Пространство разбивается на сетку из ячеек (квадратов или кубов в 3D). В каждой вершине сетки (узле) размещается случайный вектор единичной длины — как стрелка компаса, указывающая в произвольном направлении.
Векторы задают «уклон»
Каждый вектор в узле сетки определяет, как будет изменяться значение шума в окрестностях этой точки. Представьте, что вектор — это направление наибольшего подъёма холма. Если два соседних вектора направлены навстречу друг другу — между ними образуется «долина», если в одну сторону — «склон».
Интерполяция для гладкости
Для любой точки внутри сетки (например, где-то посередине между узлами) алгоритм вычисляет влияние всех окружающих её векторов. Сначала он смотрит, как каждый из четырёх ближайших векторов (в 2D) «тянет» значение в своей зоне, затем плавно смешивает (интерполирует) эти влияния. Именно интерполяция обеспечивает плавные переходы.
Результат
Благодаря такой структуре, шум Перлина не имеет явных повторяющихся паттернов и выглядит как природные образования. Сетка векторов — это «скелет», который придаёт шуму упорядоченную случайность.
Почему именно векторы?
Градиенты: Векторы в узлах — это градиенты (направления роста). Они указывают, в какую сторону значение шума должно увеличиваться быстрее всего.
Скалярное произведение: Влияние вектора на точку вычисляется через скалярное произведение вектора градиента и вектора смещения от узла до точки. Это даёт значение «схожести направлений»: если точка лежит в направлении вектора — значение положительное, если против — отрицательное.
Контролируемая случайность: Случайные векторы гарантируют, что каждый запуск генератора создаёт уникальную текстуру, но при этом сетка обеспечивает согласованность значений в соседних точках.
Онлайн: Как работает сетка векторов
Фрактальная схема
Фрактальная схема отвечает за уровень детализации шума. Её физический смысл в следующем. В природе, на каком бы приближении от объекта не находился, всегда есть свой уровень детализации. Видим гору с самолёта — это общий вид, с земли — видим камни и мох, через лупу — видим текстуру камней. Фрактальная схема делает то же самое с шумом. Это как вложить несколько «уровней приближения» в один алгоритм.
Как это работает в нашем случае. Берем базовый шум Перлина — это наш «вид из самолета», крупные формы. Затем добавляем уменьшенные копии, приближаемся к объекту.
Каждая следующая копия:
- В 2 раза детализированнее (частота ×2)
- В 2-4 раза слабее (амплитуда ÷ persistence)
А потом складываем слои:
Общий шум = Слой1 + Слой2 + Слой3 + …
Такие слои принятой называть октавой. Октава шума — это звучит стильно!
В коде выглядит так:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function TPerlinNoise.FractalPerlinNoise(X, Y: Double; Octaves: Integer; Persistence: Double): Double; var Total: Double; Frequency: Double; Amplitude: Double; MaxAmplitude: Double; i: Integer; begin Total := 0.0; Frequency := 1.0; Amplitude := 1.0; // Для нормализации результата в пределах 0..1 MaxAmplitude := 0.0; for i := 0 to Octaves - 1 do begin Total := Total + PerlinNoise(X*Frequency, Y*Frequency)*Amplitude; MaxAmplitude := MaxAmplitude + Amplitude; Frequency := Frequency * 2; Amplitude := Amplitude * Persistence; end; // Нормализация результата Result := Total / MaxAmplitude; end; |
Природа фрактальна: ветка похожа на дерево, камень — на гору. Фрактальный шум повторяет этот принцип. Количество октав зависит только от желаемого результата. Хотите плавные облака? 3 октавы. Резкие скалы? 8 октав.
Именно благодаря этому, алгоритм 1983 года до сих пор создает самые реалистичные ландшафты в играх и кино. Это не просто математика — это поэзия, воплощенная в коде.
«Перлин показал, что красота природы может быть выражена через элегантную математику» — Джон Кармак, сооснователь id Software
Онлайн: Попробуйте сами!
Ниже живая демонстрация шума Перлина. Поиграйте с параметрами:
Оригинальный Алгоритм Перлина
Кен Перлин опубликовал улучшенный алгоритм генерации в 2002 году и теперь мы все на него ориентируемся. Алгоритм представлен сразу для 3-х координат (X,Y,Z). Приведу оригинал для java:
This code implements the algorithm I describe in a corresponding SIGGRAPH 2002 paper.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// JAVA REFERENCE IMPLEMENTATION OF IMPROVED NOISE - COPYRIGHT 2002 KEN PERLIN. public final class ImprovedNoise { static public double noise(double x, double y, double z) { int X = (int)Math.floor(x) & 255, // FIND UNIT CUBE THAT Y = (int)Math.floor(y) & 255, // CONTAINS POINT. Z = (int)Math.floor(z) & 255; x -= Math.floor(x); // FIND RELATIVE X,Y,Z y -= Math.floor(y); // OF POINT IN CUBE. z -= Math.floor(z); double u = fade(x), // COMPUTE FADE CURVES v = fade(y), // FOR EACH OF X,Y,Z. w = fade(z); int A = p[X ]+Y, AA = p[A]+Z, AB = p[A+1]+Z, // HASH COORDINATES OF B = p[X+1]+Y, BA = p[B]+Z, BB = p[B+1]+Z; // THE 8 CUBE CORNERS, return lerp(w, lerp(v, lerp(u, grad(p[AA ], x , y , z ), // AND ADD grad(p[BA ], x-1, y , z )), // BLENDED lerp(u, grad(p[AB ], x , y-1, z ), // RESULTS grad(p[BB ], x-1, y-1, z ))),// FROM 8 lerp(v, lerp(u, grad(p[AA+1], x , y , z-1 ), // CORNERS grad(p[BA+1], x-1, y , z-1 )), // OF CUBE lerp(u, grad(p[AB+1], x , y-1, z-1 ), grad(p[BB+1], x-1, y-1, z-1 )))); } static double fade(double t) { return t * t * t * (t * (t * 6 - 15) + 10); } static double lerp(double t, double a, double b) { return a + t * (b - a); } static double grad(int hash, double x, double y, double z) { int h = hash & 15; // CONVERT LO 4 BITS OF HASH CODE double u = h<8 ? x : y, // INTO 12 GRADIENT DIRECTIONS. v = h<4 ? y : h==12||h==14 ? x : z; return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v); } static final int p[] = new int[512], permutation[] = { 151,160,137,91,90,15, 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 }; static { for (int i=0; i < 256 ; i++) p[256+i] = p[i] = permutation[i]; } } |
Принципиально всё то же самое. Отличие в способе генерации сетки векторов. Перлин предлагает использовать заранее сформированный массив значений от 0..255. На самом деле, подразумевается, что массив может быть любым, главное, чтобы значения не повторялись.
Подробно рассмотрим этот алгоритм в следующий раз. Не хочется безразмерно расширять статью. Сейчас всё локально — разбор основ алгоритма и основных понятий. Нюансы оригинального алгоритма и примеры — тема отдельной статьи. Подписывайтесь на телегу, все анонсы там.
Листинг и комментарии
Конечно же, предложенный класс не генерирует никаких картинок. Его задача — вернуть матрицу заказанной размерности, которая содержит некие вещественные значения шума. Потом уже делайте с этим, что хотите. Хоть облака, хоть скалы.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
//****************************************************************************** // Project: IP76.RU // Created: 2025-03-27 // Article: https://ip76.ru/perlin-noise // Описание: Реализация шума Перлина для 2D (4 вектора) //****************************************************************************** unit IP76.PerlinNoise; interface type TDoublePoint = record X, Y: Double; end; TDoublePointDynMatrix = array of array of TDoublePoint; TDoubleDynMatrix = array of array of Double; TPerlinNoise = class strict private // Сетка узловых векторов FGrid: TDoublePointDynMatrix; // Размеры сетки FSizeX: Integer; FSizeY: Integer; // Текущее смещение для счётчика случайных чисел FSeed: Integer; private // Функция затухания для сглаживания class function Fade(T: Double): Double; static; // Линейная интерполяция class function Lerp(A, B, X: Double): Double; static; // Корректировка индексов class function ModPositive(N, M: Integer): Integer; static; // Вычисление шума Перлина для точки function PerlinNoise(X, Y: Double): Double; // Фрактальный шум Перлина (сумма октав) function FractalPerlinNoise(X, Y: Double; Octaves: Integer=1; Persistence: Double=1.0): Double; public // Инициализация сетки градиентов procedure InitializeGrid(Seed: Integer; SizeX: Integer=256; SizeY: Integer=256); // Генерация шума Перлина function GeneratePerlinNoise(Width, Height, Scale: Integer; Octaves: Integer=1; Persistence: Double=1; OffsetX: Double=0; OffsetY: Double=0): TDoubleDynMatrix; end; implementation uses System.Math; procedure TPerlinNoise.InitializeGrid(Seed: Integer; SizeX: Integer=256; SizeY: Integer=256); var I, J: Integer; Angle, SinA, CosA: Double; begin FSeed := Seed; FSizeX := SizeX; FSizeY := SizeY; SetLength(FGrid, FSizeX, FSizeY); RandSeed := Seed; for J := 0 to FSizeY - 1 do for I := 0 to FSizeX - 1 do begin Angle := Random*2*PI; SinCos(Angle, SinA, CosA); FGrid[I, J].X := CosA; FGrid[I, J].Y := SinA; end; end; class function TPerlinNoise.Fade(T: Double): Double; begin Result := T * T * T * (T * (T * 6 - 15) + 10); end; class function TPerlinNoise.Lerp(A, B, X: Double): Double; begin Result := A + X * (B - A); end; class function TPerlinNoise.ModPositive(N, M: Integer): Integer; begin Result := N mod M; if Result < 0 then Inc(Result, M); end; function TPerlinNoise.PerlinNoise(X, Y: Double): Double; var X0, Y0, X1, Y1: Integer; Dx, Dy: Double; DotTL, DotTR, DotBL, DotBR: Double; Fx, Fy: Double; begin X0 := Trunc(X); Y0 := Trunc(Y); X1 := X0 + 1; Y1 := Y0 + 1; Dx := X - X0; Dy := Y - Y0; // Корректировка индексов X0 := ModPositive(X0, FSizeX); Y0 := ModPositive(Y0, FSizeY); X1 := ModPositive(X1, FSizeX); Y1 := ModPositive(Y1, FSizeY); // Скалярные произведения DotTL := FGrid[X0, Y0].X * Dx + FGrid[X0, Y0].Y * Dy; DotTR := FGrid[X1, Y0].X *(Dx-1) + FGrid[X1, Y0].Y * Dy; DotBL := FGrid[X0, Y1].X * Dx + FGrid[X0, Y1].Y *(Dy-1); DotBR := FGrid[X1, Y1].X *(Dx-1) + FGrid[X1, Y1].Y *(Dy-1); // Применение функции затухания Fx := Fade(Dx); Fy := Fade(Dy); // Интерполяция Result := Lerp( Lerp(DotTL, DotTR, Fx), Lerp(DotBL, DotBR, Fx), Fy ); end; function TPerlinNoise.FractalPerlinNoise(X, Y: Double; Octaves: Integer; Persistence: Double): Double; var Total: Double; Frequency: Double; Amplitude: Double; MaxAmplitude: Double; i: Integer; begin Total := 0.0; Frequency := 1.0; Amplitude := 1.0; // Для нормализации результата в пределах 0..1 MaxAmplitude := 0.0; for i := 0 to Octaves - 1 do begin Total := Total + PerlinNoise(X*Frequency, Y*Frequency)*Amplitude; MaxAmplitude := MaxAmplitude + Amplitude; Frequency := Frequency * 2; Amplitude := Amplitude * Persistence; end; // Нормализация результата Result := Total / MaxAmplitude; end; function TPerlinNoise.GeneratePerlinNoise(Width, Height, Scale: Integer; Octaves: Integer; Persistence: Double; OffsetX: Double; OffsetY: Double): TDoubleDynMatrix; var X, Y, W, H: Integer; begin if Length(FGrid)=0 then InitializeGrid(Random(MaxInt)); if Scale < 5 then Scale := 5; W := Width; H := Height; SetLength(Result, W, H); for Y := 0 to H - 1 do for X := 0 to W - 1 do Result[X,Y] := FractalPerlinNoise( (X+OffsetX)/Scale, (Y+OffsetY)/Scale, Octaves, Persistence); end; end. |
Чтобы отобразить картинку сгенерированного шума, можно поступить следующим образом. Сделать битмап заданного размера и форматом пикселя на 32 бита.
|
1 2 3 |
bmp := TBitmap.Create; bmp.SetSize(rct.Width, rct.Height); bmp.PixelFormat := pf32bit; |
Затем где-то вызвать генерацию шума и заполнить битмап следующим образом:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
A := FNoise.GeneratePerlinNoise(rct.Width, rct.Height, Step.Value, Octavs.Value, Persistence.Value/10, Timer1.Tag); P := bmp.ScanLine[bmp.Height-1]; for Y := 0 to bmp.Height-1 do for X := 0 to bmp.Width-1 do begin // Нормализация в [0, 255] G := Round(255 * (A[X,Y] + 1) / 2); // Ограничение диапазона G := Min(255, Max(0, G)); P^.rgbBlue := G; P^.rgbGreen := G; P^.rgbRed := G; Inc(P); end; |
Онлайн: Шум Перлина и генерация ландшафта
Настройте параметры и нажмите «Сгенерировать ландшафт». Повторное нажатие кнопки картинку не изменит — при тех же параметрах генератор шума Перлина выдаст те же самые данные. В том и прелесть шума Перлина — в предсказуемости алгоритма.
Чтобы всякий раз генерировать новый ландшафт нажмите «Случайный ландшафт». По кнопке изменится только «Seed» — будет перенастроен генератор случайных чисел и на картинке появится другой ландшафт при тех же остальных параметрах.
Скачать
Друзья, спасибо за внимание!
Исходник (zip) 63 Кб. Delphi XE 7
Исполняемый файл (zip) 1.09 Мб (Скомпилирован в XE 7 x64)