106 lines
3.4 KiB
Markdown
106 lines
3.4 KiB
Markdown
|
|
# 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.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|