diff options
Diffstat (limited to 'docs/specs/msh-animation.md')
| -rw-r--r-- | docs/specs/msh-animation.md | 105 |
1 files changed, 105 insertions, 0 deletions
diff --git a/docs/specs/msh-animation.md b/docs/specs/msh-animation.md new file mode 100644 index 0000000..811fa00 --- /dev/null +++ b/docs/specs/msh-animation.md @@ -0,0 +1,105 @@ +# MSH animation + +Документ описывает анимационные ресурсы MSH: `Res8`, `Res19` и runtime-интерполяцию. + +--- + +## 1.13. Ресурсы анимации: Res8 и Res19 + +- **Res8** — массив анимационных ключей фиксированного размера 24 байта. +- **Res19** — `uint16` mapping‑массив «frame → keyIndex` (с per-node смещением). + +### 1.13.1. Формат Res8 (ключ 24 байта) + +```c +struct AnimKey24 { + float posX; // +0x00 + float posY; // +0x04 + float posZ; // +0x08 + float time; // +0x0C + int16_t qx; // +0x10 + int16_t qy; // +0x12 + int16_t qz; // +0x14 + int16_t qw; // +0x16 +}; +``` + +Декодирование quaternion-компонент: + +```c +q = s16 * (1.0f / 32767.0f) +``` + +### 1.13.2. Формат Res19 + +Res19 читается как непрерывный массив `uint16`: + +```c +uint16_t map[]; // размер = size(Res19)/2 +``` + +Per-node управление mapping'ом берётся из заголовка узла Res1: + +- `node.hdr2` (`Res1 + 0x04`) = `mapStart` (`0xFFFF` => map отсутствует); +- `node.hdr3` (`Res1 + 0x06`) = `fallbackKeyIndex` и одновременно верхняя граница валидного `map`‑значения. + +### 1.13.3. Выбор ключа для времени `t` (`sub_10012880`) + +1) Вычислить frame‑индекс: + +```c +frame = (int64)(t - 0.5f); // x87 FISTP-путь +``` + +Для строгой 1:1 эмуляции используйте именно поведение x87 `FISTP` (а не «упрощённый floor»), т.к. путь в оригинале опирается на FPU rounding mode. + +2) Проверка условий fallback: + +- `frame >= model.animFrameCount` (`model+0x9C`, из `NResEntry(Res19).attr2`); +- `mapStart == 0xFFFF`; +- `map[mapStart + frame] >= fallbackKeyIndex`. + +Если любое условие истинно: + +```c +keyIndex = fallbackKeyIndex; +``` + +Иначе: + +```c +keyIndex = map[mapStart + frame]; +``` + +3) Сэмплирование: + +- `k0 = Res8[keyIndex]` +- `k1 = Res8[keyIndex + 1]` (для интерполяции сегмента) + +Пути: + +- если `t == k0.time` → взять `k0`; +- если `t == k1.time` → взять `k1`; +- иначе `alpha = (t - k0.time) / (k1.time - k0.time)`, `pos = lerp(k0.pos, k1.pos, alpha)`, rotation смешивается через fastproc‑интерполятор quaternion. + +### 1.13.4. Межкадровое смешивание (`sub_10012560`) + +Функция смешивает два сэмпла (например, из двух animation time-позиций) с коэффициентом `blend`: + +1) получить два `(quat, pos)` через `sub_10012880`; +2) выполнить shortest‑path коррекцию знака quaternion: + +```c +if (|q0 + q1|^2 < |q0 - q1|^2) q1 = -q1; +``` + +3) смешать quaternion (fastproc) и построить orientation‑матрицу; +4) translation писать отдельно как `lerp(pos0, pos1, blend)` в ячейки `m[3], m[7], m[11]`. + +### 1.13.5. Что хранится в `Res19.attr2` + +При загрузке `sub_10015FD0` записывает `NResEntry(Res19).attr2` в `model+0x9C`. +Это поле используется как верхняя граница frame‑индекса в п.1.13.3. + +--- + |
