Browse Source

refactor rendering of entities to specific files

djeada 2 months ago
parent
commit
412ae78979

+ 4 - 0
CMakeLists.txt

@@ -64,7 +64,9 @@ if(QT_VERSION_MAJOR EQUAL 6)
         RESOURCES
             assets/shaders/basic.vert
             assets/shaders/basic.frag
+            assets/maps/test_map.json
             assets/maps/test_map.txt
+            assets/visuals/unit_visuals.json
         DEPENDENCIES
             Qt6::QuickControls2
     )
@@ -81,7 +83,9 @@ else()
         FILES
             assets/shaders/basic.vert
             assets/shaders/basic.frag
+            assets/maps/test_map.json
             assets/maps/test_map.txt
+            assets/visuals/unit_visuals.json
     )
 endif()
 

+ 49 - 3
app/game_engine.cpp

@@ -8,9 +8,13 @@
 #include "engine/core/component.h"
 #include "render/gl/renderer.h"
 #include "render/gl/camera.h"
+#include "render/gl/resources.h"
 #include "game/systems/movement_system.h"
 #include "game/systems/combat_system.h"
 #include "game/systems/selection_system.h"
+#include "game/map/map_loader.h"
+#include "game/map/map_transformer.h"
+#include "game/visuals/visual_catalog.h"
 
 GameEngine::GameEngine() {
     m_world    = std::make_unique<Engine::Core::World>();
@@ -24,7 +28,7 @@ GameEngine::GameEngine() {
     m_selectionSystem = std::make_unique<Game::Systems::SelectionSystem>();
     m_world->addSystem(std::make_unique<Game::Systems::SelectionSystem>());
 
-    setupTestScene();
+    // Defer actual entity creation until initialize() when GL and renderer are ready
 }
 
 GameEngine::~GameEngine() = default;
@@ -53,13 +57,50 @@ void GameEngine::initialize() {
         qWarning() << "GameEngine::initialize called without a current, valid OpenGL context";
         return;
     }
+    // Create shared resources and inject to renderer before initialize
+    m_resources = std::make_shared<Render::GL::ResourceManager>();
+    m_renderer->setResources(m_resources);
     if (!m_renderer->initialize()) {
         qWarning() << "Failed to initialize renderer";
         return;
     }
     m_renderer->setCamera(m_camera.get());
+    // Try load visuals JSON
+    Game::Visuals::VisualCatalog visualCatalog;
+    QString visualsErr;
+    visualCatalog.loadFromJsonFile("assets/visuals/unit_visuals.json", &visualsErr);
+    // Try load map JSON
+    Game::Map::MapDefinition def;
+    QString mapPath = QString::fromUtf8("assets/maps/test_map.json");
+    QString err;
+    if (Game::Map::MapLoader::loadFromJsonFile(mapPath, def, &err)) {
+        m_loadedMapName = def.name;
+    // Configure camera
+    m_camera->setRTSView(def.camera.center, def.camera.distance, def.camera.tiltDeg);
+    // Cache perspective params and set initial perspective (aspect will be set in render)
+    m_camFov = def.camera.fovY; m_camNear = def.camera.nearPlane; m_camFar = def.camera.farPlane;
+    m_camera->setPerspective(m_camFov, 16.0f/9.0f, m_camNear, m_camFar);
+
+        // Configure grid on renderer
+        Render::GL::Renderer::GridParams gp;
+        gp.cellSize = def.grid.tileSize;
+        gp.extent = std::max(def.grid.width, def.grid.height) * def.grid.tileSize * 0.5f; // half-size plane scale
+        m_renderer->setGridParams(gp);
+
+        // Populate world
+        auto rt = Game::Map::MapTransformer::applyToWorld(def, *m_world, &visualCatalog);
+        if (!rt.unitIds.empty()) {
+            m_playerUnitId = rt.unitIds.front();
+        } else {
+            setupFallbackTestUnit();
+        }
+    } else {
+        qWarning() << "Map load failed:" << err << "- using fallback unit";
     m_camera->setRTSView(QVector3D(0, 0, 0), 15.0f, 45.0f);
-    m_camera->setPerspective(45.0f, 16.0f/9.0f, 0.1f, 1000.0f);
+    m_camFov = 45.0f; m_camNear = 0.1f; m_camFar = 1000.0f;
+    m_camera->setPerspective(m_camFov, 16.0f/9.0f, m_camNear, m_camFar);
+        setupFallbackTestUnit();
+    }
     m_initialized = true;
 }
 
@@ -73,13 +114,16 @@ void GameEngine::render(int pixelWidth, int pixelHeight) {
     if (!m_renderer || !m_world || !m_initialized) return;
     if (pixelWidth > 0 && pixelHeight > 0) {
         m_renderer->setViewport(pixelWidth, pixelHeight);
+           float aspect = float(pixelWidth) / float(pixelHeight);
+           // Keep current camera fov/planes from map but update aspect
+           m_camera->setPerspective(m_camera->getFOV(), aspect, m_camera->getNear(), m_camera->getFar());
     }
     m_renderer->beginFrame();
     m_renderer->renderWorld(m_world.get());
     m_renderer->endFrame();
 }
 
-void GameEngine::setupTestScene() {
+void GameEngine::setupFallbackTestUnit() {
     auto entity = m_world->createEntity();
     m_playerUnitId = entity->getId();
 
@@ -90,6 +134,8 @@ void GameEngine::setupTestScene() {
 
     auto renderable = entity->addComponent<Engine::Core::RenderableComponent>("", "");
     renderable->visible = true;
+    renderable->mesh = Engine::Core::RenderableComponent::MeshKind::Capsule;
+    renderable->color[0] = 0.8f; renderable->color[1] = 0.9f; renderable->color[2] = 1.0f;
 
     auto unit = entity->addComponent<Engine::Core::UnitComponent>();
     unit->unitType = "archer";

+ 12 - 1
app/game_engine.h

@@ -17,6 +17,7 @@ struct RenderableComponent;
 namespace Render { namespace GL {
 class Renderer;
 class Camera;
+class ResourceManager;
 } }
 
 namespace Game { namespace Systems { class SelectionSystem; } }
@@ -40,14 +41,24 @@ public:
 
 private:
     void initialize();
-    void setupTestScene();
+    void setupFallbackTestUnit();
     bool screenToGround(const QPointF& screenPt, QVector3D& outWorld);
 
     std::unique_ptr<Engine::Core::World> m_world;
     std::unique_ptr<Render::GL::Renderer> m_renderer;
     std::unique_ptr<Render::GL::Camera>   m_camera;
+    std::shared_ptr<Render::GL::ResourceManager> m_resources;
     std::unique_ptr<Game::Systems::SelectionSystem> m_selectionSystem;
     QQuickWindow* m_window = nullptr;
     Engine::Core::EntityID m_playerUnitId = 0;
     bool m_initialized = false;
+    // Map state
+    QString m_loadedMapName;
+    // Visual config (temporary; later load from assets)
+    struct UnitVisual { QVector3D color{0.8f,0.9f,1.0f}; };
+    UnitVisual m_visualArcher;
+    // Cached perspective parameters from map or defaults
+    float m_camFov = 45.0f;
+    float m_camNear = 0.1f;
+    float m_camFar = 1000.0f;
 };

+ 19 - 0
assets/maps/test_map.json

@@ -0,0 +1,19 @@
+{
+  "name": "Test Map (JSON)",
+  "grid": {
+    "width": 100,
+    "height": 100,
+    "tileSize": 1.0
+  },
+  "camera": {
+    "center": [0.0, 0.0, 0.0],
+    "distance": 15.0,
+    "tiltDeg": 45.0,
+    "fovY": 45.0,
+    "near": 0.1,
+    "far": 1000.0
+  },
+  "spawns": [
+    { "type": "archer", "x": 0.0, "z": 0.0, "playerId": 1 }
+  ]
+}

+ 9 - 0
assets/visuals/unit_visuals.json

@@ -0,0 +1,9 @@
+{
+  "units": {
+    "archer": {
+      "mesh": "Capsule",
+      "color": [0.8, 0.9, 1.0],
+      "texture": ""
+    }
+  }
+}

+ 7 - 1
engine/core/component.h

@@ -21,12 +21,18 @@ public:
 // Renderable Component
 class RenderableComponent : public Component {
 public:
+    enum class MeshKind { None, Quad, Plane, Cube, Capsule, Ring };
+
     RenderableComponent(const std::string& meshPath, const std::string& texturePath)
-        : meshPath(meshPath), texturePath(texturePath), visible(true) {}
+        : meshPath(meshPath), texturePath(texturePath), visible(true), mesh(MeshKind::Cube) {
+        color[0] = color[1] = color[2] = 1.0f;
+    }
 
     std::string meshPath;
     std::string texturePath;
     bool visible;
+    MeshKind mesh;
+    float color[3]; // RGB 0..1
 };
 
 // Unit Component (for RTS units)

+ 4 - 1
game/CMakeLists.txt

@@ -4,7 +4,10 @@ add_library(game_systems STATIC
     systems/ai_system.cpp
     systems/pathfinding.cpp
     systems/selection_system.cpp
+    map/map_loader.cpp
+    map/map_transformer.cpp
+    visuals/visual_catalog.cpp
 )
 
 target_include_directories(game_systems PUBLIC .)
-target_link_libraries(game_systems PUBLIC Qt6::Core engine_core)
+target_link_libraries(game_systems PUBLIC Qt6::Core Qt6::Gui engine_core)

+ 38 - 0
game/map/map_definition.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <QString>
+#include <QVector3D>
+#include <vector>
+
+namespace Game::Map {
+
+struct GridDefinition {
+    int width = 50;      // number of cells in X
+    int height = 50;     // number of cells in Z
+    float tileSize = 1.0f;
+};
+
+struct CameraDefinition {
+    QVector3D center{0.0f, 0.0f, 0.0f};
+    float distance = 15.0f; // RTS orbit distance
+    float tiltDeg = 45.0f;  // RTS tilt angle
+    float fovY = 45.0f;     // degrees
+    float nearPlane = 0.1f;
+    float farPlane = 1000.0f;
+};
+
+struct UnitSpawn {
+    QString type; // e.g., "archer"
+    float x = 0.0f; // world X (or grid x * tileSize)
+    float z = 0.0f; // world Z
+    int playerId = 0;
+};
+
+struct MapDefinition {
+    QString name;
+    GridDefinition grid;
+    CameraDefinition camera;
+    std::vector<UnitSpawn> spawns;
+};
+
+} // namespace Game::Map

+ 87 - 0
game/map/map_loader.cpp

@@ -0,0 +1,87 @@
+#include "map_loader.h"
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+namespace Game::Map {
+
+static bool readGrid(const QJsonObject& obj, GridDefinition& grid) {
+    if (obj.contains("width")) grid.width = obj.value("width").toInt(grid.width);
+    if (obj.contains("height")) grid.height = obj.value("height").toInt(grid.height);
+    if (obj.contains("tileSize")) grid.tileSize = float(obj.value("tileSize").toDouble(grid.tileSize));
+    return grid.width > 0 && grid.height > 0 && grid.tileSize > 0.0f;
+}
+
+static bool readCamera(const QJsonObject& obj, CameraDefinition& cam) {
+    if (obj.contains("center")) {
+        auto arr = obj.value("center").toArray();
+        if (arr.size() == 3) {
+            cam.center = {float(arr[0].toDouble(0.0)), float(arr[1].toDouble(0.0)), float(arr[2].toDouble(0.0))};
+        }
+    }
+    if (obj.contains("distance")) cam.distance = float(obj.value("distance").toDouble(cam.distance));
+    if (obj.contains("tiltDeg")) cam.tiltDeg = float(obj.value("tiltDeg").toDouble(cam.tiltDeg));
+    if (obj.contains("fovY")) cam.fovY = float(obj.value("fovY").toDouble(cam.fovY));
+    if (obj.contains("near")) cam.nearPlane = float(obj.value("near").toDouble(cam.nearPlane));
+    if (obj.contains("far")) cam.farPlane = float(obj.value("far").toDouble(cam.farPlane));
+    return true;
+}
+
+static void readSpawns(const QJsonArray& arr, std::vector<UnitSpawn>& out) {
+    out.clear();
+    out.reserve(arr.size());
+    for (const auto& v : arr) {
+        auto o = v.toObject();
+        UnitSpawn s;
+        s.type = o.value("type").toString();
+        s.x = float(o.value("x").toDouble(0.0));
+        s.z = float(o.value("z").toDouble(0.0));
+        s.playerId = o.value("playerId").toInt(0);
+        out.push_back(s);
+    }
+}
+
+bool MapLoader::loadFromJsonFile(const QString& path, MapDefinition& outMap, QString* outError) {
+    QFile f(path);
+    if (!f.open(QIODevice::ReadOnly)) {
+        if (outError) *outError = QString("Failed to open map file: %1").arg(path);
+        return false;
+    }
+    auto data = f.readAll();
+    f.close();
+
+    QJsonParseError perr;
+    auto doc = QJsonDocument::fromJson(data, &perr);
+    if (perr.error != QJsonParseError::NoError) {
+        if (outError) *outError = QString("JSON parse error at %1: %2").arg(perr.offset).arg(perr.errorString());
+        return false;
+    }
+    if (!doc.isObject()) {
+        if (outError) *outError = "Map JSON root must be an object";
+        return false;
+    }
+    auto root = doc.object();
+
+    outMap.name = root.value("name").toString("Unnamed Map");
+
+    if (root.contains("grid") && root.value("grid").isObject()) {
+        if (!readGrid(root.value("grid").toObject(), outMap.grid)) {
+            if (outError) *outError = "Invalid grid definition";
+            return false;
+        }
+    }
+
+    if (root.contains("camera") && root.value("camera").isObject()) {
+        readCamera(root.value("camera").toObject(), outMap.camera);
+    }
+
+    if (root.contains("spawns") && root.value("spawns").isArray()) {
+        readSpawns(root.value("spawns").toArray(), outMap.spawns);
+    }
+
+    return true;
+}
+
+} // namespace Game::Map

+ 14 - 0
game/map/map_loader.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include "map_definition.h"
+#include <QString>
+
+namespace Game::Map {
+
+class MapLoader {
+public:
+    // Load MapDefinition from a JSON file. Returns true on success.
+    static bool loadFromJsonFile(const QString& path, MapDefinition& outMap, QString* outError = nullptr);
+};
+
+} // namespace Game::Map

+ 48 - 0
game/map/map_transformer.cpp

@@ -0,0 +1,48 @@
+#include "map_transformer.h"
+
+#include "../../engine/core/world.h"
+#include "../../engine/core/component.h"
+
+namespace Game::Map {
+
+MapRuntime MapTransformer::applyToWorld(const MapDefinition& def, Engine::Core::World& world, const Game::Visuals::VisualCatalog* visuals) {
+    MapRuntime rt;
+    rt.unitIds.reserve(def.spawns.size());
+
+    for (const auto& s : def.spawns) {
+        auto* e = world.createEntity();
+        if (!e) continue;
+
+        auto* t = e->addComponent<Engine::Core::TransformComponent>();
+        t->position = {s.x, 0.0f, s.z};
+        t->scale = {0.5f, 0.5f, 0.5f};
+
+        auto* r = e->addComponent<Engine::Core::RenderableComponent>("", "");
+        r->visible = true;
+        // Apply visuals from catalog if provided, else use simple defaults
+        if (visuals) {
+            Game::Visuals::VisualDef def;
+            if (visuals->lookup(s.type.toStdString(), def)) {
+                Game::Visuals::applyToRenderable(def, *r);
+            }
+        }
+        if (r->color[0] == 0.0f && r->color[1] == 0.0f && r->color[2] == 0.0f) {
+            // fallback color if not set
+            r->color[0] = r->color[1] = r->color[2] = 1.0f;
+        }
+
+        auto* u = e->addComponent<Engine::Core::UnitComponent>();
+        u->unitType = s.type.toStdString();
+        if (s.type == "archer") {
+            u->health = 80; u->maxHealth = 80; u->speed = 3.0f;
+        }
+
+        e->addComponent<Engine::Core::MovementComponent>();
+
+        rt.unitIds.push_back(e->getId());
+    }
+
+    return rt;
+}
+
+} // namespace Game::Map

+ 21 - 0
game/map/map_transformer.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#include "map_definition.h"
+#include "../visuals/visual_catalog.h"
+#include <memory>
+
+namespace Engine { namespace Core { class World; using EntityID = unsigned int; } }
+
+namespace Game::Map {
+
+struct MapRuntime {
+    std::vector<Engine::Core::EntityID> unitIds;
+};
+
+class MapTransformer {
+public:
+    // Populates the world from a MapDefinition. Returns runtime info like created entity IDs.
+    static MapRuntime applyToWorld(const MapDefinition& def, Engine::Core::World& world, const Game::Visuals::VisualCatalog* visuals = nullptr);
+};
+
+} // namespace Game::Map

+ 77 - 0
game/visuals/visual_catalog.cpp

@@ -0,0 +1,77 @@
+#include "visual_catalog.h"
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include "../../engine/core/component.h"
+
+namespace Game::Visuals {
+
+VisualDef::MeshKind meshKindFromString(const QString& s) {
+    const QString t = s.trimmed().toLower();
+    if (t == "quad") return VisualDef::MeshKind::Quad;
+    if (t == "plane") return VisualDef::MeshKind::Plane;
+    if (t == "cube") return VisualDef::MeshKind::Cube;
+    if (t == "capsule") return VisualDef::MeshKind::Capsule;
+    if (t == "ring") return VisualDef::MeshKind::Ring;
+    return VisualDef::MeshKind::None;
+}
+
+static Engine::Core::RenderableComponent::MeshKind toRenderableMesh(VisualDef::MeshKind k) {
+    using RM = Engine::Core::RenderableComponent::MeshKind;
+    switch (k) {
+        case VisualDef::MeshKind::Quad: return RM::Quad;
+        case VisualDef::MeshKind::Plane: return RM::Plane;
+        case VisualDef::MeshKind::Cube: return RM::Cube;
+        case VisualDef::MeshKind::Capsule: return RM::Capsule;
+        case VisualDef::MeshKind::Ring: return RM::Ring;
+        default: return RM::None;
+    }
+}
+
+bool VisualCatalog::loadFromJsonFile(const QString& path, QString* outError) {
+    QFile f(path);
+    if (!f.open(QIODevice::ReadOnly)) {
+        if (outError) *outError = QString("Failed to open visuals file: %1").arg(path);
+        return false;
+    }
+    const QByteArray data = f.readAll();
+    f.close();
+    QJsonParseError perr{};
+    const QJsonDocument doc = QJsonDocument::fromJson(data, &perr);
+    if (doc.isNull()) {
+        if (outError) *outError = QString("JSON parse error at %1: %2").arg(perr.offset).arg(perr.errorString());
+        return false;
+    }
+    QJsonObject root = doc.object();
+    QJsonObject units = root.value("units").toObject();
+    for (auto it = units.begin(); it != units.end(); ++it) {
+        VisualDef def;
+        QJsonObject o = it.value().toObject();
+        def.mesh = meshKindFromString(o.value("mesh").toString("cube"));
+        QJsonArray col = o.value("color").toArray();
+        if (col.size() == 3) def.color = QVector3D(float(col[0].toDouble(1.0)), float(col[1].toDouble(1.0)), float(col[2].toDouble(1.0)));
+        def.texture = o.value("texture").toString("");
+        m_units.emplace(it.key().toStdString(), def);
+    }
+    return true;
+}
+
+bool VisualCatalog::lookup(const std::string& unitType, VisualDef& out) const {
+    auto it = m_units.find(unitType);
+    if (it == m_units.end()) return false;
+    out = it->second;
+    return true;
+}
+
+void applyToRenderable(const VisualDef& def, Engine::Core::RenderableComponent& r) {
+    r.mesh = toRenderableMesh(def.mesh);
+    r.color[0] = def.color.x();
+    r.color[1] = def.color.y();
+    r.color[2] = def.color.z();
+    if (!def.texture.isEmpty()) {
+        r.texturePath = def.texture.toStdString();
+    }
+}
+
+} // namespace Game::Visuals

+ 34 - 0
game/visuals/visual_catalog.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#include <QString>
+#include <QVector3D>
+#include <unordered_map>
+#include <string>
+
+namespace Engine { namespace Core { class RenderableComponent; } }
+
+namespace Game::Visuals {
+
+struct VisualDef {
+    // Mirrors Engine::Core::RenderableComponent::MeshKind by name
+    enum class MeshKind { None, Quad, Plane, Cube, Capsule, Ring };
+    MeshKind mesh = MeshKind::Cube;
+    QVector3D color{1.0f, 1.0f, 1.0f};
+    QString texture; // optional
+};
+
+class VisualCatalog {
+public:
+    bool loadFromJsonFile(const QString& path, QString* outError = nullptr);
+    bool lookup(const std::string& unitType, VisualDef& out) const;
+private:
+    std::unordered_map<std::string, VisualDef> m_units;
+};
+
+// Utility to map string to MeshKind
+VisualDef::MeshKind meshKindFromString(const QString& s);
+
+// Apply a VisualDef to an Engine::Core::RenderableComponent
+void applyToRenderable(const VisualDef& def, Engine::Core::RenderableComponent& r);
+
+} // namespace Game::Visuals

+ 4 - 0
render/CMakeLists.txt

@@ -5,6 +5,10 @@ add_library(render_gl STATIC
     gl/texture.cpp
     gl/renderer.cpp
     gl/camera.cpp
+    gl/resources.cpp
+    entity/registry.cpp
+    entity/archer_renderer.cpp
+    geom/selection_ring.cpp
 )
 
 target_include_directories(render_gl PUBLIC .)

+ 106 - 0
render/entity/archer_renderer.cpp

@@ -0,0 +1,106 @@
+#include "archer_renderer.h"
+#include "registry.h"
+#include "../gl/renderer.h"
+#include "../gl/mesh.h"
+#include "../gl/texture.h"
+#include "../geom/selection_ring.h"
+#include "../../engine/core/component.h"
+#include <QVector3D>
+
+namespace Render::GL {
+
+// Capsule builder local to the archer renderer
+static Mesh* createCapsuleMesh() {
+    using namespace Render::GL;
+    const int radial = 24;
+    const int heightSegments = 1;
+    const float radius = 0.25f;
+    const float halfH = 0.5f;
+    std::vector<Vertex> verts;
+    std::vector<unsigned int> idx;
+    // Sides
+    for (int y = 0; y <= heightSegments; ++y) {
+        float v = float(y) / float(heightSegments);
+        float py = -halfH + v * (2.0f * halfH);
+        for (int i = 0; i <= radial; ++i) {
+            float u = float(i) / float(radial);
+            float ang = u * 6.2831853f;
+            float px = radius * std::cos(ang);
+            float pz = radius * std::sin(ang);
+            QVector3D n(px, 0.0f, pz);
+            n.normalize();
+            verts.push_back({{px, py, pz}, {n.x(), n.y(), n.z()}, {u, v}});
+        }
+    }
+    int row = radial + 1;
+    for (int y = 0; y < heightSegments; ++y) {
+        for (int i = 0; i < radial; ++i) {
+            int a = y * row + i;
+            int b = y * row + i + 1;
+            int c = (y + 1) * row + i + 1;
+            int d = (y + 1) * row + i;
+            idx.push_back(a); idx.push_back(b); idx.push_back(c);
+            idx.push_back(c); idx.push_back(d); idx.push_back(a);
+        }
+    }
+    // Top cap
+    int baseTop = verts.size();
+    verts.push_back({{0.0f, halfH, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.5f, 0.5f}});
+    for (int i = 0; i <= radial; ++i) {
+        float u = float(i) / float(radial);
+        float ang = u * 6.2831853f;
+        float px = radius * std::cos(ang);
+        float pz = radius * std::sin(ang);
+        verts.push_back({{px, halfH, pz}, {0.0f, 1.0f, 0.0f}, {0.5f + 0.5f*std::cos(ang), 0.5f + 0.5f*std::sin(ang)}});
+    }
+    for (int i = 1; i <= radial; ++i) {
+        idx.push_back(baseTop); idx.push_back(baseTop + i); idx.push_back(baseTop + i + 1);
+    }
+    // Bottom cap
+    int baseBot = verts.size();
+    verts.push_back({{0.0f, -halfH, 0.0f}, {0.0f, -1.0f, 0.0f}, {0.5f, 0.5f}});
+    for (int i = 0; i <= radial; ++i) {
+        float u = float(i) / float(radial);
+        float ang = u * 6.2831853f;
+        float px = radius * std::cos(ang);
+        float pz = radius * std::sin(ang);
+        verts.push_back({{px, -halfH, pz}, {0.0f, -1.0f, 0.0f}, {0.5f + 0.5f*std::cos(ang), 0.5f + 0.5f*std::sin(ang)}});
+    }
+    for (int i = 1; i <= radial; ++i) {
+        idx.push_back(baseBot); idx.push_back(baseBot + i + 1); idx.push_back(baseBot + i);
+    }
+    return new Mesh(verts, idx);
+}
+
+static Mesh* getArcherCapsule() {
+    static std::unique_ptr<Mesh> mesh(createCapsuleMesh());
+    return mesh.get();
+}
+
+void registerArcherRenderer(EntityRendererRegistry& registry) {
+    registry.registerRenderer("archer", [](const DrawParams& p){
+        if (!p.renderer) return;
+        QVector3D color(0.8f, 0.9f, 1.0f);
+        if (p.entity) {
+            if (auto* rc = p.entity->getComponent<Engine::Core::RenderableComponent>()) {
+                color = QVector3D(rc->color[0], rc->color[1], rc->color[2]);
+            }
+        }
+        // Draw capsule (archer body)
+        p.renderer->drawMeshColored(getArcherCapsule(), p.model, color, nullptr);
+        // Draw selection ring if selected
+        if (p.entity) {
+            if (auto* unit = p.entity->getComponent<Engine::Core::UnitComponent>()) {
+                if (unit->selected) {
+                    QMatrix4x4 ringM;
+                    QVector3D pos = p.model.column(3).toVector3D();
+                    ringM.translate(pos.x(), 0.01f, pos.z());
+                    ringM.scale(0.5f, 1.0f, 0.5f);
+                    p.renderer->drawMeshColored(Render::Geom::SelectionRing::get(), ringM, QVector3D(0.2f, 0.8f, 0.2f), nullptr);
+                }
+            }
+        }
+    });
+}
+
+} // namespace Render::GL

+ 10 - 0
render/entity/archer_renderer.h

@@ -0,0 +1,10 @@
+#pragma once
+
+#include "registry.h"
+
+namespace Render::GL {
+
+// Registers the archer renderer which draws a slender capsule and optional selection ring
+void registerArcherRenderer(EntityRendererRegistry& registry);
+
+} // namespace Render::GL

+ 23 - 0
render/entity/registry.cpp

@@ -0,0 +1,23 @@
+#include "registry.h"
+#include "archer_renderer.h"
+#include "../gl/renderer.h"
+#include "../../engine/core/entity.h"
+#include "../../engine/core/component.h"
+
+namespace Render::GL {
+
+void EntityRendererRegistry::registerRenderer(const std::string& type, RenderFunc func) {
+    m_map[type] = std::move(func);
+}
+
+RenderFunc EntityRendererRegistry::get(const std::string& type) const {
+    auto it = m_map.find(type);
+    if (it != m_map.end()) return it->second;
+    return {};
+}
+
+void registerBuiltInEntityRenderers(EntityRendererRegistry& registry) {
+    registerArcherRenderer(registry);
+}
+
+} // namespace Render::GL

+ 35 - 0
render/entity/registry.h

@@ -0,0 +1,35 @@
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <QMatrix4x4>
+#include <QVector3D>
+
+namespace Engine { namespace Core { class Entity; } }
+namespace Render { namespace GL { class Renderer; class ResourceManager; class Mesh; class Texture; } }
+
+namespace Render::GL {
+
+struct DrawParams {
+    Renderer* renderer = nullptr;
+    ResourceManager* resources = nullptr;
+    Engine::Core::Entity* entity = nullptr;
+    QMatrix4x4 model;
+};
+
+using RenderFunc = std::function<void(const DrawParams&)>;
+
+class EntityRendererRegistry {
+public:
+    void registerRenderer(const std::string& type, RenderFunc func);
+    RenderFunc get(const std::string& type) const;
+private:
+    std::unordered_map<std::string, RenderFunc> m_map;
+};
+
+// Registers built-in entity renderers (e.g., archer)
+void registerBuiltInEntityRenderers(EntityRendererRegistry& registry);
+
+} // namespace Render::GL

+ 39 - 0
render/geom/selection_ring.cpp

@@ -0,0 +1,39 @@
+#include "selection_ring.h"
+#include <QVector3D>
+
+namespace Render::Geom {
+
+std::unique_ptr<Render::GL::Mesh> SelectionRing::s_mesh;
+
+static Render::GL::Mesh* createRingMesh() {
+    using namespace Render::GL;
+    std::vector<Vertex> verts;
+    std::vector<unsigned int> idx;
+    const int seg = 48;
+    const float inner = 0.8f;
+    const float outer = 1.0f;
+    for (int i = 0; i < seg; ++i) {
+        float a0 = (i / float(seg)) * 6.2831853f;
+        float a1 = ((i + 1) / float(seg)) * 6.2831853f;
+        QVector3D n(0, 1, 0);
+        QVector3D v0i(inner * std::cos(a0), 0.0f, inner * std::sin(a0));
+        QVector3D v0o(outer * std::cos(a0), 0.0f, outer * std::sin(a0));
+        QVector3D v1o(outer * std::cos(a1), 0.0f, outer * std::sin(a1));
+        QVector3D v1i(inner * std::cos(a1), 0.0f, inner * std::sin(a1));
+        size_t base = verts.size();
+        verts.push_back({{v0i.x(), 0.0f, v0i.z()}, {n.x(), n.y(), n.z()}, {0, 0}});
+        verts.push_back({{v0o.x(), 0.0f, v0o.z()}, {n.x(), n.y(), n.z()}, {1, 0}});
+        verts.push_back({{v1o.x(), 0.0f, v1o.z()}, {n.x(), n.y(), n.z()}, {1, 1}});
+        verts.push_back({{v1i.x(), 0.0f, v1i.z()}, {n.x(), n.y(), n.z()}, {0, 1}});
+        idx.push_back(base + 0); idx.push_back(base + 1); idx.push_back(base + 2);
+        idx.push_back(base + 2); idx.push_back(base + 3); idx.push_back(base + 0);
+    }
+    return new Mesh(verts, idx);
+}
+
+Render::GL::Mesh* SelectionRing::get() {
+    if (!s_mesh) s_mesh.reset(createRingMesh());
+    return s_mesh.get();
+}
+
+} // namespace Render::Geom

+ 16 - 0
render/geom/selection_ring.h

@@ -0,0 +1,16 @@
+#pragma once
+
+#include "../gl/mesh.h"
+#include <memory>
+
+namespace Render::Geom {
+
+// Lazily creates and caches a selection ring mesh (annulus) shared across renderers
+class SelectionRing {
+public:
+    static Render::GL::Mesh* get();
+private:
+    static std::unique_ptr<Render::GL::Mesh> s_mesh;
+};
+
+} // namespace Render::Geom

+ 3 - 0
render/gl/camera.h

@@ -39,6 +39,9 @@ public:
     const QVector3D& getPosition() const { return m_position; }
     const QVector3D& getTarget() const { return m_target; }
     float getFOV() const { return m_fov; }
+    float getAspect() const { return m_aspect; }
+    float getNear() const { return m_nearPlane; }
+    float getFar() const { return m_farPlane; }
 
 private:
     QVector3D m_position{0.0f, 0.0f, 0.0f};

+ 74 - 133
render/gl/renderer.cpp

@@ -5,6 +5,7 @@
 #include <QOpenGLContext>
 #include <algorithm>
 #include <cmath>
+#include "../entity/registry.h"
 
 namespace Render::GL {
 
@@ -38,8 +39,15 @@ bool Renderer::initialize() {
     if (!loadShaders()) {
         return false;
     }
-    
-    createDefaultResources();
+    // Initialize shared/default GL resources when not injected
+    if (!m_resources) m_resources = std::make_shared<ResourceManager>();
+    if (!m_resources->initialize()) {
+        qWarning() << "Failed to initialize GL resources";
+        // Non-fatal: renderer can still function in a limited capacity
+    }
+    // Set up entity renderer registry (built-ins)
+    m_entityRegistry = std::make_unique<EntityRendererRegistry>();
+    registerBuiltInEntityRenderers(*m_entityRegistry);
     
     return true;
 }
@@ -48,10 +56,7 @@ void Renderer::shutdown() {
     m_basicShader.reset();
     m_lineShader.reset();
     m_gridShader.reset();
-    m_quadMesh.reset();
-    m_capsuleMesh.reset();
-    m_ringMesh.reset();
-    m_whiteTexture.reset();
+    m_resources.reset();
 }
 
 void Renderer::beginFrame() {
@@ -65,6 +70,27 @@ void Renderer::beginFrame() {
 void Renderer::endFrame() {
     flushBatch();
 }
+void Renderer::renderGridGround() {
+    Mesh* groundMesh = (m_resources ? m_resources->ground() : nullptr);
+    if (!groundMesh || !m_camera) return;
+    QMatrix4x4 groundModel;
+    groundModel.translate(0.0f, 0.0f, 0.0f);
+    groundModel.scale(m_gridParams.extent, 1.0f, m_gridParams.extent);
+    if (m_gridShader) {
+        m_gridShader->use();
+        m_gridShader->setUniform("u_model", groundModel);
+        m_gridShader->setUniform("u_view", m_camera->getViewMatrix());
+        m_gridShader->setUniform("u_projection", m_camera->getProjectionMatrix());
+        m_gridShader->setUniform("u_gridColor", m_gridParams.gridColor);
+        m_gridShader->setUniform("u_lineColor", m_gridParams.lineColor);
+        m_gridShader->setUniform("u_cellSize", m_gridParams.cellSize);
+        m_gridShader->setUniform("u_thickness", m_gridParams.thickness);
+        groundMesh->draw();
+        m_gridShader->release();
+    } else {
+        drawMeshColored(groundMesh, groundModel, m_gridParams.gridColor);
+    }
+}
 
 void Renderer::setCamera(Camera* camera) {
     m_camera = camera;
@@ -97,8 +123,10 @@ void Renderer::drawMesh(Mesh* mesh, const QMatrix4x4& modelMatrix, Texture* text
         m_basicShader->setUniform("u_texture", 0);
         m_basicShader->setUniform("u_useTexture", true);
     } else {
-        m_whiteTexture->bind(0);
-        m_basicShader->setUniform("u_texture", 0);
+        if (m_resources && m_resources->white()) {
+            m_resources->white()->bind(0);
+            m_basicShader->setUniform("u_texture", 0);
+        }
         m_basicShader->setUniform("u_useTexture", false);
     }
     
@@ -122,8 +150,10 @@ void Renderer::drawMeshColored(Mesh* mesh, const QMatrix4x4& modelMatrix, const
         m_basicShader->setUniform("u_texture", 0);
         m_basicShader->setUniform("u_useTexture", true);
     } else {
-        m_whiteTexture->bind(0);
-        m_basicShader->setUniform("u_texture", 0);
+        if (m_resources && m_resources->white()) {
+            m_resources->white()->bind(0);
+            m_basicShader->setUniform("u_texture", 0);
+        }
         m_basicShader->setUniform("u_useTexture", false);
     }
     m_basicShader->setUniform("u_color", color);
@@ -158,26 +188,8 @@ void Renderer::renderWorld(Engine::Core::World* world) {
     if (!world) {
         return;
     }
-    // Draw ground plane with grid
-    if (m_groundMesh) {
-        QMatrix4x4 groundModel;
-        groundModel.translate(0.0f, 0.0f, 0.0f);
-        groundModel.scale(50.0f, 1.0f, 50.0f);
-        if (m_gridShader) {
-            m_gridShader->use();
-            m_gridShader->setUniform("u_model", groundModel);
-            m_gridShader->setUniform("u_view", m_camera->getViewMatrix());
-            m_gridShader->setUniform("u_projection", m_camera->getProjectionMatrix());
-            m_gridShader->setUniform("u_gridColor", QVector3D(0.15f, 0.18f, 0.15f));
-            m_gridShader->setUniform("u_lineColor", QVector3D(0.22f, 0.25f, 0.22f));
-                m_gridShader->setUniform("u_cellSize", 1.0f);
-                m_gridShader->setUniform("u_thickness", 0.06f);
-            m_groundMesh->draw();
-            m_gridShader->release();
-        } else {
-            drawMeshColored(m_groundMesh.get(), groundModel, QVector3D(0.15f, 0.2f, 0.15f));
-        }
-    }
+    // Draw ground plane with grid using helper
+    renderGridGround();
     
     // Get all entities with both transform and renderable components
     auto renderableEntities = world->getEntitiesWith<Engine::Core::RenderableComponent>();
@@ -198,22 +210,39 @@ void Renderer::renderWorld(Engine::Core::World* world) {
         modelMatrix.rotate(transform->rotation.z, QVector3D(0, 0, 1));
         modelMatrix.scale(transform->scale.x, transform->scale.y, transform->scale.z);
         
-        // Use capsule mesh for units
+        // If entity has a unitType, try registry-based renderer first
+        bool drawnByRegistry = false;
+        if (auto* unit = entity->getComponent<Engine::Core::UnitComponent>()) {
+            if (!unit->unitType.empty() && m_entityRegistry) {
+                auto fn = m_entityRegistry->get(unit->unitType);
+                if (fn) {
+                    DrawParams params{this, m_resources.get(), entity, modelMatrix};
+                    fn(params);
+                    drawnByRegistry = true;
+                }
+            }
+        }
+        if (drawnByRegistry) continue;
+        // Else choose mesh based on RenderableComponent hint
         RenderCommand command;
         command.modelMatrix = modelMatrix;
-        command.mesh = m_capsuleMesh ? m_capsuleMesh.get() : m_quadMesh.get();
-        command.texture = m_whiteTexture.get();
-        command.color = QVector3D(0.8f, 0.9f, 1.0f);
-        submitRenderCommand(command);
-
-        // Draw selection ring if selected
-        auto unit = entity->getComponent<Engine::Core::UnitComponent>();
-        if (unit && unit->selected) {
-            QMatrix4x4 ringModel;
-            ringModel.translate(transform->position.x, 0.01f, transform->position.z);
-            ringModel.scale(0.5f, 1.0f, 0.5f);
-            renderSelectionRing(ringModel, QVector3D(0.2f, 0.8f, 0.2f));
+        Mesh* meshToDraw = nullptr;
+        switch (renderable->mesh) {
+            case Engine::Core::RenderableComponent::MeshKind::Quad:    meshToDraw = m_resources? m_resources->quad() : nullptr; break;
+            case Engine::Core::RenderableComponent::MeshKind::Plane:   meshToDraw = m_resources? m_resources->ground() : nullptr; break;
+            case Engine::Core::RenderableComponent::MeshKind::Cube:    meshToDraw = m_resources? m_resources->unit() : nullptr; break;
+            case Engine::Core::RenderableComponent::MeshKind::Capsule: meshToDraw = nullptr; break; // handled by specific renderer when available
+            case Engine::Core::RenderableComponent::MeshKind::Ring:    meshToDraw = nullptr; break; // rings are drawn explicitly when needed
+            case Engine::Core::RenderableComponent::MeshKind::None:    default: break;
         }
+        if (!meshToDraw && m_resources) meshToDraw = m_resources->unit();
+        if (!meshToDraw && m_resources) meshToDraw = m_resources->quad();
+        command.mesh = meshToDraw;
+        command.texture = (m_resources ? m_resources->white() : nullptr);
+        // Use per-entity color if set, else a default
+        command.color = QVector3D(renderable->color[0], renderable->color[1], renderable->color[2]);
+        submitRenderCommand(command);
+        // Selection ring is drawn by entity-specific renderer if desired
     }
 }
 
@@ -312,85 +341,7 @@ bool Renderer::loadShaders() {
     return true;
 }
 
-void Renderer::createDefaultResources() {
-    m_quadMesh = std::unique_ptr<Mesh>(createQuadMesh());
-    m_groundMesh = std::unique_ptr<Mesh>(createPlaneMesh(1.0f, 1.0f, 64));
-    // Create a simple identifiable "archer": crossed body panels + small head disk + front bow arc
-    {
-        std::vector<Vertex> verts;
-        std::vector<unsigned int> idx;
-        auto addQuad = [&](const QVector3D& a, const QVector3D& b, const QVector3D& c, const QVector3D& d, const QVector3D& n){
-            size_t base = verts.size();
-            verts.push_back({{a.x(), a.y(), a.z()}, {n.x(), n.y(), n.z()}, {0,0}});
-            verts.push_back({{b.x(), b.y(), b.z()}, {n.x(), n.y(), n.z()}, {1,0}});
-            verts.push_back({{c.x(), c.y(), c.z()}, {n.x(), n.y(), n.z()}, {1,1}});
-            verts.push_back({{d.x(), d.y(), d.z()}, {n.x(), n.y(), n.z()}, {0,1}});
-            idx.push_back(base+0); idx.push_back(base+1); idx.push_back(base+2);
-            idx.push_back(base+2); idx.push_back(base+3); idx.push_back(base+0);
-        };
-        float h = 1.6f; // height
-        float r = 0.25f; // width
-        // vertical crossed body
-        addQuad({-r,0,-0.0f},{ r,0, 0.0f},{ r,h, 0.0f},{-r,h, 0.0f},{0,0,1});
-        addQuad({0,-0.0f,-r},{0, 0.0f, r},{0, h, r},{0, h,-r},{1,0,0});
-        // head as small flat disk on top
-        {
-            int seg = 16; float headY = h + 0.15f; float headR = 0.18f; QVector3D n(0,1,0);
-            for (int i=0;i<seg;i++){
-                float a0 = (i    / float(seg)) * 6.2831853f;
-                float a1 = ((i+1)/ float(seg)) * 6.2831853f;
-                QVector3D c0(headR*std::cos(a0), headY, headR*std::sin(a0));
-                QVector3D c1(headR*std::cos(a1), headY, headR*std::sin(a1));
-                QVector3D center(0, headY, 0);
-                size_t base = verts.size();
-                verts.push_back({{center.x(),center.y(),center.z()},{n.x(),n.y(),n.z()},{0,0}});
-                verts.push_back({{c0.x(),c0.y(),c0.z()},{n.x(),n.y(),n.z()},{1,0}});
-                verts.push_back({{c1.x(),c1.y(),c1.z()},{n.x(),n.y(),n.z()},{1,1}});
-                idx.push_back(base+0); idx.push_back(base+1); idx.push_back(base+2);
-            }
-        }
-        // simple bow arc in front (thin quad strip)
-        {
-            float by0 = 0.4f, by1 = 1.2f; float bx = 0.35f; QVector3D n(0,0,1);
-            addQuad({bx,by0,-0.01f},{bx,by0,0.01f},{bx,by1,0.01f},{bx,by1,-0.01f}, n);
-        }
-        m_capsuleMesh = std::make_unique<Mesh>(verts, idx);
-    }
-    // Selection ring (flat torus approximated by triangle fan)
-    {
-        std::vector<Vertex> verts;
-        std::vector<unsigned int> idx;
-        int seg = 48;
-        float inner = 0.8f;
-        float outer = 1.0f;
-        for (int i=0;i<seg;i++){
-            float a0 = (i    / float(seg)) * 6.2831853f;
-            float a1 = ((i+1)/ float(seg)) * 6.2831853f;
-            QVector3D n(0,1,0);
-            QVector3D v0i(inner*std::cos(a0), 0.0f, inner*std::sin(a0));
-            QVector3D v0o(outer*std::cos(a0), 0.0f, outer*std::sin(a0));
-            QVector3D v1o(outer*std::cos(a1), 0.0f, outer*std::sin(a1));
-            QVector3D v1i(inner*std::cos(a1), 0.0f, inner*std::sin(a1));
-            size_t base = verts.size();
-            verts.push_back({{v0i.x(),0.0f,v0i.z()}, {n.x(),n.y(),n.z()}, {0,0}});
-            verts.push_back({{v0o.x(),0.0f,v0o.z()}, {n.x(),n.y(),n.z()}, {1,0}});
-            verts.push_back({{v1o.x(),0.0f,v1o.z()}, {n.x(),n.y(),n.z()}, {1,1}});
-            verts.push_back({{v1i.x(),0.0f,v1i.z()}, {n.x(),n.y(),n.z()}, {0,1}});
-            idx.push_back(base+0); idx.push_back(base+1); idx.push_back(base+2);
-            idx.push_back(base+2); idx.push_back(base+3); idx.push_back(base+0);
-        }
-        m_ringMesh = std::make_unique<Mesh>(verts, idx);
-    }
-
-    m_whiteTexture = std::make_unique<Texture>();
-    m_whiteTexture->createEmpty(1, 1, Texture::Format::RGBA);
-
-    // Fill with white color
-    unsigned char whitePixel[4] = {255, 255, 255, 255};
-    m_whiteTexture->bind();
-    initializeOpenGLFunctions();
-    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
-}
+// Removed: default resource creation is now handled by ResourceManager
 
 void Renderer::sortRenderQueue() {
     // Simple sorting by texture to reduce state changes
@@ -400,16 +351,6 @@ void Renderer::sortRenderQueue() {
         });
 }
 
-void Renderer::renderSelectionRing(const QMatrix4x4& model, const QVector3D& color) {
-    if (!m_ringMesh || !m_basicShader || !m_camera) return;
-    m_basicShader->use();
-    m_basicShader->setUniform("u_model", model);
-    m_basicShader->setUniform("u_view", m_camera->getViewMatrix());
-    m_basicShader->setUniform("u_projection", m_camera->getProjectionMatrix());
-    m_basicShader->setUniform("u_useTexture", false);
-    m_basicShader->setUniform("u_color", color);
-    m_ringMesh->draw();
-    m_basicShader->release();
-}
+// Selection ring helper removed; entity renderers can draw ring using drawMeshColored with shared ring mesh
 
 } // namespace Render::GL

+ 29 - 8
render/gl/renderer.h

@@ -4,9 +4,11 @@
 #include "camera.h"
 #include "mesh.h"
 #include "texture.h"
+#include "resources.h"
 #include <QOpenGLFunctions_3_3_Core>
 #include <memory>
 #include <vector>
+#include <optional>
 
 namespace Engine::Core {
 class World;
@@ -14,6 +16,7 @@ class Entity;
 }
 
 namespace Render::GL {
+class EntityRendererRegistry;
 
 struct RenderCommand {
     Mesh* mesh = nullptr;
@@ -36,6 +39,28 @@ public:
     
     void setCamera(Camera* camera);
     void setClearColor(float r, float g, float b, float a = 1.0f);
+    // Optional: inject an external ResourceManager owned by the app
+    void setResources(const std::shared_ptr<ResourceManager>& resources) { m_resources = resources; }
+
+    // Lightweight, app-facing helpers
+    void renderGridGround();
+
+    // Read-only access to default meshes/textures for app-side batching
+    Mesh* getMeshQuad()    const { return m_resources ? m_resources->quad()    : nullptr; }
+    Mesh* getMeshPlane()   const { return m_resources ? m_resources->ground()  : nullptr; }
+    Mesh* getMeshCube()    const { return m_resources ? m_resources->unit()    : nullptr; }
+    // Meshes now provided by entity-specific renderers or shared geom providers
+    Texture* getWhiteTexture() const { return m_resources ? m_resources->white() : nullptr; }
+    
+    struct GridParams {
+        float cellSize = 1.0f;
+        float thickness = 0.06f; // fraction of cell (0..0.5)
+        QVector3D gridColor{0.15f, 0.18f, 0.15f};
+        QVector3D lineColor{0.22f, 0.25f, 0.22f};
+        float extent = 50.0f; // half-size of plane scaling
+    };
+    void setGridParams(const GridParams& gp) { m_gridParams = gp; }
+    const GridParams& gridParams() const { return m_gridParams; }
     
     // Immediate mode rendering
     void drawMesh(Mesh* mesh, const QMatrix4x4& modelMatrix, Texture* texture = nullptr);
@@ -46,7 +71,7 @@ public:
     void submitRenderCommand(const RenderCommand& command);
     void flushBatch();
     
-    // Render ECS entities
+    // Legacy: still available but apps are encouraged to issue draw calls explicitly
     void renderWorld(Engine::Core::World* world);
     
 private:
@@ -59,19 +84,15 @@ private:
     std::vector<RenderCommand> m_renderQueue;
     
     // Default resources
-    std::unique_ptr<Mesh> m_quadMesh;
-    std::unique_ptr<Mesh> m_capsuleMesh; // simple unit mesh
-    std::unique_ptr<Mesh> m_ringMesh;    // selection ring
-    std::unique_ptr<Mesh> m_groundMesh;
-    std::unique_ptr<Texture> m_whiteTexture;
+    std::shared_ptr<ResourceManager> m_resources;
+    std::unique_ptr<EntityRendererRegistry> m_entityRegistry;
 
     int m_viewportWidth = 0;
     int m_viewportHeight = 0;
+    GridParams m_gridParams;
     
     bool loadShaders();
-    void createDefaultResources();
     void sortRenderQueue();
-    void renderSelectionRing(const QMatrix4x4& model, const QVector3D& color);
 };
 
 } // namespace Render::GL

+ 23 - 0
render/gl/resources.cpp

@@ -0,0 +1,23 @@
+#include "resources.h"
+#include <cmath>
+#include <QVector3D>
+
+namespace Render::GL {
+
+bool ResourceManager::initialize() {
+    initializeOpenGLFunctions();
+
+    m_quadMesh.reset(createQuadMesh());
+    m_groundMesh.reset(createPlaneMesh(1.0f, 1.0f, 64));
+    m_unitMesh.reset(createCubeMesh());
+
+    // White texture
+    m_whiteTexture = std::make_unique<Texture>();
+    m_whiteTexture->createEmpty(1, 1, Texture::Format::RGBA);
+    unsigned char whitePixel[4] = {255, 255, 255, 255};
+    m_whiteTexture->bind();
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
+    return true;
+}
+
+} // namespace Render::GL

+ 31 - 0
render/gl/resources.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <memory>
+#include <QOpenGLFunctions_3_3_Core>
+#include "mesh.h"
+#include "texture.h"
+
+namespace Render::GL {
+
+class ResourceManager : protected QOpenGLFunctions_3_3_Core {
+public:
+    ResourceManager() = default;
+    ~ResourceManager() = default;
+
+    // Must be called with a current, valid GL context
+    bool initialize();
+
+    Mesh* quad() const { return m_quadMesh.get(); }
+    Mesh* ground() const { return m_groundMesh.get(); }
+    Mesh* unit() const { return m_unitMesh.get(); }
+    Texture* white() const { return m_whiteTexture.get(); }
+
+private:
+    std::unique_ptr<Mesh> m_quadMesh;
+    std::unique_ptr<Mesh> m_groundMesh;
+    std::unique_ptr<Mesh> m_unitMesh;  // generic placeholder unit mesh
+    // Capsule and selection ring meshes have been moved to entity/geom modules
+    std::unique_ptr<Texture> m_whiteTexture;
+};
+
+} // namespace Render::GL