143 lines
41 KiB
Markdown
143 lines
41 KiB
Markdown
# Импорт ассетов и надёжный обмен с Blender
|
||
|
||
Исследование от 17.09.2026; синхронизация решений 18.09.2026. Для Faset Engine принято **импортировать опубликованное поколение ассета, сохранять идентичность его частей независимо от имён и хранить пользовательские изменения вне результата импортёра**. Основная новая задача — определить, что именно осталось прежним после экспорта, какие зависимости изменились и когда новый результат можно сделать активным.
|
||
|
||
Документ углубляет [контракт MCP/Blender](12-mcp-and-blender-integration.md), [Godot UX](08-godot-ux-source-study.md) и [редактор Blender](10-blender-editor-patterns.md). Основные контракты приняты в [архитектуре](../ARCHITECTURE.md), этапы до/после MVP — в [PLAN.md](../../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. [Быстрый фильтр](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L562), [версия и настройки](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L680), [checksums](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L727).
|
||
|
||
Путь результата вычисляется из имени и hash **пути** исходника, а не только его содержимого. Поэтому move способен изменить адрес кэша при сохранении идентичности ресурса. `_reimport_file` получает прежний UID и параметры из sidecar, добавляет defaults, вызывает importer и записывает outputs, UID, format version и параметры. Контрольные суммы хранятся отдельно от пользовательских настроек. [Адрес кэша](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/io/resource_importer.cpp#L541), [чтение прежних параметров](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L2826), [вызов importer](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L2922), [раздельное сохранение](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L3018).
|
||
|
||
Обработка ошибки тоже часть архитектуры: sidecar может получить `valid=false`; последующая проверка не запускает бесконечный автоматический retry для уже неудачного импорта. Из этого **не следует** сохранение последнего рабочего результата или атомарность нескольких outputs: показанная функция вызывает importer, а затем отдельно пишет `.import` и `.md5`. [Неудачный результат](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L2954), [подавление цикла ошибок](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L630).
|
||
|
||
**Принятый принцип 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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/io/resource_uid.cpp#L127), [дубликат и перемещение](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L912).
|
||
|
||
Для внутренних элементов есть отдельные ключи. Настройки узла находят по `import_id`, с fallback `PATH:` + путь от корня. Mesh и material используют `import_id`, а при его отсутствии имя. Для внешне сохраняемого subresource `convert_path_to_uid` предпочитает существующий UID; иначе может получить его из source UID и логического ключа. Это полезное пространство имён, но переименование самого ключа не становится автоматически безопасным. [Узлы](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/import/3d/resource_importer_scene.cpp#L1138), [mesh](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/import/3d/resource_importer_scene.cpp#L2811), [material](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/import/3d/resource_importer_scene.cpp#L1626), [производный UID](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/import/3d/resource_importer_scene.cpp#L3199).
|
||
|
||
**Принятый принцип 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. [Вход импортёра](https://github.com/Unity-Technologies/UnityCsReference/blob/6b50e5544f6efcca1f44dbace3d1778b465ac6d0/Modules/AssetPipelineEditor/Public/ScriptedImporter.cs#L25), [регистрация](https://github.com/Unity-Technologies/UnityCsReference/blob/6b50e5544f6efcca1f44dbace3d1778b465ac6d0/Modules/AssetPipelineEditor/Public/ScriptedImporter.cs#L121), [native boundary](https://github.com/Unity-Technologies/UnityCsReference/blob/6b50e5544f6efcca1f44dbace3d1778b465ac6d0/Editor/Mono/AssetPipeline/AssetImportContext.bindings.cs#L35), [контракт identifier, Unity 6.0](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/AssetImporters.AssetImportContext.AddObjectToAsset.html).
|
||
|
||
API отдельно выражает `DependsOnSourceAsset`, `DependsOnArtifact` и `DependsOnCustomDependency`. C# проверяет аргументы и вызывает native bindings; отсюда виден контракт, но не устройство хранилища, scheduler или crash-safe commit. External remap — ещё один механизм: `SourceAssetIdentifier` содержит type/name, `GetExternalObjectMap` собирает пары из native-массивов. Это не тот же идентификатор, что stable local ID результата. [Разделение зависимостей](https://github.com/Unity-Technologies/UnityCsReference/blob/6b50e5544f6efcca1f44dbace3d1778b465ac6d0/Editor/Mono/AssetPipeline/AssetImportContext.bindings.cs#L66), [artifact dependency](https://github.com/Unity-Technologies/UnityCsReference/blob/6b50e5544f6efcca1f44dbace3d1778b465ac6d0/Editor/Mono/AssetPipeline/AssetImportContext.bindings.cs#L156), [custom dependency](https://github.com/Unity-Technologies/UnityCsReference/blob/6b50e5544f6efcca1f44dbace3d1778b465ac6d0/Editor/Mono/AssetPipeline/AssetImportContext.bindings.cs#L232), [remap key](https://github.com/Unity-Technologies/UnityCsReference/blob/6b50e5544f6efcca1f44dbace3d1778b465ac6d0/Editor/Mono/AssetPipeline/AssetImporter.bindings.cs#L25), [external map](https://github.com/Unity-Technologies/UnityCsReference/blob/6b50e5544f6efcca1f44dbace3d1778b465ac6d0/Editor/Mono/AssetPipeline/AssetImporter.bindings.cs#L150).
|
||
|
||
**Принятый принцип 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. [Фоновый экспорт](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/modules/gltf/editor/editor_import_blend_runner.cpp#L85), [запуск процесса](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/modules/gltf/editor/editor_import_blend_runner.cpp#L160), [настройки и последующий импорт](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/modules/gltf/editor/editor_scene_importer_blend.cpp#L277).
|
||
|
||
В Blender `save` переключает object mode при необходимости, меняет frame для экспорта, вызывает gather и write, затем возвращает frame. Это код с контекстом и побочными изменениями UI-состояния, поэтому helper должен явно выбрать scene/collection и восстанавливать собственный временный контекст при ошибках. `generate_extras` фильтрует custom properties и преобразует значения; node exporter подключает это только при включённом extras. Собственная строка UUID проходит здесь как данные, но стандарт не назначает ей семантику. [save](https://github.com/blender/blender/blob/28d47268bddcb9dc69143f0e2d9410969da16311/scripts/addons_core/io_scene_gltf2/blender/exp/export.py#L21), [extras](https://github.com/blender/blender/blob/28d47268bddcb9dc69143f0e2d9410969da16311/scripts/addons_core/io_scene_gltf2/blender/com/extras.py#L23), [node extras](https://github.com/blender/blender/blob/28d47268bddcb9dc69143f0e2d9410969da16311/scripts/addons_core/io_scene_gltf2/blender/exp/nodes.py#L265).
|
||
|
||
Для устойчивых IDs есть важные ограничения. Обычное чтение `.blend` сбрасывает `session_uid`, поэтому он не подходит для межсессионных asset references. Custom ID properties сохраняются в `.blend`, но копирование datablock копирует и properties: дублирование объекта может дублировать наш UUID. [Сброс session UID](https://github.com/blender/blender/blob/28d47268bddcb9dc69143f0e2d9410969da16311/source/blender/blenloader/intern/readfile.cc#L2275), [копирование properties](https://github.com/blender/blender/blob/28d47268bddcb9dc69143f0e2d9410969da16311/source/blender/blenkernel/intern/lib_id.cc#L1732), [сериализация](https://github.com/blender/blender/blob/28d47268bddcb9dc69143f0e2d9410969da16311/source/blender/blenkernel/intern/lib_id.cc#L2895).
|
||
|
||
**Необязательный 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 содержит:
|
||
|
||
```json
|
||
{
|
||
"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](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#glb-file-format-specification).
|
||
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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/import/3d/resource_importer_scene.cpp#L1687), [custom tracks](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/import/3d/resource_importer_scene.cpp#L1971).
|
||
|
||
**Принятый принцип 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 и структурных изменений](01-architecture.md).
|
||
|
||
## 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. [Формирование материала](https://github.com/blender/blender/blob/28d47268bddcb9dc69143f0e2d9410969da16311/scripts/addons_core/io_scene_gltf2/blender/exp/material/materials.py#L347), [официальный material workflow, Manual 4.0](https://docs.blender.org/manual/en/4.0/addons/import_export/scene_gltf2.html#materials).
|
||
|
||
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](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#materials).
|
||
|
||
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](https://github.com/blender/blender/blob/28d47268bddcb9dc69143f0e2d9410969da16311/scripts/addons_core/io_scene_gltf2/blender/exp/nodes.py#L597), [glTF coordinates](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#coordinate-system-and-units).
|
||
|
||
Анимация не имеет обязательного соответствия «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 диагностируются. [Сборка и объединение](https://github.com/blender/blender/blob/28d47268bddcb9dc69143f0e2d9410969da16311/scripts/addons_core/io_scene_gltf2/blender/exp/animation/action.py#L227). 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](https://github.com/erincatto/box2d/blob/77619f4f7baebe5117a2e3ddc3ac8c404e82d243/src/hull.c#L87), [предел vertices](https://github.com/erincatto/box2d/blob/77619f4f7baebe5117a2e3ddc3ac8c404e82d243/include/box2d/collision.h#L25), [официальное описание chains](https://box2d.org/documentation/md_collision.html).
|
||
|
||
**Box3D.** `b3CreateMesh` проверяет входной layout, опционально выполняет welding, отбрасывает degenerate triangles, строит BVH, сортирует triangles вместе с material indices и при настройке вычисляет adjacency edges. Обычный triangle index из cooked query поэтому нельзя считать индексом исходного Blender polygon. В source `b3CreateMeshShape` проверяет `B3_MESH_VERSION`. [Cooker](https://github.com/erincatto/box3d/blob/f555ee42084e0b43cbffa863f40bff8117c08896/src/mesh.c#L1586), [порядок triangles и edges](https://github.com/erincatto/box3d/blob/f555ee42084e0b43cbffa863f40bff8117c08896/src/mesh.c#L1799), [version check](https://github.com/erincatto/box3d/blob/f555ee42084e0b43cbffa863f40bff8117c08896/src/shape.c#L409).
|
||
|
||
Документация относит 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](https://github.com/erincatto/box3d/blob/f555ee42084e0b43cbffa863f40bff8117c08896/docs/collision.md#triangle-meshes), [владение geometry](https://github.com/erincatto/box3d/blob/f555ee42084e0b43cbffa863f40bff8117c08896/src/shape.c#L133).
|
||
|
||
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` соответствуют [манифесту](source-manifest.json). Недостающие пять 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 не измерялись. Позднейшие проверки собственного MVP-профиля Faset с Blender опубликованы в [результатах проверок](../validation/README.md).
|