#include "unitrenderer.h" #include "pathfinder.h" #include #include struct VertexPN { float x, y, z; float nx, ny, nz; }; UnitRenderer::UnitRenderer() : m_program(nullptr), m_vertexCount(0), m_initialized(false), m_roadVao(), m_roadVbo(), m_roadBuffersInitialized(false) { } UnitRenderer::~UnitRenderer() { if (m_program) delete m_program; m_vao.destroy(); } void UnitRenderer::initialize() { if (m_initialized) return; initializeOpenGLFunctions(); m_program = new QOpenGLShaderProgram(); m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/bart/git/Ascent-of-Man/shaders/unit.vert"); m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/bart/git/Ascent-of-Man/shaders/unit.frag"); m_program->link(); buildCylinderMesh(); m_roadVao.create(); m_roadVbo.create(); m_roadVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw); m_roadBuffersInitialized = true; m_initialized = true; } void UnitRenderer::buildCylinderMesh() { std::vector vertices; const int sectors = 24; const float pi = 3.14159265359f; for (int i = 0; i < sectors; ++i) { float a1 = 2.0f * pi * i / sectors; float a2 = 2.0f * pi * (i + 1) / sectors; float c1 = std::cos(a1), s1 = std::sin(a1); float c2 = std::cos(a2), s2 = std::sin(a2); // --- Side Quad (2 Triangles) --- vertices.push_back({c1, s1, 0.0f, c1, s1, 0.0f}); vertices.push_back({c2, s2, 0.0f, c2, s2, 0.0f}); vertices.push_back({c2, s2, 1.0f, c2, s2, 0.0f}); // Tri 2 vertices.push_back({c1, s1, 0.0f, c1, s1, 0.0f}); vertices.push_back({c2, s2, 1.0f, c2, s2, 0.0f}); vertices.push_back({c1, s1, 1.0f, c1, s1, 0.0f}); // --- Top Cap --- vertices.push_back({0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f}); vertices.push_back({c1, s1, 1.0f, 0.0f, 0.0f, 1.0f}); vertices.push_back({c2, s2, 1.0f, 0.0f, 0.0f, 1.0f}); // --- Bottom Cap --- vertices.push_back({0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f}); vertices.push_back({c2, s2, 0.0f, 0.0f, 0.0f, -1.0f}); vertices.push_back({c1, s1, 0.0f, 0.0f, 0.0f, -1.0f}); } m_vertexCount = (int)vertices.size(); m_vao.create(); m_vao.bind(); m_vbo.create(); m_vbo.bind(); m_vbo.setUsagePattern(QOpenGLBuffer::StaticDraw); m_vbo.allocate(vertices.data(), (int)(vertices.size() * sizeof(VertexPN))); m_program->enableAttributeArray(0); m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(VertexPN)); m_program->enableAttributeArray(1); m_program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, sizeof(VertexPN)); m_vao.release(); m_vbo.release(); } void UnitRenderer::renderRoads( const std::map, TileRoad> &roads, const RoadConfigManager &roadConfigs, Matrix2D *mapPoints, int numPoints05, const QMatrix4x4 &projection, const QMatrix4x4 &vMatrix, const QVector3D &lightDir, int cityViewRadius, int cityViewTileX, int cityViewTileY) { if (!m_initialized || !m_program || roads.empty()) return; if (!m_roadBuffersInitialized) { m_roadVao.create(); m_roadVbo.create(); m_roadVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw); m_roadBuffersInitialized = true; } float meanderAmp = roadConfigs.meanderAmplitude(); int subdivisions = std::max(2, roadConfigs.meanderSubdivisions()); struct RoadBatchGeometry { std::vector casingVertices; std::vector surfaceVertices; }; std::unordered_map batches; for (const auto &pair : roads) { const TileRoad &road = pair.second; if (cityViewRadius >= 0) { if (Pathfinder::hexDistance(cityViewTileX, cityViewTileY, road.tileX, road.tileY) > cityViewRadius) { continue; } } const RoadTypeConfig *rCfg = roadConfigs.getRoadType(road.roadTypeId); float roadW = rCfg ? rCfg->worldWidth : 0.11f; float casingW = roadW + 0.035f; QVector2D center = Pathfinder::hexToWorld(road.tileX, road.tileY); RoadBatchGeometry &batch = batches[road.roadTypeId.toStdString()]; // If isolated tile with no connections, draw small circular depot / disk if (road.connectionMask == 0) { float gz = Unit::getTerrainHeight(center.x(), center.y(), mapPoints, numPoints05); const int capSectors = 12; const float pi = 3.14159265359f; float rSurf = roadW * 0.9f; float rCase = casingW * 0.9f; for (int s = 0; s < capSectors; ++s) { float a1 = 2.0f * pi * s / capSectors; float a2 = 2.0f * pi * (s + 1) / capSectors; batch.casingVertices.push_back({center.x(), center.y(), gz + 0.012f, 0, 0, 1}); batch.casingVertices.push_back({center.x() + std::cos(a1) * rCase, center.y() + std::sin(a1) * rCase, gz + 0.012f, 0, 0, 1}); batch.casingVertices.push_back({center.x() + std::cos(a2) * rCase, center.y() + std::sin(a2) * rCase, gz + 0.012f, 0, 0, 1}); } for (int s = 0; s < capSectors; ++s) { float a1 = 2.0f * pi * s / capSectors; float a2 = 2.0f * pi * (s + 1) / capSectors; batch.surfaceVertices.push_back({center.x(), center.y(), gz + 0.016f, 0, 0, 1}); batch.surfaceVertices.push_back({center.x() + std::cos(a1) * rSurf, center.y() + std::sin(a1) * rSurf, gz + 0.016f, 0, 0, 1}); batch.surfaceVertices.push_back({center.x() + std::cos(a2) * rSurf, center.y() + std::sin(a2) * rSurf, gz + 0.016f, 0, 0, 1}); } continue; } // Center junction cap (circular disk covering junction gaps) { float gz = Unit::getTerrainHeight(center.x(), center.y(), mapPoints, numPoints05); const int capSectors = 12; const float pi = 3.14159265359f; float rSurf = (roadW * 0.5f) * 1.15f; float rCase = (casingW * 0.5f) * 1.15f; for (int s = 0; s < capSectors; ++s) { float a1 = 2.0f * pi * s / capSectors; float a2 = 2.0f * pi * (s + 1) / capSectors; batch.casingVertices.push_back({center.x(), center.y(), gz + 0.012f, 0, 0, 1}); batch.casingVertices.push_back({center.x() + std::cos(a1) * rCase, center.y() + std::sin(a1) * rCase, gz + 0.012f, 0, 0, 1}); batch.casingVertices.push_back({center.x() + std::cos(a2) * rCase, center.y() + std::sin(a2) * rCase, gz + 0.012f, 0, 0, 1}); batch.surfaceVertices.push_back({center.x(), center.y(), gz + 0.016f, 0, 0, 1}); batch.surfaceVertices.push_back({center.x() + std::cos(a1) * rSurf, center.y() + std::sin(a1) * rSurf, gz + 0.016f, 0, 0, 1}); batch.surfaceVertices.push_back({center.x() + std::cos(a2) * rSurf, center.y() + std::sin(a2) * rSurf, gz + 0.016f, 0, 0, 1}); } } // For each connected edge, generate meandering ribbon quad strip for (int e = 0; e < 6; ++e) { if (!road.hasConnection(e)) continue; QVector2D endP = center + getHexEdgeMidpoint(e); QVector2D dir = endP - center; float len = dir.length(); QVector2D perp = (len > 0.0001f) ? QVector2D(-dir.y() / len, dir.x() / len) : QVector2D(0, 0); float seed = std::sin(float(road.tileX * 41 + road.tileY * 113 + e * 29)) * 43758.5453f; float meanderSign = (seed - std::floor(seed) > 0.5f) ? 1.0f : -1.0f; const float pi = 3.14159265359f; std::vector spinePoints; spinePoints.reserve(subdivisions + 1); for (int i = 0; i <= subdivisions; ++i) { float t = (float)i / (float)subdivisions; float s = meanderSign * meanderAmp * std::sin(t * pi); spinePoints.push_back(center + dir * t + perp * s); } float halfSurf = roadW * 0.5f; float halfCase = casingW * 0.5f; for (int i = 0; i < subdivisions; ++i) { QVector2D p0 = spinePoints[i]; QVector2D p1 = spinePoints[i + 1]; QVector2D segDir = p1 - p0; float segLen = segDir.length(); QVector2D segPerp = (segLen > 0.0001f) ? QVector2D(-segDir.y() / segLen, segDir.x() / segLen) : perp; float z0 = Unit::getTerrainHeight(p0.x(), p0.y(), mapPoints, numPoints05); float z1 = Unit::getTerrainHeight(p1.x(), p1.y(), mapPoints, numPoints05); float cz0 = z0 + 0.012f; float cz1 = z1 + 0.012f; QVector3D cL0(p0.x() - segPerp.x() * halfCase, p0.y() - segPerp.y() * halfCase, cz0); QVector3D cR0(p0.x() + segPerp.x() * halfCase, p0.y() + segPerp.y() * halfCase, cz0); QVector3D cL1(p1.x() - segPerp.x() * halfCase, p1.y() - segPerp.y() * halfCase, cz1); QVector3D cR1(p1.x() + segPerp.x() * halfCase, p1.y() + segPerp.y() * halfCase, cz1); batch.casingVertices.push_back({cL0.x(), cL0.y(), cL0.z(), 0, 0, 1}); batch.casingVertices.push_back({cR0.x(), cR0.y(), cR0.z(), 0, 0, 1}); batch.casingVertices.push_back({cR1.x(), cR1.y(), cR1.z(), 0, 0, 1}); batch.casingVertices.push_back({cL0.x(), cL0.y(), cL0.z(), 0, 0, 1}); batch.casingVertices.push_back({cR1.x(), cR1.y(), cR1.z(), 0, 0, 1}); batch.casingVertices.push_back({cL1.x(), cL1.y(), cL1.z(), 0, 0, 1}); float sz0 = z0 + 0.016f; float sz1 = z1 + 0.016f; QVector3D sL0(p0.x() - segPerp.x() * halfSurf, p0.y() - segPerp.y() * halfSurf, sz0); QVector3D sR0(p0.x() + segPerp.x() * halfSurf, p0.y() + segPerp.y() * halfSurf, sz0); QVector3D sL1(p1.x() - segPerp.x() * halfSurf, p1.y() - segPerp.y() * halfSurf, sz1); QVector3D sR1(p1.x() + segPerp.x() * halfSurf, p1.y() + segPerp.y() * halfSurf, sz1); batch.surfaceVertices.push_back({sL0.x(), sL0.y(), sL0.z(), 0, 0, 1}); batch.surfaceVertices.push_back({sR0.x(), sR0.y(), sR0.z(), 0, 0, 1}); batch.surfaceVertices.push_back({sR1.x(), sR1.y(), sR1.z(), 0, 0, 1}); batch.surfaceVertices.push_back({sL0.x(), sL0.y(), sL0.z(), 0, 0, 1}); batch.surfaceVertices.push_back({sR1.x(), sR1.y(), sR1.z(), 0, 0, 1}); batch.surfaceVertices.push_back({sL1.x(), sL1.y(), sL1.z(), 0, 0, 1}); } } } if (batches.empty()) return; glEnable(GL_DEPTH_TEST); glDepthMask(GL_TRUE); glEnable(GL_POLYGON_OFFSET_FILL); glPolygonOffset(-2.0f, -2.0f); m_program->bind(); m_program->setUniformValue("u_lightDir", lightDir); QMatrix4x4 model; m_program->setUniformValue("modelMatrix", model); QMatrix4x4 mvp = projection * vMatrix; m_program->setUniformValue("MVP", mvp); m_roadVao.bind(); m_roadVbo.bind(); for (auto &bPair : batches) { const RoadTypeConfig *rCfg = roadConfigs.getRoadType(QString::fromStdString(bPair.first)); QColor caseCol = rCfg ? rCfg->getBorderColor() : QColor(107, 75, 35); QColor surfCol = rCfg ? rCfg->getColor() : QColor(196, 154, 98); RoadBatchGeometry &geom = bPair.second; // Draw Casing if (!geom.casingVertices.empty()) { m_roadVbo.allocate(geom.casingVertices.data(), (int)(geom.casingVertices.size() * sizeof(VertexPN))); m_program->enableAttributeArray(0); m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(VertexPN)); m_program->enableAttributeArray(1); m_program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, sizeof(VertexPN)); m_program->setUniformValue("u_color", QVector4D(caseCol.redF(), caseCol.greenF(), caseCol.blueF(), 1.0f)); glDrawArrays(GL_TRIANGLES, 0, (GLsizei)geom.casingVertices.size()); } // Draw Surface if (!geom.surfaceVertices.empty()) { m_roadVbo.allocate(geom.surfaceVertices.data(), (int)(geom.surfaceVertices.size() * sizeof(VertexPN))); m_program->enableAttributeArray(0); m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(VertexPN)); m_program->enableAttributeArray(1); m_program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, sizeof(VertexPN)); m_program->setUniformValue("u_color", QVector4D(surfCol.redF(), surfCol.greenF(), surfCol.blueF(), 1.0f)); glDrawArrays(GL_TRIANGLES, 0, (GLsizei)geom.surfaceVertices.size()); } } m_roadVbo.release(); m_roadVao.release(); m_program->release(); glDisable(GL_POLYGON_OFFSET_FILL); } void UnitRenderer::renderUnits( const std::vector> &units, const std::vector &cities, const PlayerManager &players, const QMatrix4x4 &projection, const QMatrix4x4 &vMatrix, const QVector3D &lightDir, int selectedUnitId, int cityViewRadius, int cityViewTileX, int cityViewTileY) { if (!m_initialized || !m_program) return; glEnable(GL_DEPTH_TEST); m_program->bind(); m_program->setUniformValue("u_lightDir", lightDir); m_vao.bind(); // 1. Render Cities (as stylized hexagonal / cylindrical city bastions) for (const auto &c : cities) { if (cityViewRadius >= 0) { if (c.tileX != cityViewTileX || c.tileY != cityViewTileY) { continue; } } const Player *p = players.getPlayer(c.playerId); QColor col = p ? p->primaryColor : QColor(100, 100, 100); QMatrix4x4 model; model.translate(c.worldPos.x(), c.worldPos.y(), c.worldPos.z()); model.scale(0.30f, 0.30f, 0.45f); QMatrix4x4 mvp = projection * vMatrix * model; m_program->setUniformValue("modelMatrix", model); m_program->setUniformValue("MVP", mvp); m_program->setUniformValue("u_color", QVector4D(col.redF(), col.greenF(), col.blueF(), 1.0f)); glDrawArrays(GL_TRIANGLES, 0, m_vertexCount); } // 2. Render Units (Cylinders in player primary color) const float unitRadius = 0.18f; const float unitHeight = 0.36f; for (const auto &unit : units) { if (unit->isDead()) continue; if (cityViewRadius >= 0) { if (Pathfinder::hexDistance(cityViewTileX, cityViewTileY, unit->tileX, unit->tileY) > cityViewRadius) { continue; } } const Player *player = players.getPlayer(unit->playerId); QColor col = player ? player->primaryColor : QColor(200, 200, 200); // Flash white/red if attacked or damaged if (unit->attackFlashTimer > 0.0f) { float f = unit->attackFlashTimer / 0.5f; col = QColor( (int)(col.red() * (1.0f - f) + 255 * f), (int)(col.green() * (1.0f - f) + 255 * f), (int)(col.blue() * (1.0f - f) + 255 * f) ); } QMatrix4x4 model; model.translate(unit->worldPos.x(), unit->worldPos.y(), unit->worldPos.z()); model.scale(unitRadius, unitRadius, unitHeight); QMatrix4x4 mvp = projection * vMatrix * model; m_program->setUniformValue("modelMatrix", model); m_program->setUniformValue("MVP", mvp); m_program->setUniformValue("u_color", QVector4D(col.redF(), col.greenF(), col.blueF(), 1.0f)); glDrawArrays(GL_TRIANGLES, 0, m_vertexCount); // Ground selection base disk if selected if (unit->id == selectedUnitId) { QMatrix4x4 selModel; selModel.translate(unit->worldPos.x(), unit->worldPos.y(), unit->worldPos.z()); selModel.scale(unitRadius * 1.6f, unitRadius * 1.6f, 0.04f); QMatrix4x4 selMvp = projection * vMatrix * selModel; m_program->setUniformValue("modelMatrix", selModel); m_program->setUniformValue("MVP", selMvp); m_program->setUniformValue("u_color", QVector4D(1.0f, 0.9f, 0.2f, 0.9f)); glDrawArrays(GL_TRIANGLES, 0, m_vertexCount); } } m_vao.release(); m_program->release(); } QPointF UnitRenderer::projectToScreen( const QVector3D &worldPoint, const QMatrix4x4 &projection, const QMatrix4x4 &vMatrix, int viewportW, int viewportH, bool &inFront) { QMatrix4x4 vp = projection * vMatrix; QVector4D clip = vp.map(QVector4D(worldPoint, 1.0f)); inFront = (clip.w() > 0.01f); if (!inFront) { return QPointF(-9999, -9999); } QVector3D ndc = clip.toVector3DAffine(); float sx = (ndc.x() + 1.0f) * 0.5f * viewportW; float sy = (1.0f - (ndc.y() + 1.0f) * 0.5f) * viewportH; return QPointF(sx, sy); }