Compare commits
1 commit
main
...
doom-rende
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d341f870c |
4 changed files with 1057 additions and 2 deletions
|
|
@ -33,6 +33,7 @@ add_executable(DoorNroll
|
||||||
src/hacking/DbdSkillCheck.cpp
|
src/hacking/DbdSkillCheck.cpp
|
||||||
src/inventory/InventoryUI.cpp
|
src/inventory/InventoryUI.cpp
|
||||||
src/render/CyberRenderer.cpp
|
src/render/CyberRenderer.cpp
|
||||||
|
src/render/SoftwareRenderer.cpp
|
||||||
src/render/UIRenderer.cpp
|
src/render/UIRenderer.cpp
|
||||||
src/hub/HubRoom.cpp
|
src/hub/HubRoom.cpp
|
||||||
src/hub/RestRoom.cpp
|
src/hub/RestRoom.cpp
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
#include "hacking/HackingGame.hpp"
|
#include "hacking/HackingGame.hpp"
|
||||||
#include "combat/CombatSystem.hpp"
|
#include "combat/CombatSystem.hpp"
|
||||||
#include "inventory/InventoryUI.hpp"
|
#include "inventory/InventoryUI.hpp"
|
||||||
#include "render/CyberRenderer.hpp"
|
#include "render/SoftwareRenderer.hpp"
|
||||||
#include "render/UIRenderer.hpp"
|
#include "render/UIRenderer.hpp"
|
||||||
#include "render/ScreenTransition.hpp"
|
#include "render/ScreenTransition.hpp"
|
||||||
#include "hub/HubRoom.hpp"
|
#include "hub/HubRoom.hpp"
|
||||||
|
|
@ -63,7 +63,7 @@ private:
|
||||||
CombatSystem m_combat;
|
CombatSystem m_combat;
|
||||||
InventoryUI m_inventoryUI;
|
InventoryUI m_inventoryUI;
|
||||||
|
|
||||||
CyberRenderer m_renderer;
|
SoftwareRenderer m_renderer;
|
||||||
UIRenderer m_uiRenderer;
|
UIRenderer m_uiRenderer;
|
||||||
ScreenTransition m_transition;
|
ScreenTransition m_transition;
|
||||||
|
|
||||||
|
|
|
||||||
941
src/render/SoftwareRenderer.cpp
Normal file
941
src/render/SoftwareRenderer.cpp
Normal file
|
|
@ -0,0 +1,941 @@
|
||||||
|
#include "SoftwareRenderer.hpp"
|
||||||
|
#include "hub/HubRoom.hpp"
|
||||||
|
#include "hub/RestRoom.hpp"
|
||||||
|
#include "combat/CombatSystem.hpp"
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
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<std::string> 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<int>(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<int>& mapGrid,
|
||||||
|
int wallTex, int floorTex, int ceilTex,
|
||||||
|
float animTime,
|
||||||
|
const std::vector<BillboardSprite>& sprites) {
|
||||||
|
float fov = 0.66f;
|
||||||
|
float planeX = -dirY * fov;
|
||||||
|
float planeY = dirX * fov;
|
||||||
|
|
||||||
|
std::vector<int> wallDrawStart(FB_WIDTH);
|
||||||
|
std::vector<int> 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<BillboardSprite> 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<int> 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<BillboardSprite> 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&>(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<int> 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<BillboardSprite> 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<int> 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<BillboardSprite> 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<int> 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<BillboardSprite> 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
|
||||||
113
src/render/SoftwareRenderer.hpp
Normal file
113
src/render/SoftwareRenderer.hpp
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/Common.hpp"
|
||||||
|
#include "dungeon/Dungeon.hpp"
|
||||||
|
#include "dungeon/Player.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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<Color> 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<int>& mapGrid,
|
||||||
|
int wallTex, int floorTex, int ceilTex,
|
||||||
|
float animTime,
|
||||||
|
const std::vector<BillboardSprite>& sprites);
|
||||||
|
|
||||||
|
void presentFramebuffer();
|
||||||
|
|
||||||
|
// Framebuffer & Raylib Texture
|
||||||
|
std::vector<Color> m_framebuffer;
|
||||||
|
std::vector<float> 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
|
||||||
Loading…
Add table
Reference in a new issue