432 lines
17 KiB
C++
432 lines
17 KiB
C++
#include "unitrenderer.h"
|
|
#include "pathfinder.h"
|
|
#include <cmath>
|
|
#include <vector>
|
|
|
|
struct VertexPN {
|
|
float x, y, z;
|
|
float nx, ny, nz;
|
|
};
|
|
|
|
UnitRenderer::UnitRenderer()
|
|
: m_program(nullptr), m_vertexCount(0), m_initialized(false), m_roadBuffersInitialized(false)
|
|
{
|
|
}
|
|
|
|
UnitRenderer::~UnitRenderer()
|
|
{
|
|
if (m_program) delete m_program;
|
|
if (m_roadBuffersInitialized) {
|
|
m_roadVao.destroy();
|
|
m_roadVbo.destroy();
|
|
}
|
|
}
|
|
|
|
void UnitRenderer::initialize()
|
|
{
|
|
if (m_initialized) return;
|
|
initializeOpenGLFunctions();
|
|
|
|
m_program = new QOpenGLShaderProgram();
|
|
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/bart/project/tiletest/shaders/unit.vert");
|
|
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/bart/project/tiletest/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<VertexPN> 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) ---
|
|
// Tri 1
|
|
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<std::pair<int, int>, TileRoad> &roads,
|
|
const RoadConfigManager &roadConfigs,
|
|
Matrix2D<TerrainPoint> *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<VertexPN> casingVertices;
|
|
std::vector<VertexPN> surfaceVertices;
|
|
};
|
|
std::unordered_map<std::string, RoadBatchGeometry> 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<QVector2D> 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<std::shared_ptr<Unit>> &units,
|
|
const std::vector<City> &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
|
|
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);
|
|
}
|