Hero shot

Я был одержим процедурными картами с ещё детства, когда кидал кубики на таблицы случайных подземелий из AD&D Dungeon Master's Guide. В этом есть что-то волшебное — ты не проектируешь подземелье, а исследуешь его, помещение за помещением, а кубики решают, попадёшь ли ты в сокровищницу или в тупик с кучей крыс.

Спустя годы я решил создать собственный генератор карт. Он создаёт маленькие средневековые островные миры с дорогами, реками, побережьями, горами, лесами и деревьями. И всё это полностью процедурным образом. Генератор написан на Three.js WebGPU с TSL-шейдерами, примерно 4100 шестиугольников в 19 сетках генерируются за ~20 секунд.

«Каркассон», который создаёт компьютер

Основой генератора стал Wave Function Collapse (WFC) — алгоритм, созданный Максимом Гуминым и ставший одним из любимых инструментов разработчиков игр с процедурной генерацией.

Carcassonne board game

Если вы когда-нибудь играли в «Каркассон», то уже понимаете, что такое WFC. У нас есть стопка плиток, мы размещаем их так, чтобы всё совпадало. У каждой плитки есть грани — трава, дорога, город. Грани соседних плиток должны совпадать. Грань с дорогой должна соединяться с другой такой же гранью. Трава должна соединяться с травой. Единственное отличие заключается в том, что компьютер делает это быстрее и меньше жалуется, когда зайдёт в тупик.

У моего проекта есть тонкость: у шестиугольных плиток (тайлов) шесть граней вместо четырёх. То есть у плитки становится на 50% больше ограничений и происходит комбинаторный взрыв. Квадратный WFC — хорошо исследованная территория. А с шестиугольным WFC история более сложная.

Определения тайлов

Для этой карты есть 30 разных тайлов, определяющих траву, воду, дороги, реки, побережья и склоны. У каждого тайла есть определение, описывающее тип рельефа для каждой из шести граней плюс веса, используемые для того, чтобы предпочитать одни тайлы другим.

Tile set showing road, river, and terrain variants
30 типов тайлов, у каждого из которых 6 поворотов и 5 уровней возвышения. То есть у каждой ячейки есть 900 потенциальных значений.

Например, этот трёхсторонний перекрёсток имеет три грани с дорогой и три грани с травой. Определение тайла выглядит так:

{ name: 'ROAD_D', mesh: 'hex_road_D',
  edges: { NE: 'road', E: 'grass', SE: 'road', SW: 'grass', W: 'road', NW: 'grass' },
  weight: 2 }
A 3-way road junction tile with its 6 edge types labeled
Каждый тайл определяет шесть типов граней. Единственное правило заключается в сопоставлении граней.

Как работает WFC

  1. Начинаем с хаоса. Каждая ячейка в сетке изначально является суперпозицией всех возможных тайлов — всех 30 типов, всех 6 поворотов, всех 5 уровней возвышения.

  2. Выполняется коллапс ячейки с наибольшим количеством ограничений. Выбираем ячейку с наименьшим количеством оставшихся вариантов (самой низкой энтропией). Случайно выбираем одно из её допустимых состояний.

  3. Распространение. Этот выбор накладывает ограничения на соседние тайлы. Удаляем все состояния соседей, грани которых не согласуются с исходной ячейкой. И этот принцип каскадно распространяется наружу — один коллапс может уничтожить сотни вероятностей в сетке.

  4. Повторяем процесс, пока не найдём решения для всех ячеек или не зайдём в тупик.

Попадание в тупик — это интересный момент.

Проблема нескольких сеток

WFC надёжно работает для маленьких сеток. Но при увеличении сетки шанс зайти в тупик быстро возрастает. В сетке из 217 шестиугольников сбоев почти не бывает. В сетке из 4123 ячеек они происходят регулярно.

Решением может стать модульный WFC. Вместо одного гигантского решения карта разделяется на 19 сеток шестиугольников, выстроенных в два кольца вокруг центра, что суммарно даёт приблизительно 4100 ячеек. Решение для каждой сетки ищется отдельно, но должно соответствовать тем тайлам, которые уже размещены на соседних сетках. Эти граничные тайлы становятся фиксированными ограничениями.

И иногда такие ограничения попросту несовместимы. Никакой возврат назад в текущей сетке не может устранить проблему, которая уже фиксирована в соседней. И на решение этой задачи я потратил очень много времени.

Debug view showing grid boundaries
Решено 5 из 19 сеток. Каждая сетка — отдельное решение WFC, ограниченное тайлами соседней сетки.

Возвращение назад

И в этом заключается неприятный секрет WFC: у него часто возникают сбои. Мы последовательно делаем случайный выбор, распространяем ограничения, рано или поздно загоняя себя в угол, где у какой-то ячейки остаётся ноль допустимых вариантов. Поздравляю, этот пазл невозможно решить.

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

Но одного возврата назад недостаточно. Настоящая проблема заключается в границах между сетками.

Система восстановления

Попробовав множество неудачных подходов, я остановился на многослойной системе восстановления:

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

Слой 2: локальный WFC. При неудаче основного решения солвер выполняет мини-WFC для небольшой области радиусом 2 вокруг проблемной области — повторно решает 19 ячеек в области пересечения для создания более совместимой границы. Он выполняет до 5 попыток, каждая из которых нацелена на отдельную проблемную ячейку. Локальный WFC стал прорывом: вместо того, чтобы решать невозможную задачу, нужно вернуться назад и изменить саму задачу.

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

Before: neighbor conflict blocks the solve
Режим отладки с демонстрацией расправы над ячейками. Фиолетовые = конфликт с соседями. Красные = поломанные тайлы.

Третье измерение

Эта карта не плоская, а имеет пять уровней возвышений. Океан и Трава начинаются на уровне 0, однако склоны и обрывы могут сдвигаться вверх и вниз. Низкие склоны поднимаются на 1 уровень, высокие — на 2. Тайл дороги на уровне 3 должен соединяться с другим тайлом дороги на уровне 3 или с тайлом склона, обеспечивающим переходы между уровнями. Если ошибиться, что дороги будут упираться в грани обрывов, а реки — течь наверх. Ось возвышения превращает 2D-задачу ограничений в 3D-задачу, и из-за этого возникает большое разнообразие тайлов (и куча сбоев солвера).

Natural
Цвета возвышений
Отладочные цвета
Отладочные цвета

Тайлы раскрашиваются при помощи основанного на нодах PBR-материала MeshPhysicalNodeMaterial с нодом цвета TSL. Возвышение каждого тайла кодируется в цвет инстанса, который шейдер использует для смешивания между двумя текстурами палитр — рельеф внизу получает летние цвета, наверху — зимние.

Координаты шестиугольников: неожиданная странность

Математика шестиугольников странная. Так как у нас есть не четыре, а шесть направлений, нельзя просто сопоставить позиции шестиугольников с 2D-координатами x,y. Наивный подход заключается в смещении координат — нумерации ячеек слева направо, сверху вниз, как в обычной сетке. Это работает до тех пор, пока нам не нужно находить соседей, вычислять расстояния и выполнять любые действия, касающиеся направлений. Потом всё сразу становится запутанным, появляются разные формулы для чётных и нечётных строк.

Hexagonal Offset Coords
Смещённые координаты: всё просто, пока нам не приходится делать с ними что-нибудь полезное

Более удобное решение: кубические координаты (q, r, s, где s = -q-r). Это система 3D-координат для трёх осей шестиугольников. Поиск соседей становится тривиальной задачей — достаточно прибавить или вычесть 1 из двух координат.

Здорово то, что для WFC не важна геометрия. Для него только важно, какая грань какой соответствует, то есть это задача на графах. Координаты шестиугольников важны только для рендеринга и для структуры из нескольких сеток, где сами 19 сеток выстроены в виде шестиугольника из шестиугольников со своими собственными смещёнными позициями.

Если вы когда-нибудь работали с сетками шестиугольников, то обязаны выразить благодарность Амиту Пателю из Red Blob Games. Его руководство по сеткам шестиугольников — это канонический справочный материал.

Деревья, здания и почему не всё должно быть WFC

Изначально я пытался использовать WFC для размещения деревьев и зданий. Это оказалось плохой идеей. WFC отлично справляется с сопоставлением локальных граней, но ужасен в создании крупномасштабных паттернов. Деревья будут разбросаны случайно, а не сгруппированы в леса, здания будут распределены равномерно, а не объединены в деревни.

Решение: старый добрый шум Перлина. Глобальное поле шума определяет плотность размещения деревьев и зданий совершенно отдельно от WFC. Области, в которых шум больше порогового значения, располагаются деревья; зданиями управляет немного другой шум. Благодаря этому мы получаем органичную кластеризацию — леса, опушки, деревья — которую ни за что бы не обеспечил WFC. Также я использовал дополнительную логику для размещения здания в концах дорог, портов и ветряных мельниц на побережьях, земляных сооружений на вершинах холмов и так далее.

WFC занимается рельефом. Шум генерирует декорации. Каждый инструмент делает то, в чём он хорош.

Village with clustered buildings and surrounding forests
Здания группируются вдоль дорог. Леса образуют естественные группы. Всё это генерируется не WFC, а а при помощи шума.

Вода: сложнее, чем кажется

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

Close-up of ocean debug view

Мерцание

Я хотел получить мультяшный эффект мерцания на поверхности воды в стиле Zelda: The Wind Waker. Изначально я пытался генерировать каустику процедурно при помощи четырёх слоёв шума Вороного. Оказалось, что этот процесс сильно нагружает GPU, а выглядит не очень красиво. В итоге я решил сэмплировать маленькую текстуру каустики со скроллингом при помощи простой маски шума; это выглядело намного лучше и почти не требовало затрат. Иногда правильным решением оказывается самое простое.

Прибрежные волны

Волны — это полосы синусоид, излучаемые наружу от линий побережья. На такое решение меня вдохновил красивый эффект побережья в Bad North. Чтобы знать, насколько далеко от берега находится каждый пиксель, система рендерит маску побережья — ортографический рендер всей карты с видом сверху, где белым обозначена суша, а чёрным вода, после чего она расширяется и размывается в градиент. Шейдер волн считывает этот градиент, чтобы размещать анимированные полосы синусоид с равными интервалами, а шум позволяет добавить разнообразия.

Flat blue plane
Послойное создание эффекта воды

Проблема бухт

При ровных побережьях это работало замечательно. Но в вогнутых бухтах и заливах волны оказывались жирными и некрасивыми. В бухтах градиент на основе размытия распределял один и тот же диапазон значений на более широкое физическое пространство, растягивая полосы воды.

Я пробовал это исправить разными способами:

  • Производные в экранном пространстве для распознавания растягивания градиентов — на одном уровне зума это работало, но ломалось на других.

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

  • Дополнительные проходы растяжения — влияли и на ровные побережья.

Фундаментальная проблема заключалась в том, что размытие кодирует количество суши по соседству, а не расстояние до ближайшего края побережья. Это совершенно разные параметры, и никакая постобработка не позволила бы извлечь из размытия истинное расстояние.

Для решения я реализовал выполняемое CPU зондирование окружённости сушей, проверяющее соседей каждой водной ячейки для выявления бухт; при этом записывается отдельная текстура маски, делающая волны в замкнутых областях более тонкими. Это похоже на хак, но он работает, а грани с водой становятся красивыми и тонкими.

Создание тайлов в Blender

Ассеты 3D-тайлов взяты из потрясающего низкополигонального Medieval Hexagon pack KayKit. Но для завершения подмножества тайлсета мне не хватало важных соединений, поэтому вспомнил старое и создал в Blender новые тайлы: наклонные реки, завершения рек, соединения рек и побережий, а также множество вариаций граней с обрывами.

Главное ограничение: каждый тайл имеет ширину ровно в 2 единицы мира, а типы граней должны идеально совпадать на границах шестиугольников. Для правильного создания UV необходимо, чтобы текстурный атлас корректно накладывался на швы между тайлами. Если UV не совпадёт хотя бы на несколько пикселей, то будет заметен шов, разрушающий иллюзию.

Blender viewport showing hex tiles

Украшательства

Алгоритм создаст вам правильную карту, но нужно ещё превратить её в место, которое бы хотелось посетить.

Completed map beauty shot

WebGPU и TSL-шейдеры

В качестве рендерера используется Three.js с WebGPU и TSL (Three.js Shading Language) — новая система шейдеров на основе нодов, заменившая сырой GLSL. Все мои визуальные эффекты написаны на языке TSL, который похож на немного инопланетный диалект JavaScript, выполняемый в GPU.

Стек постобработки

Новый сырой рендер выглядит... нормально. Плоско. Как настольная игра, сфотографированная при лампах дневного света. Придать ему атмосферности может конвейер постобработки:

  1. GTAO Ambient Occlusion — затемняет сгибы между тайлами, вокруг зданий и деревьев. Благодаря этому всё выглядит более массивным. Результат AO подвергается удалению шума для снижения ряби. Этот процесс выполняется при половинном разрешении, потому что AO и удаление шума — это затратные операции.

  2. Depth of Field — размытие tilt-shift в зависимости от расстояния до камеры придаёт ощущение миниатюры/диорамы. Фокусная длина DOF масштабируется с зумом камеры, чтобы при увеличении зума DOF тоже увеличивалось.

  3. Виньетирование + зерно плёнки — небольшое затемнение краёв и шум. Ровно столько, чтобы добавить аналоговости.

Normals

Динамические карты теней

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

Fixed shadow map

Оптимизации

На карте тысячи тайлов и объектов. При отрисовке всего этого по отдельности частота кадров резко упадёт. Решить проблему можно двумя способами:

BatchedMesh — каждая сетка шестиугольников получает два BatchedMesh: один для тайлов, другой для объектов. Удобство BatchedMesh заключается в том, что каждый меш может иметь отдельную геометрию и преобразования, но все они рендерятся за один вызов отрисовки. GPU занимается преобразованиями и смещениями геометрии для каждого инстанса, поэтому после подготовки затраты CPU, по сути, равны нулю.

Вся сцена рендерится всего за несколько вызовов отрисовки вне зависимости от сложности карты. Благодаря этому базовый рендер остаётся малозатратным, поэтому можно потратить бюджет GPU на AO, DoF и цветокоррекцию.

Один общий материал — все меши в сцене имеют один материал. UV мешей отображаются в маленькую текстуру палитры, чтобы им всем можно было извлекать свой цвет из одного изображения. Благодаря использованию всего одного материала между вызовами отрисовки не происходит переключения между состояниями шейдеров, поэтому GPU может без пауз обрабатывать 38 BatchedMesh.

В результате мы получаем 4100+ ячеек, 38 BatchedMesh, и вся картинка рендерится в 60fps на десктопах и мобильных.

snow day

Подведём итог

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

Числа

30 типов тайлов

19 сеток шестиугольников

~4100 ячеек

2 вызова отрисовки на сетку

Максимум 500 возвратов назад

5 попыток локального WFC

~20 секунд на создание всех сеток

Показатель успеха 100% (проверено на 500 прогонах)

Технологический стек

  • Three.js r183 с рендерером WebGPU

  • TSL (Three.js Shading Language) для всех шейдеров

  • Веб-воркеры для решения WFC вне потоков

  • Vite для сборок

  • BatchedMesh для эффективного рендеринга тайлов (один вызов отрисовки)

  • Генератор случайных чисел с порождающими значениями для создания детерминированных, воспроизводимых карт

Попробуйте сами

Интерактивное демо — нажимайте на шестиугольные кнопки для расширения карты или нажмите на «Build All», чтобы сгенерировать всё целиком. Там есть полная панель GUI с более чем 50 настройками на случай, если вы захотите поэкспериментировать с освещением, цветокоррекцией, эффектами воды и параметрами WFC.

Полный исходный код выложен на GitHub

Благодарности и ссылки

Комментарии (32)


  1. zeroprin-87
    11.03.2026 02:35

    Думал поиграть получиться, но не нет.


    1. Pochemuk
      11.03.2026 02:35

      У меня тоже ссылка не работает. Пишет, что устройство или браузер не соответствует требованиям.

      Пробовал на разных браузерах. Но на работе у меня Win8.1ю Дома попробую на Win10.

      Если у кого получилось зайти в демку - напишите, что требуется.


      1. WatchMaster
        11.03.2026 02:35

        Я просто с мобильного телефона открыл - получилось через chrome


    1. diomas
      11.03.2026 02:35

      тоже пишет WebGPU is not available on your device or browser.
      хотя на странице https://get.webgl.org/ пишет Your browser supports WebGL и кубик крутится


      1. Pochemuk
        11.03.2026 02:35

        Аналогичный случай. Кубик крутится, шарик вертится. А демка не открывается.


    1. iamkisly
      11.03.2026 02:35

      В win10 edge получилось
      уиии !!!
      уиии !!!

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


  1. Norseman
    11.03.2026 02:35

    Обалденно классная разработка. Спасибо за интересную статью! :)


  1. ArgentMind
    11.03.2026 02:35

    Великолепие! Спасибо большое за красивое решение и отличный результат


  1. Pochemuk
    11.03.2026 02:35

    Тема генерации тайловых карт не раскрыта в плане приближения к реальным ландшафтам:

    1. Как обеспечить, что горы не будут группироваться в сплошные массивы, а будут образовывать хребты и отроги?

    2. Как обеспечить, что реки будут стекать с гор и возвышенностей (водоразделов), а не пересекать их?

    3. Как обеспечить, что реки будут сливаться, а не разделяться? Но возможно образование рукавов и стариц.

    4. Как обеспечить, что реки будут впадать в моря (можно и в озера, но при этом должны и вытекать из них), а не прерываться или заканчиваться в чистом поле?

    5. Как обеспечить, что дороги будут соединять города/посёлки/святилища/шахты и т.д., а не обрываться и не закольцовываться?

    6. Как обеспечить более-менее оптимальную схему дорог с учетом проходимости ландшафта?

    7. И т.д.


    1. NickDoom
      11.03.2026 02:35

      Твик вероятностей «первого тайла» при помощи не только Перлина :)


      1. Pochemuk
        11.03.2026 02:35

        Поясните, пожалуйста.

        Вероятность на то и вероятность, что вероятно всё, даже самое невероятное :)

        Как используя процедурную тайловую генерацию объединить три города дорогами по схеме "звезда"?

        Тут, скорее, надо с другой стороны танцевать:

        1. Сначала сгенерить крупнокалиберную географию - моря, реки, хребты, отроги.

        2. Затем сгенерить карты высот на основе удаления от гор-морей (и учитывать, что реки должны течь с уменьшением высоты или по равнине). Зашумить карты высот хоть Перлином, хоть еще как. Лишь бы рядом с реками не находились более низкие участки.

        3. Потом сгенерить ландшафты и биомы на свободных клетках с учетом высот и удалений от гор/рек/морей. Т.е. это та же самая тайловая генерация, но высота будет играть роль регулятора вероятности.

        4. Потом поставить города недалеко от рек и морей. Шахты у гор и в болотах, капища в лесах и холмах, руины везде.

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

        Вот как-то так.


        1. NickDoom
          11.03.2026 02:35

          Выполняется коллапс ячейки с наибольшим количеством ограничений. Выбираем ячейку с наименьшим количеством оставшихся вариантов (самой низкой энтропией). Случайно выбираем одно из её допустимых состояний.

          …вот к этой случайности подмешиваем вот это вот:

          Сначала сгенерить крупнокалиберную географию - моря, реки, хребты, отроги

          …да и хоть вот это вот:

          Потом поставить города недалеко от рек и морей. Шахты у гор и в болотах, капища в лесах и холмах, руины везде.

          В результате кварталы начнут раскидываться не абы как, а преимущественно привязанными к центру города (которые посчитали более умным алгоритмом, построенным на принципах логистики или даже хоть внутримировой экономики), а вокруг них уже коллапс обстроит преимущественно город — но не забывая о том, что через город может протекать не только центральная река, а часть кварталов может быть за лесом, в качестве пригорода.

          Вероятность на то и вероятность, что вероятно всё, даже самое невероятное :)

          Ага! Случайный коллапс в квартал и образование забытой Древними Богами деревушки посреди нигде :)

          Всё так, всё так :) :) :)


          1. Pochemuk
            11.03.2026 02:35

            Эх ... Даже в WarLords-2 захотелось поиграть :)

            Города, руины, дороги, сложные ландшафты. Влияние ландшафта на характеристики юнитов.

            И очень похожие на сгенерированные WFC карты. Хотя, быть такого не может. Игрушка старая - ей более 30 лет. А WFC получил распространение несколько лет назад.


          1. Pochemuk
            11.03.2026 02:35

            Я тут немного освежил память по поводу п.5:

            На карте с расположенными объектами строится сеть Штейнера. Вернее, её подобие. Потому что настоящая сеть Штейнера строится на непрерывной поверхности с Евклидовой метрикой, а нам придется работать на дискретной гексагональной метрике. Да еще в условиях переменной стоимости прокладки дорог.

            Первое обстоятельство, само по себе, не сильно усложняет проблему нахождения перекрёстков. Просто найти тайл, включающий точку Торричелли (если такая есть) и уточнить его симплекс-методом.

            Но вот второе обстоятельство, в условиях сложного ландшафта, делает эту задачу нетривиальной:

            1. Найденный тайл может содержать реку. Построить Т-образный мост на картинке можно, но на практике это лишено всякого смысла.

            2. Найденный тайл может находиться в заливе или озере. Тогда никакой симплекс-метод не улучшит суммарную стоимость прокладки дорог. Да и где виданы дорожные развязки над водой?

            3. Найденный тайл может находиться в окружении такого сложного ландшафта, что симплекс-метод, в лучшем случае, поможет слегка улучшить стоимость прокладки дорог (локальный минимум), но не найдет наилучшего способа (глобальный минимум).

            Навскидку, пример для последнего случая:

            Точка Торричелли находится в лесу внутри подковообразного хребта. Симплекс-метод определяет, что выбор тайла у основания подковы позволит укоротить два пути по лесу за счет удлинения одного пути по лесу. Но оптимальный способ прокладки дорог может заключаться в огибании хребта с внешней стороны подковы. Потому что там нет лесов вообще - одни равнины. Да и через горы прорубаться не надо. Пути получатся длиннее, но дешевле.

            Можно было бы просто для всех тайлов внутри треугольника городов рассчитать суммарную стоимость дорог до этих трёх городов. Но дело в том, что ландшафт может быть таким сложным, что перекрёсток выгоднее расположить вообще за пределами треугольника городов. А это значит, что на него окажут влияние другие дороги и перекрёстки.

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


  1. realbtr
    11.03.2026 02:35

    Чисто математически, в рамках исследования динамических систем, аттракторов в фазовых пространствах, основная идея устойчивой генерации пазлов в иерархии устойчивых циклов. То есть весь пазл разбивается на замкнутые траектории, а потом они объединяются и плитки пересечения траекторий работают как якоря. Проблема «последней плитки» переносится в комбинаторику минимального замкнутого цикла, а эта комбинаторика компактнее.

    Терминология тут не совсем популярная, наполовину я ее генерирую в процессе, иногда нахожу решения у других. Вообще Дмитриев и Кузнецов глубоко исследовали перемежающиеся циклы состояний динамических систем, а вот теорема разрешимости в явном виде была сформулирована Марковым и с тех пор прогресса в ее обобщениях нет (никому не нужно было, пока не выяснилось, что все что нам нужно это внимание)


  1. LeoLev
    11.03.2026 02:35

    Очень креативно!


  1. wmlab
    11.03.2026 02:35

    У волновых функций такая же проблема, что и у сверточных сетей - локальность. Влияние "через клетку" не учитывается. Например, на одном тайле заканчивается река, на соседнем она продолжается, на следующем тайле опять заканчивается, что в физическом мире является абсурдом. Мы уже перешли через эту стадию, от генеративных сетей к механизму внимания, когда каждый токен может соотноситься с любым другим, не обязательно с соседним.


    1. Pochemuk
      11.03.2026 02:35

      Я уже писал выше, что река не может "заканчиваться" иначе, чем впадением в море.

      Так же как и дороги не могут прерываться, кроме как на объектах, которые они соединяют.

      Петли дорог и рек на равнинах - тоже могут возникнуть в некоторых случаях.


      1. wmlab
        11.03.2026 02:35


        1. alliumnsk
          11.03.2026 02:35

          достаточно задать направление реки в тайле, чтобы реки не могли заканчиваться слепо


          1. Pochemuk
            11.03.2026 02:35

            Да, направление - первое, что приходит на ум. Но вряд ли его одного будет достаточно.


            1. iamkisly
              11.03.2026 02:35

              Водоемы всегда объединяются. Река всегда стремится к "большой воде". Представьте что у вас есть два магнита, при приближении в их взаимное притяжение усиливается. В промежуточных тайлах должны усиливаться веса. Так образуются устья и дельты, они неизбежны.


        1. Pochemuk
          11.03.2026 02:35

          Ну так это тайлы из предлагаемой реализации.

          Для реки это может быть исток или затока. Или залив моря.

          У дороги это может быть участок, непосредственно примыкающий к поселению или каким-либо руинам. Не рисовать же на самих тайлах построек различные способы подведения дорог. Тем более, что деревья и постройки не помещаются тайлами, а помещаются поверх них следующим слоем. Т.е. на этом огрызке дороги может сверху располагаться куча щебня, бывшая храмом местного божка.


          1. wmlab
            11.03.2026 02:35

            Так о чем я и говорю. Я привел только грубый пример с концами рек в конструкции 3x1. Пример может быть сложнее - кольцевая река, когда река просто течет по кругу, переливаясь сама в себя. Или разделяется, а потом сливается. Тайтлы соединены без ошибок, но образуют нелепую конструкцию. Решений два - либо обучить движок физическим законам - дать понимание высот и физики рек, либо поступить, поступили в энкодерах - обучить движок на наборе правильных карт, чтобы он заучил не только соседние тайтлы, но и дальние взаимосвязи. Иначе будут получаться "шесть пальцев". Это уже будет не коллапс волновой функции (вчерашняя технология), а современный энкодер.


            1. Pochemuk
              11.03.2026 02:35

              Если начинать генерацию реки с истока (якорный тайл), то закольцовки не будет. Собственно, направление, даже если его явно не задавать, уже будет реализовано при этом.

              Разделяться на рукава река может. Только, ненадолго. Вот это уже сложнее реализовать. И в устье реки, обычно, разбиваются на множество рукавов.

              А еще есть старицы - тупиковые застойные участки на месте бывшего русла.


              1. Ndochp
                11.03.2026 02:35

                Если начинать генерацию реки с истока (якорный тайл), то закольцовки не будет. Собственно, направление, даже если его явно не задавать, уже будет реализовано при этом.

                Вопрос не в рукавах и мегаостровах на несколько тайлов, а в том, что соединившись 2 рукава не отменяют необходимости течь дальше к морю, а при случайной генерации это не запрещено получается. (два поворота после развилки и прямой участок между ними)
                Но опять же лечится направлением течения. Ну увеличится число перекрестков, но кажется не смертельно. Хотя если это основание степени, то может и смертельно.


                1. Pochemuk
                  11.03.2026 02:35

                  Да, теперь понятно. Ни один из рукавов, по-отдельности, не запрещает появление прямого участка. А на самом деле он может соединить эти рукава "встречь".

                  Ну и точно так же возможно соединение встречь двух рек.

                  Так что, без указания направления течения не обойтись. Но это увеличит число тайлов с реками. Так вместо одного тайла с Т-образным слиянием/разделением рек (4 тайла с учетом поворотов) появятся 4 тайла с разными направлениями течения. А с учетом поворотов и отражений - 24.


      1. iamkisly
        11.03.2026 02:35

        Петли дорог и рек на равнинах - тоже могут возникнуть в некоторых случаях
        Петли дорог и рек на равнинах - тоже могут возникнуть в некоторых случаях

        смотрится это все равно чужеродно и лучше перестраивать к острову


  1. LiliJulie
    11.03.2026 02:35

    Вообще горы как универсальный пластырь для проблем генерации - это прекрасно



  1. WatchMaster
    11.03.2026 02:35

    На примере вот что заметил, я в графике не силен, но почему то размытие появляется и на удаленных тайлах и на ближних, как будто в фокусе центральная часть только, это так задумано? Просто из моих скудных знаний о композиции и детализации в рисунках, как будто бы так не должно быть. Опять же с мобильного chrome заходил, может это тоже роляет. А статья зачетная, спасибо!


  1. iamkisly
    11.03.2026 02:35

    Нужно задать минимальную длину рек. Потому что реки состоящие из тайла устья и слепого тайла смешны и существовать не должны. Иначе это похоже на купированный собачий хвост.