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

143 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Импорт ассетов и надёжный обмен с 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).