Files
Faset_Engine/docs/studies/17-asset-pipeline-and-blender-roundtrip.md
T

41 KiB
Raw Blame History

Импорт ассетов и надёжный обмен с Blender

Исследование от 17.09.2026; синхронизация решений 18.09.2026. Для Faset Engine принято импортировать опубликованное поколение ассета, сохранять идентичность его частей независимо от имён и хранить пользовательские изменения вне результата импортёра. Основная новая задача — определить, что именно осталось прежним после экспорта, какие зависимости изменились и когда новый результат можно сделать активным.

Документ углубляет контракт MCP/Blender, Godot UX и редактор Blender. Основные контракты приняты в архитектуре, этапы до/после MVP — в PLAN.md. Точные имена API, поля примерного manifest и dependency pins уточняются реализацией. Box2D/Box3D выбраны; исследовательские SHA ниже не фиксируют версии зависимостей Faset. Ни импортёры, ни движки здесь не запускались; тесты ниже — критерии будущей реализации.

Два равноправных входа. Стандартный .gltf/.glb импортируется без Blender и без специального add-on. Faset создаёт собственные asset metadata/recipe и импортирует проверенный snapshot файлов. Используется обычный официальный Blender без модификации исходников; optional Python add-on сохраняет IDs частей и публикует GLB + manifest удобной командой. Расширенный bundle ниже описывает именно этот дополнительный надёжный roundtrip, не обязательный формат любого входного ресурса.

Без устойчивых source IDs нельзя гарантировать matching после rename/reorder/split. Sidecar сохраняет уже назначенные engine IDs, но не доказывает, что новый glTF node — прежний объект. Неоднозначность требует diagnostic/remap; эвристика имени не становится гарантией. Геометрия/rig/animation принадлежат Blender; gameplay, physics settings и instance overrides — Faset. MCP запускает импорт и читает его editor diagnostics, но не редактирует/инспектирует runtime world и отсутствует в Player.

1. Godot: что запускает повторный импорт

Подтверждено исходниками. Проверка имеет два уровня. _is_test_for_reimport_needed сначала сопоставляет времена изменения исходника и .import; при соответствующей настройке проверяет отсутствие outputs. Следующий _test_for_reimport проверяет checksum sidecar, сохранённые importer/UID/outputs, актуальность project-dependent settings и контрольные суммы исходного и производных файлов. Рост get_format_version() относительно сохранённой версии также требует импорта. Это не универсальный content-addressed build graph: внешний быстрый фильтр доверяет совпадению timestamps. Быстрый фильтр, версия и настройки, checksums.

Путь результата вычисляется из имени и hash пути исходника, а не только его содержимого. Поэтому move способен изменить адрес кэша при сохранении идентичности ресурса. _reimport_file получает прежний UID и параметры из sidecar, добавляет defaults, вызывает importer и записывает outputs, UID, format version и параметры. Контрольные суммы хранятся отдельно от пользовательских настроек. Адрес кэша, чтение прежних параметров, вызов importer, раздельное сохранение.

Обработка ошибки тоже часть архитектуры: sidecar может получить valid=false; последующая проверка не запускает бесконечный автоматический retry для уже неудачного импорта. Из этого не следует сохранение последнего рабочего результата или атомарность нескольких outputs: показанная функция вызывает importer, а затем отдельно пишет .import и .md5. Неудачный результат, подавление цикла ошибок.

Принятый принцип Faset; детали реализации. Watcher только сообщает «возможно изменилось». Для воспроизводимого импорта нужен digest реальных входов и зафиксированный recipe. Первая версия может инвалидировать целый bundle; позднее разделить mesh, texture, animation и collider jobs. Важнее получить объяснение why_reimport: source bytes / recipe / importer build / dependency artifact / target profile / missing output. Ошибка становится состоянием с диагностикой и последним успешным поколением, а не поводом перезапускаться при каждом обновлении дерева файлов.

2. UID файла не решает идентичность внутренних объектов

Подтверждено Godot. ResourceUID::create_id_for_path первоначально использует seed из имени проекта, пути в нижнем регистре и MD5 файла. Устойчивость при следующих импортах обеспечивается сохранённым UID и registry, а не повторным вычислением этой формулы. При обнаружении двух существующих файлов с одинаковым UID сканер назначает новому файлу другой ID; если старого пути больше нет, может обновить сопоставление прежнего ID. Создание UID, дубликат и перемещение.

Для внутренних элементов есть отдельные ключи. Настройки узла находят по import_id, с fallback PATH: + путь от корня. Mesh и material используют import_id, а при его отсутствии имя. Для внешне сохраняемого subresource convert_path_to_uid предпочитает существующий UID; иначе может получить его из source UID и логического ключа. Это полезное пространство имён, но переименование самого ключа не становится автоматически безопасным. Узлы, mesh, material, производный UID.

Принятый принцип Faset; детали реализации. Развести четыре понятия:

  • asset_id — постоянная идентичность публикуемого ассета, например двери целиком.
  • source_id — идентичность исходного Object/Mesh/Material/Action, назначенная в Blender.
  • output_id — идентичность части, на которую ссылается движок: mesh, material slot, clip, collider или узел импортируемой сцены.
  • content_hash и generation — конкретное содержимое и согласованная версия результатов.

Ссылка сцены — (asset_id, output_id, expected_kind). Имя, glTF index, путь файла и runtime handle в неё не входят. Один source может давать несколько outputs; несколько Actions могут образовать один clip. Для простых соответствий output ID допустимо выводить из asset_id + source_id + постоянная semantic role; изменение содержимого или версии compiler не должно само менять ID. Split/merge и смена типа output требуют явного migration/remap, а не новой случайной нумерации.

3. Unity: зависимости должны различать исходник и результат

Подтверждено C# и документацией. Native-код вызывает ScriptedImporter.GenerateAssetData, который передаёт контекст в OnImportAsset; регистрация importer передаёт version, extension, очередь и настройку caching. AssetImportContext создаётся native-стороной. AddObjectToAsset(identifier, object) добавляет часть результата; официальный контракт требует воспроизводить один и тот же identifier при повторном импорте, уникальный внутри asset. Вход импортёра, регистрация, native boundary, контракт identifier, Unity 6.0.

API отдельно выражает DependsOnSourceAsset, DependsOnArtifact и DependsOnCustomDependency. C# проверяет аргументы и вызывает native bindings; отсюда виден контракт, но не устройство хранилища, scheduler или crash-safe commit. External remap — ещё один механизм: SourceAssetIdentifier содержит type/name, GetExternalObjectMap собирает пары из native-массивов. Это не тот же идентификатор, что stable local ID результата. Разделение зависимостей, artifact dependency, custom dependency, remap key, external map.

Принятый принцип Faset; детали реализации. Import context предоставляет read_source, read_artifact, read_setting и записывает зависимости автоматически. Смена текстуры должна инвалидировать читающий её material stage; смена только placement экземпляра не должна перекомпилировать texture. В ключ входят importer build digest, canonical options, target capabilities и фактически прочитанные dependency digests. Blender/exporter version относится к export recipe; изменение .blend, давшее идентичный опубликованный glTF и metadata, само по себе не обязано пересобирать runtime mesh. Случайные зависимости от рабочего каталога, времени или последнего UI preset исключаются контрактом importer.

4. Что реально делает Blender exporter

Godot показывает готовую границу процессов: background Blender открывает .blend, вызывает bpy.ops.export_scene.gltf с явными options; затем Godot импортирует полученный glTF. Это подтверждает полезность такого обмена, но не делает Blender обязательным runtime dependency. Фоновый экспорт, запуск процесса, настройки и последующий импорт.

В Blender save переключает object mode при необходимости, меняет frame для экспорта, вызывает gather и write, затем возвращает frame. Это код с контекстом и побочными изменениями UI-состояния, поэтому helper должен явно выбрать scene/collection и восстанавливать собственный временный контекст при ошибках. generate_extras фильтрует custom properties и преобразует значения; node exporter подключает это только при включённом extras. Собственная строка UUID проходит здесь как данные, но стандарт не назначает ей семантику. save, extras, node extras.

Для устойчивых IDs есть важные ограничения. Обычное чтение .blend сбрасывает session_uid, поэтому он не подходит для межсессионных asset references. Custom ID properties сохраняются в .blend, но копирование datablock копирует и properties: дублирование объекта может дублировать наш UUID. Сброс session UID, копирование properties, сериализация.

Необязательный Python helper/add-on в официальном Blender: хранить namespaced UUID properties, проверять uniqueness в публикации и сохранять IDs в authoring-файл. Два Objects могут законно ссылаться на один Mesh ID; два разных Mesh datablocks с одним ID — ошибка. Если helper наблюдал операцию duplicate, новый Object получает новый ID. Если обнаружены уже сохранённые дубликаты и непонятно, кто оригинал, показать DuplicateSourceId и явную команду fork identity; не выбирать по порядку обхода. Linked library data требуют отдельного namespace исходной библиотеки или подготовленных IDs в библиотеке; в v1 не обещать автоматическую устойчивость произвольного linked/generated контента.

5. Минимальный bundle и публикация поколения

Эскиз проектного формата для расширенного обмена, не существующий стандарт и не финальная wire schema. Пример bundle.json optional add-on содержит:

{
  "schema_version": 1,
  "asset_id": "<uuid>",
  "generation": "<digest-of-canonical-manifest-body>",
  "source": {"document_id": "<uuid>", "path_hint": "door.blend"},
  "exporter": {"blender_build": "<commit>", "helper_version": 1},
  "recipe": {"profile": "faset-gltf-v1", "digest": "<hash>"},
  "files": [{"path": "payload/<hash>.glb", "sha256": "<hash>"}],
  "outputs": [{
    "output_id": "<uuid>", "kind": "mesh", "source_ids": ["<uuid>"],
    "role": "render_mesh", "name": "DoorLeaf",
    "locator": {"file": 0, "json_pointer": "/meshes/2"}
  }],
  "dependencies": [],
  "profile": {"coordinates": "gltf-rh-y-up", "linear_unit": "meter"}
}

generation считают без собственного поля; canonicalization и hash algorithm входят в спецификацию schema. Locator действителен только внутри данного поколения и получается после окончательного формирования glTF. Helper сопоставляет extras с outputs, проверяет единственность и полноту; имени недостаточно. Полные export options сохраняются в versioned recipe, engine import/cook options — отдельно. Generated collider, LOD или mesh variant могут появляться только в derived manifest импортёра; export manifest не обязан заранее знать все платформенные outputs.

Принятый принцип публикации; последовательность для расширенного bundle:

  1. Helper готовит временный каталог, экспортирует payload и формирует manifest. Проверяет glTF, доступность всех URI, IDs, соответствие профилю и hashes. .glb сам по себе не гарантирует отсутствие внешних файлов — это разрешено форматом. glTF, GLB structure.
  2. Неизменяемые payload files получают окончательные имена; manifest публикуется последним через замену одного файла. Требования к atomic replace/durability проверяются отдельно на целевых filesystem Linux/Windows. Watcher реагирует на commit manifest, а не на каждый временный файл.
  3. Import job фиксирует snapshot manifest, recipe и dependencies; пишет derived outputs в отдельное поколение. Перед commit повторно проверяет digests/revision: более поздний экспорт не должен быть затёрт завершившимся старым job.
  4. Registry атомарно переключает активный manifest одного ассета после validation. Читатель получает весь прежний или новый набор outputs. Сбой и cancel до commit оставляют предыдущий набор; незавершённый staging удаляется при восстановлении.

Нужно различать новый source уже опубликован и новый imported asset принят проектом. Ошибочный импорт показывает pending source generation и прежнюю active generation. Это честнее, чем выдавать старую картинку за успешный reimport. Git хранит authoring IDs, source bundle и recipes; build cache и незавершённый staging восстанавливаются. Уборка старых payload/derived generations учитывает действующие manifests, jobs и открытые snapshots; бесконечное накопление не является частью дизайна.

6. Overrides и разбор rename/delete

Godot уже различает внешний авторский материал и импортируемое содержимое: material settings могут подставить ресурс по UID с fallback path. При сохранении animation опция keep_custom_tracks копирует только неимпортированные tracks из прежнего ресурса; это конкретная политика сохранения, не общий трёхсторонний merge. External material, custom tracks.

Принятый принцип Faset; детали реализации. Хранить imported baseline, override patches и provenance отдельно. Ключ patch включает цепочку вложенных InstanceId, ObjectId/ComponentId/FieldId либо устойчивый resource output/slot ID с проверкой TypeId. Instance chain не совпадает с transform path, имя не входит в адрес. Material override относится к semantic slot, не к номеру primitive. При новом baseline применять только совместимые patches; удалённая цель или изменившийся тип поля создают conflict с предыдущим значением и контекстом. Унаследованное значение обновляется автоматически; явный override сохраняется, даже если прежнее значение случайно совпадало с baseline.

Проверочный walkthrough: дверь размещена тремя экземплярами; второму назначен другой материал, третьему добавлены gameplay и локальное смещение ручки.

Rename при наличии устойчивых source IDs. Blender DoorLeaf становится Panel, mesh меняется, порядок glTF arrays перестраивается. Source/output IDs прежние: importer обновляет label и locator, placement и overrides остаются. Перенос файла ассета аналогично меняет registry path, не идентичность.

Delete. Ручка исчезает из нового экспорта. Reverse-reference index находит локальный patch третьего экземпляра и возможные прямые ссылки других сцен. Candidate generation готова, но её активация получает NeedsResolution. Можно явно сопоставить прежний output совместимому новому, убрать зависимость или отделить старую ручку в авторский asset. Нельзя приклеить patch к «похожему» узлу по имени. Для v1 достаточно держать прежнее поколение активным до разрешения обязательных ссылок; исправления сцен проходят обычные document transactions. Это не обещание одной атомарной транзакции поверх всех файлов проекта.

Split/merge. Одну ручку заменили двумя частями или несколько materials объединили. Сохранение старого ID допускается только при определённой семантической преемственности; прочие outputs получают новые IDs. Миграция указывает mappings, а не только список renamed names. Идентичность collider и visual mesh также независима: смена triangulation не обязана обнулять gameplay-ссылку на коллайдер.

Связь с принятыми scene overrides

Повторно используемая сцена и её вложенные экземпляры сохраняют отдельные override layers. В v1 допустимы field overrides, новые объекты/компоненты, suppression унаследованного объекта с поддеревом и reparent внутри одного экземпляра. Нельзя переносить объект через границу nested instance; массив меняется целиком. Revert удаляет override. Variant inheritance, Apply to template и сложное слияние массивов отложены. Удаление output импортёром и пользовательский suppression различаются по provenance, но обе операции обязаны выявлять оставшиеся обязательные ссылки. Правила identity и структурных изменений.

7. Профиль материалов, координат и анимации

Материальный exporter собирает конкретные поля PBR, textures и extensions. Это преобразование поддерживаемого представления; не перенос произвольного Blender shader graph в движок. Для первого профиля предлагаются metallic/roughness, base color, normal, occlusion, emissive, alpha mode и double-sided; необязательные расширения имеют явную политику fallback, неизвестные required extensions блокируют импорт. Procedural appearance заранее bake в поддерживаемые textures. Формирование материала, официальный material workflow, Manual 4.0.

Color space — часть texture usage/recipe: base-color RGB декодируется из sRGB, данные roughness/metallic/normal обрабатываются как данные. Один image source может иметь разные usage-specific outputs. Профиль проверяет tangent basis, UV set и packing; материал физики с friction/restitution хранится отдельно от материала поверхности renderer. glTF material semantics.

glTF задаёт правую систему, Y-up и метры. В Blender exporter location swizzle при gltf_yup(x,z,-y), rotation и scale преобразуются отдельно. Faset должен нормализовать весь профиль один раз: geometry, node transforms, inverse bind matrices, animation и colliders. scene.unit_settings в UI не заменяет тест фактического размера экспорта. Negative determinant требует согласованного winding/tangent handling; shear и non-uniform scale на иерархии требуют явного bake/reject правила. Swizzle, glTF coordinates.

Анимация не имеет обязательного соответствия «Action = clip»: gather_actions_animations собирает результаты по объектам, а ACTIVE_ACTIONS может объединить их. Значит, clip identity описывает набор source Actions/slots и recipe range, а не animation array index. V1 фиксирует rest pose, frame range/rate, clip grouping, root-motion policy и допустимые influences. Constraints/IK bake в поддерживаемое движение; неподдерживаемые channels диагностируются. Сборка и объединение. Bone/slot rename и удаление joints проверяются отдельно от переименования clip.

8. Collider cooking для выбранных Box2D и Box3D

Box2D. b2ComputeHull ограничивает число входных точек, сваривает близкие и удаляет коллинеарные; неудача возвращает пустой hull. В проверенном header предел — 8 vertices. Поэтому нельзя отправить произвольный контур спрайта непосредственно как один polygon. Наш v1: ручные circle/capsule/box, валидируемые convex polygons; сложный contour — отдельное упрощение и convex decomposition с ограничением числа частей. Для окружения возможны chains с правильными соседями/winding, а не набор несвязанных сегментов. Hull implementation, предел vertices, официальное описание chains.

Box3D. b3CreateMesh проверяет входной layout, опционально выполняет welding, отбрасывает degenerate triangles, строит BVH, сортирует triangles вместе с material indices и при настройке вычисляет adjacency edges. Обычный triangle index из cooked query поэтому нельзя считать индексом исходного Blender polygon. В source b3CreateMeshShape проверяет B3_MESH_VERSION. Cooker, порядок triangles и edges, version check.

Документация относит triangle meshes к static geometry. Для Faset v1 предлагаются dynamic bodies из convex shapes и static mesh/terrain отдельно. Указатель geometry тоже имеет жизненный цикл: рассмотренная ветка shape хранит mesh data pointer, тогда как hull проходит через world database; освобождение старого поколения после reimport должно ждать удаления его physics shapes. Назначение mesh, владение geometry.

Collider recipe включает source output, 2D projection/3D local frame, единицы, scale policy, shape mode, welding/decomposition параметры, physics build и schema. Physics switch не должен тайно менять массу/pivot: authoring показывает cooked bounds, volume/area, части и diagnostics. Изменение цвета не запускает geometry cook; изменение baked scale запускает.

Безопасный минимальный формат — собственные versioned primitives/vertices/indices и параметры; создание backend BVH при загрузке учитывается отдельно во времени startup. Перенос BVH полностью в offline cook требует проверенного сериализуемого backend-формата и совместимости версий. Нельзя объявить произвольный memory dump b3MeshData вечным переносимым asset format только потому, что в структуре есть version. Если позднее появится runtime asset reload, смена collider выполняется в physics safe point, а старые данные удерживаются до завершения использования. В MVP Player использует snapshot запуска; reimport обновляет authoring/imported generation для следующего запуска и не подразумевает live runtime MCP.

9. Приёмка и новые выводы

Первый комплект fixtures: дверь с shared mesh/material, три экземпляра, material-slot override, skeleton с двумя clips, метрический калибровочный объект, negative scale, convex collider и static mesh со швом. Проверки:

  • Со source IDs: rename Object/Mesh/Material, move source и перестановка glTF arrays сохраняют semantic IDs и overrides. Без IDs: обычный glTF/GLB импорт работает, а неоднозначное сопоставление диагностируется без обещания rename-safe matching.
  • Duplicate Object сохраняет shared Mesh, duplicate datablock выявляет повторный source ID; повторный экспорт и очистка cache не создают новые identities.
  • Delete/split/type change показывают точные затронутые references; unresolved generation не заменяет рабочую.
  • Изменения recipe, importer build, зависимой texture и collider scale дают правильные причины invalidation; placement экземпляра не делает лишний cook.
  • Обрыв после payload, после derived output и перед registry commit не даёт смешанного поколения. Старый job не побеждает более новый export.
  • На Linux/Windows совпадают IDs, dependency graph и semantic output; byte-identical артефакты проверяются только для явно детерминированных stages.
  • Проверяются масштаб, pivots, skin bind pose, clip ranges, normal maps, winding и скольжение по collider seams. Cache hit не принимается за доказательство корректности.

Новые относительно предыдущего обзора выводы: UID файла недостаточен для rename частей; UUID property наследуется при duplicate; Actions и outputs могут иметь соответствие многие-ко-многим; зависимости source/artifact/settings нужно различать; публикация source и активация imported generation — две разные точки; physics geometry имеет отдельную идентичность, версию и срок жизни.

Охват источников. Локальные тела Godot 9c776068d6ed23acd0c78bfe534272d1d2a3a619, Blender 28d47268bddcb9dc69143f0e2d9410969da16311, UnityCsReference 6b50e5544f6efcca1f44dbace3d1778b465ac6d0 соответствуют манифесту. Недостающие пять Blender exporter files прочитаны из official raw source того же commit без расширения sparse checkout. Дополнительно прочитаны отдельные Box2D files commit 77619f4f7baebe5117a2e3ddc3ac8c404e82d243 и Box3D files/docs commit f555ee42084e0b43cbffa863f40bff8117c08896; это исследовательские pins, не выбор версий Faset. Их permalink lines сверены по скачанным файлам. Веб-сверка: спецификация glTF 2.0, Unity 6.0 API, Box2D collision documentation; Blender Manual 4.0 использован только для общего material workflow, текущие механизмы сверены по source 5.3 alpha. Native asset database Unity, весь exporter, автоматическая convex decomposition и переносимость cooked binaries не исследованы полностью. Производительность и roundtrip пока не измерялись.