aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/msh-animation.md
blob: 811fa000bbe9a51e2dc7a1c39d02175508c234c5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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.

---