aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/msh-animation.md
blob: 8aa2796930893726c122472ec21e26bf8148400a (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
106
107
108
109
110
111
112
# 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`.
- В текущем окружении полный игровой корпус MSH не подключен в `testdata`, поэтому массовый прогон здесь не выполнялся.