ИИ-генерация уровней для Godot: от GLB до настоящей сцены

Как занести сгенерированную локацию в Godot через GLB и довести её до уровня: коллизии, окклюдеры, свет и повторный импорт без потери работы.

Городок с улицами, переходами, кирпичными домами и коттеджами, построенный агентом в Cuberta

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

Почему glTF — кратчайший путь в Godot

Страница поддерживаемых форматов в документации Godot откровенна насчёт иерархии: glTF 2.0 указан как рекомендованный — и текстовый .gltf, и бинарный .glb. FBX работает: с 4.3 он идёт через импортёр ufbx, а не через старый конвертер FBX2glTF. Файлы .blend импортируются напрямую, но этот путь под капотом вызывает собственный glTF-экспортёр Blender.

Ничего не приходится переинтерпретировать. Godot использует правую систему координат с осью Y вверх и -Z как направлением взгляда камеры — ровно та договорённость, что закреплена в спецификации glTF 2.0, — и обе стороны работают в метрах. Дверь 2,1 м в исходнике остаётся дверью 2,1 м во вьюпорте, и никто не трогает поле масштаба. Подробный разбор форматов — в статье GLB против FBX для игровых движков.

Одно следствие задаёт весь дальнейший процесс: Godot не может сохранять поверх исходного 3D-файла. Рядом создаётся .import, а сконвертированные ресурсы лежат в скрытой папке .godot внутри проекта, так что исходный .glb остаётся источником истины, а импортированная сцена каждый раз пересобирается из него. Поддержка первоклассна и в собранных проектах, так что тот же файл можно загрузить в рантайме через GLTFDocument.

Cuberta экспортирует GLB с текстурами, так что район, который агент разложил по земле — дороги, перекрёстки, тротуары, кварталы зданий, уличная мебель, свет, — приезжает одним файлом: копируете его в res://.

Настройки импорта, которые важны для района

Выделите .glb в доке FileSystem — и док Import заполнится опциями. Большинство значений по умолчанию годятся для персонажа. Очень немногие рассчитаны на целый город.

ОпцияДля районаПочему
Meshes > Generate LODsОставить включённымДальние здания теряют треугольники сами; в 4.6 LOD стали лучше держать форму мешей из отдельных частей.
Meshes > Create Shadow MeshesВключитьСваривает вершины для теневого прохода: платите раз на импорте, экономите каждый кадр.
Meshes > Light BakingStatic Lightmaps — только если будете запекатьОпция генерирует UV2 на импорте — самая долгая часть большого файла.
Meshes > Lightmap Texel Size0,5 и вышеЗначение 0,2 рассчитано на комнату, а не на район.
Meshes > Ensure TangentsВыключить, если нет карт нормалейМеньше файл, быстрее импорт.
glTF > Embedded Texture HandlingExtract TexturesНастоящие файлы изображений с собственным VRAM-сжатием и мипмапами.
Animation > ImportВыключить для статикиИмпортировать нечего.

Кнопка Advanced… под доком — или двойной клик по файлу — открывает диалог Advanced Import Settings. Именно там делается работа по узлам: слева дерево всех узлов glTF, посередине превью, справа опции узла, включая Skip Import для служебной геометрии, которая всегда просачивается в экспорт.

Как сделать сцену редактируемой: наследование и инстансы

Импортированную сцену нельзя править напрямую, и это намеренно: дерево пересобирается при каждом переимпорте, так что правки внутри него были бы стёрты. Выберите Scene > Open Scene… на .glb — Godot предложит унаследованную сцену; правый клик и New Inherited Scene ведут туда же.

Задокументированных ограничений там ровно два: узлы базовой сцены нельзя удалить (добавлять новые где угодно можно) и подресурсы нельзя редактировать на месте. Для материалов лечится пунктом Actions… > Extract Materials: он пишет .tres и переключает сцену на ссылки. Альтернатива — инстанцировать локацию внутрь уровня .tscn и держать правки там.

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

Коллизии, которые район может себе позволить

Godot генерирует коллизии двумя путями. Поузлово в диалоге Advanced Import Settings опция Generate > Physics создаёт родительский PhysicsBody3D, а формы коллизий становятся соседями MeshInstance3D; Body Type выбирает StaticBody3D, RigidBody3D или Area3D, Shape Type — саму форму. Документация высказывается прямо: Trimesh даёт точную потреугольную коллизию, но работает только со статичным телом, и «для статичной геометрии уровня используйте Trimesh». У Decompose Convex есть Precision: выше значение — детальнее коллизия и дороже CPU.

Второй путь — суффиксы в именах узлов исходного файла. -col добавляет дочернюю статическую коллизию по треугольникам, -colonly убирает видимый меш и оставляет StaticBody3D, -convcol и -convcolonly делают выпуклые варианты. Разделителем может быть -, $ или _, регистр не важен — это полезно знать и в обратную сторону, потому что сгенерированный объект по имени shop_col вас удивит. Поставьте nodes/use_node_type_suffixes в false, и механизм замолчит.

  • Земля, дороги, тротуары, бордюры, лестницы — Trimesh. По этому ходит игрок, и точность формы здесь и есть смысл.
  • Коробки зданий — не Trimesh. Фасад на 30 000 треугольников даёт коллайдер на 30 000 треугольников, в который вы будете только упираться. Хватит пары боксов руками.
  • Уличная мебель, заборы, фонари — выпуклая коллизия либо вообще ничего там, куда игрок не дойдёт.
  • Растительность — обычно ничего. Коллайдер на каждой карточке листвы — счёт, который никто не соглашался оплачивать.

Рекомендация Godot короче: когда возможно, используйте несколько примитивных форм вместо треугольного меша или выпуклых оболочек. С 4.6 физический движок по умолчанию — Jolt: быстрее прежнего, но не настолько, чтобы город из trimesh-зданий стал хорошей идеей.

Свет: запекать или отдать SDFGI

ПараметрLightmapGISDFGI
ЗапеканиеМинуты на GPU, повторно после любого изменения геометрииНет; каскады строятся вокруг камеры
Нужен UV2Да — Light Baking в Static LightmapsНет
ДинамикаПолучает непрямой свет через пробыПолучает GI, но не вносит вклад
ОтраженияНет; нужен ReflectionProbe или SkyСвои, только на непрозрачных материалах
Цена в рантаймеПочти нулевая; идёт на встроенной графикеСамый дорогой вариант GI в Godot
Ломается наСкорости итерацийБыстром движении камеры — видны сдвиги каскадов

В документации есть фраза про лайтмапы, которая выглядит как закрытие вопроса:

Не подходит для процедурно генерируемых уровней.

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

Поэтому разделение не техническое, а временнóе. Пока планировка движется, работайте на SDFGI: включается в Environment, запекания не требует вообще и просит только, чтобы у мешей был режим глобального освещения Static — этим управляет опция Light Baking в доке импорта. Когда планировка устоялась, переключайтесь на Static Lightmaps и запекайте один раз. Развёртка UV2 кэшируется между переимпортами, а файлы .unwrap_cache должны лежать в системе контроля версий: они держат UV2 одинаковыми на разных машинах и версиях движка.

Окклюзия, потому что город — это в основном стены

Документация Godot по оптимизации берёт городок как разобранный пример: идя по улице, вы видите несколько зданий, небо и пару птиц, а наивный рендерер отправляет на отрисовку соседнюю улицу, людей на ней и здания за ними. Depth prepass избавляет от полного шейдинга, но не от отправки.

Включите Rendering > Occlusion Culling > Use Occlusion Culling в настройках проекта — понадобится переключатель Advanced, перезапуск не нужен. Добавьте OccluderInstance3D, выделите его и нажмите Bake Occluders в 3D-вьюпорте. Польза от запекания решается тремя вещами:

  • Запекаются только узлы MeshInstance3D. MultiMeshInstance3D, частицы и CSG игнорируются, так что растительность, перенесённая в MultiMesh, перестала быть окклюдером.
  • Прозрачные материалы исключены. Площадь из стеклянных башен не перекрывает ничего. Документация отмечает, что метод эффективнее всего в интерьерах с множеством небольших комнат и нормальными непрозрачными стенами.
  • Пользуйтесь маской и упрощением. Bake > Cull Mask убирает динамику из запекания; Bake > Simplification меняет точность на время CPU — если объекты пропадают там, где должны быть видны, значение задрано.

Окклюдеры можно получить и на импорте: Generate > Occluder предлагает Mesh + Occluder или Occluder Only с настройкой Simplification Distance, а суффиксы -occ и -occonly делают то же из исходного файла. Чтобы увидеть, что реально отсекается, включите Perspective > Display Advanced > Occlusion Culling Buffer.

Количество узлов, инстансинг и где начинает болеть

Сгенерированный район приезжает как один MeshInstance3D на каждый экспортированный объект. Несколько сотен узлов — не проблема; проблема в drawcall'ах. Forward+ инстансит автоматически, когда узлы MeshInstance3D делят один меш и один материал, без настройки, — но только для непрозрачных материалов и alpha-test, никогда для альфа-блендинга и вообще никак в рендерерах Mobile и Compatibility.

Это ловушка, которую стоит проверить первой. Экспортёр, выдающий каждому объекту собственный материал, молча выключает автоматический инстансинг, и сорок одинаковых фонарей становятся сорока drawcall'ами. Смотрите на количество материалов раньше, чем на количество треугольников: Extract Materials и сведение дубликатов на один ресурс часто дают для кадра больше, чем любая работа с мешами, — а это отдельная тема.

MultiMesh — ответ для того, что приходит тысячами: трава, брусчатка, секции забора, деревья. Документация ставит границу так: тысячи постоянно обрабатываемых экземпляров — идите напрямую через серверы, сотни тысяч и миллионы — остаётся только MultiMesh. Плата в том, что отдельные экземпляры не отсекаются по фрустуму: MultiMesh рисуется целиком или не рисуется вовсе, поэтому режьте его по кварталам.

По дальности Mesh LOD с опции импорта работает сам, а Visibility Ranges — ручное дополнение. Пример в документации — как раз городской квартал: дайте низкодетальному мешу BatchOfHouses значение Visibility Range Begin и назначьте его Visibility Parent для четырёх детальных домов. Смотрите при этом на Objects Drawn и Draw Calls во вкладке Monitors отладчика: интуиция насчёт того, что сработало, ошибается примерно в половине случаев.

Переимпорт без потери недели

Вот сценарий, который решает, рабочий ли это процесс. Вы меняете городок в генераторе, экспортируете, перезаписываете location.glb, Godot переимпортирует. Что происходит с двумя днями работы на стороне Godot?

Часть переживает это по устройству системы. Извлечённые материалы при переимпорте не перезаписываются — с оговоркой, что переименование материала в исходнике рвёт связь. Кэш развёртки UV2 сохраняется, анимации, сохранённые в файл, сохраняют добавленные дорожки. Тихо не переживает всё, что адресуется через NodePath внутрь импортированного дерева: если экспорт переименовал Building_014 или переставил соседей, переопределения в унаследованной сцене приземляются в никуда. Есть и открытый на 4.7 баг: сохранение унаследованной сцены после переимпорта GLB пересоздаёт уникальные идентификаторы узлов, и diff'ы .tscn зарастают шумом.

  • Держите свою работу соседями, а не детьми. Прокси-коллизии, окклюдеры, узел LightmapGI и точки спавна кладите под собственный узел, а не в дети импортированных узлов, чьи имена вы не контролируете.
  • Используйте import script. В доке импорта есть поле Import Script > Path, указывающее на EditorScenePostImport с функцией _post_import(scene). Назначение коллизий по шаблону имени, установка режимов GI, подмена материалов — всё, что выражено кодом, само отрабатывает при каждом переимпорте.
  • Экспортируйте кусками. Один .glb на район либо один на тип здания плюс файл планировки. Тогда правка гавани переимпортирует гавань и не трогает старый город. В Cuberta это сводится к тому, чтобы отметить, где агенту строить, а где нет.

Преимущество Godot здесь реальное, но узкое. Он снимает трение формата — ни конвертации, ни переговоров о единицах, ни переворота осей, — и это стоит больше, чем звучит, когда одну локацию вы переимпортируете двадцать раз. Чего он не снимает — левел-дизайн. Импортированное дерево — это геометрия с именами; коллизии, окклюзия, свет и решение о том, что здесь один объект, а что тысяча, по-прежнему за вами. Автоматизировать имеет смысл ровно одно: чтобы всё это происходило одинаково каждый раз, когда вы нажимаете Reimport.