campaign_map_view.cpp 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526
  1. #include "campaign_map_view.h"
  2. #include "../utils/resource_utils.h"
  3. #include <QDateTime>
  4. #include <QDebug>
  5. #include <QFile>
  6. #include <QJsonArray>
  7. #include <QJsonDocument>
  8. #include <QJsonObject>
  9. #include <QMatrix4x4>
  10. #include <QOpenGLContext>
  11. #include <QOpenGLFramebufferObject>
  12. #include <QOpenGLFramebufferObjectFormat>
  13. #include <QOpenGLFunctions_3_3_Core>
  14. #include <QOpenGLShaderProgram>
  15. #include <QOpenGLTexture>
  16. #include <QVariantMap>
  17. #include <QVector2D>
  18. #include <QVector3D>
  19. #include <QVector4D>
  20. #include <QtGlobal>
  21. #include <QtMath>
  22. #include <cmath>
  23. #include <cstring>
  24. #include <unordered_map>
  25. #include <utility>
  26. #include <vector>
  27. namespace {
  28. auto build_mvp_matrix(float width, float height, float yaw_deg, float pitch_deg,
  29. float distance, float pan_u, float pan_v) -> QMatrix4x4 {
  30. const float view_w = qMax(1.0F, width);
  31. const float view_h = qMax(1.0F, height);
  32. const float aspect = view_w / view_h;
  33. QMatrix4x4 projection;
  34. projection.perspective(45.0F, aspect, 0.1F, 10.0F);
  35. const float clamped_pan_u = qBound(-0.5F, pan_u, 0.5F);
  36. const float clamped_pan_v = qBound(-0.5F, pan_v, 0.5F);
  37. const QVector3D center(0.5F + clamped_pan_u, 0.0F, 0.5F + clamped_pan_v);
  38. const float yaw_rad = qDegreesToRadians(yaw_deg);
  39. const float pitch_rad = qDegreesToRadians(pitch_deg);
  40. const float clamped_distance =
  41. qMax(CampaignMapView::kMinOrbitDistance, distance);
  42. const float cos_pitch = std::cos(pitch_rad);
  43. const float sin_pitch = std::sin(pitch_rad);
  44. const float cos_yaw = std::cos(yaw_rad);
  45. const float sin_yaw = std::sin(yaw_rad);
  46. QVector3D eye(center.x() + clamped_distance * sin_yaw * cos_pitch,
  47. center.y() + clamped_distance * sin_pitch,
  48. center.z() + clamped_distance * cos_yaw * cos_pitch);
  49. QMatrix4x4 view;
  50. view.lookAt(eye, center, QVector3D(0.0F, 0.0F, 1.0F));
  51. QMatrix4x4 model;
  52. return projection * view * model;
  53. }
  54. auto point_in_triangle(const QVector2D &p, const QVector2D &a,
  55. const QVector2D &b, const QVector2D &c) -> bool {
  56. const QVector2D v0 = c - a;
  57. const QVector2D v1 = b - a;
  58. const QVector2D v2 = p - a;
  59. const float dot00 = QVector2D::dotProduct(v0, v0);
  60. const float dot01 = QVector2D::dotProduct(v0, v1);
  61. const float dot02 = QVector2D::dotProduct(v0, v2);
  62. const float dot11 = QVector2D::dotProduct(v1, v1);
  63. const float dot12 = QVector2D::dotProduct(v1, v2);
  64. const float denom = dot00 * dot11 - dot01 * dot01;
  65. if (qFuzzyIsNull(denom)) {
  66. return false;
  67. }
  68. const float inv_denom = 1.0F / denom;
  69. const float u = (dot11 * dot02 - dot01 * dot12) * inv_denom;
  70. const float v = (dot00 * dot12 - dot01 * dot02) * inv_denom;
  71. return (u >= 0.0F) && (v >= 0.0F) && (u + v <= 1.0F);
  72. }
  73. struct LineSpan {
  74. int start = 0;
  75. int count = 0;
  76. };
  77. struct LineLayer {
  78. GLuint vao = 0;
  79. GLuint vbo = 0;
  80. std::vector<LineSpan> spans;
  81. QVector4D color = QVector4D(1.0F, 1.0F, 1.0F, 1.0F);
  82. float width = 1.0F;
  83. bool ready = false;
  84. };
  85. struct ProvinceSpan {
  86. int start = 0;
  87. int count = 0;
  88. QVector4D color = QVector4D(0.0F, 0.0F, 0.0F, 0.0F);
  89. QVector4D base_color = QVector4D(0.0F, 0.0F, 0.0F, 0.0F);
  90. QString id;
  91. };
  92. struct ProvinceLayer {
  93. GLuint vao = 0;
  94. GLuint vbo = 0;
  95. std::vector<ProvinceSpan> spans;
  96. bool ready = false;
  97. };
  98. struct QStringHash {
  99. std::size_t operator()(const QString &s) const noexcept { return qHash(s); }
  100. };
  101. struct CampaignMapTextureCache {
  102. static CampaignMapTextureCache &instance() {
  103. static CampaignMapTextureCache s_instance;
  104. return s_instance;
  105. }
  106. QOpenGLTexture *get_or_load(const QString &resource_path) {
  107. if (!m_allow_loading) {
  108. qWarning() << "CampaignMapTextureCache: Attempted to load texture after "
  109. "initialization:"
  110. << resource_path;
  111. return nullptr;
  112. }
  113. auto it = m_textures.find(resource_path);
  114. if (it != m_textures.end() && it->second != nullptr) {
  115. return it->second;
  116. }
  117. const QString path = Utils::Resources::resolveResourcePath(resource_path);
  118. QImage image(path);
  119. if (image.isNull()) {
  120. qWarning() << "CampaignMapTextureCache: Failed to load texture" << path;
  121. return nullptr;
  122. }
  123. QImage rgba = image.convertToFormat(QImage::Format_RGBA8888);
  124. auto *texture = new QOpenGLTexture(rgba);
  125. texture->setWrapMode(QOpenGLTexture::ClampToEdge);
  126. texture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
  127. m_textures[resource_path] = texture;
  128. return texture;
  129. }
  130. void set_loading_allowed(bool allowed) { m_allow_loading = allowed; }
  131. void clear() {
  132. QOpenGLContext *ctx = QOpenGLContext::currentContext();
  133. if (ctx != nullptr && ctx->isValid()) {
  134. for (auto &pair : m_textures) {
  135. delete pair.second;
  136. }
  137. }
  138. m_textures.clear();
  139. }
  140. private:
  141. CampaignMapTextureCache() = default;
  142. ~CampaignMapTextureCache() { clear(); }
  143. std::unordered_map<QString, QOpenGLTexture *, QStringHash> m_textures;
  144. bool m_allow_loading = true;
  145. };
  146. class CampaignMapRenderer : public QQuickFramebufferObject::Renderer,
  147. protected QOpenGLFunctions_3_3_Core {
  148. public:
  149. CampaignMapRenderer() = default;
  150. ~CampaignMapRenderer() override { cleanup(); }
  151. void render() override {
  152. if (!ensure_initialized()) {
  153. return;
  154. }
  155. glViewport(0, 0, m_size.width(), m_size.height());
  156. glEnable(GL_DEPTH_TEST);
  157. glDisable(GL_CULL_FACE);
  158. glEnable(GL_BLEND);
  159. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  160. glClearColor(0.157F, 0.267F, 0.361F, 1.0F);
  161. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  162. QMatrix4x4 mvp;
  163. compute_mvp(mvp);
  164. draw_textured_layer(m_waterTexture, m_quadVao, 6, mvp, 1.0F, -0.01F);
  165. if (m_landVertexCount > 0) {
  166. draw_textured_layer(m_baseTexture, m_landVao, m_landVertexCount, mvp,
  167. 1.0F, 0.0F);
  168. } else {
  169. draw_textured_layer(m_baseTexture, m_quadVao, 6, mvp, 1.0F, 0.0F);
  170. }
  171. glDisable(GL_DEPTH_TEST);
  172. draw_province_layer(m_provinceLayer, mvp, 0.002F);
  173. draw_line_layer(m_provinceBorderLayer, mvp, 0.0045F);
  174. draw_line_layer(m_coastLayer, mvp, 0.004F);
  175. draw_line_layer(m_riverLayer, mvp, 0.003F);
  176. draw_progressive_path_layers(m_pathLayer, mvp, 0.006F);
  177. if (!m_hover_province_id.isEmpty()) {
  178. qint64 now = QDateTime::currentMSecsSinceEpoch();
  179. if (now - m_last_update_time >= 16) {
  180. m_last_update_time = now;
  181. update();
  182. }
  183. }
  184. }
  185. auto createFramebufferObject(const QSize &size)
  186. -> QOpenGLFramebufferObject * override {
  187. m_size = size;
  188. QOpenGLFramebufferObjectFormat fmt;
  189. fmt.setAttachment(QOpenGLFramebufferObject::Depth);
  190. fmt.setSamples(0);
  191. return new QOpenGLFramebufferObject(size, fmt);
  192. }
  193. void synchronize(QQuickFramebufferObject *item) override {
  194. auto *view = dynamic_cast<CampaignMapView *>(item);
  195. if (view == nullptr) {
  196. return;
  197. }
  198. m_orbit_yaw = view->orbitYaw();
  199. m_orbit_pitch = view->orbitPitch();
  200. m_orbit_distance = view->orbitDistance();
  201. m_pan_u = view->panU();
  202. m_pan_v = view->panV();
  203. QString new_hover_id = view->hoverProvinceId();
  204. if (m_hover_province_id != new_hover_id) {
  205. m_hover_start_time = QDateTime::currentMSecsSinceEpoch();
  206. m_hover_province_id = new_hover_id;
  207. }
  208. m_current_mission = view->currentMission();
  209. if (m_province_state_version != view->provinceStateVersion() &&
  210. m_provinceLayer.ready) {
  211. apply_province_overrides(view->provinceOverrides());
  212. m_province_state_version = view->provinceStateVersion();
  213. }
  214. }
  215. private:
  216. QSize m_size;
  217. bool m_initialized = false;
  218. QOpenGLShaderProgram m_textureProgram;
  219. QOpenGLShaderProgram m_lineProgram;
  220. GLuint m_quadVao = 0;
  221. GLuint m_quadVbo = 0;
  222. GLuint m_landVao = 0;
  223. GLuint m_landVbo = 0;
  224. int m_landVertexCount = 0;
  225. QOpenGLTexture *m_baseTexture = nullptr;
  226. QOpenGLTexture *m_waterTexture = nullptr;
  227. LineLayer m_coastLayer;
  228. LineLayer m_riverLayer;
  229. LineLayer m_pathLayer;
  230. LineLayer m_provinceBorderLayer;
  231. ProvinceLayer m_provinceLayer;
  232. float m_orbit_yaw = 180.0F;
  233. float m_orbit_pitch = 55.0F;
  234. float m_orbit_distance = 2.4F;
  235. float m_pan_u = 0.0F;
  236. float m_pan_v = 0.0F;
  237. QString m_hover_province_id;
  238. int m_province_state_version = 0;
  239. int m_current_mission = 7;
  240. qint64 m_hover_start_time = 0;
  241. qint64 m_last_update_time = 0;
  242. auto ensure_initialized() -> bool {
  243. if (m_initialized) {
  244. return true;
  245. }
  246. QOpenGLContext *ctx = QOpenGLContext::currentContext();
  247. if (ctx == nullptr || !ctx->isValid()) {
  248. qWarning() << "CampaignMapRenderer: No valid OpenGL context";
  249. return false;
  250. }
  251. initializeOpenGLFunctions();
  252. if (!init_shaders()) {
  253. return false;
  254. }
  255. init_quad();
  256. auto &tex_cache = CampaignMapTextureCache::instance();
  257. tex_cache.set_loading_allowed(true);
  258. m_waterTexture = tex_cache.get_or_load(
  259. QStringLiteral(":/assets/campaign_map/campaign_water.png"));
  260. m_baseTexture = tex_cache.get_or_load(
  261. QStringLiteral(":/assets/campaign_map/campaign_base_color.png"));
  262. tex_cache.set_loading_allowed(false);
  263. init_land_mesh();
  264. init_line_layer(m_coastLayer,
  265. QStringLiteral(":/assets/campaign_map/coastlines_uv.json"),
  266. QVector4D(0.15F, 0.13F, 0.11F, 1.0F), 2.0F);
  267. init_line_layer(m_riverLayer,
  268. QStringLiteral(":/assets/campaign_map/rivers_uv.json"),
  269. QVector4D(0.35F, 0.45F, 0.55F, 0.85F), 1.5F);
  270. init_line_layer(m_pathLayer,
  271. QStringLiteral(":/assets/campaign_map/hannibal_path.json"),
  272. QVector4D(0.78F, 0.2F, 0.12F, 0.9F), 2.0F);
  273. init_province_layer(m_provinceLayer,
  274. QStringLiteral(":/assets/campaign_map/provinces.json"));
  275. init_borders_layer(m_provinceBorderLayer,
  276. QStringLiteral(":/assets/campaign_map/provinces.json"),
  277. QVector4D(0.25F, 0.22F, 0.20F, 0.65F), 1.2F);
  278. m_initialized = true;
  279. return true;
  280. }
  281. auto init_shaders() -> bool {
  282. static const char *kTexVert = R"(
  283. #version 330 core
  284. layout(location = 0) in vec2 a_pos;
  285. uniform mat4 u_mvp;
  286. uniform float u_z;
  287. out vec2 v_uv;
  288. void main() {
  289. vec3 world = vec3(1.0 - a_pos.x, u_z, a_pos.y);
  290. gl_Position = u_mvp * vec4(world, 1.0);
  291. v_uv = a_pos;
  292. }
  293. )";
  294. static const char *kTexFrag = R"(
  295. #version 330 core
  296. in vec2 v_uv;
  297. uniform sampler2D u_tex;
  298. uniform float u_alpha;
  299. out vec4 fragColor;
  300. void main() {
  301. vec2 uv = vec2(v_uv.x, 1.0 - v_uv.y);
  302. vec4 texel = texture(u_tex, uv);
  303. fragColor = vec4(texel.rgb, texel.a * u_alpha);
  304. }
  305. )";
  306. static const char *kLineVert = R"(
  307. #version 330 core
  308. layout(location = 0) in vec2 a_pos;
  309. uniform mat4 u_mvp;
  310. uniform float u_z;
  311. void main() {
  312. vec3 world = vec3(1.0 - a_pos.x, u_z, a_pos.y);
  313. gl_Position = u_mvp * vec4(world, 1.0);
  314. }
  315. )";
  316. static const char *kLineFrag = R"(
  317. #version 330 core
  318. uniform vec4 u_color;
  319. out vec4 fragColor;
  320. void main() {
  321. fragColor = u_color;
  322. }
  323. )";
  324. if (!m_textureProgram.addShaderFromSourceCode(QOpenGLShader::Vertex,
  325. kTexVert)) {
  326. qWarning()
  327. << "CampaignMapRenderer: Failed to compile texture vertex shader";
  328. return false;
  329. }
  330. if (!m_textureProgram.addShaderFromSourceCode(QOpenGLShader::Fragment,
  331. kTexFrag)) {
  332. qWarning()
  333. << "CampaignMapRenderer: Failed to compile texture fragment shader";
  334. return false;
  335. }
  336. if (!m_textureProgram.link()) {
  337. qWarning() << "CampaignMapRenderer: Failed to link texture shader";
  338. return false;
  339. }
  340. if (!m_lineProgram.addShaderFromSourceCode(QOpenGLShader::Vertex,
  341. kLineVert)) {
  342. qWarning() << "CampaignMapRenderer: Failed to compile line vertex shader";
  343. return false;
  344. }
  345. if (!m_lineProgram.addShaderFromSourceCode(QOpenGLShader::Fragment,
  346. kLineFrag)) {
  347. qWarning()
  348. << "CampaignMapRenderer: Failed to compile line fragment shader";
  349. return false;
  350. }
  351. if (!m_lineProgram.link()) {
  352. qWarning() << "CampaignMapRenderer: Failed to link line shader";
  353. return false;
  354. }
  355. return true;
  356. }
  357. void init_quad() {
  358. if (m_quadVao != 0) {
  359. return;
  360. }
  361. static const float kQuadVerts[] = {
  362. 0.0F, 0.0F, 1.0F, 0.0F, 1.0F, 1.0F, 0.0F, 0.0F, 1.0F, 1.0F, 0.0F, 1.0F,
  363. };
  364. glGenVertexArrays(1, &m_quadVao);
  365. glGenBuffers(1, &m_quadVbo);
  366. glBindVertexArray(m_quadVao);
  367. glBindBuffer(GL_ARRAY_BUFFER, m_quadVbo);
  368. glBufferData(GL_ARRAY_BUFFER, sizeof(kQuadVerts), kQuadVerts,
  369. GL_STATIC_DRAW);
  370. glEnableVertexAttribArray(0);
  371. glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float),
  372. reinterpret_cast<void *>(0));
  373. glBindVertexArray(0);
  374. }
  375. void init_land_mesh() {
  376. const QString path = Utils::Resources::resolveResourcePath(
  377. QStringLiteral(":/assets/campaign_map/land_mesh.bin"));
  378. QFile file(path);
  379. if (!file.open(QIODevice::ReadOnly)) {
  380. qWarning() << "CampaignMapRenderer: Failed to open land mesh" << path;
  381. return;
  382. }
  383. const QByteArray data = file.readAll();
  384. if (data.isEmpty() || (data.size() % sizeof(float) != 0)) {
  385. qWarning() << "CampaignMapRenderer: Land mesh is empty or invalid";
  386. return;
  387. }
  388. const int floatCount = data.size() / static_cast<int>(sizeof(float));
  389. if (floatCount % 2 != 0) {
  390. qWarning() << "CampaignMapRenderer: Land mesh float count is odd";
  391. return;
  392. }
  393. std::vector<float> verts(static_cast<size_t>(floatCount));
  394. memcpy(verts.data(), data.constData(), static_cast<size_t>(data.size()));
  395. m_landVertexCount = floatCount / 2;
  396. if (m_landVertexCount == 0) {
  397. return;
  398. }
  399. glGenVertexArrays(1, &m_landVao);
  400. glGenBuffers(1, &m_landVbo);
  401. glBindVertexArray(m_landVao);
  402. glBindBuffer(GL_ARRAY_BUFFER, m_landVbo);
  403. glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(data.size()),
  404. verts.data(), GL_STATIC_DRAW);
  405. glEnableVertexAttribArray(0);
  406. glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float),
  407. reinterpret_cast<void *>(0));
  408. glBindVertexArray(0);
  409. }
  410. void init_line_layer(LineLayer &layer, const QString &resource_path,
  411. const QVector4D &color, float width) {
  412. const QString path = Utils::Resources::resolveResourcePath(resource_path);
  413. QFile file(path);
  414. if (!file.open(QIODevice::ReadOnly)) {
  415. qWarning() << "CampaignMapRenderer: Failed to open line layer" << path;
  416. return;
  417. }
  418. const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
  419. if (!doc.isObject()) {
  420. qWarning() << "CampaignMapRenderer: Line layer JSON invalid" << path;
  421. return;
  422. }
  423. const QJsonArray lines = doc.object().value("lines").toArray();
  424. std::vector<float> verts;
  425. verts.reserve(lines.size() * 8);
  426. std::vector<LineSpan> spans;
  427. int cursor = 0;
  428. for (const auto &line_val : lines) {
  429. const QJsonArray line = line_val.toArray();
  430. if (line.isEmpty()) {
  431. continue;
  432. }
  433. const int start = cursor;
  434. int count = 0;
  435. for (const auto &pt_val : line) {
  436. const QJsonArray pt = pt_val.toArray();
  437. if (pt.size() < 2) {
  438. continue;
  439. }
  440. verts.push_back(static_cast<float>(pt.at(0).toDouble()));
  441. verts.push_back(static_cast<float>(pt.at(1).toDouble()));
  442. ++count;
  443. ++cursor;
  444. }
  445. if (count >= 2) {
  446. spans.push_back({start, count});
  447. }
  448. }
  449. if (verts.empty() || spans.empty()) {
  450. return;
  451. }
  452. glGenVertexArrays(1, &layer.vao);
  453. glGenBuffers(1, &layer.vbo);
  454. glBindVertexArray(layer.vao);
  455. glBindBuffer(GL_ARRAY_BUFFER, layer.vbo);
  456. glBufferData(GL_ARRAY_BUFFER,
  457. static_cast<GLsizeiptr>(verts.size() * sizeof(float)),
  458. verts.data(), GL_STATIC_DRAW);
  459. glEnableVertexAttribArray(0);
  460. glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float),
  461. reinterpret_cast<void *>(0));
  462. glBindVertexArray(0);
  463. layer.color = color;
  464. layer.width = width;
  465. layer.spans = std::move(spans);
  466. layer.ready = true;
  467. }
  468. void init_province_layer(ProvinceLayer &layer, const QString &resource_path) {
  469. const QString path = Utils::Resources::resolveResourcePath(resource_path);
  470. QFile file(path);
  471. if (!file.open(QIODevice::ReadOnly)) {
  472. qWarning() << "CampaignMapRenderer: Failed to open provinces" << path;
  473. return;
  474. }
  475. const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
  476. if (!doc.isObject()) {
  477. qWarning() << "CampaignMapRenderer: Provinces JSON invalid" << path;
  478. return;
  479. }
  480. const QJsonArray provinces = doc.object().value("provinces").toArray();
  481. if (provinces.isEmpty()) {
  482. return;
  483. }
  484. std::vector<float> verts;
  485. std::vector<ProvinceSpan> spans;
  486. int cursor = 0;
  487. for (const auto &prov_val : provinces) {
  488. const QJsonObject prov = prov_val.toObject();
  489. const QString province_id = prov.value("id").toString();
  490. const QJsonArray tri = prov.value("triangles").toArray();
  491. if (tri.isEmpty()) {
  492. continue;
  493. }
  494. const int start = cursor;
  495. int count = 0;
  496. for (const auto &pt_val : tri) {
  497. const QJsonArray pt = pt_val.toArray();
  498. if (pt.size() < 2) {
  499. continue;
  500. }
  501. verts.push_back(static_cast<float>(pt.at(0).toDouble()));
  502. verts.push_back(static_cast<float>(pt.at(1).toDouble()));
  503. ++count;
  504. ++cursor;
  505. }
  506. if (count >= 3) {
  507. QVector4D color(0.0F, 0.0F, 0.0F, 0.0F);
  508. const QJsonArray color_arr = prov.value("color").toArray();
  509. if (color_arr.size() >= 4) {
  510. color = QVector4D(static_cast<float>(color_arr.at(0).toDouble()),
  511. static_cast<float>(color_arr.at(1).toDouble()),
  512. static_cast<float>(color_arr.at(2).toDouble()),
  513. static_cast<float>(color_arr.at(3).toDouble()));
  514. }
  515. spans.push_back({start, count, color, color, province_id});
  516. }
  517. }
  518. if (verts.empty() || spans.empty()) {
  519. return;
  520. }
  521. glGenVertexArrays(1, &layer.vao);
  522. glGenBuffers(1, &layer.vbo);
  523. glBindVertexArray(layer.vao);
  524. glBindBuffer(GL_ARRAY_BUFFER, layer.vbo);
  525. glBufferData(GL_ARRAY_BUFFER,
  526. static_cast<GLsizeiptr>(verts.size() * sizeof(float)),
  527. verts.data(), GL_STATIC_DRAW);
  528. glEnableVertexAttribArray(0);
  529. glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float),
  530. reinterpret_cast<void *>(0));
  531. glBindVertexArray(0);
  532. layer.spans = std::move(spans);
  533. layer.ready = true;
  534. }
  535. void init_borders_layer(LineLayer &layer, const QString &resource_path,
  536. const QVector4D &color, float width) {
  537. const QString path = Utils::Resources::resolveResourcePath(resource_path);
  538. QFile file(path);
  539. if (!file.open(QIODevice::ReadOnly)) {
  540. qWarning() << "CampaignMapRenderer: Failed to open borders" << path;
  541. return;
  542. }
  543. const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
  544. if (!doc.isObject()) {
  545. qWarning() << "CampaignMapRenderer: Borders JSON invalid" << path;
  546. return;
  547. }
  548. const QJsonArray lines = doc.object().value("borders").toArray();
  549. std::vector<float> verts;
  550. verts.reserve(lines.size() * 8);
  551. std::vector<LineSpan> spans;
  552. int cursor = 0;
  553. for (const auto &line_val : lines) {
  554. const QJsonArray line = line_val.toArray();
  555. if (line.isEmpty()) {
  556. continue;
  557. }
  558. const int start = cursor;
  559. int count = 0;
  560. for (const auto &pt_val : line) {
  561. const QJsonArray pt = pt_val.toArray();
  562. if (pt.size() < 2) {
  563. continue;
  564. }
  565. verts.push_back(static_cast<float>(pt.at(0).toDouble()));
  566. verts.push_back(static_cast<float>(pt.at(1).toDouble()));
  567. ++count;
  568. ++cursor;
  569. }
  570. if (count >= 2) {
  571. spans.push_back({start, count});
  572. }
  573. }
  574. if (verts.empty() || spans.empty()) {
  575. return;
  576. }
  577. glGenVertexArrays(1, &layer.vao);
  578. glGenBuffers(1, &layer.vbo);
  579. glBindVertexArray(layer.vao);
  580. glBindBuffer(GL_ARRAY_BUFFER, layer.vbo);
  581. glBufferData(GL_ARRAY_BUFFER,
  582. static_cast<GLsizeiptr>(verts.size() * sizeof(float)),
  583. verts.data(), GL_STATIC_DRAW);
  584. glEnableVertexAttribArray(0);
  585. glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float),
  586. reinterpret_cast<void *>(0));
  587. glBindVertexArray(0);
  588. layer.color = color;
  589. layer.width = width;
  590. layer.spans = std::move(spans);
  591. layer.ready = true;
  592. }
  593. auto load_texture(const QString &resource_path) -> QOpenGLTexture * {
  594. const QString path = Utils::Resources::resolveResourcePath(resource_path);
  595. QImage image(path);
  596. if (image.isNull()) {
  597. qWarning() << "CampaignMapRenderer: Failed to load texture" << path;
  598. return nullptr;
  599. }
  600. QImage rgba = image.convertToFormat(QImage::Format_RGBA8888);
  601. auto *texture = new QOpenGLTexture(rgba);
  602. texture->setWrapMode(QOpenGLTexture::ClampToEdge);
  603. texture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
  604. return texture;
  605. }
  606. void compute_mvp(QMatrix4x4 &out_mvp) const {
  607. out_mvp = build_mvp_matrix(
  608. static_cast<float>(m_size.width()), static_cast<float>(m_size.height()),
  609. m_orbit_yaw, m_orbit_pitch, m_orbit_distance, m_pan_u, m_pan_v);
  610. }
  611. void draw_textured_layer(QOpenGLTexture *texture, GLuint vao,
  612. int vertex_count, const QMatrix4x4 &mvp, float alpha,
  613. float z_offset) {
  614. if (texture == nullptr || vao == 0 || vertex_count <= 0) {
  615. return;
  616. }
  617. m_textureProgram.bind();
  618. m_textureProgram.setUniformValue("u_mvp", mvp);
  619. m_textureProgram.setUniformValue("u_z", z_offset);
  620. m_textureProgram.setUniformValue("u_alpha", alpha);
  621. m_textureProgram.setUniformValue("u_tex", 0);
  622. glActiveTexture(GL_TEXTURE0);
  623. texture->bind();
  624. glBindVertexArray(vao);
  625. glDrawArrays(GL_TRIANGLES, 0, vertex_count);
  626. glBindVertexArray(0);
  627. texture->release();
  628. m_textureProgram.release();
  629. }
  630. void draw_line_layer(const LineLayer &layer, const QMatrix4x4 &mvp,
  631. float z_offset) {
  632. if (!layer.ready || layer.vao == 0 || layer.spans.empty()) {
  633. return;
  634. }
  635. glLineWidth(layer.width);
  636. m_lineProgram.bind();
  637. m_lineProgram.setUniformValue("u_mvp", mvp);
  638. m_lineProgram.setUniformValue("u_z", z_offset);
  639. m_lineProgram.setUniformValue("u_color", layer.color);
  640. glBindVertexArray(layer.vao);
  641. for (const auto &span : layer.spans) {
  642. glDrawArrays(GL_LINE_STRIP, span.start, span.count);
  643. }
  644. glBindVertexArray(0);
  645. m_lineProgram.release();
  646. }
  647. void draw_progressive_path_layers(const LineLayer &layer,
  648. const QMatrix4x4 &mvp, float z_offset) {
  649. if (!layer.ready || layer.vao == 0 || layer.spans.empty()) {
  650. return;
  651. }
  652. const int max_mission =
  653. qBound(0, m_current_mission, static_cast<int>(layer.spans.size()) - 1);
  654. glEnable(GL_LINE_SMOOTH);
  655. glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
  656. m_lineProgram.bind();
  657. m_lineProgram.setUniformValue("u_mvp", mvp);
  658. m_lineProgram.setUniformValue("u_z", z_offset);
  659. glBindVertexArray(layer.vao);
  660. for (int i = 0; i <= max_mission; ++i) {
  661. if (i >= static_cast<int>(layer.spans.size())) {
  662. break;
  663. }
  664. const auto &span = layer.spans[static_cast<size_t>(i)];
  665. QVector4D border_color;
  666. float border_width;
  667. if (i == max_mission) {
  668. border_color = QVector4D(0.15F, 0.08F, 0.05F, 0.85F);
  669. border_width = 18.0F;
  670. } else if (i == max_mission - 1) {
  671. border_color = QVector4D(0.15F, 0.08F, 0.05F, 0.70F);
  672. border_width = 16.0F;
  673. } else {
  674. const float age_factor = 1.0F - (max_mission - i) * 0.08F;
  675. border_color = QVector4D(0.15F * age_factor, 0.08F * age_factor,
  676. 0.05F * age_factor, 0.55F * age_factor);
  677. border_width = 14.0F;
  678. }
  679. glLineWidth(border_width);
  680. m_lineProgram.setUniformValue("u_color", border_color);
  681. glDrawArrays(GL_LINE_STRIP, span.start, span.count);
  682. }
  683. for (int i = 0; i <= max_mission; ++i) {
  684. if (i >= static_cast<int>(layer.spans.size())) {
  685. break;
  686. }
  687. const auto &span = layer.spans[static_cast<size_t>(i)];
  688. QVector4D highlight_color;
  689. float highlight_width;
  690. if (i == max_mission) {
  691. highlight_color = QVector4D(0.95F, 0.75F, 0.35F, 0.90F);
  692. highlight_width = 12.0F;
  693. } else if (i == max_mission - 1) {
  694. highlight_color = QVector4D(0.85F, 0.65F, 0.30F, 0.80F);
  695. highlight_width = 10.0F;
  696. } else {
  697. const float age_factor = 1.0F - (max_mission - i) * 0.08F;
  698. highlight_color = QVector4D(0.70F * age_factor, 0.50F * age_factor,
  699. 0.25F * age_factor, 0.65F * age_factor);
  700. highlight_width = 8.5F;
  701. }
  702. glLineWidth(highlight_width);
  703. m_lineProgram.setUniformValue("u_color", highlight_color);
  704. glDrawArrays(GL_LINE_STRIP, span.start, span.count);
  705. }
  706. for (int i = 0; i <= max_mission; ++i) {
  707. if (i >= static_cast<int>(layer.spans.size())) {
  708. break;
  709. }
  710. const auto &span = layer.spans[static_cast<size_t>(i)];
  711. QVector4D color;
  712. float width;
  713. if (i == max_mission) {
  714. color = QVector4D(0.80F, 0.15F, 0.10F, 1.0F);
  715. width = 7.0F;
  716. } else if (i == max_mission - 1) {
  717. color = QVector4D(0.70F, 0.15F, 0.10F, 0.95F);
  718. width = 6.0F;
  719. } else {
  720. const float age_factor = 1.0F - (max_mission - i) * 0.08F;
  721. color = QVector4D(0.55F * age_factor, 0.12F * age_factor,
  722. 0.08F * age_factor, 0.85F * age_factor);
  723. width = 5.0F;
  724. }
  725. glLineWidth(width);
  726. m_lineProgram.setUniformValue("u_color", color);
  727. glDrawArrays(GL_LINE_STRIP, span.start, span.count);
  728. }
  729. glBindVertexArray(0);
  730. m_lineProgram.release();
  731. glDisable(GL_LINE_SMOOTH);
  732. }
  733. void apply_province_overrides(
  734. const QHash<QString, CampaignMapView::ProvinceVisual> &overrides) {
  735. if (!m_provinceLayer.ready || m_provinceLayer.spans.empty()) {
  736. return;
  737. }
  738. for (auto &span : m_provinceLayer.spans) {
  739. const auto it = overrides.find(span.id);
  740. if (it != overrides.end() && it->has_color) {
  741. span.color = it->color;
  742. } else {
  743. span.color = span.base_color;
  744. }
  745. }
  746. }
  747. void draw_province_layer(const ProvinceLayer &layer, const QMatrix4x4 &mvp,
  748. float z_offset) {
  749. if (!layer.ready || layer.vao == 0 || layer.spans.empty()) {
  750. return;
  751. }
  752. m_lineProgram.bind();
  753. m_lineProgram.setUniformValue("u_mvp", mvp);
  754. m_lineProgram.setUniformValue("u_z", z_offset);
  755. glBindVertexArray(layer.vao);
  756. for (const auto &span : layer.spans) {
  757. if (span.color.w() <= 0.0F) {
  758. continue;
  759. }
  760. QVector4D color = span.color;
  761. if (!m_hover_province_id.isEmpty() && span.id == m_hover_province_id) {
  762. qint64 elapsed =
  763. QDateTime::currentMSecsSinceEpoch() - m_hover_start_time;
  764. float pulse_cycle = 1200.0F;
  765. float pulse =
  766. 0.5F + 0.5F * std::sin(elapsed * 2.0F * M_PI / pulse_cycle);
  767. float brightness_boost = 0.3F + 0.15F * pulse;
  768. color = QVector4D(qMin(1.0F, color.x() + brightness_boost),
  769. qMin(1.0F, color.y() + brightness_boost),
  770. qMin(1.0F, color.z() + brightness_boost),
  771. qMin(1.0F, color.w() + 0.2F));
  772. }
  773. m_lineProgram.setUniformValue("u_color", color);
  774. glDrawArrays(GL_TRIANGLES, span.start, span.count);
  775. }
  776. glBindVertexArray(0);
  777. m_lineProgram.release();
  778. }
  779. void cleanup() {
  780. if (QOpenGLContext::currentContext() == nullptr) {
  781. return;
  782. }
  783. if (m_quadVbo != 0) {
  784. glDeleteBuffers(1, &m_quadVbo);
  785. m_quadVbo = 0;
  786. }
  787. if (m_quadVao != 0) {
  788. glDeleteVertexArrays(1, &m_quadVao);
  789. m_quadVao = 0;
  790. }
  791. if (m_landVbo != 0) {
  792. glDeleteBuffers(1, &m_landVbo);
  793. m_landVbo = 0;
  794. }
  795. if (m_landVao != 0) {
  796. glDeleteVertexArrays(1, &m_landVao);
  797. m_landVao = 0;
  798. }
  799. if (m_coastLayer.vbo != 0) {
  800. glDeleteBuffers(1, &m_coastLayer.vbo);
  801. m_coastLayer.vbo = 0;
  802. }
  803. if (m_coastLayer.vao != 0) {
  804. glDeleteVertexArrays(1, &m_coastLayer.vao);
  805. m_coastLayer.vao = 0;
  806. }
  807. if (m_riverLayer.vbo != 0) {
  808. glDeleteBuffers(1, &m_riverLayer.vbo);
  809. m_riverLayer.vbo = 0;
  810. }
  811. if (m_riverLayer.vao != 0) {
  812. glDeleteVertexArrays(1, &m_riverLayer.vao);
  813. m_riverLayer.vao = 0;
  814. }
  815. if (m_pathLayer.vbo != 0) {
  816. glDeleteBuffers(1, &m_pathLayer.vbo);
  817. m_pathLayer.vbo = 0;
  818. }
  819. if (m_pathLayer.vao != 0) {
  820. glDeleteVertexArrays(1, &m_pathLayer.vao);
  821. m_pathLayer.vao = 0;
  822. }
  823. if (m_provinceBorderLayer.vbo != 0) {
  824. glDeleteBuffers(1, &m_provinceBorderLayer.vbo);
  825. m_provinceBorderLayer.vbo = 0;
  826. }
  827. if (m_provinceBorderLayer.vao != 0) {
  828. glDeleteVertexArrays(1, &m_provinceBorderLayer.vao);
  829. m_provinceBorderLayer.vao = 0;
  830. }
  831. if (m_provinceLayer.vbo != 0) {
  832. glDeleteBuffers(1, &m_provinceLayer.vbo);
  833. m_provinceLayer.vbo = 0;
  834. }
  835. if (m_provinceLayer.vao != 0) {
  836. glDeleteVertexArrays(1, &m_provinceLayer.vao);
  837. m_provinceLayer.vao = 0;
  838. }
  839. m_baseTexture = nullptr;
  840. m_waterTexture = nullptr;
  841. }
  842. };
  843. } // namespace
  844. CampaignMapView::CampaignMapView() {
  845. setMirrorVertically(true);
  846. QOpenGLContext *ctx = QOpenGLContext::currentContext();
  847. if (ctx == nullptr) {
  848. qWarning() << "CampaignMapView: No OpenGL context available";
  849. qWarning()
  850. << "CampaignMapView: 3D rendering will not work in software mode";
  851. qWarning()
  852. << "CampaignMapView: Try running without QT_QUICK_BACKEND=software";
  853. }
  854. }
  855. void CampaignMapView::load_provinces_for_hit_test() {
  856. if (m_provinces_loaded) {
  857. return;
  858. }
  859. m_provinces_loaded = true;
  860. m_provinces.clear();
  861. const QString path = Utils::Resources::resolveResourcePath(
  862. QStringLiteral(":/assets/campaign_map/provinces.json"));
  863. QFile file(path);
  864. if (!file.open(QIODevice::ReadOnly)) {
  865. return;
  866. }
  867. const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
  868. if (!doc.isObject()) {
  869. return;
  870. }
  871. const QJsonArray provinces = doc.object().value("provinces").toArray();
  872. for (const auto &prov_val : provinces) {
  873. const QJsonObject prov = prov_val.toObject();
  874. const QJsonArray tri = prov.value("triangles").toArray();
  875. if (tri.isEmpty()) {
  876. continue;
  877. }
  878. ProvinceHit province;
  879. province.id = prov.value("id").toString();
  880. province.name = prov.value("name").toString();
  881. province.owner = prov.value("owner").toString();
  882. province.triangles.reserve(static_cast<size_t>(tri.size()));
  883. for (const auto &pt_val : tri) {
  884. const QJsonArray pt = pt_val.toArray();
  885. if (pt.size() < 2) {
  886. continue;
  887. }
  888. province.triangles.emplace_back(static_cast<float>(pt.at(0).toDouble()),
  889. static_cast<float>(pt.at(1).toDouble()));
  890. }
  891. if (province.triangles.size() >= 3) {
  892. m_provinces.push_back(std::move(province));
  893. }
  894. }
  895. if (!m_province_overrides.isEmpty()) {
  896. for (auto &province : m_provinces) {
  897. const auto it = m_province_overrides.find(province.id);
  898. if (it != m_province_overrides.end() && !it->owner.isEmpty()) {
  899. province.owner = it->owner;
  900. }
  901. }
  902. }
  903. }
  904. void CampaignMapView::load_province_labels() {
  905. if (m_province_labels_loaded) {
  906. return;
  907. }
  908. m_province_labels_loaded = true;
  909. m_province_labels.clear();
  910. const QString path = Utils::Resources::resolveResourcePath(
  911. QStringLiteral(":/assets/campaign_map/provinces.json"));
  912. QFile file(path);
  913. if (!file.open(QIODevice::ReadOnly)) {
  914. return;
  915. }
  916. const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
  917. if (!doc.isObject()) {
  918. return;
  919. }
  920. const QJsonArray provinces = doc.object().value("provinces").toArray();
  921. for (const auto &prov_val : provinces) {
  922. const QJsonObject prov = prov_val.toObject();
  923. QVariantMap entry;
  924. entry.insert(QStringLiteral("id"), prov.value("id").toString());
  925. entry.insert(QStringLiteral("name"), prov.value("name").toString());
  926. entry.insert(QStringLiteral("owner"), prov.value("owner").toString());
  927. const QJsonArray label_uv = prov.value("label_uv").toArray();
  928. if (label_uv.size() >= 2) {
  929. QVariantList label_list;
  930. label_list.reserve(2);
  931. label_list.push_back(label_uv.at(0).toDouble());
  932. label_list.push_back(label_uv.at(1).toDouble());
  933. entry.insert(QStringLiteral("label_uv"), label_list);
  934. }
  935. const QJsonArray cities = prov.value("cities").toArray();
  936. QVariantList city_list;
  937. city_list.reserve(cities.size());
  938. for (const auto &city_val : cities) {
  939. const QJsonObject city = city_val.toObject();
  940. const QString name = city.value("name").toString();
  941. const QJsonArray uv = city.value("uv").toArray();
  942. if (name.isEmpty() || uv.size() < 2) {
  943. continue;
  944. }
  945. QVariantList uv_list;
  946. uv_list.reserve(2);
  947. uv_list.push_back(uv.at(0).toDouble());
  948. uv_list.push_back(uv.at(1).toDouble());
  949. QVariantMap city_entry;
  950. city_entry.insert(QStringLiteral("name"), name);
  951. city_entry.insert(QStringLiteral("uv"), uv_list);
  952. city_list.push_back(city_entry);
  953. }
  954. entry.insert(QStringLiteral("cities"), city_list);
  955. m_province_labels.push_back(entry);
  956. }
  957. if (!m_province_overrides.isEmpty()) {
  958. QVariantList updated;
  959. updated.reserve(m_province_labels.size());
  960. for (const auto &entry_val : m_province_labels) {
  961. QVariantMap entry = entry_val.toMap();
  962. const QString id = entry.value(QStringLiteral("id")).toString();
  963. const auto it = m_province_overrides.find(id);
  964. if (it != m_province_overrides.end() && !it->owner.isEmpty()) {
  965. entry.insert(QStringLiteral("owner"), it->owner);
  966. }
  967. updated.push_back(entry);
  968. }
  969. m_province_labels = std::move(updated);
  970. }
  971. emit provinceLabelsChanged();
  972. }
  973. QVariantList CampaignMapView::provinceLabels() {
  974. load_province_labels();
  975. return m_province_labels;
  976. }
  977. void CampaignMapView::applyProvinceState(const QVariantList &states) {
  978. QHash<QString, ProvinceVisual> next_overrides;
  979. next_overrides.reserve(states.size());
  980. for (const auto &state_val : states) {
  981. const QVariantMap state = state_val.toMap();
  982. const QString id = state.value(QStringLiteral("id")).toString();
  983. if (id.isEmpty()) {
  984. continue;
  985. }
  986. ProvinceVisual visual;
  987. visual.owner = state.value(QStringLiteral("owner")).toString();
  988. const QVariantList color_list =
  989. state.value(QStringLiteral("color")).toList();
  990. if (color_list.size() >= 4) {
  991. visual.color = QVector4D(static_cast<float>(color_list.at(0).toDouble()),
  992. static_cast<float>(color_list.at(1).toDouble()),
  993. static_cast<float>(color_list.at(2).toDouble()),
  994. static_cast<float>(color_list.at(3).toDouble()));
  995. visual.has_color = true;
  996. }
  997. next_overrides.insert(id, visual);
  998. }
  999. m_province_overrides = std::move(next_overrides);
  1000. ++m_province_state_version;
  1001. if (m_provinces_loaded) {
  1002. for (auto &province : m_provinces) {
  1003. const auto it = m_province_overrides.find(province.id);
  1004. if (it != m_province_overrides.end() && !it->owner.isEmpty()) {
  1005. province.owner = it->owner;
  1006. }
  1007. }
  1008. }
  1009. if (m_province_labels_loaded) {
  1010. QVariantList updated;
  1011. updated.reserve(m_province_labels.size());
  1012. for (const auto &entry_val : m_province_labels) {
  1013. QVariantMap entry = entry_val.toMap();
  1014. const QString id = entry.value(QStringLiteral("id")).toString();
  1015. const auto it = m_province_overrides.find(id);
  1016. if (it != m_province_overrides.end() && !it->owner.isEmpty()) {
  1017. entry.insert(QStringLiteral("owner"), it->owner);
  1018. }
  1019. updated.push_back(entry);
  1020. }
  1021. m_province_labels = std::move(updated);
  1022. emit provinceLabelsChanged();
  1023. }
  1024. update();
  1025. }
  1026. QString CampaignMapView::provinceAtScreen(float x, float y) {
  1027. load_provinces_for_hit_test();
  1028. if (m_provinces.empty()) {
  1029. return {};
  1030. }
  1031. const float w = static_cast<float>(width());
  1032. const float h = static_cast<float>(height());
  1033. if (w <= 0.0F || h <= 0.0F) {
  1034. return {};
  1035. }
  1036. const float ndc_x = (2.0F * x / w) - 1.0F;
  1037. const float ndc_y = 1.0F - (2.0F * y / h);
  1038. const QMatrix4x4 mvp = build_mvp_matrix(w, h, m_orbit_yaw, m_orbit_pitch,
  1039. m_orbit_distance, m_pan_u, m_pan_v);
  1040. bool inverted = false;
  1041. const QMatrix4x4 inv = mvp.inverted(&inverted);
  1042. if (!inverted) {
  1043. return {};
  1044. }
  1045. QVector4D near_p = inv * QVector4D(ndc_x, ndc_y, -1.0F, 1.0F);
  1046. QVector4D far_p = inv * QVector4D(ndc_x, ndc_y, 1.0F, 1.0F);
  1047. if (qFuzzyIsNull(near_p.w()) || qFuzzyIsNull(far_p.w())) {
  1048. return {};
  1049. }
  1050. QVector3D near_v = QVector3D(near_p.x(), near_p.y(), near_p.z()) / near_p.w();
  1051. QVector3D far_v = QVector3D(far_p.x(), far_p.y(), far_p.z()) / far_p.w();
  1052. const QVector3D dir = far_v - near_v;
  1053. if (qFuzzyIsNull(dir.y())) {
  1054. return {};
  1055. }
  1056. const float t = -near_v.y() / dir.y();
  1057. if (t < 0.0F) {
  1058. return {};
  1059. }
  1060. const QVector3D hit = near_v + dir * t;
  1061. const float u = 1.0F - hit.x();
  1062. const float v = hit.z();
  1063. if (u < 0.0F || u > 1.0F || v < 0.0F || v > 1.0F) {
  1064. return {};
  1065. }
  1066. const QVector2D p(u, v);
  1067. for (const auto &province : m_provinces) {
  1068. const auto &triangles = province.triangles;
  1069. for (size_t i = 0; i + 2 < triangles.size(); i += 3) {
  1070. if (point_in_triangle(p, triangles[i], triangles[i + 1],
  1071. triangles[i + 2])) {
  1072. return province.id;
  1073. }
  1074. }
  1075. }
  1076. return {};
  1077. }
  1078. QVariantMap CampaignMapView::provinceInfoAtScreen(float x, float y) {
  1079. load_provinces_for_hit_test();
  1080. QVariantMap info;
  1081. if (m_provinces.empty()) {
  1082. return info;
  1083. }
  1084. const float w = static_cast<float>(width());
  1085. const float h = static_cast<float>(height());
  1086. if (w <= 0.0F || h <= 0.0F) {
  1087. return info;
  1088. }
  1089. const float ndc_x = (2.0F * x / w) - 1.0F;
  1090. const float ndc_y = 1.0F - (2.0F * y / h);
  1091. const QMatrix4x4 mvp = build_mvp_matrix(w, h, m_orbit_yaw, m_orbit_pitch,
  1092. m_orbit_distance, m_pan_u, m_pan_v);
  1093. bool inverted = false;
  1094. const QMatrix4x4 inv = mvp.inverted(&inverted);
  1095. if (!inverted) {
  1096. return info;
  1097. }
  1098. QVector4D near_p = inv * QVector4D(ndc_x, ndc_y, -1.0F, 1.0F);
  1099. QVector4D far_p = inv * QVector4D(ndc_x, ndc_y, 1.0F, 1.0F);
  1100. if (qFuzzyIsNull(near_p.w()) || qFuzzyIsNull(far_p.w())) {
  1101. return info;
  1102. }
  1103. QVector3D near_v = QVector3D(near_p.x(), near_p.y(), near_p.z()) / near_p.w();
  1104. QVector3D far_v = QVector3D(far_p.x(), far_p.y(), far_p.z()) / far_p.w();
  1105. const QVector3D dir = far_v - near_v;
  1106. if (qFuzzyIsNull(dir.y())) {
  1107. return info;
  1108. }
  1109. const float t = -near_v.y() / dir.y();
  1110. if (t < 0.0F) {
  1111. return info;
  1112. }
  1113. const QVector3D hit = near_v + dir * t;
  1114. const float u = 1.0F - hit.x();
  1115. const float v = hit.z();
  1116. if (u < 0.0F || u > 1.0F || v < 0.0F || v > 1.0F) {
  1117. return info;
  1118. }
  1119. const QVector2D p(u, v);
  1120. for (const auto &province : m_provinces) {
  1121. const auto &triangles = province.triangles;
  1122. for (size_t i = 0; i + 2 < triangles.size(); i += 3) {
  1123. if (point_in_triangle(p, triangles[i], triangles[i + 1],
  1124. triangles[i + 2])) {
  1125. info.insert(QStringLiteral("id"), province.id);
  1126. info.insert(QStringLiteral("name"), province.name);
  1127. info.insert(QStringLiteral("owner"), province.owner);
  1128. return info;
  1129. }
  1130. }
  1131. }
  1132. return info;
  1133. }
  1134. QPointF CampaignMapView::screenPosForUv(float u, float v) {
  1135. const float w = static_cast<float>(width());
  1136. const float h = static_cast<float>(height());
  1137. if (w <= 0.0F || h <= 0.0F) {
  1138. return {};
  1139. }
  1140. const float clamped_u = qBound(0.0F, u, 1.0F);
  1141. const float clamped_v = qBound(0.0F, v, 1.0F);
  1142. const QMatrix4x4 mvp = build_mvp_matrix(w, h, m_orbit_yaw, m_orbit_pitch,
  1143. m_orbit_distance, m_pan_u, m_pan_v);
  1144. const QVector4D world(1.0F - clamped_u, 0.0F, clamped_v, 1.0F);
  1145. const QVector4D clip = mvp * world;
  1146. if (qFuzzyIsNull(clip.w())) {
  1147. return {};
  1148. }
  1149. const float ndc_x = clip.x() / clip.w();
  1150. const float ndc_y = clip.y() / clip.w();
  1151. const float screen_x = (ndc_x + 1.0F) * 0.5F * w;
  1152. const float screen_y = (1.0F - (ndc_y + 1.0F) * 0.5F) * h;
  1153. return QPointF(screen_x, screen_y);
  1154. }
  1155. void CampaignMapView::load_hannibal_paths() {
  1156. if (m_hannibal_paths_loaded) {
  1157. return;
  1158. }
  1159. m_hannibal_paths_loaded = true;
  1160. m_hannibal_paths.clear();
  1161. const QString path = Utils::Resources::resolveResourcePath(
  1162. QStringLiteral(":/assets/campaign_map/hannibal_path.json"));
  1163. QFile file(path);
  1164. if (!file.open(QIODevice::ReadOnly)) {
  1165. qWarning() << "Failed to load Hannibal paths from" << path;
  1166. return;
  1167. }
  1168. const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
  1169. if (!doc.isObject()) {
  1170. return;
  1171. }
  1172. const QJsonArray lines = doc.object().value("lines").toArray();
  1173. for (const auto &line_val : lines) {
  1174. const QJsonArray line = line_val.toArray();
  1175. std::vector<QVector2D> path;
  1176. path.reserve(static_cast<size_t>(line.size()));
  1177. for (const auto &pt_val : line) {
  1178. const QJsonArray pt = pt_val.toArray();
  1179. if (pt.size() < 2) {
  1180. continue;
  1181. }
  1182. path.emplace_back(static_cast<float>(pt.at(0).toDouble()),
  1183. static_cast<float>(pt.at(1).toDouble()));
  1184. }
  1185. if (!path.empty()) {
  1186. m_hannibal_paths.push_back(std::move(path));
  1187. }
  1188. }
  1189. }
  1190. QPointF CampaignMapView::hannibalIconPosition() {
  1191. load_hannibal_paths();
  1192. if (m_hannibal_paths.empty()) {
  1193. return {};
  1194. }
  1195. const int mission_idx = qBound(0, m_current_mission,
  1196. static_cast<int>(m_hannibal_paths.size()) - 1);
  1197. const auto &path = m_hannibal_paths[static_cast<size_t>(mission_idx)];
  1198. if (path.empty()) {
  1199. return {};
  1200. }
  1201. const QVector2D &endpoint = path.back();
  1202. return screenPosForUv(endpoint.x(), endpoint.y());
  1203. }
  1204. void CampaignMapView::setOrbitYaw(float yaw) {
  1205. if (qFuzzyCompare(m_orbit_yaw, yaw)) {
  1206. return;
  1207. }
  1208. m_orbit_yaw = yaw;
  1209. emit orbitYawChanged();
  1210. update();
  1211. }
  1212. void CampaignMapView::setOrbitPitch(float pitch) {
  1213. const float clamped = qBound(5.0F, pitch, 90.0F);
  1214. if (qFuzzyCompare(m_orbit_pitch, clamped)) {
  1215. return;
  1216. }
  1217. m_orbit_pitch = clamped;
  1218. emit orbitPitchChanged();
  1219. update();
  1220. }
  1221. void CampaignMapView::setOrbitDistance(float distance) {
  1222. const float clamped = qBound(kMinOrbitDistance, distance, kMaxOrbitDistance);
  1223. if (qFuzzyCompare(m_orbit_distance, clamped)) {
  1224. return;
  1225. }
  1226. m_orbit_distance = clamped;
  1227. emit orbitDistanceChanged();
  1228. update();
  1229. }
  1230. void CampaignMapView::setPanU(float pan) {
  1231. const float clamped = qBound(-0.5F, pan, 0.5F);
  1232. if (qFuzzyCompare(m_pan_u, clamped)) {
  1233. return;
  1234. }
  1235. m_pan_u = clamped;
  1236. emit panUChanged();
  1237. update();
  1238. }
  1239. void CampaignMapView::setPanV(float pan) {
  1240. const float clamped = qBound(-0.5F, pan, 0.5F);
  1241. if (qFuzzyCompare(m_pan_v, clamped)) {
  1242. return;
  1243. }
  1244. m_pan_v = clamped;
  1245. emit panVChanged();
  1246. update();
  1247. }
  1248. void CampaignMapView::setHoverProvinceId(const QString &province_id) {
  1249. if (m_hover_province_id == province_id) {
  1250. return;
  1251. }
  1252. m_hover_province_id = province_id;
  1253. emit hoverProvinceIdChanged();
  1254. update();
  1255. }
  1256. void CampaignMapView::setCurrentMission(int mission) {
  1257. const int clamped = qBound(0, mission, 7);
  1258. if (m_current_mission == clamped) {
  1259. return;
  1260. }
  1261. m_current_mission = clamped;
  1262. emit currentMissionChanged();
  1263. update();
  1264. }
  1265. auto CampaignMapView::createRenderer() const -> Renderer * {
  1266. QOpenGLContext *ctx = QOpenGLContext::currentContext();
  1267. if ((ctx == nullptr) || !ctx->isValid()) {
  1268. qCritical()
  1269. << "CampaignMapView::createRenderer() - No valid OpenGL context";
  1270. qCritical()
  1271. << "Running in software rendering mode - map view not available";
  1272. return nullptr;
  1273. }
  1274. return new CampaignMapRenderer();
  1275. }