gmod-rs/docs/DIARY.md
Niko Marmeladkov 3de8b8945f
Some checks are pending
CI / checks (push) Waiting to run
Слияние feat/anim: RAW-дорожки, анимации по состоянию, вьюмодель
Из ветки feat/anim (d42d646):
- распаковка постоянных дорожек: Quaternion48 (RAWROT), Quaternion64
  (RAWROT2), Vector48 (RAWPOS) — раньше такие кости застревали в позе покоя
  (в @taunt_muscle_base 24 дорожки из 53);
- engine::animation — выбор секвенции по состоянию: стоя/шаг/бег/присед/
  воздух, 8 направлений относительно взгляда, сохранение фазы walk↔run;
- клиент: покачивание вьюмодели при ходьбе, инерция взгляда, одиночный
  выстрел fire по ЛКМ, компенсация RAWPOS-сдвига корня (у v_pistol 68 юнитов).

Разрешение конфликтов: вьюмодель осталась в камерном базисе (мои правки —
смещения через up камеры, доворот model_yaw), анимации и покачивание взяты
из ветки; play_model_animation с шестым аргументом model_yaw во всех вызовах.

Проверено: 23 сьюта, 29 картовых тестов, 15 тестов world_real — зелёные.
2026-10-23 14:08:03 +03:00

280 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Дневник агента: где я и что дальше
Живой документ. Обновляю по ходу работы, чтобы после автоматического
уплотнения контекста ничего не терялось. Коротко: состояние, замеры, следующий
шаг, грабли.
Обновлён: 2026-10-23, день.
---
## 1. Что просит пользователь (приоритеты)
**Ускоренный путь до Sandbox-UX** (его слова): «быстрее дойти до того, как смогу
спавнить через Q-меню, иметь норм модель игрока, таскать физганом, Weapon всё
сделать». Поэтому очередь переставлена (docs/PARITY.md A25):
1. A23 оружие (SWEP из Lua + вьюмодель + аттачменты) — сюда же A20 (модель
игрока: один и тот же рендер моделей).
2. A21 физика взаимодействия: физган (луч → проп → таскание, вращение,
заморозка), без полной гибернации.
3. A24 спавн: сначала консольные команды на данных из Lua (`gm_spawn`,
`gm_give`, режимы `gmod_tool`), потом окно Q.
4. A22 Sandbox целиком: undo/cleanup/duplicator.
5. Визуальная точность (A10 свет на пиксель, A11–A12 вода, A16 трава/декали,
A18 скайбокс) — параллельно, Sandbox не блокирует.
## 2. Где я сейчас (в работе)
**A20/A23, первый срез сделан: модель игрока в мире.** Рендер тот же, что у
пропов (статический меш `PropMesh`), скелет и анимации — следующий срез.
Как включается и как проверять:
```bash
SOURCE_RS_PLAYER_MODEL= DISPLAY=:99 SDL_VIDEODRIVER=x11 cargo run -q -p client -- \
map --assets ~/gmod_server/server gm_construct --hidden --frames 4 --noclip \
--player-pos="-2438 -966 48" --flythrough /tmp/opencode/pl4 --flythrough-frames 4 \
--route="-2438 -880 75|6 270; -2438 -1180 75|6 90"
```
Пустое значение — модель по умолчанию (`models/player/police.mdl`), иначе —
путь к модели. Курс разворачивается на 180° (у моделей Source ось Y влево).
Движок: `World::spawn_model_instance` и `World::move_model_instance`,
`ModelInstance` (экземпляр без сущности). Грабли: индексы модели **локальные** —
при вставке в общий буфер к ним обязательно прибавляется `first_vertex`
(без этого был мусор в кадре).
**Спавн пропа из игры сделан (ядро A24).** `World::spawn_prop` (геометрия из
`append_model_geometry`, сущность `prop_physics`, тело в физике, экземпляр
пропа) + в клиенте клавиша `G` (спавн по взгляду) и аргумент
`--spawn <модель>` (отладка и скрипты). Проверено кадром: арбуз заспавнен и
отрисован (`--spawn models/props_junk/watermelon01.mdl`, камера у
`-2500 -1050 60|5 0`).
Грабли: буферы рендера созданы на сборке мира — после спавна рендерер
пересобирается целиком (медленно, но работает); позже нужна до-аллокация
вершин под спавн.
**Физган, первый срез (A21/A23).** ЛКМ — взять ближайшее тело под прицелом
(луч из глаз, расстояние 8..260 юнитов, допуск 40 в стороны), пока держим —
тело каждый кадр телепортируется на луч взгляда на взятом расстоянии; отпустил —
свободно. `SOURCE_RS_PHYSGUN=1` держит ближайшее тело без мыши (для скриптов).
**Нужна интерактивная проверка физгана:** мои скриптовые кадры упираются в
стены — камеру без глаз не навести. Проверять так: встать у кучи пропов
(она в районе `-2930 -1370 -64`), посмотреть на проп, зажать ЛКМ.
**Синхронизация физики и картинки (жалоба «позиция рендера обновляется
только при спавне нового»).** Причина найдена: мир двигал вершины в памяти, а
буфер рендера заливался один раз при сборке — потому спавн (пересборка
рендерера) и «лечил». Сделано: `World::moved_ranges` + `take_moved_ranges()`
(диапазоны заполняет `rewrite_instance`), `WorldRenderer::upload_vertex_ranges`
(точечная заливка `queue.write_buffer`) и вызов в кадре клиента.
**Не сходится (диагностика `probe_moving_prop_reports_vertex_ranges`):**
коробка в тесте задана в мировых координатах, а меш обязан быть в локальных —
видно по двойному смещению вершины; плюс список диапазонов почему-то пуст после
`step_physics` (проверить `is_sleeping` и ранний выход по совпадению позы —
вероятно, тело «спит» после телепорта). Проверять в интерактиве: заспавнить
арбуз, посмотреть, что он падает на экране.
**Найдены и закрыты две поломки по жалобе «физика висит на экране»:**
1. `move_prop` остался со своей копией перезаписи вершин и не запоминал
диапазоны — теперь он идёт через `rewrite_instance` (замер: было «диапазонов 0»
каждый шаг, стало «диапазонов 1 [(39585, 8)]»).
2. Буфер вершин мира создавался **без `COPY_DST`** — заливка падала с ошибкой
wgpu (у пользователя это роняло клиент). Теперь `VERTEX | COPY_DST`, для
индексов `INDEX | COPY_DST` плюс `upload_index_ranges`.
Плюс `WorldRenderer::refresh_batches`: рендерер копировал список батчей при
сборке, поэтому **новая** геометрия (спавн) в кадр не попадала без полной
пересборки. Теперь список обновляется, когда появились новые индексы, а полная
пересборка нужна только при новой текстуре.
**Открытый вопрос: вьюмодель.** Создание работает (модель в буфере, вершины
+1000, батч есть, габарит ±15), но в кадре её нет — при этом модель игрока
рисуется. Разница в том, что модель игрока в том тесте не двигалась (кадры
пролёта), а вьюмодель обязана двигаться каждый кадр. Значит сломано именно
звено «перенос позы каждый кадр + заливка на GPU». Включается
`SOURCE_RS_VIEW_MODEL` (по умолчанию физган, есть откат на монтировку —
`v_physgun.mdl` в ассетах не нашёлся, грузится `v_crowbar.mdl`).
**Вьюмодель: СДЕЛАНО (первый срез).** Механизм проверен кадром: пистолет
(`SOURCE_RS_VIEW_MODEL=models/weapons/v_pistol.mdl`) стоит в кадре. Две причины,
почему раньше не рисовалась: (1) разворот на 180° уводил ствол за камеру —
у моделей оружия Source геометрия вынесена вперёд по оси модели; (2) у моделей
оружия геометрия смещена от начала координат (у пистолета центр габарита
`≈(0,0,47)`, у монтировки `−52` по Y), поэтому привязываться надо к **центру
габарита** модели (`engine::model::rotate_model_vector(centre, ...)`), а не к её
origin. Посадка: глаз + 20 вперёд + 6 вправо − 10 вниз − центр.
Что осталось по вьюмодели: `v_physgun.mdl` в ассетах нет (GMod-модель), поэтому
по умолчанию пистолет; отдельный FOV вьюмодели и анимации — позже.
**Старые факты (сохранены):** Материал вьюмодели в мире —
индекс 54, имя `crowbar_cyl` (виден в `--debug-materials`). Рендер одним этим
материалом (`--only-material 54`) даёт **пустой кадр** — то есть дело не в позе:
батч вьюмодели не доходит до отрисовки. Проверять по порядку: (1) попал ли батч
в `renderer.batches` после `refresh_batches` (он копирует список только когда
`moved_index_ranges` непуст, а они забираются в блоке физики — возможно,
диапазоны берутся ДО того, как `refresh_batches` увидит их); (2) совпадает ли
`batch.material` с позицией материала в `world.materials` (дедупликация по
имени); (3) `--only-material` фильтрует батчи, но не мешает ли ему что-то ещё.
**2026-10-23, сессия по жалобам пользователя.** Закрыто коммитом `f58eafe`:
- падение wgpu на втором спавне («Copy at offset … overrunning the bounds»):
буферы GPU не росли — добавлены `ensure_capacity` (рост вдвое с перезаливкой)
и `refresh_materials` (bind group для новых материалов, иначе новые батчи
молча не рисовались);
- вьюмодель: обновляется после `player.update` и `camera.pos` (раньше
отставала на кадр — «плывёт при повороте»), по умолчанию `w_pistol.mdl`,
курс −90° (у модели ствол вдоль +Y, а не +X), дистанция 26, анимация `idle01`;
- `bone_world_transforms` обходит кости рекурсивно (порядок в файле не
гарантирован);
- HUD: отладочные строки (verts/tris, лог Lua) скрыты по умолчанию, клавиша `.
**Коллизия пропов (2026-10-23).** Коллайдер-коробка ставилась от **нуля
модели**, хотя геометрия часто смещена (у куклы центр AABB на 13,5 юнита выше
нуля) — пропы висели на невидимых выступах. Добавлен
`Physics::add_prop_offset` (смещение коллайдера в координатах модели,
поворачивается вместе с телом), оба места создания тел (пропы карты и спавн)
передают `bounds_center`. Коробки пропов для коллизии игрока теперь тоже
учитывают центр и поворот (`World::prop_collision_boxes`, AABB описанной
коробки по модулю матрицы поворота).
Замер: тест `real_map_props_rest_on_ground` — после 10 с физики низ коробки
каждого пропа лежит на опоре (допуск 4 юнита, луч от центра вниз); кукла
остаётся с зазором 10 юнитов, потому что лежит краем на столе — центровой луч
опору не видит.
**Известная проблема:** `v_pistol.mdl` рисуется плоским «листом» (2157 вершин,
2 меша, 1 материал, скиннинг при этом цел — тест `real_view_model_skin_stays_coherent`
даёт максимальный сдвиг 4 юнита). Похоже на проблему загрузки геометрии именно
этой модели; до выяснения по умолчанию стоит `w_pistol.mdl`.
**Запущены параллельно (воркдеревья от `f58eafe`):** `feat/android` — APK под
Android 12+; `feat/hdr` — HDR, тонмаппинг, bloom и мягкие тени/свет; `feat/anim` —
RAWROT/RAWPOS/RAWROT2, выбор анимации по состоянию (идёт/бежит/приседает/
прыжок), покачивание вьюмодели.
**A26, состояние на 2026-10-17:** сделаны секвенции (с определением шага записи
по данным), кости и веса вершин, слияние включённых моделей (у игрока 465
секвенций), чтение `.ani` (7,1 МБ, 1563 анимации, 247 блоков), декодер кадров
(`mstudioanim_t`, проверен: 280 кадров, 51 дорожка), сборка кватерниона с
нормировкой 1/32768 (выбрана замером: умножение на rot_scale давало 1,63 —
для кватерниона невозможно) и CPU-скиннинг (виден кадром).
**Следующий шаг — проигрывание.** План, чтобы не искать заново:
1. `PropMesh` (или `ModelInstance`) должен держать **данные анимации**:
`.ani` (7 МБ) + список блоков + описания анимаций, ссылкой `Arc`, — сейчас
они читаются при загрузке модели и выбрасываются;
2. в движке `pose_model_instance(index, origin, angles, bone_quats: &[[f32;4]])`:
локальные кватернионы костей в мировые через родителей
(`world[i] = world[parent] · (T(pos_i) · R(q_i) · T(−pos_i))`), затем скиннинг
теми же весами (функция скиннинга уже есть — сейчас её ведёт синус);
3. в клиенте: выбрать секвенцию (`@idle_all_01` через последовательность
activity), взять кадр по времени (`t · fps`, циклом), собрать кватернионы
костей из дорожек и позвать `pose_model_instance`;
4. затем выбор по состоянию (`ACT_MP_*`) и блендинг, и снять синус.
**A26 «анимации» — план записан** (docs/PARITY.md A26). Ключевое открытие: в
`formats` скелет и веса **уже читаются** (`mdl.bones()`, `vvd.vertices()`,
`vtx.body_parts()`), не хватает разбора секвенций/`.ani`, костей в
`ModelVertex` и CPU-скиннинга. Заливка вершин на GPU для этого уже готова —
тот же путь `moved_ranges`, что у движущихся пропов.
**A26, сделано 2026-10-23 (этот заход):** постоянные дорожки
`RAWROT`/`RAWROT2`/`RAWPOS` разобраны по `compressed_vector.h` (Quaternion48 6
байт, Quaternion64 8, Vector48 — три half-float). Замеры: `v_pistol @idle01` —
10 дорожек, 7 RAWROT + 2 RAWPOS; `m_anm @taunt_muscle_base` — 53 дорожки, 24
RAWROT2, кватернионы единичные, неизвестных флагов нет. `@taunt` теперь
двигает 3145 из 3147 вершин между кадрами 0 и 30.
Выбор анимации по состоянию — `engine::animation`: `idle_all_01`, `a_WalkN`,
`a_RunN`, `@cidle_all`, `a_CwalkN`, `@jump_fist` (первая существующая), восемь
направлений относительно взгляда. Фаза walk↔run сохраняется, смена типа —
сброс. Клиент играет выбранное имя; вьюмодель качается при ходьбе и отстаёт
при повороте, ЛКМ без захвата проигрывает `fire`. Проверки:
`real_animation_raw_flags_decode`, `real_player_taunt_moves_vertices_with_raw_tracks`,
юнит-тесты `engine::animation`.
**Следующее:** (а) до-аллокация вершин под спавн вместо пересборки рендерера;
(б) блендинг анимаций игрока и вьюмодели; (в) вращение пропа колесом и
заморозка у физгана; (г) оружие в руках связывает `@idle_*`/`@jump_*` и
вьюмодель; (д) консольные команды спавна на данных из Lua (`gm_spawn`).
## 3. Что уже сделано и лежит в `main`
Последние коммиты (свежие сверху):
- `4ad0405` очередь: A23 оружие, A24 Q-меню, A25 ускоренный порядок;
- `7a8a750` очередь: A20 модель игрока, A21 полная физика, A22 Sandbox через GLua;
- `257ac07`, `d35efef` **A13 туман сделан** целиком (чтение + шейдер);
- `72bad60` A19: подшаг не гасит скорость о склон (35°: 65 → 147);
- `5bdbdbf`, `e638607` A19: замер по углам + найден `flRedirectCoeff` в движке;
- `3bfd078`, `d88988f` рельеф: треугольники отсекают шаг, нормаль наружу (стены
по рёбрам полигонов ушли);
- `b74824b` склон: догон земли трассировкой (рампы 242–250 вместо 197,8);
- `23c5034` присед: 250/150/520/216,7 по спеке, камера 46 → 28 (была выше
хитбокса);
- `aa11713`, `d220b5a` рампы и стыки: отсечение по последней входной грани,
касание ребром не преграда;
- `86a6cb6`, `5380184` инструменты: слой коллизий, дамп кирпича, обход карты.
Все 23 сьюта и 28 картовых тестов зелёные.
## 4. Замеры, которые нельзя потерять
- **A19 (скорость на склоне)**: замер `probe_slope_angles` (синтетический клин).
Было: 5° 239, 10° 232, 15° 196, 20° 152, 25° 116, 30° 88, 35° **65**, 40° 49,
44° 39. Стало: 239 / 233 / 226 / 214 / 195 / 175 / **147** / 132 / 107.
Цель — 250 до 44°. Остаток теряется на прямом (скользящем) пути: иногда он
выигрывает выбор у подшага. Инструмент: `SOURCE_RS_TRACE_STEP=1` печатает,
какой путь победил (`step_move`).
- **Механизм движка для скорости на склоне**: `gamemovement.cpp`, `ClipVelocity`
(строка ~3144) — блок `flRedirectCoeff`: `out *= (len + |v·n|·coeff)/len`.
Наивный перенос копит бонус за кадр (15° → 442 юнита/с) — откачен.
- **Туман карты gm_construct**: цвет (0,557 0,643 0,698), start 1024, end
250000, max_density 1,0, farz −1 (дальний, на ближних дистанциях нейтрален).
- **Габариты/линейки Source**: тело 72/36 (стоя/присед), глаза 64/28
(`VEC_VIEW`/`VEC_DUCK_VIEW`), `sv_walkable_normal` 0,7 (45,6°), `DIST_EPSILON`
1/32, `sv_stepsize` 18, воздушный потолок желаемой скорости 30.
- **Коллизия gm_construct**: 2118 кирпичей; 140 за габаритами карты (3D-скайбокс,
часть с несогласованными плоскостями — см. E19.2); 11776 треугольника рельефа,
у всех намотка «гео внутрь» (нормаль разворачиваем в `clip_triangle`).
- **Присед**: обрезка `HandleDuckingSpeedCrop` = 0.33333333 только в приседе и
на земле, желаемая скорость 450 (бег 650), максимум 250 / 520.
## 5. Инструменты диагностики (не переизобретать)
- `SOURCE_RS_DEBUG_COLLISION=<радиус>` — слой коллизий: красные рёбра кирпичей,
жёлтые габариты, голубой рельеф, зелёное тело.
- `probe_brush_dump` (`SOURCE_RS_TEST_BSP_AT="x,y,z,радиус"`) — полный дамп
кирпича: содержимое с расшифровкой, владелец-сущность, каждая грань с
текстурой, габарит, попадание в коллизию.
- `probe_ramp_movement` — движение из точки: 8 направлений, рывки, залипания,
кто мешает (кирпич + текстуры).
- `probe_slanted_surface` — лучи по наклонным кирпичам против настоящей формы.
- `probe_slope_angles` — таблица скорости по углам (приёмка A19).
- `probe_phantom_walls_scan`, `probe_collision_contents` — обход карты и состав
коллизии.
- `real_map_*` — 28 картовых тестов, требуют `SOURCE_RS_TEST_BSP`.
- Запуск клиента: `DISPLAY=:99 SDL_VIDEODRIVER=x11 cargo run -q -p client --
map --assets ~/gmod_server/server gm_construct ...`, для кадров нужен
`--flythrough <dir>`.
## 6. Грабли (уже наступал)
- `Vec3` без `Index`: писать покомпонентно.
- `%[fx:mean*255]` в ImageMagick без имени канала врёт — считать
`mean.r/g/b`.
- Тесты с картой имеют свой `VirtualFs::new()` (189 материалов) — индексы
материалов не как в рантайме (54).
- Правки кода делать `edit`, не sed/python, кроме больших перестановок блоков —
и после каждой проверять `cargo clippy` и тесты.
- Перед коммитом всегда: `cargo fmt --all && cargo clippy --workspace
--all-targets -- -D warnings && cargo test --workspace` + картовые тесты.