Місто — це рекурсивно поділена прямокутна форма
Розріжте будівлі та ви отримаєте план міста у вигляді дерева. Почніть із одного великого прямокутника — всього міста — і розріжте його на дві частини прямою дорогою. Розріжте кожну половину ще раз, і знову, поки шматок не стане достатньо малим, щоб бути окремим кварталом. Це бінарне просторове розділення, такий самий принцип, що використовується для створення планів підземельних рівнів та рендерингових конвеєрів, застосований до міських кварталів замість цього. Кожен розріз стає сегментом дороги, а кожна листова гілка дерева — це ділянка, яка вже торкається дороги принаймні однією стороною — що точно не може гарантувати випадкове розміщення властивостей.
Орієнтація розрізу та зміщення не є фіксованими — вони беруться з генератора випадкових чисел на основі заданого значення, тому однакова застава завжди генерує однакове розділення в одному й тому ж вузлі дерева. Ця єдина властивість робить процедурну генерацію відтворюваною: достатньо 32-бітних цілих чисел, щоб відтворити все місто, байт за байтом, на іншому комп’ютері.
Дві родини мережі доріг
Реальні міста поєднують дві дуже різні логіки генерації, і це відображається в їхніх процедурних моделях. Мережа на основі сітки — більшість північноамериканських центральних районів, Ексампле Барселони — походить від осейно-орієнтованого рекурсивного поділу: кожен зріз є горизонтальним або вертикальним, створюючи прямокутні блоки відносно однакового розміру. Мережа органічного типу — ядро середньовічної Європи, прилеглих до пагорбів районів — походить від росту доріг назовні один сегмент за раз вздовж найпростішого шляху, ближче до L-системи, ніж до дерева розділення: кожна кінцева точка дороги розширюється, розгалужується або зупиняється на основі місцевих правил і трохи шуму, тому жодні дві гілки не схожі одна на одну.
subdivide(rect, depth): if rect.area MAX_DEPTH: emit_parcel(rect); return axis = choose_axis(rect) // віддає перевагу довшим сторонам split = rect.size(axis) * jitter(0.35, 0.65) roadA, roadB = rect.cut(axis, split, ROAD_WIDTH) subdivide(roadA, depth + 1) subdivide(roadB, depth + 1) Найпереконливіші генератори поєднують обидва: спочатку робиться органічний артеріальний скелет, який вирощується між кількома точками насіння, а нерегулярні клітини, які залишає цей скелет, заповнюються стилем сітки рекурсивного поділу — що відповідає тому, як багато справжніх міст дійсно зростали: спочатку артерії, потім заповнення.
subdivide(rect, depth):
if rect.area < MIN_BLOCK or depth > MAX_DEPTH:
emit_parcel(rect); return
axis = choose_axis(rect) // prefers the longer side
split = rect.size(axis) * jitter(0.35, 0.65)
roadA, roadB = rect.cut(axis, split, ROAD_WIDTH)
subdivide(roadA, depth + 1)
subdivide(roadB, depth + 1)
Зонирование як поле щільності, а не правила
Місто не має однорідного вигляду, і генератор не повинен розглядати його як таке. Поле щільності центру – найвище в точці насіння, зменшується з віддаленням, можливо, з додатковими підвищеннями на транспортних вузлах – впливає на три рішення одночасно: наскільки глибоко повторюється підрозділ (глибше біля центру означає менші блоки), яка частка площі кожного блоку займає будівля (вища біля центру) і наскільки високою може бути будівля на цій ділянці. Результат – витончена лінія горизонту від щільного ядра до низьких передмість без жодного правила, яке б вказувало, де саме знаходиться «центр» — це випливає з поля.
Від сліду до будівлі
Після того, як ділянка існує, перетворення її на будівлю є відносно простим завданням: вставте межу ділянки на невелику відстань назад, витягніть форму цього ділянки вгору на висоту, зібрану з поля локальної інтенсивності, та за бажанням розділіть цей витяг на поверхи з повторюваним текстурним фасадом. Деякі генератори змінюють форму основи самої, наприклад, L-подібний вежі на кутовому місці або квартальні блоки в центрі міста – але лише поле висоти вже робить більшу частину роботи щодо створення горизонтного рядка, а не поля однакових коробок.
Чому детермінізм важливіший за реалізм
Жоден процедурний генератор міст не намагається відтворити існуюче місцевість ділянка в ділянку – цінність полягає у безмежному, на вимогу, відтворюваному контенті. Оскільки кожен випадковий вибір робиться з потоку, засіяного ключем за розташуванням, місто може бути згенеровано у вигляді плиток за запитом, коли рухається камера, не зберігаючи нічого, окрім насіння та кількох параметрів, і будь-яка плитка може бути згенерована ідентично пізніше. Це той самий трюк, який використовується в Minecraft для рельєфу та у No Man's Sky для планет, застосований до вулиць і горизонтів замість печер і біомів.
Часті запитання
Чи місто, згенероване за допомогою симулятора, коли-неколи буде абсолютно однаковим двічі?
Лише якщо ви використовуватимете один і той самий seed. Весь макет – дорожне дерево, поділ на блоки, зонування та висоти будівель – походить від одного потоку псевдо-випадкових чисел, тому одна ціла числова seed відтворює одне місто абсолютно точно. Змініть seed і кожне подальше рішення призведе до іншого міста.
Чому процедурні міста зазвичай виглядають як сітка в центрі міста та лабіринт на краях?
Це свідома градієнта щільності: поблизу seed point, поділ схилений до невеликих, регулярних прямокутних блоків, оскільки справжні центральні райони вимірюються і є регулярними; далі від центру дозволено більші та менш регулярні блоки, що імітує органічний розвиток передмість вздовж існуючих доріг.
Чому не просто розміщувати будівлі випадково замість рекурсивного поділу на частини?
Бо випадкове розміщення не має поняття про мережу вулиць, тому будівлі будуть плавати в просторі або перекриватися. Рекурсивний поділ гарантує, що кожна частина, яку він виробляє, вже межує з дорогою з одного боку, що робить результат придатним для будівництва та пішохідного руху, а не просто декоративним шумом.
Спробуйте наживо
Усе, що вище, працює прямо у вашому браузері — відкрийте Generative City і змінюйте параметри під час роботи. Нічого не встановлюється, нічого не завантажується на сервер, уся модель живе в одній вкладці.
▶ Відкрити симуляцію Generative City