#include "engine/framebuffer.h" #include #include #include // ----------------------------------------------------------------------------- // Световая рампа // ----------------------------------------------------------------------------- uint8_t g_shadeLut[RAMP_LEVELS][3][256]; const uint8_t BAYER4[4][4] = { { 0, 8, 2, 10}, {12, 4, 14, 6}, { 3, 11, 1, 9}, {15, 7, 13, 5}, }; namespace { // Цвет полумрака и цвет пересвета. Между ними рампа плавно интерполирует. constexpr float COLD[3] = {0.60f, 0.74f, 1.00f}; // холодный синий в тенях constexpr float WARM[3] = {1.00f, 0.92f, 0.76f}; // тёплый пересвет во вспышке inline float Smooth01(float t) { return t * t * (3.0f - 2.0f * t); } struct ShadeRampInit { ShadeRampInit() { BuildShadeRamp(1.35f, 1.10f); } }; ShadeRampInit g_rampInit; } // namespace void BuildShadeRamp(float gamma, float maxGain) { for (int lvl = 0; lvl < RAMP_LEVELS; ++lvl) { // Ступень 0 — чистый чёрный: она обязана совпадать с цветом Clear, // иначе невидимые тайлы нельзя пропускать при отрисовке. if (lvl == 0) { for (int ch = 0; ch < 3; ++ch) for (int v = 0; v < 256; ++v) g_shadeLut[lvl][ch][v] = 0; continue; } const float t = float(lvl) / float(RAMP_LEVELS - 1); const float gain = std::pow(t, gamma) * maxGain; const float mix = Smooth01(t); for (int ch = 0; ch < 3; ++ch) { const float tint = COLD[ch] + (WARM[ch] - COLD[ch]) * mix; const float k = gain * tint; for (int v = 0; v < 256; ++v) { const float out = float(v) * k; g_shadeLut[lvl][ch][v] = uint8_t(out < 0.0f ? 0.0f : (out > 255.0f ? 255.0f : out)); } } } } // ----------------------------------------------------------------------------- void Framebuffer::Init(int width, int height) { w = width; h = height; px.assign(size_t(w) * size_t(h), 0u); } void Framebuffer::Clear(uint32_t color) { std::fill(px.begin(), px.end(), color); } namespace { // Общая обрезка спрайта по буферу. false — рисовать нечего. inline bool ClipSprite(const Sprite& s, int x, int y, int fbW, int fbH, int& dstX0, int& dstY0, int& sx0, int& sy0, int& sx1, int& sy1) { dstX0 = x - s.ax; dstY0 = y - s.ay; sx0 = std::max(0, -dstX0); sy0 = std::max(0, -dstY0); sx1 = std::min(s.w, fbW - dstX0); sy1 = std::min(s.h, fbH - dstY0); // Пустой диапазон -> выходим ДО вычисления адресов: иначе индекс уезжает // за границы vector (Debug ловит это как "vector subscript out of range"). return sx0 < sx1 && sy0 < sy1; } } // namespace void Framebuffer::Blit(const Sprite& s, int x, int y) { int dstX0, dstY0, sx0, sy0, sx1, sy1; if (!ClipSprite(s, x, y, w, h, dstX0, dstY0, sx0, sy0, sx1, sy1)) return; for (int sy = sy0; sy < sy1; ++sy) { const uint32_t* src = s.px.data() + size_t(sy) * s.w + sx0; uint32_t* dst = px.data() + size_t(dstY0 + sy) * w + (dstX0 + sx0); for (int sx = sx0; sx < sx1; ++sx, ++src, ++dst) { const uint32_t c = *src; if (AlphaOf(c) != 0) *dst = c; // альфа-тест, без блендинга } } } void Framebuffer::BlitTinted(const Sprite& s, int x, int y, uint32_t tint) { int dstX0, dstY0, sx0, sy0, sx1, sy1; if (!ClipSprite(s, x, y, w, h, dstX0, dstY0, sx0, sy0, sx1, sy1)) return; for (int sy = sy0; sy < sy1; ++sy) for (int sx = sx0; sx < sx1; ++sx) if (AlphaOf(s.px[size_t(sy) * s.w + sx]) != 0) px[size_t(dstY0 + sy) * w + (dstX0 + sx)] = tint; } void Framebuffer::BlitShaded(const Sprite& s, int x, int y, const ShadeQuad& q) { int dstX0, dstY0, sx0, sy0, sx1, sy1; if (!ClipSprite(s, x, y, w, h, dstX0, dstY0, sx0, sy0, sx1, sy1)) return; constexpr float MAX_LVL = float(RAMP_LEVELS - 1); for (int sy = sy0; sy < sy1; ++sy) { const float py = float(sy < q.clampRow ? sy : q.clampRow); const float rowBase = q.a + q.c * py + q.f * py * py; // Прямые разности по строке: во внутреннем цикле два сложения. const float fx0 = float(sx0); float lit = rowBase + q.b * fx0 + q.e * fx0 * fx0; float dl = q.b + q.e * (2.0f * fx0 + 1.0f); const float ddl = 2.0f * q.e; const uint8_t* bayerRow = BAYER4[(dstY0 + sy) & 3]; const uint32_t* src = s.px.data() + size_t(sy) * s.w + sx0; uint32_t* dst = px.data() + size_t(dstY0 + sy) * w + (dstX0 + sx0); for (int sx = sx0; sx < sx1; ++sx, ++src, ++dst) { const uint32_t c = *src; if (AlphaOf(c) != 0) { const float dither = (float(bayerRow[(dstX0 + sx) & 3]) + 0.5f) * (1.0f / 16.0f); int lvl = int(lit * MAX_LVL + dither); if (lvl < 0) lvl = 0; else if (lvl >= RAMP_LEVELS) lvl = RAMP_LEVELS - 1; if (lvl != 0) *dst = ShadePixel(c, lvl); } lit += dl; dl += ddl; } } } void Framebuffer::BlitMul(const Sprite& mask, int x, int y, uint32_t k256) { int dstX0, dstY0, sx0, sy0, sx1, sy1; if (!ClipSprite(mask, x, y, w, h, dstX0, dstY0, sx0, sy0, sx1, sy1)) return; for (int sy = sy0; sy < sy1; ++sy) for (int sx = sx0; sx < sx1; ++sx) if (AlphaOf(mask.px[size_t(sy) * mask.w + sx]) != 0) { uint32_t& d = px[size_t(dstY0 + sy) * w + (dstX0 + sx)]; d = ScaleColor8(d, k256); } } void Framebuffer::FillRect(int x, int y, int rw, int rh, uint32_t c) { const int x0 = std::max(0, x); const int y0 = std::max(0, y); const int x1 = std::min(w, x + rw); const int y1 = std::min(h, y + rh); for (int yy = y0; yy < y1; ++yy) for (int xx = x0; xx < x1; ++xx) px[size_t(yy) * w + xx] = c; } void Framebuffer::Line(int x0, int y0, int x1, int y1, uint32_t c) { // Брезенхем. Put сам отсекает по границам. int dx = std::abs(x1 - x0); int dy = -std::abs(y1 - y0); const int sx = (x0 < x1) ? 1 : -1; const int sy = (y0 < y1) ? 1 : -1; int err = dx + dy; for (;;) { Put(x0, y0, c); if (x0 == x1 && y0 == y1) break; const int e2 = 2 * err; if (e2 >= dy) { err += dy; x0 += sx; } if (e2 <= dx) { err += dx; y0 += sy; } } } void Framebuffer::CircleOutline(int cx, int cy, int r, uint32_t c) { int x = r; int y = 0; int err = 1 - r; while (x >= y) { Put(cx + x, cy + y, c); Put(cx + y, cy + x, c); Put(cx - y, cy + x, c); Put(cx - x, cy + y, c); Put(cx - x, cy - y, c); Put(cx - y, cy - x, c); Put(cx + y, cy - x, c); Put(cx + x, cy - y, c); ++y; if (err < 0) err += 2 * y + 1; else { --x; err += 2 * (y - x) + 1; } } } void Framebuffer::CircleFilled(int cx, int cy, int r, uint32_t c) { const int r2 = r * r; for (int y = -r; y <= r; ++y) for (int x = -r; x <= r; ++x) if (x * x + y * y <= r2) Put(cx + x, cy + y, c); } // ----------------------------------------------------------------------------- // Аддитивные примитивы // ----------------------------------------------------------------------------- void Framebuffer::AddBeam(float x0, float y0, float x1, float y1, uint32_t headCol, uint32_t tailCol, int halo) { // Отсечение по расширенному прямоугольнику кадра: длинный закадровый штрих // не должен крутить тысячи холостых итераций. const float pad = float(halo) + 2.0f; if ((x0 < -pad && x1 < -pad) || (x0 > float(w) + pad && x1 > float(w) + pad)) return; if ((y0 < -pad && y1 < -pad) || (y0 > float(h) + pad && y1 > float(h) + pad)) return; const float dx = x1 - x0; const float dy = y1 - y0; const float len = std::sqrt(dx * dx + dy * dy); const int steps = int(len) + 1; if (steps > 4 * (w + h)) return; // защита от вырожденных значений const float inv = 1.0f / float(steps); for (int i = 0; i <= steps; ++i) { const float t = float(i) * inv; // 0 = хвост, 1 = голова const float px_ = x0 + dx * t; const float py_ = y0 + dy * t; // Цвет плывёт от хвоста к голове; хвост дополнительно гаснет. const uint32_t core = AddSat(ScaleColor8(tailCol, uint32_t((1.0f - t) * 256.0f)), ScaleColor8(headCol, uint32_t(t * 256.0f))); const int ix = int(px_); const int iy = int(py_); AddPx(ix, iy, core); // Боковой ореол: та же линия вполсилы на соседних пикселях. if (halo > 0) { const uint32_t glow = ScaleColor8(core, 96); AddPx(ix + 1, iy, glow); AddPx(ix - 1, iy, glow); AddPx(ix, iy + 1, glow); AddPx(ix, iy - 1, glow); } } } void Framebuffer::AddGlowDisc(int cx, int cy, int radius, uint32_t color) { if (radius <= 0) return; if (cx + radius < 0 || cx - radius >= w || cy + radius < 0 || cy - radius >= h) return; const float inv = 1.0f / float(radius); for (int y = -radius; y <= radius; ++y) { for (int x = -radius; x <= radius; ++x) { const float d = std::sqrt(float(x * x + y * y)) * inv; if (d >= 1.0f) continue; const float k = (1.0f - d) * (1.0f - d); // квадратичное затухание AddPx(cx + x, cy + y, ScaleColor8(color, uint32_t(k * 256.0f))); } } } // ----------------------------------------------------------------------------- void ScreenBlitter::Init(int width, int height) { Image img = GenImageColor(width, height, BLANK); ImageFormat(&img, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); Texture2D* t = new Texture2D(LoadTextureFromImage(img)); UnloadImage(img); SetTextureFilter(*t, TEXTURE_FILTER_POINT); tex = t; texW = width; texH = height; } void ScreenBlitter::Shutdown() { if (!tex) return; Texture2D* t = static_cast(tex); UnloadTexture(*t); delete t; tex = nullptr; } // Прямоугольник кадра в окне: ЦЕЛЫЙ масштаб и центрирование. // // На borderless fullscreen окно почти никогда не кратно 480x270, и дробное // растяжение размывает пиксель: одни строки удваиваются, соседние нет. // Поэтому берём наибольший целый масштаб, а остаток отдаём чёрным полям. ScreenRect ScreenFitRect(int fbW, int fbH, int windowW, int windowH) { if (fbW <= 0 || fbH <= 0) return {}; int scale = std::min(windowW / fbW, windowH / fbH); if (scale < 1) scale = 1; // окно меньше кадра: лучше обрезать, чем мылить ScreenRect r; r.scale = scale; r.w = fbW * scale; r.h = fbH * scale; r.x = (windowW - r.w) / 2; r.y = (windowH - r.h) / 2; return r; } void ScreenBlitter::Present(const Framebuffer& fb, int windowW, int windowH) const { if (!tex) return; if (fb.w != texW || fb.h != texH) return; // размер разошёлся — не грузим мусор Texture2D* t = static_cast(tex); UpdateTexture(*t, fb.px.data()); const ScreenRect fit = ScreenFitRect(fb.w, fb.h, windowW, windowH); const Rectangle src = {0.0f, 0.0f, float(fb.w), float(fb.h)}; const Rectangle dst = {float(fit.x), float(fit.y), float(fit.w), float(fit.h)}; DrawTexturePro(*t, src, dst, {0.0f, 0.0f}, 0.0f, WHITE); }