# 03 — Рендер: софтверный буфер, изометрия, спрайты ## Фреймбуфер [engine/framebuffer.h](../engine/framebuffer.h) — `std::vector` 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. стена никогда не темнее самого яркого соседа-пола — иначе сквозь пропущенный тёмный блок проступит освещённый пол тайла (tx−1, ty−1), который рисуется раньше по глубине.