Files
fparkan/docs/specs/msh-animation.md

127 lines
5.0 KiB
Markdown
Raw Permalink Normal View History

# 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» на длинных анимациях.