aboutsummaryrefslogtreecommitdiffstats
path: root/src/engine/renderer.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/engine/renderer.cpp')
-rw-r--r--src/engine/renderer.cpp285
1 files changed, 285 insertions, 0 deletions
diff --git a/src/engine/renderer.cpp b/src/engine/renderer.cpp
new file mode 100644
index 0000000..0be0e53
--- /dev/null
+++ b/src/engine/renderer.cpp
@@ -0,0 +1,285 @@
+#include "engine/renderer.hpp"
+#include "engine/camera.hpp"
+#include "stb_image.h"
+#include "imgui.h"
+#include "imgui_impl_sdl3.h"
+#include "imgui_impl_sdlrenderer3.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <cmath>
+#include <vector>
+#include <algorithm>
+
+bool Renderer::init(SDL_Window* window, int width, int height) {
+ width_ = width;
+ height_ = height;
+
+ sdl_renderer_ = SDL_CreateRenderer(window, NULL);
+ if (!sdl_renderer_) {
+ fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError());
+ return false;
+ }
+
+ SDL_SetRenderDrawColor(sdl_renderer_, 0x1a, 0x1a, 0x2e, 0xff);
+
+ const char* name = SDL_GetRendererName(sdl_renderer_);
+ printf("Renderer: %s %dx%d\n", name ? name : "unknown", width, height);
+
+ // Dear ImGui (dev UI).
+ IMGUI_CHECKVERSION();
+ ImGui::CreateContext();
+ ImGui::StyleColorsDark();
+ if (ImGui_ImplSDL3_InitForSDLRenderer(window, sdl_renderer_) &&
+ ImGui_ImplSDLRenderer3_Init(sdl_renderer_)) {
+ imgui_ready_ = true;
+ }
+ return true;
+}
+
+void Renderer::shutdown() {
+ if (imgui_ready_) {
+ ImGui_ImplSDLRenderer3_Shutdown();
+ ImGui_ImplSDL3_Shutdown();
+ ImGui::DestroyContext();
+ imgui_ready_ = false;
+ }
+ if (sdl_renderer_) { SDL_DestroyRenderer(sdl_renderer_); sdl_renderer_ = nullptr; }
+}
+
+void Renderer::begin_frame(Camera& camera) {
+ current_cam_ = &camera;
+ SDL_SetRenderDrawColor(sdl_renderer_, 0x1a, 0x1a, 0x2e, 0xff);
+ SDL_RenderClear(sdl_renderer_);
+ if (imgui_ready_) {
+ ImGui_ImplSDLRenderer3_NewFrame();
+ ImGui_ImplSDL3_NewFrame();
+ ImGui::NewFrame();
+ }
+}
+
+void Renderer::end_frame() {
+ if (imgui_ready_) {
+ ImGui::Render();
+ ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), sdl_renderer_);
+ }
+ SDL_RenderPresent(sdl_renderer_);
+ current_cam_ = nullptr;
+}
+
+void Renderer::resize(int width, int height) {
+ width_ = width;
+ height_ = height;
+}
+
+void Renderer::draw_box(float cx, float cy,
+ float half_w, float half_h,
+ float angle,
+ float r, float g, float b, float a) {
+ if (!current_cam_) return;
+
+ // Convert world coords to screen pixels
+ const auto& cam = *current_cam_;
+ float zoom = cam.zoom();
+ float vp_w = static_cast<float>(cam.viewport_width());
+ float vp_h = static_cast<float>(cam.viewport_height());
+
+ // Screen position of world origin
+ float ox = vp_w * 0.5f - cam.position().x * zoom;
+ float oy = vp_h * 0.5f + cam.position().y * zoom;
+
+ // Box center in screen space
+ float sx = ox + cx * zoom;
+ float sy = oy - cy * zoom; // y flips
+
+ // Box half-size in screen pixels
+ float shw = half_w * zoom;
+ float shh = half_h * zoom;
+
+ SDL_SetRenderDrawColor(sdl_renderer_,
+ static_cast<uint8_t>(r * 255),
+ static_cast<uint8_t>(g * 255),
+ static_cast<uint8_t>(b * 255),
+ static_cast<uint8_t>(a * 255));
+
+ if (angle == 0.0f) {
+ // Fast path: axis-aligned rect
+ SDL_FRect rect = {
+ sx - shw, sy - shh,
+ shw * 2.0f, shh * 2.0f
+ };
+ SDL_RenderFillRect(sdl_renderer_, &rect);
+ } else {
+ // Rotated: use SDL_RenderGeometry with 4 vertices
+ float ca = std::cos(angle);
+ float sa = std::sin(angle);
+
+ SDL_Vertex verts[4];
+ float corners[4][2] = {
+ {-half_w, -half_h}, { half_w, -half_h},
+ { half_w, half_h}, {-half_w, half_h}
+ };
+ for (int i = 0; i < 4; i++) {
+ float rx = corners[i][0] * ca - corners[i][1] * sa;
+ float ry = corners[i][0] * sa + corners[i][1] * ca;
+ verts[i].position.x = ox + (cx + rx) * zoom;
+ verts[i].position.y = oy - (cy + ry) * zoom;
+ // SDL_Vertex.color is SDL_FColor (0..1 floats), NOT 0..255.
+ verts[i].color = {r, g, b, a};
+ }
+ int indices[6] = {0, 1, 2, 0, 2, 3};
+ SDL_RenderGeometry(sdl_renderer_, NULL, verts, 4, indices, 6);
+ }
+}
+
+SDL_Texture* Renderer::load_texture(const std::string& path) {
+ auto it = tex_cache_.find(path);
+ if (it != tex_cache_.end()) return it->second;
+
+ // "forts:REL" resolves to the user's own Forts install (bring-your-own game
+ // files, like a Doom source port + WAD). Nothing is copied into the repo.
+ std::string file = path;
+ if (path.rfind("forts:", 0) == 0) {
+ const char* env = std::getenv("FORTS_DATA");
+ std::string root = env ? env
+ : "/mnt/games/SteamLibrary/steamapps/common/Forts/data";
+ file = root + "/" + path.substr(6);
+ }
+
+ int w = 0, h = 0, n = 0;
+ unsigned char* px = stbi_load(file.c_str(), &w, &h, &n, 4);
+ SDL_Texture* tex = nullptr;
+ if (px) {
+ tex = SDL_CreateTexture(sdl_renderer_, SDL_PIXELFORMAT_RGBA32,
+ SDL_TEXTUREACCESS_STATIC, w, h);
+ if (tex) {
+ SDL_UpdateTexture(tex, nullptr, px, w * 4);
+ SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
+ SDL_SetTextureScaleMode(tex, SDL_SCALEMODE_LINEAR);
+ }
+ stbi_image_free(px);
+ printf("Texture: loaded %s (%dx%d)\n", path.c_str(), w, h);
+ } else {
+ fprintf(stderr, "Texture: FAILED to load %s\n", path.c_str());
+ }
+ tex_cache_[path] = tex; // cache even nullptr so we don't retry every frame
+ return tex;
+}
+
+void Renderer::draw_sprite(float cx, float cy, float half_w, float half_h,
+ float angle, SDL_Texture* tex,
+ float r, float g, float b, float a) {
+ if (!current_cam_ || !tex) return;
+ const auto& cam = *current_cam_;
+ float zoom = cam.zoom();
+ float ox = cam.viewport_width() * 0.5f - cam.position().x * zoom;
+ float oy = cam.viewport_height() * 0.5f + cam.position().y * zoom;
+ float ca = std::cos(angle), sa = std::sin(angle);
+
+ const float corners[4][2] = {
+ {-half_w, -half_h}, { half_w, -half_h}, { half_w, half_h}, {-half_w, half_h}
+ };
+ const float uv[4][2] = { {0,1}, {1,1}, {1,0}, {0,0} }; // flip V (screen y is down)
+ SDL_FColor col = {r, g, b, a};
+ SDL_Vertex v[4];
+ for (int i = 0; i < 4; i++) {
+ float rx = corners[i][0]*ca - corners[i][1]*sa;
+ float ry = corners[i][0]*sa + corners[i][1]*ca;
+ v[i].position = { ox + (cx + rx) * zoom, oy - (cy + ry) * zoom };
+ v[i].color = col;
+ v[i].tex_coord = { uv[i][0], uv[i][1] };
+ }
+ int idx[6] = {0, 1, 2, 0, 2, 3};
+ SDL_RenderGeometry(sdl_renderer_, tex, v, 4, idx, 6);
+}
+
+void Renderer::draw_arc(float cx, float cy, float radius_world,
+ float a0, float a1, float r, float g, float b, float a) {
+ if (!current_cam_) return;
+ const auto& cam = *current_cam_;
+ float zoom = cam.zoom();
+ float ox = cam.viewport_width() * 0.5f - cam.position().x * zoom;
+ float oy = cam.viewport_height() * 0.5f + cam.position().y * zoom;
+ float scx = ox + cx * zoom, scy = oy - cy * zoom; // centre (screen)
+
+ const float PI = 3.14159265358979323846f;
+ int n = std::max(1, (int)(std::fabs(a1 - a0) / (2.0f * PI) * 64.0f) + 1);
+ SDL_FColor col = {r, g, b, a};
+ fan_scratch_.clear();
+ for (int i = 0; i < n; i++) {
+ float t0 = a0 + (a1 - a0) * (float)i / n;
+ float t1 = a0 + (a1 - a0) * (float)(i + 1) / n;
+ // world direction (y up) -> screen (y down)
+ SDL_Vertex c { {scx, scy}, col, {0, 0} };
+ SDL_Vertex p0{ {scx + std::cos(t0) * radius_world * zoom,
+ scy - std::sin(t0) * radius_world * zoom}, col, {0, 0} };
+ SDL_Vertex p1{ {scx + std::cos(t1) * radius_world * zoom,
+ scy - std::sin(t1) * radius_world * zoom}, col, {0, 0} };
+ fan_scratch_.push_back(c); fan_scratch_.push_back(p0); fan_scratch_.push_back(p1);
+ }
+ SDL_RenderGeometry(sdl_renderer_, nullptr, fan_scratch_.data(),
+ (int)fan_scratch_.size(), nullptr, 0);
+}
+
+void Renderer::draw_radial_timer(float cx, float cy, float radius_world,
+ float frac,
+ float r, float g, float b, float a) {
+ if (!current_cam_) return;
+ if (frac < 0.0f) frac = 0.0f;
+ if (frac > 1.0f) frac = 1.0f;
+
+ const auto& cam = *current_cam_;
+ float zoom = cam.zoom();
+ float vp_w = static_cast<float>(cam.viewport_width());
+ float vp_h = static_cast<float>(cam.viewport_height());
+ float ox = vp_w * 0.5f - cam.position().x * zoom;
+ float oy = vp_h * 0.5f + cam.position().y * zoom;
+ float sx = ox + cx * zoom;
+ float sy = oy - cy * zoom;
+ float rad = radius_world * zoom;
+
+ const int SEG = 32;
+ const float PI = 3.14159265358979323846f;
+
+ auto fan = [&](float start, float sweep, SDL_FColor col) {
+ if (sweep <= 0.0f) return;
+ int n = std::max(1, (int)(SEG * (sweep / (2.0f * PI)) + 0.5f));
+ fan_scratch_.clear(); // reused buffer, no per-call alloc
+ for (int i = 0; i < n; i++) {
+ float a0 = start + sweep * (float)i / n;
+ float a1 = start + sweep * (float)(i + 1) / n;
+ fan_scratch_.push_back({ {sx, sy}, col, {0, 0} });
+ fan_scratch_.push_back({ {sx + std::cos(a0) * rad, sy + std::sin(a0) * rad}, col, {0, 0} });
+ fan_scratch_.push_back({ {sx + std::cos(a1) * rad, sy + std::sin(a1) * rad}, col, {0, 0} });
+ }
+ SDL_RenderGeometry(sdl_renderer_, NULL, fan_scratch_.data(),
+ (int)fan_scratch_.size(), NULL, 0);
+ };
+
+ // Dim background ring (full circle), then the remaining wedge on top.
+ // Start at top (-90deg) and sweep clockwise (screen y is down).
+ fan(-PI * 0.5f, 2.0f * PI, SDL_FColor{0.0f, 0.0f, 0.0f, 0.45f});
+ fan(-PI * 0.5f, 2.0f * PI * frac, SDL_FColor{r, g, b, a});
+}
+
+void Renderer::draw_hud_bar(float x, float y, float fill,
+ float r, float g, float b) {
+ const float BAR_W = 100.0f;
+ const float BAR_H = 14.0f;
+ const float PAD = 2.0f;
+
+ SDL_FRect bg = {x, y, BAR_W, BAR_H};
+ SDL_SetRenderDrawColor(sdl_renderer_, 0x20, 0x20, 0x20, 0xcc);
+ SDL_RenderFillRect(sdl_renderer_, &bg);
+
+ if (fill > 0.0f) {
+ if (fill > 1.0f) fill = 1.0f;
+ SDL_FRect fg = {x + PAD, y + PAD,
+ (BAR_W - PAD * 2) * fill, BAR_H - PAD * 2};
+ SDL_SetRenderDrawColor(sdl_renderer_,
+ static_cast<uint8_t>(r * 255),
+ static_cast<uint8_t>(g * 255),
+ static_cast<uint8_t>(b * 255), 0xcc);
+ SDL_RenderFillRect(sdl_renderer_, &fg);
+ }
+}