Implement tile-targeted road route construction and OpenGL 3D layered road rendering
This commit is contained in:
@@ -24,23 +24,6 @@ enum class ActionMode {
|
|||||||
BuildRoad
|
BuildRoad
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TileRoad {
|
|
||||||
QString roadTypeId = "dirt"; // "dirt", "stone", "rail"
|
|
||||||
int tileX = 0;
|
|
||||||
int tileY = 0;
|
|
||||||
int playerId = 1;
|
|
||||||
unsigned char connectionMask = 0; // 6 bits: bit e (0..5) is 1 if connected to neighbor across edge e
|
|
||||||
QVector3D worldPos;
|
|
||||||
|
|
||||||
bool hasConnection(int edge) const {
|
|
||||||
return (connectionMask & (1 << (edge % 6))) != 0;
|
|
||||||
}
|
|
||||||
void setConnection(int edge, bool connected) {
|
|
||||||
if (connected) connectionMask |= (1 << (edge % 6));
|
|
||||||
else connectionMask &= ~(1 << (edge % 6));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TileImprovement {
|
struct TileImprovement {
|
||||||
QString improvementId; // "farm"
|
QString improvementId; // "farm"
|
||||||
int tileX;
|
int tileX;
|
||||||
|
|||||||
+158
-6
@@ -862,7 +862,107 @@ void QSceneRenderer::drawPathAndRange(QPainter &painter, Unit *unit, const QMatr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Attack range visualization if Attack mode is active
|
// 2. Road Route visualization if unit is constructing a multi-tile road route
|
||||||
|
if (unit->hasRoadRoute()) {
|
||||||
|
QPainterPath routeLine;
|
||||||
|
bool first = true;
|
||||||
|
for (size_t i = unit->roadRouteIndex; i < unit->roadRoute.size(); ++i) {
|
||||||
|
auto hex = unit->roadRoute[i];
|
||||||
|
QVector2D w = Pathfinder::hexToWorld(hex.first, hex.second);
|
||||||
|
float z = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05);
|
||||||
|
bool inFront = false;
|
||||||
|
QPointF sPos = UnitRenderer::projectToScreen(QVector3D(w.x(), w.y(), z + 0.06f), Projection, vMatrix, width(), height(), inFront);
|
||||||
|
if (inFront) {
|
||||||
|
if (first) {
|
||||||
|
routeLine.moveTo(sPos);
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
routeLine.lineTo(sPos);
|
||||||
|
}
|
||||||
|
painter.setPen(QPen(QColor(140, 95, 30, 200), 1.2));
|
||||||
|
painter.setBrush(QColor(255, 215, 60, 220));
|
||||||
|
painter.drawEllipse(sPos, 3.5, 3.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
painter.setPen(QPen(QColor(255, 195, 45, 230), 2.8, Qt::DashLine));
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawPath(routeLine);
|
||||||
|
|
||||||
|
auto destHex = unit->roadRoute.back();
|
||||||
|
QVector2D destW = Pathfinder::hexToWorld(destHex.first, destHex.second);
|
||||||
|
float destZ = Unit::getTerrainHeight(destW.x(), destW.y(), m_point, m_numPoints05);
|
||||||
|
bool inFront = false;
|
||||||
|
QPointF sDest = UnitRenderer::projectToScreen(QVector3D(destW.x(), destW.y(), destZ + 0.06f), Projection, vMatrix, width(), height(), inFront);
|
||||||
|
if (inFront) {
|
||||||
|
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.008f) + 1.0f) * 0.5f;
|
||||||
|
painter.setPen(QPen(QColor(255, 215, 50, (int)(180 + pulse * 75)), 2.2));
|
||||||
|
painter.setBrush(QColor(255, 190, 40, 45));
|
||||||
|
painter.drawEllipse(sDest, 14.0f, 14.0f);
|
||||||
|
painter.drawLine(sDest.x() - 18, sDest.y(), sDest.x() + 18, sDest.y());
|
||||||
|
painter.drawLine(sDest.x(), sDest.y() - 18, sDest.x(), sDest.y() + 18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Road Build targeting preview when in ActionMode::BuildRoad
|
||||||
|
if (GameManager::instance().currentActionMode() == ActionMode::BuildRoad && unit->config.canBuildRoad()) {
|
||||||
|
int hx = -1, hy = -1;
|
||||||
|
if (screenToHex(m_mouseCurrentPos, vMatrix, hx, hy) && (hx != unit->tileX || hy != unit->tileY)) {
|
||||||
|
if (TerrainMovementManager::instance().isHexPassable(hx, hy, MovementDomain::Land, m_point, m_numPoints05, &unit->config)) {
|
||||||
|
auto preview = Pathfinder::findPath(unit->tileX, unit->tileY, hx, hy, MovementDomain::Land, m_point, m_numPoints05, &unit->config);
|
||||||
|
if (preview.size() > 1) {
|
||||||
|
QPainterPath previewLine;
|
||||||
|
bool first = true;
|
||||||
|
for (const auto &p : preview) {
|
||||||
|
QVector2D w = Pathfinder::hexToWorld(p.first, p.second);
|
||||||
|
float z = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05);
|
||||||
|
bool inFront = false;
|
||||||
|
QPointF sPos = UnitRenderer::projectToScreen(QVector3D(w.x(), w.y(), z + 0.06f), Projection, vMatrix, width(), height(), inFront);
|
||||||
|
if (inFront) {
|
||||||
|
if (first) {
|
||||||
|
previewLine.moveTo(sPos);
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
previewLine.lineTo(sPos);
|
||||||
|
}
|
||||||
|
painter.setPen(QPen(QColor(160, 110, 40, 200), 1.0));
|
||||||
|
painter.setBrush(QColor(255, 230, 90, 230));
|
||||||
|
painter.drawEllipse(sPos, 3.5, 3.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
painter.setPen(QPen(QColor(255, 220, 60, 240), 3.0, Qt::DashLine));
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawPath(previewLine);
|
||||||
|
|
||||||
|
// Destination reticle
|
||||||
|
QVector2D destW = Pathfinder::hexToWorld(hx, hy);
|
||||||
|
float destZ = Unit::getTerrainHeight(destW.x(), destW.y(), m_point, m_numPoints05);
|
||||||
|
bool inFront = false;
|
||||||
|
QPointF sDest = UnitRenderer::projectToScreen(QVector3D(destW.x(), destW.y(), destZ + 0.06f), Projection, vMatrix, width(), height(), inFront);
|
||||||
|
if (inFront) {
|
||||||
|
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.01f) + 1.0f) * 0.5f;
|
||||||
|
painter.setPen(QPen(QColor(255, 225, 70, (int)(180 + pulse * 75)), 2.5));
|
||||||
|
painter.setBrush(QColor(255, 210, 50, 50));
|
||||||
|
painter.drawEllipse(sDest, 16.0f, 16.0f);
|
||||||
|
painter.drawLine(sDest.x() - 22, sDest.y(), sDest.x() + 22, sDest.y());
|
||||||
|
painter.drawLine(sDest.x(), sDest.y() - 22, sDest.x(), sDest.y() + 22);
|
||||||
|
|
||||||
|
QString targetTip = Localization::instance().get("ui.unit.btn_road_route", "Route (%1 hexes)").arg(preview.size() - 1);
|
||||||
|
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||||||
|
QFontMetrics fm = painter.fontMetrics();
|
||||||
|
int tw = fm.horizontalAdvance(targetTip) + 16;
|
||||||
|
QRect tipRect((int)sDest.x() - tw / 2, (int)sDest.y() - 34, tw, 20);
|
||||||
|
painter.setBrush(QColor(20, 24, 34, 230));
|
||||||
|
painter.setPen(QPen(QColor(255, 215, 60), 1.2));
|
||||||
|
painter.drawRoundedRect(tipRect, 4, 4);
|
||||||
|
painter.setPen(QColor(255, 235, 120));
|
||||||
|
painter.drawText(tipRect, Qt::AlignCenter, targetTip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Attack range visualization if Attack mode is active
|
||||||
if (GameManager::instance().currentActionMode() == ActionMode::Attack) {
|
if (GameManager::instance().currentActionMode() == ActionMode::Attack) {
|
||||||
int range = unit->config.attackRange;
|
int range = unit->config.attackRange;
|
||||||
for (const auto &u : GameManager::instance().getUnits()) {
|
for (const auto &u : GameManager::instance().getUnits()) {
|
||||||
@@ -2367,7 +2467,19 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
|||||||
} else if (ab.actionId == "boerderij_bouwen" || ab.actionId == "build_farm") {
|
} else if (ab.actionId == "boerderij_bouwen" || ab.actionId == "build_farm") {
|
||||||
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
|
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
|
||||||
} else if (ab.actionId == "build_road" || ab.actionId == "weg_bouwen") {
|
} else if (ab.actionId == "build_road" || ab.actionId == "weg_bouwen") {
|
||||||
GameManager::instance().commandBuildRoadSelected(m_point, m_numPoints05);
|
Unit *sel = GameManager::instance().getSelectedUnit();
|
||||||
|
if (sel && sel->hasRoadRoute()) {
|
||||||
|
sel->clearRoadRoute();
|
||||||
|
sel->cancelBuilding();
|
||||||
|
GameManager::instance().setActionMode(ActionMode::None);
|
||||||
|
GameManager::instance().addFloatingText(
|
||||||
|
Localization::instance().get("ui.unit.road_route_cancelled", "Road route cancelled"),
|
||||||
|
sel->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 120, 100));
|
||||||
|
} else if (GameManager::instance().currentActionMode() == ActionMode::BuildRoad) {
|
||||||
|
GameManager::instance().setActionMode(ActionMode::None);
|
||||||
|
} else {
|
||||||
|
GameManager::instance().setActionMode(ActionMode::BuildRoad);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
update();
|
update();
|
||||||
return;
|
return;
|
||||||
@@ -2422,6 +2534,11 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
|||||||
GameManager::instance().setActionMode(ActionMode::None);
|
GameManager::instance().setActionMode(ActionMode::None);
|
||||||
update();
|
update();
|
||||||
return;
|
return;
|
||||||
|
} else if (GameManager::instance().currentActionMode() == ActionMode::BuildRoad) {
|
||||||
|
GameManager::instance().commandBuildRoadToTileSelected(hitX, hitY, m_point, m_numPoints05);
|
||||||
|
GameManager::instance().setActionMode(ActionMode::None);
|
||||||
|
update();
|
||||||
|
return;
|
||||||
} else if (GameManager::instance().currentActionMode() == ActionMode::Attack) {
|
} else if (GameManager::instance().currentActionMode() == ActionMode::Attack) {
|
||||||
Unit *enemy = GameManager::instance().getUnitAt(hitX, hitY);
|
Unit *enemy = GameManager::instance().getUnitAt(hitX, hitY);
|
||||||
if (enemy && enemy->playerId != selected->playerId) {
|
if (enemy && enemy->playerId != selected->playerId) {
|
||||||
@@ -2450,6 +2567,11 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
|||||||
update();
|
update();
|
||||||
return;
|
return;
|
||||||
} else if (event->button() == Qt::RightButton) {
|
} else if (event->button() == Qt::RightButton) {
|
||||||
|
if (GameManager::instance().currentActionMode() == ActionMode::BuildRoad) {
|
||||||
|
GameManager::instance().setActionMode(ActionMode::None);
|
||||||
|
update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Right click: Attack enemy unit or move selected unit directly to hex!
|
// Right click: Attack enemy unit or move selected unit directly to hex!
|
||||||
if (selected) {
|
if (selected) {
|
||||||
Unit *enemy = GameManager::instance().getUnitAt(hitX, hitY);
|
Unit *enemy = GameManager::instance().getUnitAt(hitX, hitY);
|
||||||
@@ -2627,11 +2749,28 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e)
|
|||||||
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
|
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
|
||||||
}
|
}
|
||||||
else if (e->key() == Qt::Key_R && sel && sel->config.canBuildRoad()) {
|
else if (e->key() == Qt::Key_R && sel && sel->config.canBuildRoad()) {
|
||||||
GameManager::instance().commandBuildRoadSelected(m_point, m_numPoints05);
|
if (sel->hasRoadRoute()) {
|
||||||
|
sel->clearRoadRoute();
|
||||||
|
sel->cancelBuilding();
|
||||||
|
GameManager::instance().setActionMode(ActionMode::None);
|
||||||
|
GameManager::instance().addFloatingText(
|
||||||
|
Localization::instance().get("ui.unit.road_route_cancelled", "Road route cancelled"),
|
||||||
|
sel->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 120, 100));
|
||||||
|
} else {
|
||||||
|
GameManager::instance().setActionMode(
|
||||||
|
GameManager::instance().currentActionMode() == ActionMode::BuildRoad ? ActionMode::None : ActionMode::BuildRoad
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (e->key() == Qt::Key_Escape) {
|
else if (e->key() == Qt::Key_Escape) {
|
||||||
if (GameManager::instance().currentActionMode() != ActionMode::None) {
|
if (GameManager::instance().currentActionMode() != ActionMode::None) {
|
||||||
GameManager::instance().setActionMode(ActionMode::None);
|
GameManager::instance().setActionMode(ActionMode::None);
|
||||||
|
} else if (sel && sel->hasRoadRoute()) {
|
||||||
|
sel->clearRoadRoute();
|
||||||
|
sel->cancelBuilding();
|
||||||
|
GameManager::instance().addFloatingText(
|
||||||
|
Localization::instance().get("ui.unit.road_route_cancelled", "Road route cancelled"),
|
||||||
|
sel->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 120, 100));
|
||||||
} else if (sel && sel->hasQueuedAction()) {
|
} else if (sel && sel->hasQueuedAction()) {
|
||||||
sel->clearActionQueue();
|
sel->clearActionQueue();
|
||||||
GameManager::instance().addFloatingText(
|
GameManager::instance().addFloatingText(
|
||||||
@@ -2787,11 +2926,25 @@ void QSceneRenderer::paintGL()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Render 3D Units & Cities (Cylinders & Bastions)
|
// 2. Render 3D Roads (above terrain elevation, writes depth so units and cities render on top)
|
||||||
float fSunAngle = 45.0f;
|
float fSunAngle = 45.0f;
|
||||||
QVector3D vSunPos(-sin(fSunAngle * PI / 180.0f), -cos(fSunAngle * PI / 180.0f), 0.6f);
|
QVector3D vSunPos(-sin(fSunAngle * PI / 180.0f), -cos(fSunAngle * PI / 180.0f), 0.6f);
|
||||||
vSunPos.normalize();
|
vSunPos.normalize();
|
||||||
|
|
||||||
|
m_unitRenderer.renderRoads(
|
||||||
|
GameManager::instance().getTileRoads(),
|
||||||
|
GameManager::instance().roadConfigs(),
|
||||||
|
m_point,
|
||||||
|
m_numPoints05,
|
||||||
|
Projection,
|
||||||
|
vMatrix,
|
||||||
|
vSunPos,
|
||||||
|
cityViewRadius,
|
||||||
|
cityView ? cityView->tileX : -1,
|
||||||
|
cityView ? cityView->tileY : -1
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Render 3D Units & Cities (Cylinders & Bastions)
|
||||||
Unit *selected = GameManager::instance().getSelectedUnit();
|
Unit *selected = GameManager::instance().getSelectedUnit();
|
||||||
m_unitRenderer.renderUnits(
|
m_unitRenderer.renderUnits(
|
||||||
GameManager::instance().getUnits(),
|
GameManager::instance().getUnits(),
|
||||||
@@ -2806,10 +2959,9 @@ void QSceneRenderer::paintGL()
|
|||||||
cityView ? cityView->tileY : -1
|
cityView ? cityView->tileY : -1
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Render 2D UI & HUD Overlays via QPainter
|
// 4. Render 2D UI & HUD Overlays via QPainter
|
||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
drawCityBorders(painter, vMatrix);
|
drawCityBorders(painter, vMatrix);
|
||||||
drawRoadsOverlay(painter, vMatrix);
|
|
||||||
drawImprovementsOverlay(painter, vMatrix);
|
drawImprovementsOverlay(painter, vMatrix);
|
||||||
if (!cityView) {
|
if (!cityView) {
|
||||||
drawPathAndRange(painter, selected, vMatrix);
|
drawPathAndRange(painter, selected, vMatrix);
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ protected:
|
|||||||
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
|
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||||
// City & Territory Overlays
|
// City & Territory Overlays
|
||||||
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
|
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||||
void drawRoadsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
|
||||||
void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||||
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||||
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
|
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
|
||||||
|
|||||||
@@ -4,6 +4,24 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
#include <QVector3D>
|
||||||
|
|
||||||
|
struct TileRoad {
|
||||||
|
QString roadTypeId = "dirt"; // "dirt", "stone", "rail"
|
||||||
|
int tileX = 0;
|
||||||
|
int tileY = 0;
|
||||||
|
int playerId = 1;
|
||||||
|
unsigned char connectionMask = 0; // 6 bits: bit e (0..5) is 1 if connected to neighbor across edge e
|
||||||
|
QVector3D worldPos;
|
||||||
|
|
||||||
|
bool hasConnection(int edge) const {
|
||||||
|
return (connectionMask & (1 << (edge % 6))) != 0;
|
||||||
|
}
|
||||||
|
void setConnection(int edge, bool connected) {
|
||||||
|
if (connected) connectionMask |= (1 << (edge % 6));
|
||||||
|
else connectionMask &= ~(1 << (edge % 6));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
struct RoadTypeConfig {
|
struct RoadTypeConfig {
|
||||||
QString id = "dirt";
|
QString id = "dirt";
|
||||||
|
|||||||
+225
-1
@@ -9,13 +9,17 @@ struct VertexPN {
|
|||||||
};
|
};
|
||||||
|
|
||||||
UnitRenderer::UnitRenderer()
|
UnitRenderer::UnitRenderer()
|
||||||
: m_program(nullptr), m_vertexCount(0), m_initialized(false)
|
: m_program(nullptr), m_vertexCount(0), m_initialized(false), m_roadBuffersInitialized(false)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
UnitRenderer::~UnitRenderer()
|
UnitRenderer::~UnitRenderer()
|
||||||
{
|
{
|
||||||
if (m_program) delete m_program;
|
if (m_program) delete m_program;
|
||||||
|
if (m_roadBuffersInitialized) {
|
||||||
|
m_roadVao.destroy();
|
||||||
|
m_roadVbo.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UnitRenderer::initialize()
|
void UnitRenderer::initialize()
|
||||||
@@ -29,6 +33,12 @@ void UnitRenderer::initialize()
|
|||||||
m_program->link();
|
m_program->link();
|
||||||
|
|
||||||
buildCylinderMesh();
|
buildCylinderMesh();
|
||||||
|
|
||||||
|
m_roadVao.create();
|
||||||
|
m_roadVbo.create();
|
||||||
|
m_roadVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw);
|
||||||
|
m_roadBuffersInitialized = true;
|
||||||
|
|
||||||
m_initialized = true;
|
m_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +96,220 @@ void UnitRenderer::buildCylinderMesh()
|
|||||||
m_vbo.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(
|
void UnitRenderer::renderUnits(
|
||||||
const std::vector<std::shared_ptr<Unit>> &units,
|
const std::vector<std::shared_ptr<Unit>> &units,
|
||||||
const std::vector<City> &cities,
|
const std::vector<City> &cities,
|
||||||
|
|||||||
@@ -10,9 +10,13 @@
|
|||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <map>
|
||||||
#include "unit.h"
|
#include "unit.h"
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "city.h"
|
#include "city.h"
|
||||||
|
#include "roadconfig.h"
|
||||||
|
#include "matrix2d.h"
|
||||||
|
#include "terrain.h"
|
||||||
|
|
||||||
class UnitRenderer : protected QOpenGLFunctions_4_0_Core {
|
class UnitRenderer : protected QOpenGLFunctions_4_0_Core {
|
||||||
public:
|
public:
|
||||||
@@ -20,6 +24,18 @@ public:
|
|||||||
~UnitRenderer();
|
~UnitRenderer();
|
||||||
|
|
||||||
void initialize();
|
void initialize();
|
||||||
|
void 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 = -1,
|
||||||
|
int cityViewTileX = -1,
|
||||||
|
int cityViewTileY = -1
|
||||||
|
);
|
||||||
void renderUnits(
|
void renderUnits(
|
||||||
const std::vector<std::shared_ptr<Unit>> &units,
|
const std::vector<std::shared_ptr<Unit>> &units,
|
||||||
const std::vector<City> &cities,
|
const std::vector<City> &cities,
|
||||||
@@ -49,6 +65,10 @@ private:
|
|||||||
QOpenGLVertexArrayObject m_vao;
|
QOpenGLVertexArrayObject m_vao;
|
||||||
int m_vertexCount;
|
int m_vertexCount;
|
||||||
bool m_initialized;
|
bool m_initialized;
|
||||||
|
|
||||||
|
QOpenGLBuffer m_roadVbo;
|
||||||
|
QOpenGLVertexArrayObject m_roadVao;
|
||||||
|
bool m_roadBuffersInitialized;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // UNITRENDERER_H
|
#endif // UNITRENDERER_H
|
||||||
|
|||||||
Reference in New Issue
Block a user