From 1d341f870c86a76a787a515e45bc165c2b9ed2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=A4=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D0=B5=D0=B5=D0=B2?= Date: Wed, 7 Oct 2026 18:14:08 +0300 Subject: [PATCH] feat(render): Implement classic Doom-style 2.5D software raycasting renderer --- CMakeLists.txt | 1 + src/core/Game.hpp | 4 +- src/render/SoftwareRenderer.cpp | 941 ++++++++++++++++++++++++++++++++ src/render/SoftwareRenderer.hpp | 113 ++++ 4 files changed, 1057 insertions(+), 2 deletions(-) create mode 100644 src/render/SoftwareRenderer.cpp create mode 100644 src/render/SoftwareRenderer.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 62f4253..2df208f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ add_executable(DoorNroll src/hacking/DbdSkillCheck.cpp src/inventory/InventoryUI.cpp src/render/CyberRenderer.cpp + src/render/SoftwareRenderer.cpp src/render/UIRenderer.cpp src/hub/HubRoom.cpp src/hub/RestRoom.cpp diff --git a/src/core/Game.hpp b/src/core/Game.hpp index 57f7a2b..3e080ad 100644 --- a/src/core/Game.hpp +++ b/src/core/Game.hpp @@ -6,7 +6,7 @@ #include "hacking/HackingGame.hpp" #include "combat/CombatSystem.hpp" #include "inventory/InventoryUI.hpp" -#include "render/CyberRenderer.hpp" +#include "render/SoftwareRenderer.hpp" #include "render/UIRenderer.hpp" #include "render/ScreenTransition.hpp" #include "hub/HubRoom.hpp" @@ -63,7 +63,7 @@ private: CombatSystem m_combat; InventoryUI m_inventoryUI; - CyberRenderer m_renderer; + SoftwareRenderer m_renderer; UIRenderer m_uiRenderer; ScreenTransition m_transition; diff --git a/src/render/SoftwareRenderer.cpp b/src/render/SoftwareRenderer.cpp new file mode 100644 index 0000000..02f1b6c --- /dev/null +++ b/src/render/SoftwareRenderer.cpp @@ -0,0 +1,941 @@ +#include "SoftwareRenderer.hpp" +#include "hub/HubRoom.hpp" +#include "hub/RestRoom.hpp" +#include "combat/CombatSystem.hpp" +#include +#include +#include +#include +#include + +namespace dnr { + +SoftwareRenderer::SoftwareRenderer() = default; +SoftwareRenderer::~SoftwareRenderer() { + cleanup(); +} + +void SoftwareRenderer::init() { + if (m_initialized) return; + + m_framebuffer.resize(FB_WIDTH * FB_HEIGHT, Color{ 6, 8, 12, 255 }); + m_depthBuffer.resize(FB_WIDTH, 1000.0f); + + m_screenImage.data = m_framebuffer.data(); + m_screenImage.width = FB_WIDTH; + m_screenImage.height = FB_HEIGHT; + m_screenImage.mipmaps = 1; + m_screenImage.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; + + m_screenTexture = LoadTextureFromImage(m_screenImage); + SetTextureFilter(m_screenTexture, TEXTURE_FILTER_POINT); + + generateCpuTextures(); + + m_initialized = true; +} + +void SoftwareRenderer::cleanup() { + if (m_initialized) { + UnloadTexture(m_screenTexture); + m_initialized = false; + } +} + +void SoftwareRenderer::triggerGlitch(float intensity) { + m_glitchTimer = std::max(m_glitchTimer, intensity); +} + +// ---------------------------------------------------------------------------- +// Texture Generation & Loading +// ---------------------------------------------------------------------------- +void SoftwareRenderer::generateCpuTextures() { + auto makeTex = [](CpuTexture& t, int w = TEX_SIZE, int h = TEX_SIZE) { + t.width = w; + t.height = h; + t.pixels.resize(w * h, Color{ 30, 30, 30, 255 }); + }; + + // 1. Dungeon Wall: Dark tactical gunmetal panel with reinforced seams & rivets + makeTex(m_texDungeonWall); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isBorder = (x < 4 || x > 251 || y < 4 || y > 251 || x == 127 || x == 128 || y == 127 || y == 128); + bool isRivet = ((x == 12 || x == 243 || x == 118 || x == 137) && (y % 32 >= 14 && y % 32 <= 17)); + int noise = ((x * 19 + y * 37) % 9) - 4; + int base = isBorder ? 18 : (isRivet ? 95 : 38 + noise); + m_texDungeonWall.pixels[y * TEX_SIZE + x] = Color{ + (unsigned char)std::clamp(base - 4, 8, 255), + (unsigned char)std::clamp(base, 10, 255), + (unsigned char)std::clamp(base + 8, 14, 255), + 255 + }; + } + } + + // 2. Dungeon Floor: Industrial steel grating + makeTex(m_texDungeonFloor); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isGrate = (x % 16 <= 2 || y % 16 <= 2); + int base = isGrate ? 12 : 32 + ((x ^ y) % 7); + m_texDungeonFloor.pixels[y * TEX_SIZE + x] = Color{ + (unsigned char)base, + (unsigned char)(base + 3), + (unsigned char)(base + 7), + 255 + }; + } + } + + // 3. Dungeon Ceiling: Metallic beam ribs with dim red emergency light strip + makeTex(m_texDungeonCeil); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isStrip = (x >= 124 && x <= 131 && y % 64 >= 20 && y % 64 <= 44); + int base = ((y % 32 < 4) ? 14 : 26) + ((x * 13) % 5); + if (isStrip) { + m_texDungeonCeil.pixels[y * TEX_SIZE + x] = Color{ 220, 25, 45, 255 }; + } else { + m_texDungeonCeil.pixels[y * TEX_SIZE + x] = Color{ + (unsigned char)base, + (unsigned char)(base + 2), + (unsigned char)(base + 6), + 255 + }; + } + } + } + + // 4. Dungeon Blast Door: Heavy pneumatic airlock door with yellow warning chevron + makeTex(m_texDungeonDoor); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isSeam = (x == 127 || x == 128 || y == 64 || y == 192); + bool isWarning = (y >= 115 && y <= 140 && ((x + y) / 16) % 2 == 0); + if (isWarning) { + m_texDungeonDoor.pixels[y * TEX_SIZE + x] = Color{ 235, 160, 20, 255 }; + } else if (isSeam) { + m_texDungeonDoor.pixels[y * TEX_SIZE + x] = Color{ 14, 18, 24, 255 }; + } else { + int base = 48 + ((x * 7 + y * 11) % 6); + m_texDungeonDoor.pixels[y * TEX_SIZE + x] = Color{ + (unsigned char)base, + (unsigned char)(base + 2), + (unsigned char)(base + 6), + 255 + }; + } + } + } + + // 5. Parquet Floor: Dark smoked walnut planks + makeTex(m_texParquet); + for (int y = 0; y < TEX_SIZE; ++y) { + int row = y / 32; + int rowY = y % 32; + for (int x = 0; x < TEX_SIZE; ++x) { + int shift = (row % 2 == 0) ? 0 : 48; + int col = (x + shift) / 96; + int colX = (x + shift) % 96; + bool isSeam = (rowY == 0 || colX == 0); + if (isSeam) { + m_texParquet.pixels[y * TEX_SIZE + x] = Color{ 16, 12, 10, 255 }; + } else { + int base = 42 + ((row * 13 + col * 23) % 11) - 5; + m_texParquet.pixels[y * TEX_SIZE + x] = Color{ + (unsigned char)std::clamp(base + 8, 15, 120), + (unsigned char)std::clamp(base + 2, 12, 110), + (unsigned char)std::clamp(base - 2, 10, 95), + 255 + }; + } + } + } + + // 6. Rug: Luxury Persian / cyber antique rug + makeTex(m_texRug); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + int mDist = std::max(std::abs(x - 128), std::abs(y - 128)); + Color c; + if (mDist > 115) c = Color{ 115, 92, 48, 255 }; // Outer gold border + else if (mDist > 105) c = Color{ 22, 28, 42, 255 }; // Dark groove + else if (mDist > 98) c = Color{ 145, 110, 55, 255 }; // Inner accent + else c = Color{ 36, 44, 62, 255 }; // Midnight blue field + m_texRug.pixels[y * TEX_SIZE + x] = c; + } + } + + // 7. Plaster Wall: Dark slate graphite + makeTex(m_texPlasterWall); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + int grain = ((x * 23 + y * 41 + (x ^ y) * 17) % 13) - 6; + int base = 34 + grain; + m_texPlasterWall.pixels[y * TEX_SIZE + x] = Color{ + (unsigned char)std::clamp(base, 12, 90), + (unsigned char)std::clamp(base + 4, 15, 95), + (unsigned char)std::clamp(base + 10, 18, 110), + 255 + }; + } + } + + // 8. Room Window: Try loading Room_window.jpg + makeTex(m_texWindowView); + bool loadedWindow = false; + const std::vector winPaths = { + "assets/Pictures/Room_window.jpg", + "assets/Pictures/Room_window.png", + "../assets/Pictures/Room_window.jpg", + "assets/Room_window.jpg" + }; + for (const auto& wp : winPaths) { + if (FileExists(wp.c_str())) { + Image img = LoadImage(wp.c_str()); + if (img.data) { + ImageResize(&img, TEX_SIZE, TEX_SIZE); + ImageFormat(&img, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); + Color* pix = (Color*)img.data; + m_texWindowView.pixels.assign(pix, pix + TEX_SIZE * TEX_SIZE); + UnloadImage(img); + loadedWindow = true; + break; + } + } + } + if (!loadedWindow) { + // Fallback procedural night skyline + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + float t = y / 256.0f; + int b = static_cast(18 + (1.0f - t) * 75); + m_texWindowView.pixels[y * TEX_SIZE + x] = Color{ + (unsigned char)(b * 0.7f), + (unsigned char)(b * 0.4f), + (unsigned char)b, + 255 + }; + } + } + } + + // 9. Cyber Gate & Other Textures + makeTex(m_texCyberGate); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isFrame = (x < 24 || x > 231 || y < 24); + bool isGateCore = !isFrame && (std::abs(x - 128) < 85); + if (isGateCore) { + m_texCyberGate.pixels[y * TEX_SIZE + x] = Color{ 0, 255, 128, 220 }; + } else if (isFrame) { + m_texCyberGate.pixels[y * TEX_SIZE + x] = Color{ 24, 32, 44, 255 }; + } else { + m_texCyberGate.pixels[y * TEX_SIZE + x] = Color{ 10, 14, 20, 255 }; + } + } + } + + // 10. Ancient Palace Textures (Rest Room) + makeTex(m_texAncientWall); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + int brickY = y / 32; + int shift = (brickY % 2 == 0) ? 0 : 32; + int brickX = (x + shift) / 64; + bool isMortar = (y % 32 <= 2 || (x + shift) % 64 <= 2); + int base = isMortar ? 20 : 54 + ((brickX * 17 + brickY * 31) % 15) - 7; + m_texAncientWall.pixels[y * TEX_SIZE + x] = Color{ + (unsigned char)(base + 6), + (unsigned char)base, + (unsigned char)(base - 4), + 255 + }; + } + } + m_texAncientFloor = m_texAncientWall; + m_texAncientCeil = m_texAncientWall; + + // 11. SPRITES: Drone Sprite + makeTex(m_texDrone); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + int dx = x - 128; + int dy = y - 128; + float r = std::sqrt(float(dx * dx + dy * dy)); + if (r < 75.0f) { + // Drone body chassis + if (r < 28.0f) { + m_texDrone.pixels[y * TEX_SIZE + x] = Color{ 240, 245, 255, 255 }; // Glowing core + } else if (std::abs(dy) < 14 && dx > -45 && dx < 45) { + m_texDrone.pixels[y * TEX_SIZE + x] = Color{ 230, 25, 45, 255 }; // Laser visor + } else { + int b = 35 + (int)(r * 0.4f); + m_texDrone.pixels[y * TEX_SIZE + x] = Color{ (unsigned char)b, (unsigned char)(b + 5), (unsigned char)(b + 12), 255 }; + } + } else if (r < 82.0f && (std::abs(dx) > 55 || std::abs(dy) > 55)) { + m_texDrone.pixels[y * TEX_SIZE + x] = Color{ 60, 75, 95, 255 }; // Thruster pods + } else { + m_texDrone.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; // Transparent background + } + } + } + + // 12. SPRITES: Elite Drone + makeTex(m_texDroneElite); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + int dx = x - 128; + int dy = y - 128; + float r = std::sqrt(float(dx * dx + dy * dy)); + if (r < 85.0f) { + if (r < 32.0f) { + m_texDroneElite.pixels[y * TEX_SIZE + x] = Color{ 255, 60, 75, 255 }; // Crimson reactor + } else if (r > 65.0f && r < 75.0f) { + m_texDroneElite.pixels[y * TEX_SIZE + x] = Color{ 220, 25, 45, 255 }; // Armor shroud + } else { + int b = 25 + (int)(r * 0.35f); + m_texDroneElite.pixels[y * TEX_SIZE + x] = Color{ (unsigned char)(b + 15), (unsigned char)b, (unsigned char)b, 255 }; + } + } else { + m_texDroneElite.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; + } + } + } + + // 13. SPRITES: Desk with Dual Monitors + makeTex(m_texDesk); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isTable = (y >= 170 && y <= 230 && x >= 30 && x <= 226); + bool isLegs = (y > 230 && ((x >= 35 && x <= 50) || (x >= 206 && x <= 221))); + bool isMonLeft = (y >= 70 && y <= 165 && x >= 45 && x <= 125); + bool isMonRight = (y >= 70 && y <= 165 && x >= 135 && x <= 215); + + if (isMonLeft) { + bool isCode = (y % 12 >= 8 && x % 20 < 15); + m_texDesk.pixels[y * TEX_SIZE + x] = isCode ? Color{ 220, 230, 245, 255 } : Color{ 14, 20, 32, 255 }; + } else if (isMonRight) { + int rx = x - 175; + int ry = y - 117; + bool isRadar = (std::abs(rx * rx + ry * ry - 900) < 60 || rx == 0 || ry == 0); + m_texDesk.pixels[y * TEX_SIZE + x] = isRadar ? Color{ 0, 255, 180, 255 } : Color{ 8, 16, 26, 255 }; + } else if (isTable) { + m_texDesk.pixels[y * TEX_SIZE + x] = Color{ 48, 38, 32, 255 }; + } else if (isLegs) { + m_texDesk.pixels[y * TEX_SIZE + x] = Color{ 25, 28, 35, 255 }; + } else { + m_texDesk.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; + } + } + } + + // 14. SPRITES: Cyber Bed + makeTex(m_texBed); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isBedFrame = (y >= 160 && y <= 245 && x >= 35 && x <= 220); + bool isPillow = (y >= 170 && y <= 200 && x >= 45 && x <= 95); + if (isPillow) { + m_texBed.pixels[y * TEX_SIZE + x] = Color{ 210, 218, 230, 255 }; + } else if (isBedFrame) { + m_texBed.pixels[y * TEX_SIZE + x] = Color{ 26, 32, 48, 255 }; + } else { + m_texBed.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; + } + } + } + + // 15. SPRITES: Loot Cache Crate + makeTex(m_texLootCache); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isBox = (y >= 140 && y <= 240 && x >= 55 && x <= 201); + if (isBox) { + bool isLed = (y >= 175 && y <= 195 && x >= 115 && x <= 141); + if (isLed) { + m_texLootCache.pixels[y * TEX_SIZE + x] = Color{ 255, 30, 50, 255 }; + } else { + int b = 38 + ((x % 32 < 2 || y % 32 < 2) ? -12 : 0); + m_texLootCache.pixels[y * TEX_SIZE + x] = Color{ (unsigned char)b, (unsigned char)(b + 4), (unsigned char)(b + 8), 255 }; + } + } else { + m_texLootCache.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; + } + } + } + + // 16. SPRITES: Exit Portal Beacon + makeTex(m_texExitPortal); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + int dx = x - 128; + bool isCore = (std::abs(dx) < 22 && y >= 60 && y <= 235); + bool isBase = (y >= 235 && std::abs(dx) < 65); + if (isCore) { + m_texExitPortal.pixels[y * TEX_SIZE + x] = Color{ 240, 30, 60, 255 }; + } else if (isBase) { + m_texExitPortal.pixels[y * TEX_SIZE + x] = Color{ 30, 38, 52, 255 }; + } else { + m_texExitPortal.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; + } + } + } + + // 17. SPRITES: Proximity Mine + makeTex(m_texMine); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isPlate = (y >= 210 && y <= 245 && x >= 70 && x <= 186); + bool isBlinker = (y >= 218 && y <= 232 && x >= 118 && x <= 138); + if (isBlinker) { + m_texMine.pixels[y * TEX_SIZE + x] = Color{ 255, 40, 50, 255 }; + } else if (isPlate) { + m_texMine.pixels[y * TEX_SIZE + x] = Color{ 36, 42, 54, 255 }; + } else { + m_texMine.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; + } + } + } + + // 18. SPRITES: Cyber Shrine Terminal + makeTex(m_texShrine); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isMonolith = (y >= 70 && y <= 245 && x >= 85 && x <= 171); + bool isScreen = (y >= 90 && y <= 150 && x >= 95 && x <= 161); + if (isScreen) { + m_texShrine.pixels[y * TEX_SIZE + x] = Color{ 230, 30, 50, 255 }; + } else if (isMonolith) { + m_texShrine.pixels[y * TEX_SIZE + x] = Color{ 20, 26, 36, 255 }; + } else { + m_texShrine.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; + } + } + } + + // 19. SPRITES: Laser Beam Strip + makeTex(m_texLaserBeam); + for (int y = 0; y < TEX_SIZE; ++y) { + for (int x = 0; x < TEX_SIZE; ++x) { + bool isBeam = (y >= 120 && y <= 136); + if (isBeam) { + m_texLaserBeam.pixels[y * TEX_SIZE + x] = (y >= 126 && y <= 130) ? WHITE : Color{ 240, 30, 60, 255 }; + } else { + m_texLaserBeam.pixels[y * TEX_SIZE + x] = Color{ 0, 0, 0, 0 }; + } + } + } +} + +const SoftwareRenderer::CpuTexture& SoftwareRenderer::getTexture(int index) const { + switch (index) { + case 1: return m_texDungeonWall; + case 2: return m_texDungeonFloor; + case 3: return m_texDungeonCeil; + case 4: return m_texDungeonDoor; + case 5: return m_texParquet; + case 6: return m_texRug; + case 7: return m_texPlasterWall; + case 8: return m_texWindowView; + case 9: return m_texCyberGate; + case 10: return m_texAncientWall; + case 11: return m_texAncientFloor; + case 12: return m_texAncientCeil; + case 13: return m_texDrone; + case 14: return m_texDroneElite; + case 15: return m_texDesk; + case 16: return m_texBed; + case 17: return m_texLootCache; + case 18: return m_texExitPortal; + case 19: return m_texMine; + case 20: return m_texShrine; + case 21: return m_texLaserBeam; + default: return m_texDungeonWall; + } +} + +// ---------------------------------------------------------------------------- +// Core DDA Raycaster with Diminishing Light & Horizontal Spans +// ---------------------------------------------------------------------------- +void SoftwareRenderer::renderSceneRaycast(float posX, float posY, float dirX, float dirY, + int mapW, int mapH, + const std::vector& mapGrid, + int wallTex, int floorTex, int ceilTex, + float animTime, + const std::vector& sprites) { + float fov = 0.66f; + float planeX = -dirY * fov; + float planeY = dirX * fov; + + std::vector wallDrawStart(FB_WIDTH); + std::vector wallDrawEnd(FB_WIDTH); + + const auto& texWall = getTexture(wallTex); + const auto& texFloor = getTexture(floorTex); + const auto& texCeil = getTexture(ceilTex); + + // 1. Raycast Wall Columns + for (int x = 0; x < FB_WIDTH; ++x) { + float cameraX = 2.0f * x / float(FB_WIDTH) - 1.0f; + float rayDirX = dirX + planeX * cameraX; + float rayDirY = dirY + planeY * cameraX; + + int mapX = int(posX); + int mapY = int(posY); + + float deltaDistX = (rayDirX == 0.0f) ? 1e30f : std::abs(1.0f / rayDirX); + float deltaDistY = (rayDirY == 0.0f) ? 1e30f : std::abs(1.0f / rayDirY); + + float perpWallDist = 0.0f; + int stepX = 0; + int stepY = 0; + float sideDistX = 0.0f; + float sideDistY = 0.0f; + + if (rayDirX < 0) { + stepX = -1; + sideDistX = (posX - mapX) * deltaDistX; + } else { + stepX = 1; + sideDistX = (mapX + 1.0f - posX) * deltaDistX; + } + + if (rayDirY < 0) { + stepY = -1; + sideDistY = (posY - mapY) * deltaDistY; + } else { + stepY = 1; + sideDistY = (mapY + 1.0f - posY) * deltaDistY; + } + + int hit = 0; + int side = 0; // 0 = X-axis, 1 = Y-axis + int hitTile = 1; + + while (hit == 0) { + if (sideDistX < sideDistY) { + sideDistX += deltaDistX; + mapX += stepX; + side = 0; + } else { + sideDistY += deltaDistY; + mapY += stepY; + side = 1; + } + + if (mapX < 0 || mapX >= mapW || mapY < 0 || mapY >= mapH) { + hit = 1; + hitTile = 1; + break; + } + + int t = mapGrid[mapY * mapW + mapX]; + if (t > 0) { + hit = 1; + hitTile = t; + } + } + + if (side == 0) perpWallDist = (sideDistX - deltaDistX); + else perpWallDist = (sideDistY - deltaDistY); + if (perpWallDist < 0.1f) perpWallDist = 0.1f; + + m_depthBuffer[x] = perpWallDist; + + int lineHeight = int(FB_HEIGHT / perpWallDist); + int drawStart = -lineHeight / 2 + FB_HEIGHT / 2; + if (drawStart < 0) drawStart = 0; + int drawEnd = lineHeight / 2 + FB_HEIGHT / 2; + if (drawEnd >= FB_HEIGHT) drawEnd = FB_HEIGHT - 1; + + wallDrawStart[x] = drawStart; + wallDrawEnd[x] = drawEnd; + + // Texture coordinate calculation + float wallX = 0.0f; + if (side == 0) wallX = posY + perpWallDist * rayDirY; + else wallX = posX + perpWallDist * rayDirX; + wallX -= std::floor(wallX); + + int texX = int(wallX * float(TEX_SIZE)); + if (side == 0 && rayDirX > 0) texX = TEX_SIZE - texX - 1; + if (side == 1 && rayDirY < 0) texX = TEX_SIZE - texX - 1; + + // Doom diminishing light formula + float shade = 1.0f / (1.0f + perpWallDist * 0.14f); + if (side == 1) shade *= 0.80f; // Ambient directional shadow + + // Tactical flashlight cone focus in center + float angleDot = (rayDirX * dirX + rayDirY * dirY); + float cone = std::clamp((angleDot - 0.75f) / 0.25f, 0.0f, 1.0f); + shade = std::clamp(shade * (0.65f + cone * 0.60f), 0.08f, 1.0f); + + // Select wall texture (doors use door texture, window wall uses window) + const CpuTexture* currWallTex = &texWall; + if (hitTile == 4) currWallTex = &m_texDungeonDoor; + else if (hitTile == 8) currWallTex = &m_texWindowView; + else if (hitTile == 9) currWallTex = &m_texCyberGate; + + float step = 1.0f * TEX_SIZE / lineHeight; + float texPos = (drawStart - FB_HEIGHT / 2 + lineHeight / 2) * step; + + for (int y = drawStart; y <= drawEnd; ++y) { + int texY = int(texPos) & (TEX_SIZE - 1); + texPos += step; + + Color c = currWallTex->sample(texX, texY); + Color finalCol = { + (unsigned char)(c.r * shade), + (unsigned char)(c.g * shade), + (unsigned char)(c.b * shade), + 255 + }; + m_framebuffer[y * FB_WIDTH + x] = finalCol; + } + } + + // 2. Horizontal Floor and Ceiling Spans + float rayDirX0 = dirX - planeX; + float rayDirY0 = dirY - planeY; + float rayDirX1 = dirX + planeX; + float rayDirY1 = dirY + planeY; + + for (int y = FB_HEIGHT / 2 + 1; y < FB_HEIGHT; ++y) { + int p = y - FB_HEIGHT / 2; + float posZ = 0.5f * FB_HEIGHT; + float rowDistance = posZ / p; + + float floorStepX = rowDistance * (rayDirX1 - rayDirX0) / FB_WIDTH; + float floorStepY = rowDistance * (rayDirY1 - rayDirY0) / FB_WIDTH; + + float floorX = posX + rowDistance * rayDirX0; + float floorY = posY + rowDistance * rayDirY0; + + float rowShade = std::clamp(1.0f / (1.0f + rowDistance * 0.16f), 0.06f, 1.0f); + + for (int x = 0; x < FB_WIDTH; ++x) { + int tx = int(TEX_SIZE * (floorX - std::floor(floorX))) & (TEX_SIZE - 1); + int ty = int(TEX_SIZE * (floorY - std::floor(floorY))) & (TEX_SIZE - 1); + + floorX += floorStepX; + floorY += floorStepY; + + // Draw floor pixel if below wall column + if (y > wallDrawEnd[x]) { + Color fc = texFloor.sample(tx, ty); + m_framebuffer[y * FB_WIDTH + x] = Color{ + (unsigned char)(fc.r * rowShade), + (unsigned char)(fc.g * rowShade), + (unsigned char)(fc.b * rowShade), + 255 + }; + } + + // Draw ceiling pixel symmetrically + int ceilY = FB_HEIGHT - y - 1; + if (ceilY < wallDrawStart[x]) { + Color cc = texCeil.sample(tx, ty); + m_framebuffer[ceilY * FB_WIDTH + x] = Color{ + (unsigned char)(cc.r * rowShade), + (unsigned char)(cc.g * rowShade), + (unsigned char)(cc.b * rowShade), + 255 + }; + } + } + } + + // 3. Depth-Sorted Billboard Sprites + if (!sprites.empty()) { + std::vector sortedSprites = sprites; + for (auto& spr : sortedSprites) { + float dx = spr.x - posX; + float dy = spr.y - posY; + spr.distSq = dx * dx + dy * dy; + } + + std::sort(sortedSprites.begin(), sortedSprites.end(), [](const BillboardSprite& a, const BillboardSprite& b) { + return a.distSq > b.distSq; + }); + + for (const auto& spr : sortedSprites) { + float spriteX = spr.x - posX; + float spriteY = spr.y - posY; + + float invDet = 1.0f / (planeX * dirY - dirX * planeY); + float transformX = invDet * (dirY * spriteX - dirX * spriteY); + float transformY = invDet * (-planeY * spriteX + planeX * spriteY); + + if (transformY <= 0.15f) continue; // Behind or right on top of camera + + int spriteScreenX = int((FB_WIDTH / 2) * (1.0f + transformX / transformY)); + int spriteHeight = std::abs(int(FB_HEIGHT / transformY * spr.scale)); + int vShift = int(spr.vOffset * spriteHeight); + + int drawStartY = -spriteHeight / 2 + FB_HEIGHT / 2 + vShift; + if (drawStartY < 0) drawStartY = 0; + int drawEndY = spriteHeight / 2 + FB_HEIGHT / 2 + vShift; + if (drawEndY >= FB_HEIGHT) drawEndY = FB_HEIGHT - 1; + + int spriteWidth = std::abs(int(FB_HEIGHT / transformY * spr.scale)); + int drawStartX = -spriteWidth / 2 + spriteScreenX; + if (drawStartX < 0) drawStartX = 0; + int drawEndX = spriteWidth / 2 + spriteScreenX; + if (drawEndX >= FB_WIDTH) drawEndX = FB_WIDTH - 1; + + float sprShade = std::clamp(1.0f / (1.0f + transformY * 0.14f), 0.12f, 1.0f); + const auto& sprTex = getTexture(spr.texIndex); + + for (int stripe = drawStartX; stripe <= drawEndX; ++stripe) { + int texX = int((stripe - (-spriteWidth / 2 + spriteScreenX)) * TEX_SIZE / spriteWidth); + if (texX < 0 || texX >= TEX_SIZE) continue; + + if (transformY < m_depthBuffer[stripe]) { + for (int y = drawStartY; y <= drawEndY; ++y) { + int d = (y - vShift) * 256 - FB_HEIGHT * 128 + spriteHeight * 128; + int texY = ((d * TEX_SIZE) / spriteHeight) / 256; + texY = std::clamp(texY, 0, TEX_SIZE - 1); + + Color c = sprTex.sample(texX, texY); + if (c.a > 20) { + Color finalCol; + if (spr.tint.r != 255 || spr.tint.g != 255 || spr.tint.b != 255) { + float tr = spr.tint.r / 255.0f; + float tg = spr.tint.g / 255.0f; + float tb = spr.tint.b / 255.0f; + finalCol = Color{ + (unsigned char)(c.r * tr * sprShade), + (unsigned char)(c.g * tg * sprShade), + (unsigned char)(c.b * tb * sprShade), + 255 + }; + } else { + finalCol = Color{ + (unsigned char)(c.r * sprShade), + (unsigned char)(c.g * sprShade), + (unsigned char)(c.b * sprShade), + 255 + }; + } + m_framebuffer[y * FB_WIDTH + stripe] = finalCol; + } + } + } + } + } + } +} + +// ---------------------------------------------------------------------------- +// Presentation with 2X Integer Upscaling +// ---------------------------------------------------------------------------- +void SoftwareRenderer::presentFramebuffer() { + float dt = GetFrameTime(); + if (m_glitchTimer > 0.0f) { + m_glitchTimer -= dt * 2.0f; + // CRT horizontal displacement slice artifact + int sliceY = (std::rand() % (FB_HEIGHT - 30)); + int shift = (std::rand() % 15) - 7; + for (int y = sliceY; y < sliceY + 20 && y < FB_HEIGHT; ++y) { + for (int x = 0; x < FB_WIDTH; ++x) { + int srcX = std::clamp(x + shift, 0, FB_WIDTH - 1); + m_framebuffer[y * FB_WIDTH + x] = m_framebuffer[y * FB_WIDTH + srcX]; + } + } + } + + UpdateTexture(m_screenTexture, m_framebuffer.data()); + + // 2x Nearest-Neighbor Upscale to 1280x720 window + Rectangle src = { 0.0f, 0.0f, (float)FB_WIDTH, (float)FB_HEIGHT }; + Rectangle dst = { 0.0f, 0.0f, (float)GetScreenWidth(), (float)GetScreenHeight() }; + DrawTexturePro(m_screenTexture, src, dst, Vector2{ 0.0f, 0.0f }, 0.0f, WHITE); +} + +// ---------------------------------------------------------------------------- +// Exploration 3D Raycasting +// ---------------------------------------------------------------------------- +void SoftwareRenderer::render3D(const Dungeon& dungeon, const Player& player, float animTime) { + if (!m_initialized) init(); + + int mapW = dungeon.getWidth(); + int mapH = dungeon.getHeight(); + + std::vector grid(mapW * mapH, 0); + for (int y = 0; y < mapH; ++y) { + for (int x = 0; x < mapW; ++x) { + TileType t = dungeon.getTile(x, y); + if (t == TileType::WALL) grid[y * mapW + x] = 1; + else if (t == TileType::DOOR_LOCKED || t == TileType::DOOR_UNLOCKED) grid[y * mapW + x] = 4; + else grid[y * mapW + x] = 0; + } + } + + float posX = player.getVisualPosition().x / 2.0f; + float posY = player.getVisualPosition().z / 2.0f; + + float rad = player.getVisualYaw() * (3.14159265f / 180.0f); + float dirX = std::sin(rad); + float dirY = -std::cos(rad); + + // Build sprites list: Caches, Portals, Mines, Shrines, Lasers, Drone Patrols + std::vector sprites; + + for (int y = 0; y < mapH; ++y) { + for (int x = 0; x < mapW; ++x) { + TileType t = dungeon.getTile(x, y); + if (t == TileType::LOOT_CACHE) { + sprites.push_back({ x + 0.5f, y + 0.5f, 17, 0.75f, 0.15f, WHITE, false }); + } else if (t == TileType::EXIT_PORTAL) { + sprites.push_back({ x + 0.5f, y + 0.5f, 18, 0.95f, 0.05f, WHITE, false }); + } + } + } + + for (const auto& l : dungeon.getLasers()) { + if (!l.disarmed) { + sprites.push_back({ l.x + 0.5f, l.y + 0.5f, 21, 0.85f, 0.0f, (l.active ? TacticalColors::BrightRed : Color{ 70, 70, 80, 255 }), false }); + } + } + + for (const auto& m : dungeon.getMines()) { + if (!m.defused && !m.triggered) { + sprites.push_back({ m.x + 0.5f, m.y + 0.5f, 19, 0.6f, 0.35f, WHITE, false }); + } + } + + for (const auto& s : dungeon.getShrines()) { + sprites.push_back({ s.x + 0.5f, s.y + 0.5f, 20, 0.9f, 0.05f, (s.used ? TacticalColors::SteelMuted : WHITE), false }); + } + + auto& enemies = const_cast(dungeon).getEnemies(); + for (const auto& me : enemies) { + if (me.alive) { + sprites.push_back({ me.x + 0.5f, me.y + 0.5f, me.enemy.isElite() ? 14 : 13, 0.85f, 0.0f, me.enemy.getColor(), false }); + } + } + + renderSceneRaycast(posX, posY, dirX, dirY, mapW, mapH, grid, 1, 2, 3, animTime, sprites); + presentFramebuffer(); +} + +// ---------------------------------------------------------------------------- +// Combat 3D Raycasting +// ---------------------------------------------------------------------------- +void SoftwareRenderer::renderCombat3D(const Dungeon& dungeon, const Player& player, const CombatSystem& combat, float animTime) { + if (!m_initialized) init(); + + int mapW = dungeon.getWidth(); + int mapH = dungeon.getHeight(); + + std::vector grid(mapW * mapH, 0); + for (int y = 0; y < mapH; ++y) { + for (int x = 0; x < mapW; ++x) { + TileType t = dungeon.getTile(x, y); + grid[y * mapW + x] = (t == TileType::WALL) ? 1 : 0; + } + } + + float posX = player.getVisualPosition().x / 2.0f; + float posY = player.getVisualPosition().z / 2.0f; + + float rad = player.getVisualYaw() * (3.14159265f / 180.0f); + float dirX = std::sin(rad); + float dirY = -std::cos(rad); + + std::vector sprites; + const auto& enemy = combat.getEnemy(); + if (enemy && enemy->isAlive()) { + float bob = std::sin(animTime * 3.5f) * 0.05f; + float distFront = 2.2f; + + float droneX = posX + dirX * distFront; + float droneY = posY + dirY * distFront; + + bool isHit = (combat.getPhase() == CombatPhase::PLAYER_ACTION_ANIM); + Color tint = isHit ? WHITE : enemy->getColor(); + + sprites.push_back({ droneX, droneY, enemy->isElite() ? 14 : 13, 1.05f, bob, tint, false }); + } + + renderSceneRaycast(posX, posY, dirX, dirY, mapW, mapH, grid, 1, 2, 3, animTime, sprites); + presentFramebuffer(); +} + +// ---------------------------------------------------------------------------- +// Apartment Hub 2.5D Raycasting +// ---------------------------------------------------------------------------- +void SoftwareRenderer::renderHub(const HubRoom& hub, const Player& player, float animTime) { + if (!m_initialized) init(); + + // 6x6 cozy apartment room + const int roomW = 6; + const int roomH = 6; + std::vector grid(roomW * roomH, 0); + + for (int y = 0; y < roomH; ++y) { + for (int x = 0; x < roomW; ++x) { + if (x == 0 || y == 5) grid[y * roomW + x] = 7; // Plaster Wall + else if (x == 5) grid[y * roomW + x] = 8; // Room Window (East wall with Room_window.jpg!) + else if (y == 0) grid[y * roomW + x] = 9; // Cyber Gate (North wall) + else grid[y * roomW + x] = 0; + } + } + + float posX = std::clamp(player.getVisualPosition().x / 2.0f, 1.2f, 4.8f); + float posY = std::clamp(player.getVisualPosition().z / 2.0f, 1.2f, 4.8f); + + float rad = player.getVisualYaw() * (3.14159265f / 180.0f); + float dirX = std::sin(rad); + float dirY = -std::cos(rad); + + // Place apartment furniture as authentic 2.5D Doom-style sprites + std::vector sprites = { + { 3.0f, 1.3f, 15, 1.2f, 0.10f, WHITE, false }, // Workstation Desk with Dual Glowing Monitors + { 1.4f, 4.0f, 16, 1.2f, 0.15f, WHITE, false } // Midnight Indigo Cyber Bed + }; + + renderSceneRaycast(posX, posY, dirX, dirY, roomW, roomH, grid, 7, 5, 7, animTime, sprites); + presentFramebuffer(); +} + +// ---------------------------------------------------------------------------- +// Safe Haven Rest Room 2.5D Raycasting +// ---------------------------------------------------------------------------- +void SoftwareRenderer::renderRestRoom(const RestRoom& restRoom, const Player& player, float animTime) { + if (!m_initialized) init(); + + const int roomW = 6; + const int roomH = 6; + std::vector grid(roomW * roomH, 0); + + for (int y = 0; y < roomH; ++y) { + for (int x = 0; x < roomW; ++x) { + if (x == 0 || x == 5 || y == 0 || y == 5) { + grid[y * roomW + x] = 10; // Ancient Palace Wall + } else { + grid[y * roomW + x] = 0; + } + } + } + + float posX = std::clamp(player.getVisualPosition().x / 2.0f, 1.2f, 4.8f); + float posY = std::clamp(player.getVisualPosition().z / 2.0f, 1.2f, 4.8f); + + float rad = player.getVisualYaw() * (3.14159265f / 180.0f); + float dirX = std::sin(rad); + float dirY = -std::cos(rad); + + // Workbench and Cyber Portal + std::vector sprites = { + { 3.0f, 1.2f, 15, 1.1f, 0.10f, WHITE, false }, + { 3.0f, 4.6f, 18, 0.95f, 0.05f, WHITE, false } + }; + + renderSceneRaycast(posX, posY, dirX, dirY, roomW, roomH, grid, 10, 11, 12, animTime, sprites); + presentFramebuffer(); +} + +} // namespace dnr diff --git a/src/render/SoftwareRenderer.hpp b/src/render/SoftwareRenderer.hpp new file mode 100644 index 0000000..b928774 --- /dev/null +++ b/src/render/SoftwareRenderer.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include "core/Common.hpp" +#include "dungeon/Dungeon.hpp" +#include "dungeon/Player.hpp" +#include +#include +#include + +namespace dnr { + +class HubRoom; +class RestRoom; +class CombatSystem; + +class SoftwareRenderer { +public: + SoftwareRenderer(); + ~SoftwareRenderer(); + + void init(); + void cleanup(); + + void render3D(const Dungeon& dungeon, const Player& player, float animTime); + void renderCombat3D(const Dungeon& dungeon, const Player& player, const CombatSystem& combat, float animTime); + void renderHub(const HubRoom& hub, const Player& player, float animTime); + void renderRestRoom(const RestRoom& restRoom, const Player& player, float animTime); + + void triggerGlitch(float intensity = 0.7f); + void setGraphicsQuality(GraphicsQuality q) { m_graphicsQuality = q; } + GraphicsQuality getGraphicsQuality() const { return m_graphicsQuality; } + +private: + static constexpr int FB_WIDTH = 640; + static constexpr int FB_HEIGHT = 360; + static constexpr int TEX_SIZE = 256; + + struct CpuTexture { + int width = TEX_SIZE; + int height = TEX_SIZE; + std::vector pixels; + + Color sample(int x, int y) const { + if (pixels.empty()) return MAGENTA; + int px = (x >= 0 ? x : 0) % width; + int py = (y >= 0 ? y : 0) % height; + return pixels[py * width + px]; + } + }; + + struct BillboardSprite { + float x = 0.0f; + float y = 0.0f; + int texIndex = 0; + float scale = 1.0f; + float vOffset = 0.0f; // vertical shift in height units + Color tint = WHITE; + bool additive = false; + float distSq = 0.0f; + }; + + void generateCpuTextures(); + void renderSceneRaycast(float posX, float posY, float dirX, float dirY, + int mapW, int mapH, + const std::vector& mapGrid, + int wallTex, int floorTex, int ceilTex, + float animTime, + const std::vector& sprites); + + void presentFramebuffer(); + + // Framebuffer & Raylib Texture + std::vector m_framebuffer; + std::vector m_depthBuffer; + Image m_screenImage{}; + Texture2D m_screenTexture{}; + bool m_initialized = false; + GraphicsQuality m_graphicsQuality = GraphicsQuality::BALANCED; + float m_glitchTimer = 0.0f; + + // CPU Textures + CpuTexture m_texDungeonWall; + CpuTexture m_texDungeonFloor; + CpuTexture m_texDungeonCeil; + CpuTexture m_texDungeonDoor; + + CpuTexture m_texParquet; + CpuTexture m_texRug; + CpuTexture m_texPlasterWall; + CpuTexture m_texWindowView; + CpuTexture m_texPoster; + CpuTexture m_texBed; + CpuTexture m_texDesk; + CpuTexture m_texCyberGate; + + CpuTexture m_texAncientWall; + CpuTexture m_texAncientFloor; + CpuTexture m_texAncientCeil; + CpuTexture m_texWorkbench; + + // Sprite Textures + CpuTexture m_texDrone; + CpuTexture m_texDroneElite; + CpuTexture m_texLootCache; + CpuTexture m_texExitPortal; + CpuTexture m_texLaserBeam; + CpuTexture m_texMine; + CpuTexture m_texShrine; + + const CpuTexture& getTexture(int index) const; +}; + +} // namespace dnr