Files
fparkan/docs/specs/msh-animation.md
T
Valentin Popov 50c2cf4686
Docs Deploy / Build and Deploy MkDocs (push) Successful in 2m6s
Test / Lint (push) Failing after 1m10s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
chore: remove Python tooling and resource viewer
2026-06-22 00:35:19 +04:00

127 lines
5.0 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.
# 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. Статус валидации
- Форматные проверки были покрыты legacy-валидатором.
- Корпусная валидация анимационных инвариантов выполнена на полном 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» на длинных анимациях.