# MSH animation `MSH animation` описывает связку `Res8 + Res19` и runtime-правила сэмплирования/смешивания поз. Связанные страницы: - [MSH core](msh-core.md) - [Render pipeline](render.md) ## 1. Ресурсы анимации ### 1.1. `Res8` (пул ключей) ```c struct AnimKey24 { float pos_x; float pos_y; float pos_z; float time; int16_t qx; int16_t qy; int16_t qz; int16_t qw; }; ``` Декодирование quaternion-компонент: `q = s16 / 32767.0`. ### 1.2. `Res19` (карта кадров) ```c uint16_t map_words[]; // size/2 элементов ``` `Res19.attr2` хранит глобальную длину таймлайна (число кадров). ### 1.3. Связь с `Res1` Для каждого узла: - `anim_map_start` (`hdr2`) — начало блока в `Res19` или `0xFFFF`. - `fallback_key` (`hdr3`) — индекс fallback-ключа в `Res8`. ## 2. Сэмплирование узла Вход: время `t`, текущий узел. Выход: `quat(w,x,y,z)` и `pos(x,y,z)`. ### 2.1. Индекс кадра Движок использует x87-совместимое округление для выражения `t - 0.5`. Для 1:1 повторения нужно сохранить ту же политику плавающей точки. ### 2.2. Выбор key index 1. Если кадр вне диапазона `frame_count` -> `fallback_key`. 2. Если `anim_map_start == 0xFFFF` -> `fallback_key`. 3. Иначе берётся `map_words[anim_map_start + frame]`: - если значение `>= fallback_key`, тоже используется `fallback_key`; - иначе используется значение из map. ### 2.3. Интерполяция Если выбран fallback, возвращается ровно этот ключ без интерполяции. Иначе: 1. Берутся соседние ключи `k0` и `k1`. 2. Если `t` точно равен `k0.time` или `k1.time`, возвращается соответствующий ключ. 3. Иначе: - `alpha = (t - k0.time) / (k1.time - k0.time)` - `pos = lerp(k0.pos, k1.pos, alpha)` - `quat = slerp_like(k0.quat, k1.quat, alpha)` Кватернион в runtime хранится в порядке `[w, x, y, z]`. ## 3. Смешивание двух сэмплов При blending между позами A и B: 1. Выбираются валидные стороны по `blend` и валидности времени. 2. Если активна одна сторона, берётся она. 3. Если активны обе: - применяется shortest-path flip для `qB`; - выполняется quaternion blend; - позиция смешивается линейно. Матрица строится из quaternion, а translation подставляется отдельным шагом. ## 4. Каноника writer Рекомендуемые правила: 1. Ключи узлов писать подряд в `Res8` в порядке узлов. 2. `fallback_key` узла указывает на последний ключ его трека. 3. Для узлов с map выделять блок длины `frame_count` в `Res19`. 4. Для статических узлов: `anim_map_start = 0xFFFF`, один ключ с `time=0`. 5. `Res8.attr1 = key_count`, `Res8.attr3 = 4`. 6. `Res19.attr1 = map_word_count`, `Res19.attr2 = frame_count`, `Res19.attr3 = 2`. ## 5. Валидация перед сохранением - `Res8.size % 24 == 0` - `Res19.size % 2 == 0` - каждый `fallback_key < key_count` - для узла с map: `anim_map_start + frame_count <= map_word_count` - внутри трека времена ключей строго возрастают ## 6. Статус валидации - Форматные проверки включены в `tools/msh_doc_validator.py`. - Корпусная валидация анимационных инвариантов включена в прогон `tools/msh_doc_validator.py` на полном retail-наборе. ## 7. Статус покрытия и что осталось до 100% Закрыто: 1. Контракт `Res8 + Res19` и fallback-логика выбора ключа. 2. Базовая интерполяция поз и blending двух сэмплов. 3. Канонические инварианты writer path для существующих ассетов. Осталось: 1. Полная фиксация численного поведения на всех FP-edge-case (включая платформенные различия округления). 2. Полный writer-профиль для авторинга новых анимаций без опоры на reference copy-through. 3. Набор runtime parity-тестов «frame-by-frame pose equivalence» на длинных анимациях.