squad-proto/docs/03-rendering.md
z.kirill 7880f275bd Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).

Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.

Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.

Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».

Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.

Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.

Документация — CLAUDE.md как оглавление, содержание в docs/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 13:00:49 +03:00

103 lines
7.1 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.

# 03 — Рендер: софтверный буфер, изометрия, спрайты
## Фреймбуфер
[src/render/framebuffer.h](../src/render/framebuffer.h) — `std::vector<uint32_t>` 480×270.
Формат — RGBA8, в памяти байты идут `R,G,B,A`, значит little-endian `uint32` это
`A<<24 | B<<16 | G<<8 | R`. Помощник `RGBA(r,g,b,a)` собирает цвет правильно.
Примитивы: `Clear`, `Put`, `Blit`, `BlitTinted`, `FillRect`, `Line` (Брезенхем),
`CircleOutline` (средняя точка), `CircleFilled`. Всё пишет пиксели руками.
Спрайты пишутся **альфа-тестом**: пиксель с `alpha == 0` не пишется, полупрозрачности
нет. Для эффектов есть **аддитивное** смешивание с насыщением (`AddSat`, `AddBeam`,
`AddGlowDisc`) и умножение (`ScaleColor8`, `BlitMul`), а для света — `BlitShaded`,
пропускающий цвет через световую рампу. Подробности — [10-atmosphere.md](10-atmosphere.md).
`Blit` обрезает спрайт по границам буфера ДО циклов и **выходит при пустом
диапазоне**: иначе адрес строки вычисляется за концом вектора. Цикл при этом не
выполняется, разыменования нет, но индекс уже невалиден — Debug-сборка ловит это
как `vector subscript out of range`, Release молча считает мусорный адрес.
Вывод на экран — `ScreenBlitter`: `UpdateTexture` + `DrawTexturePro` с
`TEXTURE_FILTER_POINT`. Это единственное место рендера, знающее про raylib;
`Texture2D` спрятан за `void*`, чтобы `raylib.h` не протёк в заголовок.
## Изометрия
[src/render/iso.h](../src/render/iso.h). Тайл 32×16:
```
sx = (wx - wy) * 16
sy = (wx + wy) * 8
```
Обратная проекция: `wx = sx/32 + sy/16`, `wy = sy/16 - sx/32`.
`ScreenDirToWorld` переводит направление ввода из экранных осей в мировые —
именно это делает `WASD` интуитивным. Больше изометрия нигде не используется.
**Камера** следует за интерполированным якорем экспоненциальным сглаживанием
(`1 - exp(-smooth*dt)`), смещение округляется до целых пикселей — иначе
при ×3 апскейле картинка дрожит.
## Сортировка по глубине
Ключ — `wx + wy`. Пол рисуется первым сплошным проходом и в сортировке не участвует.
Все стены и все сущности попадают в **один** список `Scene::items` и сортируются
`std::stable_sort` (стабильная — чтобы порядок был детерминирован при равных ключах).
Отсечение — по экранным координатам с запасом на высоту спрайта стены.
## Точка привязки спрайта
У `Sprite` есть поля `ax, ay` — точка внутри спрайта, которая совмещается с
экранной проекцией мировой позиции. Так блит не зависит от размера спрайта:
| Спрайт | Размер | Привязка | Смысл |
|---|---|---|---|
| пол | 32×16 | (16, 8) | центр ромба = центр тайла |
| стена | 32×32 | (16, 24) | низ блока садится на ромб пола |
| агент (стрелок) | 10×14 | (5, 12) | «ноги» на земле |
| агент (клинок) | 12×15 | (6, 13) | шире в плечах, класс читается силуэтом |
| мишень | 12×16 | (6, 14) | то же |
У пули спрайта нет: снаряд рисуется светящимся трассером, см. [10-atmosphere.md](10-atmosphere.md).
## Процедурные спрайты
[src/render/sprites.cpp](../src/render/sprites.cpp), файлов с ассетами нет.
- **Пол** — ромб 32×16, два оттенка тёмно-серого металла по шахматке `(x+y)&1`,
затемнённый рант по краю и редкие светлые крапины для фактуры.
- **Стена** — верхняя грань (тот же ромб) + вертикальные грани: для каждого столбца
ищется нижняя кромка ромба и вниз заливается на высоту тайла. Левая грань темнее
правой. Обводка по силуэту — пиксели, у которых есть прозрачный 4-сосед.
- **Агент** — капсула: пиксель внутри, если расстояние до вертикального отрезка
меньше радиуса; внешний пиксель кольца — обводка, слева-сверху блик.
5 различимых цветов, по одному на агента.
- **Агент с клинком** — та же капсула, крупнее, плюс стальная вертикаль с бликом
и гардой у правого края. Цвет уже занят номером агента, поэтому класс обязан
читаться формой. Сам взмах рисуется не спрайтом, а аддитивной дугой
([11-melee.md](11-melee.md)).
- **Мишень** — та же капсула, тускло-красная, 12×16. В темноте тело не видно —
виден только тлеющий красный «глаз», нарисованный аддитивно поверх сцены.
Порядок отрисовки кадра:
```
Clear(чёрный) → свет → пол (BlitShaded) → список (стены, мишени, агенты)
→ сортировка → блит → глаза мишеней → трассеры, вспышки и дуги клинков
(аддитивно) → ОДИН пост-проход → Present → оверлей (raylib, dev-only)
```
Три инварианта, нарушение любого ломает картинку молча:
1. альфа фреймбуфера всегда 255;
2. ступень 0 световой рампы совпадает с цветом `Clear` — только поэтому полностью
тёмные тайлы можно не рисовать;
3. стена никогда не темнее самого яркого соседа-пола — иначе сквозь пропущенный
тёмный блок проступит освещённый пол тайла (tx1, ty1), который рисуется
раньше по глубине.