squad-proto/src/render/framebuffer.h

193 lines
8.4 KiB
C
Raw Normal View History

Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 17:00:49 +07:00
#pragma once
// -----------------------------------------------------------------------------
// Софтверный фреймбуфер: RGBA8, ручная запись пикселей.
// Ни один игровой пиксель не рисуется примитивами raylib.
// raylib используется только в ScreenBlitter — финальный блит на экран.
//
// Инварианты (нарушение любого ломает картинку молча):
// * альфа фреймбуфера всегда 255;
// * ступень 0 световой рампы даёт чистый чёрный и совпадает с цветом Clear;
// * спрайты пишутся с альфа-тестом, эффекты — аддитивно.
// -----------------------------------------------------------------------------
#include <algorithm>
#include <cstdint>
#include <vector>
// В памяти байты идут R,G,B,A (PIXELFORMAT_UNCOMPRESSED_R8G8B8A8),
// значит little-endian uint32 = A<<24 | B<<16 | G<<8 | R.
constexpr uint32_t RGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255)
{
return uint32_t(r) | (uint32_t(g) << 8) | (uint32_t(b) << 16) | (uint32_t(a) << 24);
}
constexpr uint8_t AlphaOf(uint32_t c) { return uint8_t(c >> 24); }
// Цвет для аддитивных эффектов: альфа 0, чтобы такой цвет нельзя было случайно
// использовать как непрозрачный пиксель спрайта.
constexpr uint32_t FXC(uint8_t r, uint8_t g, uint8_t b) { return RGBA(r, g, b, 0); }
// Сложение цветов с насыщением по каждому каналу.
inline uint32_t AddSat(uint32_t a, uint32_t b)
{
uint32_t r = (a & 0xFFu) + (b & 0xFFu);
uint32_t g = ((a >> 8) & 0xFFu) + ((b >> 8) & 0xFFu);
uint32_t bl = ((a >> 16) & 0xFFu) + ((b >> 16) & 0xFFu);
if (r > 255u) r = 255u;
if (g > 255u) g = 255u;
if (bl > 255u) bl = 255u;
return 0xFF000000u | r | (g << 8) | (bl << 16);
}
// Умножение цвета на коэффициент в формате 8.8 (256 == x1.0).
inline uint32_t ScaleColor8(uint32_t c, uint32_t k256)
{
uint32_t r = ((c & 0xFFu) * k256) >> 8;
uint32_t g = (((c >> 8) & 0xFFu) * k256) >> 8;
uint32_t bl = (((c >> 16) & 0xFFu) * k256) >> 8;
if (r > 255u) r = 255u;
if (g > 255u) g = 255u;
if (bl > 255u) bl = 255u;
return 0xFF000000u | r | (g << 8) | (bl << 16);
}
// -----------------------------------------------------------------------------
// Световая рампа (по мотивам COLORMAP из Doom).
//
// Свет — одна скалярная величина на точку мира. Таблица переводит её в цвет:
// в полумраке картинка уходит в холодный синий, на свету нейтральна, во вспышке
// выбивается в тёплый пересвет. На пиксель остаются три выборки из таблицы
// и ноль умножений.
// -----------------------------------------------------------------------------
constexpr int RAMP_LEVELS = 32;
extern uint8_t g_shadeLut[RAMP_LEVELS][3][256];
// Перепечь таблицу. Вызывать при изменении гаммы/усиления, иначе ручки молчат.
void BuildShadeRamp(float gamma, float maxGain);
// Упорядоченный дизеринг: 32 ступени рампы выглядят как ~200, полос не видно.
extern const uint8_t BAYER4[4][4];
inline uint32_t ShadePixel(uint32_t c, int lvl)
{
const uint8_t* lr = g_shadeLut[lvl][0];
const uint8_t* lg = g_shadeLut[lvl][1];
const uint8_t* lb = g_shadeLut[lvl][2];
return 0xFF000000u | uint32_t(lr[c & 0xFFu]) | (uint32_t(lg[(c >> 8) & 0xFFu]) << 8) |
(uint32_t(lb[(c >> 16) & 0xFFu]) << 16);
}
// Билинейный патч света по спрайту: L(px,py) = a + b*px + c*py + e*px^2 + f*py^2.
// Такая форма — это ТОЧНАЯ билинейка по четырём углам ромба тайла, записанная
// в координатах спрайта; поэтому на общем ребре соседних тайлов значения
// совпадают и швов нет.
struct ShadeQuad
{
float a = 1.0f;
float b = 0.0f;
float c = 0.0f;
float e = 0.0f;
float f = 0.0f;
// Строки ниже clampRow светятся как clampRow: вертикальные грани стены
// берут свет нижней кромки своей крыши, а не экстраполяцию квадратичного
// члена на 32 строки вниз.
int clampRow = 1 << 24;
static ShadeQuad Flat(float level)
{
ShadeQuad q;
q.a = level;
return q;
}
};
// Спрайт с альфа-тестом: px[i] с alpha==0 не пишется. Блендинга нет.
// (ax, ay) — точка привязки внутри спрайта: она совмещается с экранной
// проекцией мировой позиции сущности.
struct Sprite
{
int w = 0;
int h = 0;
int ax = 0;
int ay = 0;
std::vector<uint32_t> px;
void Init(int width, int height, uint32_t fill = 0)
{
w = width;
h = height;
px.assign(size_t(w) * size_t(h), fill);
}
void Set(int x, int y, uint32_t c)
{
if (unsigned(x) < unsigned(w) && unsigned(y) < unsigned(h)) px[size_t(y) * w + x] = c;
}
uint32_t Get(int x, int y) const
{
if (unsigned(x) < unsigned(w) && unsigned(y) < unsigned(h)) return px[size_t(y) * w + x];
return 0;
}
};
struct Framebuffer
{
int w = 0;
int h = 0;
std::vector<uint32_t> px;
void Init(int width, int height);
void Clear(uint32_t color);
inline void Put(int x, int y, uint32_t c)
{
if (unsigned(x) < unsigned(w) && unsigned(y) < unsigned(h)) px[size_t(y) * w + x] = c;
}
// Блит с альфа-тестом; (x, y) — куда попадёт точка привязки спрайта.
void Blit(const Sprite& s, int x, int y);
// Тот же блит, но каждый непрозрачный пиксель заменяется на tint (для силуэтов).
void BlitTinted(const Sprite& s, int x, int y, uint32_t tint);
// Блит со светом: цвет каждого пикселя проходит через рампу.
void BlitShaded(const Sprite& s, int x, int y, const ShadeQuad& q);
// Умножение фона под маской спрайта (контактные тени, лужи).
void BlitMul(const Sprite& mask, int x, int y, uint32_t k256);
void FillRect(int x, int y, int rw, int rh, uint32_t c);
void Line(int x0, int y0, int x1, int y1, uint32_t c);
void CircleOutline(int cx, int cy, int r, uint32_t c);
void CircleFilled(int cx, int cy, int r, uint32_t c);
// --- аддитивные примитивы для эффектов ---
inline void AddPx(int x, int y, uint32_t c)
{
if (unsigned(x) < unsigned(w) && unsigned(y) < unsigned(h))
{
uint32_t& d = px[size_t(y) * w + x];
d = AddSat(d, c);
}
}
// Светящийся штрих: яркое ядро + боковой ореол, цвет плывёт от head к tail.
void AddBeam(float x0, float y0, float x1, float y1, uint32_t headCol, uint32_t tailCol,
int halo);
// Мягкое пятно света (вспышка у дула, искра попадания).
void AddGlowDisc(int cx, int cy, int radius, uint32_t color);
};
// -----------------------------------------------------------------------------
// Финальный вывод: UpdateTexture + DrawTexturePro, фильтрация POINT.
// Единственное место рендера, знающее про raylib.
// -----------------------------------------------------------------------------
struct ScreenBlitter
{
void Init(int width, int height);
void Shutdown();
void Present(const Framebuffer& fb, int windowW, int windowH) const;
private:
void* tex = nullptr; // Texture2D на куче — чтобы не тащить raylib.h в заголовок
int texW = 0;
int texH = 0;
};