Сиды и воспроизводимость

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

Главная идея урока: random() и noise() на самом деле не «настоящая» случайность, а длинная заранее заготовленная цепочка чисел. Сид (seed) — это стартовое число, с которого цепочка начинается. Одинаковый сид — одинаковая цепочка — одинаковая картинка. Задаёшь его через randomSeed() и noiseSeed().

Зачем тебе вообще нужна повторяемость

Представь: ты сидишь, обновляешь свой генеративный скетч с цыплёнком, и вдруг — бац! — на экране появляется идеальный вариант. Перья легли как надо, поза смешная, цвета совпали. Ты делаешь скриншот, тянешься за телефоном, чтобы скинуть другу… и случайно жмёшь обновление страницы. Кадр исчез. Навсегда. random() в следующий раз раскидал всё иначе, и тот самый цыплёнок утонул где-то среди миллиардов других вариантов.

Знакомое чувство потери? Так бывает, когда в любимой игре с процедурной генерацией тебе выпадает шикарный мир, а потом ты случайно стартуешь новую игру и теряешь его. И тут многие игры предлагают спасение — seed мира: маленький код, который можно записать, дать другу и получить ровно тот же самый мир. В Minecraft вводишь сид — и горы, пещеры, деревни встают точно там же, что у тебя. Это та же самая магия, которую мы сегодня освоим в p5.js.

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

Что такое сид и почему случайность — это притворство

Метафора: толстая книга случайных чисел

Вот неожиданная правда: компьютер не умеет придумывать по-настоящему случайные числа. Он притворяется. Представь, что у компьютера есть огромная толстая книга, на каждой странице которой напечатан длинный-предлинный ряд цифр, перемешанных так хитро, что никакой закономерности глазом не уловить. Когда ты зовёшь random(), компьютер просто читает следующее число из этой книги и переходит к следующему. Числа выглядят случайными — но они давно напечатаны и лежат в строгом порядке.

Теперь главный вопрос: с какой страницы начать читать? Вот это стартовое место и есть сид (seed). По умолчанию p5.js открывает книгу на случайной странице при каждом запуске — поэтому ты каждый раз получаешь новую последовательность и новую картинку. Но если ты сам скажешь «начни читать со страницы 42», то и цепочка чисел пойдёт всегда одна и та же. А раз числа те же — значит, и перья лягут одинаково, и цыплёнок получится точь-в-точь как в прошлый раз.

Сид (seed) — стартовое число генератора случайности: одинаковый сид даёт одинаковый результат каждый раз. Команда randomSeed(42) говорит: «открой книгу на странице 42 и читай оттуда».

Почему это называют «псевдослучайностью»

Из-за этой книги-обманки настоящие программисты называют такую случайность псевдослучайной — то есть «как будто случайной». Приставка «псевдо» по-гречески значит «ложный, поддельный». Числа достаточно перемешаны, чтобы для рисунка, игры или раскидывания перьев их хватало за глаза, но в глубине это строгий, повторяемый порядок. И именно эта «поддельность» — наш лучший друг: благодаря ей мы можем фиксировать результат. По-настоящему случайные числа (их берут, например, из шума атмосферы или из дрожания электронов) повторить было бы нельзя в принципе — а вот нашу книгу-обманку можно открыть на любой странице сколько угодно раз.

Может показаться, что «поддельная» случайность — это что-то второсортное. На самом деле наоборот: для творчества и игр она удобнее настоящей. Художнику-генеративщику важно не то, чтобы хаос был «честным», а то, чтобы он был красивым и чтобы понравившийся результат не утёк сквозь пальцы. Псевдослучайность даёт ровно это: бесконечное разнообразие вариантов плюс возможность вернуться к любому из них по номеру. Настоящий неповторимый хаос тут был бы только помехой — ты бы любовался шедевром, который никогда больше не увидишь.

Сид у random и сид у noise — две разные книги

Запомни важное: у random() своя книга чисел, а у noise() (шума Перлина) — отдельная. Поэтому и сидов два: randomSeed() управляет первой, а noiseSeed() — второй. Если в твоём скетче работают оба генератора, для полной воспроизводимости нужно зафиксировать оба сида. Зафиксируешь только один — половина картинки повторится, а половина опять будет скакать.

Разбираем на примерах

Пример 1: один и тот же разброс перьев каждый раз

Начнём с простого. Раскидаем вокруг CodeChick кучку перьев через random() — но так, чтобы при каждом запуске они ложились абсолютно одинаково. Секрет в одной строке: randomSeed() в самом начале draw().

function setup() {
  createCanvas(400, 400);
  noStroke();
}

function draw() {
  background(135, 206, 235);

  randomSeed(42);   // открываем книгу всегда на странице 42

  // CodeChick в центре
  fill(255, 209, 64);
  circle(200, 200, 80);
  fill(255, 140, 30);
  triangle(226, 195, 250, 200, 226, 205);

  // 40 перьев вокруг — разброс одинаковый каждый запуск
  fill(255, 235, 150);
  for (let i = 0; i < 40; i = i + 1) {
    let x = random(width);
    let y = random(height);
    let d = random(6, 16);
    ellipse(x, y, d, d * 0.5);
  }
}

Результат: на голубом небе сидит жёлтый CodeChick с оранжевым клювом, а вокруг него рассыпаны 40 светло-жёлтых перьев разного размера. Картинка выглядит хаотичной и живой — но сколько бы раз ты ни перезапускал скетч, перья ложатся точь-в-точь на те же места. Будто это вовсе не случайность, а заранее нарисованная композиция.

Вся магия — в строке randomSeed(42);. Она стоит в начале draw() и каждый кадр заново «отматывает книгу» на страницу 42. Поэтому первый random(width) всегда возвращает одно и то же число, второй — тоже своё постоянное, и так далее по списку. Убери эту строку — и перья при каждом обновлении страницы будут раскиданы заново. Поменяй число 42 на 7, на 100, на 2024 — и получишь другие, но тоже фиксированные раскладки. Каждый сид — это отдельная застывшая композиция, которую ты можешь вызвать по номеру.

Пример 2: каталог цыплят — листаем варианты по номеру сида

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

let seed = 0;   // текущий номер варианта

function setup() {
  createCanvas(400, 400);
  noStroke();
}

function draw() {
  background(245, 240, 230);
  randomSeed(seed);   // каждый кадр собираем вариант по текущему сиду

  // тело цыплёнка со случайными «настройками внешности»
  let bodyW = random(70, 110);
  let bodyH = random(60, 100);
  let beakLen = random(18, 34);
  let cheekR = random(255);

  fill(255, 209, 64);
  ellipse(200, 210, bodyW, bodyH);
  fill(255, cheekR, 30);
  triangle(200 + bodyW / 2, 200, 200 + bodyW / 2 + beakLen, 205,
           200 + bodyW / 2, 210);

  // глаз
  fill(40);
  circle(215, 195, 10);

  // подпись номера варианта
  fill(60);
  textSize(18);
  text("CodeChick #" + seed, 20, 30);
}

function mousePressed() {
  seed = seed + 1;   // клик — следующий вариант каталога
}

Результат: на светлом фоне стоит цыплёнок, у которого тело то пошире, то поуже, клюв то короткий, то длинный, а оттенок клюва меняется. В левом верхнем углу подпись вроде «CodeChick #0». Каждый клик мышью увеличивает сид на единицу — и цыплёнок мгновенно «пересобирается» в новый вариант с другой подписью: #1, #2, #3… А если ты вернёшься к #2 (например, перезапустишь скетч и кликнешь дважды), увидишь ровно того же самого цыплёнка, что был под номером 2. Номер сида — это его адрес.

Тут важно, что randomSeed(seed) стоит в начале каждого кадра, а сам seed хранится в переменной состояния вне draw() — поэтому он переживает кадры и помнит, на каком варианте мы остановились. Каждый клик в mousePressed() двигает нас на следующую «страницу каталога». Заметь, как удобно: чтобы запомнить понравившегося цыплёнка, тебе не нужен скриншот — достаточно записать его номер. Увидел красавчика под #17 — записал «17» в заметки, и в любой момент вернёшь его одной строкой seed = 17.

Обрати внимание ещё на одну тонкость: внутри одного кадра мы зовём random() несколько раз подряд — для ширины тела, высоты, длины клюва, оттенка щёк. И каждый из этих вызовов читает своё число из книги, по порядку. Поэтому важен не только сам сид, но и порядок вызовов random(): если ты переставишь строки местами или добавишь новый random() в середину, то все числа после него «сдвинутся» и вариант под номером 17 станет выглядеть иначе. Это нормально — просто помни, что сид фиксирует картинку только при неизменном коде. Меняешь логику рисования — каталог пересобирается заново.

Пример 3: фиксируем шум Перлина через noiseSeed()

Теперь покажем, что у шума своя книга и свой сид. Нарисуем «холмистый ландшафт» под цыплёнком через noise() — мягкую волнистую линию. С noiseSeed() один и тот же холм будет получаться каждый запуск, а меняя сид, мы получаем разные пейзажи — все плавные, но непохожие.

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(135, 206, 235);

  noiseSeed(10);   // фиксируем ИМЕННО шум — у него своя книга

  // волнистая земля из шума Перлина
  fill(120, 190, 90);
  noStroke();
  beginShape();
  vertex(0, height);
  for (let x = 0; x <= width; x = x + 10) {
    let h = noise(x * 0.01) * 150 + 180;
    vertex(x, h);
  }
  vertex(width, height);
  endShape(CLOSE);

  // CodeChick стоит на холме
  fill(255, 209, 64);
  circle(200, noise(200 * 0.01) * 150 + 150, 60);
  fill(255, 140, 30);
  triangle(225, noise(200 * 0.01) * 150 + 148,
           245, noise(200 * 0.01) * 150 + 152,
           225, noise(200 * 0.01) * 150 + 156);
}

Результат: на фоне голубого неба раскинулся плавный зелёный холм — мягкая волнистая линия земли без резких скачков, типичная для шума Перлина. На вершине холма сидит жёлтый CodeChick с клювом. Сколько ни перезапускай — холм всегда одной и той же формы, потому что noiseSeed(10) фиксирует шумовую книгу. Поменяй 10 на 3 или 99 — рельеф станет другим (другие пригорки и впадины), но таким же плавным.

Главный вывод примера: здесь мы трогали noiseSeed(), а не randomSeed() — потому что рисуем через noise(). Это две независимые системы. Если бы в этом же скетче ещё и перья раскидывались через random(), для полной воспроизводимости пришлось бы поставить обе строки: и randomSeed(...), и noiseSeed(...). Зафиксируешь только шум — холм застынет, но перья продолжат прыгать при каждом запуске.

Частые ошибки и подводные камни

  • Ставят randomSeed() в setup() вместо draw() при анимации. Если в скетче меняются варианты и ты хочешь, чтобы каждый кадр пересобирался по текущему сиду, randomSeed(seed) должен стоять в начале draw(). Поставишь в setup() — сид зафиксируется один раз на старте, а при смене переменной seed в draw() картинка уже не отмотается на новую страницу книги.

  • Путают randomSeed и noiseSeed. Это разные книги. randomSeed() фиксирует только random(), а noiseSeed() — только noise(). Зафиксировал не тот — и нужный генератор продолжит выдавать новое каждый запуск. Если в скетче работают оба, фиксируй оба.

  • Зовут randomSeed() в середине цикла отрисовки. Сид надо задавать до первого вызова random() в кадре — обычно сразу после background(). Если воткнуть randomSeed() между вызовами random(), ты «перематываешь книгу» на полпути, и числа после этой строки начнут повторять начало последовательности — получится неожиданный сбой композиции.

  • Думают, что сид 42 чем-то особенный. Никакой магии в конкретном числе нет — это просто номер страницы. Сид 42, 7 или 100000 одинаково хорошо работают, каждый даёт свою застывшую картинку. Число 42 любят программисты как шутку (привет «Автостопом по галактике»), но генератору всё равно.

  • Забывают записать понравившийся сид. Самая обидная ошибка: нашёл идеального цыплёнка, полюбовался — и не записал его номер. Закрыл вкладку, и без сида вернуть его уже почти нереально. Привыкай: понравился вариант — сразу запиши его сид в заметку или прямо в комментарий к коду.

Мини-практика: галерея любимых цыплят

Доделай каталог из Примера 2 в маленькую персональную галерею. План такой:

  1. Возьми за основу листалку с переменной seed и кликом мыши.
  2. Добавь больше «настроек внешности», которые зависят от random(): например случайный цвет тела (fill(random(200, 255), random(180, 230), random(40, 120))), случайный размер глаза, случайный наклон клюва. Чем больше параметров — тем разнообразнее каталог.
  3. Полистай варианты кликами и запиши номера 3–4 любимых цыплят прямо в комментарий в начале файла, например // топ: 7, 23, 41, 88.
  4. Сделай две клавиши через keyPressed(): стрелка вправо — seed = seed + 1 (следующий), стрелка влево — seed = seed - 1 (предыдущий). Так листать удобнее, чем только вперёд.

Когда заработает, попробуй продвинутый шаг: добавь в скетч ещё и фон из noise() (например волнистый холм из Примера 3) и зафиксируй его отдельным noiseSeed(). Сделай так, чтобы холм при листании цыплят не менялся (фиксированный noiseSeed(10)), а сам цыплёнок менялся по randomSeed(seed). Это наглядно покажет тебе, что две книги случайности и правда независимы: одна застыла, другая листается.

Итоги

Сегодня ты приручил случайность и научил её повторяться:

  • Случайность в p5.js — псевдослучайная. Это длинная заготовленная цепочка чисел («книга»), а не настоящий хаос. Поэтому её можно зафиксировать и воспроизвести.
  • Сид (seed) — стартовое число, «страница книги», с которой начинается цепочка. Одинаковый сид → одинаковая последовательность → одинаковый кадр.
  • randomSeed(n) фиксирует random(), а noiseSeed(n)noise(). Это две разные книги: при использовании обоих генераторов фиксируй оба сида. Ставь их в начале draw(), до первого вызова случайности.
  • Сид — это адрес варианта. Понравился кадр — запиши его номер, и вернёшь любимого цыплёнка одной строкой, без всяких скриншотов.

Теперь у тебя есть суперсила генеративного художника: ты можешь отпускать хаос на волю, ловить лучшие случайные находки и сохранять их по номеру. Это ровно тот приём, которым пользуются авторы NFT-коллекций и генеративных аватарок — у каждой работы свой сид. В следующем уроке мы пойдём дальше по разделу случайности и научимся смешивать управляемый хаос с шумом, чтобы получать ещё более живые и непредсказуемые, но при этом воспроизводимые миры вокруг CodeChick. А пока — полистай свой каталог цыплят и запиши номера тех, что заставили тебя улыбнуться!

Проверьте себя
1. Что такое сид (seed) генератора случайности в p5.js?
AСтартовое число, с которого начинается цепочка псевдослучайных чисел
BСкорость, с которой генерируются случайные числа
CМаксимальное значение, которое может вернуть random()
DСпециальная случайная функция вместо random()
2. Почему случайность в p5.js называют псевдослучайной?
AПотому что числа берутся из шума атмосферы
BПотому что это заранее заготовленная повторяемая цепочка чисел, лишь похожая на случайную
CПотому что random() работает только в setup()
DПотому что её нельзя зафиксировать
3. Какая функция фиксирует результат именно шума Перлина noise()?
ArandomSeed()
BnoiseSeed()
CframeRate()
Dseed()
4. В скетче-анимации работают и random(), и noise(). Что нужно сделать для полной воспроизводимости кадра?
AПоставить только randomSeed()
BПоставить только noiseSeed()
CЗафиксировать оба сида: и randomSeed(), и noiseSeed()
DДостаточно вызвать frameRate(1)
5. Где лучше всего ставить randomSeed(seed), если переменная seed меняется и каждый кадр должен пересобираться по текущему сиду?
AВ самом конце draw()
BОдин раз в setup()
CВ начале draw(), до первого вызова random()
DВнутри mousePressed() после смены seed
6. Ты нашёл идеального цыплёнка под номером сида 17. Как вернуть его потом?
AНикак — случайность необратима
BЗаписать число 17 и снова задать randomSeed(17)
CСделать скриншот — другого способа нет
DПерезапускать скетч, пока он случайно не выпадет снова