squad-proto/docs/03-rendering.md
z.kirill 4382af2322 Документация: границы движка, работа через модель, дизайн игры до лейтгейма
18-engine.md    границы Tile2D, проекции 2D, формат проекта, второй проект
  19-agent.md     инструменты --tool, скиллы, терминал в редакторе
  20-gdd.md       акты, петли, рост отряда, закон масштаба, бюджет 200 часов
  21-damage.md    семь стихий, статусы, десять реакций, формулы, триггеры
  22-loot.md      редкости, аффиксы, артефакты, зачарования, материалы
  23-endgame.md   Бездна, мутации, лидерборды, 262 ачивки

Плюс правки 01/02/07/15/16/17 под переехавшие пути и новые правила забега.

Дизайнерская часть держится на одном законе: угроза растёт квадратично, сила
отряда — линейно с затуханием, разрыв закрывается ЗНАНИЕМ, а не числами. Из
него выведены и экономика опыта (41 млн против 44 млн дохода), и то, почему
реакции считаются от глубины, а не от урона оружия, и почему в Бездне
множитель HP разрешён, а во всём авторском контенте запрещён.

.claude/commands: скиллы /tile2d, /room, /floor — правила этого движка,
которые нельзя вывести из кода. Главное записано первым: не отчитываться об
успехе без прохода --tool validate.

Числа в этих документах — вход для инструментов, а не украшение. Расхождение
между таблицей и выводом инструмента означает, что неправ документ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:07:45 +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 — Рендер: софтверный буфер, изометрия, спрайты
## Фреймбуфер
[engine/framebuffer.h](../engine/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` не протёк в заголовок.
## Изометрия
[engine/view2d.h](../engine/view2d.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), который рисуется
раньше по глубине.