Генеративные ландшафты и линии
Одна формула шума — и плоский холст превращается в холмистый горизонт, по которому может гулять твой CodeChick.
Генеративный ландшафт — это пейзаж, который рисует не художник, а сам код: высоту каждой точки горизонта подсказывает шум Перлина, поэтому холмы получаются плавными и живыми, а не как пила из случайных чисел.
Зачем это нужно
Запусти любую игру с бесконечным миром — Minecraft, Terraria, какой-нибудь раннер на телефоне, где земля под ногами всё время новая. Откуда берётся этот рельеф? Никто не рисовал каждый холм вручную — их сгенерировал код. И почти всегда в основе лежит ровно тот приём, который мы разберём сегодня: высоту земли в каждой точке подсказывает плавная случайность — шум.
В прошлом уроке про шум Перлина ты познакомился с функцией noise() и почувствовал разницу между ней и обычным random(): random() прыгает как попало, а noise() течёт мягко, без резких скачков. Сегодня мы поставим этот приём на службу красоте: построим из шума волнистую линию, превратим её в холмистый горизонт, заставим холмы дышать во времени и в финале сделаем из всего этого живой фон-пейзаж для цыплёнка.
К концу урока у тебя будет скетч, где CodeChick стоит на мягко колышущемся зелёном холме под небом, а сами холмы плавно перетекают сами в себя, будто их рисует невидимый ветер. И весь этот мир — буквально десяток строк кода. Поехали.
Метафора: горы — это график случайности
Представь, что ты идёшь вдоль длинной горной гряды и записываешь высоту земли через каждый шаг: тут пониже, тут повыше, тут впадина, тут вершина. Если выписать эти высоты в ряд и соединить точки линией — получится силуэт гор. По сути любой горизонт — это график: по горизонтали ты двигаешься вперёд (это координата x), а по вертикали отмечаешь высоту земли (это y).
Теперь главный вопрос: откуда брать высоту для каждого x? Если брать чистый random(), соседние точки окажутся совершенно несвязанными — одна под небесами, другая в пропасти. Линия превратится в дрожащую щётку из иголок, а не в горы. Природа так не работает: реальный склон меняется плавно, соседние точки почти на одной высоте.
Вот тут и выходит на сцену шум Перлина:noise(x)даёт плавно меняющееся число, у которого соседние значения близки друг к другу. Это идеальный «генератор рельефа» — двигаешьxпонемножку, и высота меняется мягко, как настоящий холм.
Держи в голове ещё одну картинку — ползунок громкости в плеере, который кто-то медленно двигает туда-сюда. Звук не прыгает рывками, он плавно нарастает и спадает. Шум Перлина — это и есть такой невидимый палец, который мягко водит «ползунок высоты» вверх-вниз, пока мы идём вдоль холста. А скорость, с которой мы двигаемся вдоль шума, мы выберем сами — и от неё зависит, будут у нас пологие дюны или острые скалистые пики.
Пример 1. Волнистая линия из шума
Начнём с самого простого: пройдёмся по холсту слева направо и для каждого x возьмём высоту из noise(). Соединим соседние точки — получим одну живую, волнистую линию.
function setup() {
createCanvas(600, 300);
}
function draw() {
background(20, 24, 40);
stroke(120, 220, 255);
strokeWeight(3);
noFill();
beginShape();
for (let x = 0; x <= width; x += 5) {
let n = noise(x * 0.01); // плавная случайность для этого x
let y = map(n, 0, 1, 80, 220); // переводим 0..1 в высоту на холсте
vertex(x, y);
}
endShape();
}Результат: на тёмно-синем фоне тянется от левого края к правому одна светло-голубая волнистая линия — плавная, без резких изломов, похожая на спокойную горную гряду на горизонте или на линию пульса, замедлившегося до сна. Она статична: пока не двигается, просто красиво лежит.
Что здесь происходит по шагам
- Цикл
for (let x = 0; x <= width; x += 5)идёт по холсту слева направо с шагом 5 пикселей. На каждомxмы поставим одну точку линии. noise(x * 0.01)возвращает плавное число от 0 до 1. Множитель0.01— это скорость прогулки по шуму: мы берём не самx(он рос бы слишком быстро и шум стал бы дёрганым), а его сильно уменьшенную копию, поэтому соседние точки оказываются близко в «шумовом пространстве» и высота меняется мягко.map(n, 0, 1, 80, 220)переводит число из диапазона шума (0..1) в нужную нам высоту на холсте (от 80 до 220 пикселей). Безmapвся линия прижалась бы к самому верху — ведьnoise()выдаёт числа меньше единицы.beginShape()...vertex(x, y)...endShape()— это способ нарисовать ломаную линию по множеству точек: каждыйvertexдобавляет узел, а p5.js соединяет их по порядку.
Самое важное здесь — множитель 0.01. Это ручка настройки «характера» рельефа. Поставь 0.002 — холмы станут широкими и пологими, как песчаные дюны. Поставь 0.05 — линия задёргается частыми мелкими зубцами, как горный хребет с острыми пиками. Поиграй с этим числом: оно важнее всего остального в коде. А вот если убрать множитель совсем и написать просто noise(x), шаги по шуму станут слишком большими, и линия превратится в почти случайную дрожь — ровно то, чего мы избегаем.
Пример 2. Закрашенный холм-ландшафт
Линия — это красиво, но холм должен быть плотным: земля под линией закрашена, небо над ней пустое. Превратим нашу линию в настоящий силуэт холма, замкнув фигуру по низу холста.
function setup() {
createCanvas(600, 300);
noStroke();
}
function draw() {
background(170, 210, 255); // небо
fill(90, 170, 90); // зелёная земля
beginShape();
vertex(0, height); // левый нижний угол
for (let x = 0; x <= width; x += 5) {
let n = noise(x * 0.008);
let y = map(n, 0, 1, 120, 240);
vertex(x, y); // линия горизонта
}
vertex(width, height); // правый нижний угол
endShape(CLOSE);
}Результат: верхняя часть холста — спокойное голубое небо, а снизу его перерезает плавная зелёная гряда холмов с закрашенной землёй под ней. Получается настоящий пейзаж: мягкие холмы, как где-то за городом летним днём. Силуэт всё ещё неподвижен.
Откуда берётся замкнутая фигура
Фокус в двух дополнительных точках. Линия горизонта из примера 1 — это только верхняя кромка. Чтобы закрасить землю под ней, нужно превратить кромку в замкнутый контур:
- Перед циклом мы добавляем
vertex(0, height)— точку в левом нижнем углу холста. - Потом цикл рисует всю волнистую линию горизонта слева направо.
- После цикла добавляем
vertex(width, height)— точку в правом нижнем углу. endShape(CLOSE)с ключевым словомCLOSEсоединяет последнюю точку с первой. В итоге контур обходит: низ-лево → вся линия гор → низ-право → и обратно по нижнему краю. Внутренность этого контура и есть земля, её закрашиваетfill.
Запомни приём: чтобы из линии сделать закрашенную форму, добавь по точке в нижних углах и закрой фигуру через CLOSE. Тем же способом строят залитые графики и силуэты гор в дашбордах и играх.
Пример 3. Холмы оживают во времени
Пейзаж красив, но мёртв. Заставим холмы дышать — пусть рельеф плавно перетекает сам в себя, будто его лепит ветер. Для этого добавим в шум третий аргумент — время, которое мы будем потихоньку увеличивать каждый кадр.
let t = 0; // «время» для шума
function setup() {
createCanvas(600, 300);
noStroke();
}
function draw() {
background(170, 210, 255);
fill(90, 170, 90);
beginShape();
vertex(0, height);
for (let x = 0; x <= width; x += 5) {
let n = noise(x * 0.008, t); // второй аргумент — время
let y = map(n, 0, 1, 120, 240);
vertex(x, y);
}
vertex(width, height);
endShape(CLOSE);
t += 0.01; // чуть-чуть сдвигаем время каждый кадр
}Результат: тот же зелёный холм под голубым небом, но теперь он медленно колышется — вершины плавно вырастают и оседают, впадины переползают, весь силуэт мягко перетекает сам в себя, как трава под ветром или волны очень спокойного моря. Движение неспешное и завораживающее, без рывков.
Почему это работает
Тут срабатывает важное свойство noise(): он умеет принимать несколько координат. noise(x, t) — это плавная случайность сразу в двух измерениях. Первое (x) мы используем как положение вдоль холста — оно даёт форму холмов. Второе (t) мы используем как время.
Каждый кадр мы делаем t += 0.01, то есть чуть-чуть сдвигаемся вдоль второй оси шума. А раз шум плавный по обеим осям, то и форма холма меняется плавно: соседние во времени кадры почти одинаковы, поэтому глаз видит мягкое перетекание, а не мерцание. Скорость дыхания холмов — это шаг t += 0.01: сделай 0.001 — холмы будут едва шевелиться, как при штиле; сделай 0.05 — задрожат и забурлят, будто начался шторм.
Запомни общий рецепт анимации через шум: пространственную координату даётx, а плавное движение во времени — отдельный аргументt, который понемногу растёт каждый кадр. Так анимируют не только холмы, но и колышущуюся траву, дым, рябь на воде.
Пример 4. CodeChick на живом холме
Соберём всё вместе и поселим в пейзаж нашего героя. CodeChick встанет на гребень холма прямо по центру — а раз холм дышит, цыплёнок будет мягко подниматься и опускаться вместе с землёй, будто плывёт на ней.
let t = 0;
function setup() {
createCanvas(600, 320);
noStroke();
}
function draw() {
background(170, 210, 255);
// холм
fill(90, 170, 90);
beginShape();
vertex(0, height);
for (let x = 0; x <= width; x += 5) {
let n = noise(x * 0.008, t);
let y = map(n, 0, 1, 150, 260);
vertex(x, y);
}
vertex(width, height);
endShape(CLOSE);
// высота холма ровно под центром цыплёнка
let cx = width / 2;
let groundY = map(noise(cx * 0.008, t), 0, 1, 150, 260);
chick(cx, groundY - 24, 50);
t += 0.01;
}
function chick(x, y, d) {
fill(255, 215, 0); // тело
ellipse(x, y, d, d);
fill(255, 140, 0); // клюв
triangle(x + d * 0.4, y, x + d * 0.62, y - d * 0.07, x + d * 0.62, y + d * 0.07);
fill(0); // глаз
ellipse(x + d * 0.14, y - d * 0.12, d * 0.1, d * 0.1);
}Результат: на голубом небе над дышащим зелёным холмом стоит жёлтый CodeChick с оранжевым клювом и чёрным глазом. Холм медленно колышется, и цыплёнок вместе с ним плавно покачивается вверх-вниз, будто катается на спине у спящего великана. Он всегда стоит ровно на земле — не проваливается и не висит в воздухе.
Как цыплёнок «прилипает» к земле
Главный трюк — в одной строке. Чтобы поставить героя ровно на холм, мы спрашиваем у шума ту же самую высоту, что и при рисовании линии, но для конкретного x — для центра холста cx:
let groundY = map(noise(cx * 0.008, t), 0, 1, 150, 260);
Обрати внимание: множитель 0.008 и тот же t, что и в цикле. Это критично — если взять другие числа, цыплёнок будет стоять «по высоте от другого холма» и либо повиснет, либо утонет в земле. Раз и формула одинаковая — высота под цыплёнком в точности совпадает с высотой нарисованного гребня. А - 24 поднимает центр тела чуть выше линии земли, чтобы цыплёнок стоял на холме, а не врос в него до пояса.
Частые ошибки и подводные камни
1. Забывают уменьшить x перед подачей в noise
Если написать noise(x) без множителя, шаг по шуму на каждый пиксель будет огромным, и соседние точки окажутся в совсем разных местах шума — линия превратится в дёрганую дрожь, неотличимую от random(). Всегда умножай координату на маленькое число: noise(x * 0.01). Это число и задаёт плавность рельефа.
2. Не переводят шум через map и удивляются плоской линии
noise() возвращает число от 0 до 1 — это меньше одного пикселя. Если подставить его прямо в y, вся линия слипнется в верхней кромке холста. Обязательно растягивай диапазон через map(n, 0, 1, верх, низ), чтобы холмы получили реальную высоту в пикселях.
3. Используют random() вместо noise() для рельефа
Это самая частая путаница после прошлого урока. random() на каждом x выдаёт независимое число — получится не гряда гор, а забор из случайных иголок. Для плавного рельефа нужен именно noise(): только он гарантирует, что соседние точки близки по высоте.
4. Берут случайность заново каждый кадр и получают мерцание
Если высоту холма пересчитывать через random() в draw(), картинка будет бешено дёргаться каждый кадр — глаза устанут за секунду. Плавную анимацию даёт только сдвиг по времени в шуме: noise(x * 0.008, t) и медленный рост t. Соседние кадры тогда почти одинаковы, и движение получается мягким.
5. Считают высоту земли под героем по другой формуле, чем рисуют холм
Если линию холма рисуешь с noise(x * 0.008, t), а место для цыплёнка считаешь с noise(cx * 0.01) или забываешь передать t, герой оторвётся от земли. Высота под персонажем должна считаться той же функцией с теми же числами, что и сам холм, — иначе мир развалится на два несогласованных слоя.
Мини-проект: твой собственный мир для CodeChick
Возьми пример 4 за основу и дострой настоящий ландшафт. Делай по шагам:
- Два слоя холмов. Нарисуй сначала дальний холм (другой множитель, например
0.004, и более тусклый зелёный, повыше на холсте), а поверх — ближний холм из примера 4. Получится глубина, как в платформерах. - Своя скорость ветра. Поиграй с шагом
t: найди значение, при котором холмы дышат красиво, а не слишком быстро. Запиши, какое число тебе понравилось. - Цыплёнок гуляет. Заведи переменную
chickXи медленно увеличивай её каждый кадр, а высоту бери изnoise(chickX * 0.008, t). Теперь CodeChick не стоит на месте, а шагает по холму вверх-вниз по рельефу. - Усложни: добавь на небо пару облаков из
noise()или разбросай по холму несколько зёрнышек (маленьких коричневых кружков), которые тоже стоят на высоте земли в своих точках.
Когда заработает — покрути все множители и диапазоны map. Это твой мир: сделай его хоть пустынными дюнами, хоть острыми скалами, хоть мягкими лугами. Каждое новое число рождает новую планету.
Итоги
Сегодня ты научился выращивать целые миры из одной плавной случайности. Вот что забираем с собой:
- Горизонт — это график: идём по
xслева направо, а высоту земли в каждой точке берём изnoise(). Соседние значения шума близки, поэтому холмы получаются плавными. - Координату всегда уменьшаем перед подачей в шум —
noise(x * 0.01)— и переводим результат в пиксели черезmap(n, 0, 1, верх, низ). Множитель задаёт характер рельефа. - Чтобы из линии сделать закрашенный холм, добавь точки в нижних углах и закрой фигуру через
endShape(CLOSE). - Анимация — это второй аргумент шума:
noise(x, t), гдеtпонемногу растёт каждый кадр. Так холмы дышат без рывков. - Герой держится на земле, если его высоту считать той же формулой, что и сам холм.
Генеративные ландшафты — фундамент огромного пласта графики: бесконечные миры в играх, абстрактные обои, заставки, визуализации музыки. Стоит понять связку «noise + map + vertex» — и ты можешь нарисовать любой плавный рельеф, от гор до океанских волн. В следующем уроке мы научимся управлять самой случайностью: зафиксируем сид, чтобы один и тот же красивый мир можно было получить снова и снова, и поделиться им с друзьями. До встречи в коде!