103 lines
7.1 KiB
Markdown
103 lines
7.1 KiB
Markdown
|
|
# 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. стена никогда не темнее самого яркого соседа-пола — иначе сквозь пропущенный
|
|||
|
|
тёмный блок проступит освещённый пол тайла (tx−1, ty−1), который рисуется
|
|||
|
|
раньше по глубине.
|