campaign_map_render_utils.h 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938
  1. #pragma once
  2. #include <QMatrix4x4>
  3. #include <QVector2D>
  4. #include <QVector3D>
  5. #include <QVector4D>
  6. #include <cmath>
  7. #include <vector>
  8. namespace CampaignMapRender {
  9. inline auto catmull_rom(const QVector2D &p0, const QVector2D &p1,
  10. const QVector2D &p2, const QVector2D &p3,
  11. float t) -> QVector2D {
  12. const float t2 = t * t;
  13. const float t3 = t2 * t;
  14. const float c0 = -0.5F * t3 + t2 - 0.5F * t;
  15. const float c1 = 1.5F * t3 - 2.5F * t2 + 1.0F;
  16. const float c2 = -1.5F * t3 + 2.0F * t2 + 0.5F * t;
  17. const float c3 = 0.5F * t3 - 0.5F * t2;
  18. return p0 * c0 + p1 * c1 + p2 * c2 + p3 * c3;
  19. }
  20. inline auto catmull_rom_tangent(const QVector2D &p0, const QVector2D &p1,
  21. const QVector2D &p2, const QVector2D &p3,
  22. float t) -> QVector2D {
  23. const float t2 = t * t;
  24. const float c0 = -1.5F * t2 + 2.0F * t - 0.5F;
  25. const float c1 = 4.5F * t2 - 5.0F * t;
  26. const float c2 = -4.5F * t2 + 4.0F * t + 0.5F;
  27. const float c3 = 1.5F * t2 - t;
  28. return p0 * c0 + p1 * c1 + p2 * c2 + p3 * c3;
  29. }
  30. inline auto
  31. smooth_catmull_rom(const std::vector<QVector2D> &input,
  32. int samples_per_segment = 8) -> std::vector<QVector2D> {
  33. if (input.size() < 2) {
  34. return input;
  35. }
  36. std::vector<QVector2D> result;
  37. result.reserve(input.size() * static_cast<size_t>(samples_per_segment));
  38. for (size_t i = 0; i + 1 < input.size(); ++i) {
  39. const QVector2D &p0 = (i == 0) ? input[0] : input[i - 1];
  40. const QVector2D &p1 = input[i];
  41. const QVector2D &p2 = input[i + 1];
  42. const QVector2D &p3 =
  43. (i + 2 >= input.size()) ? input[input.size() - 1] : input[i + 2];
  44. for (int s = 0; s < samples_per_segment; ++s) {
  45. const float t =
  46. static_cast<float>(s) / static_cast<float>(samples_per_segment);
  47. result.push_back(catmull_rom(p0, p1, p2, p3, t));
  48. }
  49. }
  50. result.push_back(input.back());
  51. return result;
  52. }
  53. struct MiterParams {
  54. float max_miter_ratio = 3.0F;
  55. float min_denom = 0.2F;
  56. };
  57. enum class CapStyle { Flat, Round, Square };
  58. enum class JoinStyle { Miter, Round, Bevel };
  59. struct StrokeMeshConfig {
  60. float width = 4.0F;
  61. CapStyle start_cap = CapStyle::Round;
  62. CapStyle end_cap = CapStyle::Round;
  63. JoinStyle join_style = JoinStyle::Miter;
  64. MiterParams miter_params;
  65. int cap_segments = 6;
  66. int join_segments = 4;
  67. };
  68. inline auto perp_ccw(const QVector2D &v) -> QVector2D {
  69. return QVector2D(-v.y(), v.x());
  70. }
  71. inline auto safe_normalize(const QVector2D &v,
  72. float epsilon = 1e-5F) -> QVector2D {
  73. const float len = v.length();
  74. if (len < epsilon) {
  75. return QVector2D(0.0F, 0.0F);
  76. }
  77. return v / len;
  78. }
  79. inline auto generate_round_cap(const QVector2D &center,
  80. const QVector2D &direction, float half_width,
  81. int segments,
  82. bool is_start) -> std::vector<QVector2D> {
  83. std::vector<QVector2D> verts;
  84. verts.reserve(static_cast<size_t>((segments + 1) * 2));
  85. const QVector2D perp = perp_ccw(direction) * half_width;
  86. const float pi = 3.14159265358979F;
  87. const float start_angle = is_start ? pi * 0.5F : -pi * 0.5F;
  88. const float end_angle = is_start ? -pi * 0.5F : pi * 0.5F;
  89. for (int i = 0; i <= segments; ++i) {
  90. const float t = static_cast<float>(i) / static_cast<float>(segments);
  91. const float angle = start_angle + t * (end_angle - start_angle);
  92. const float cos_a = std::cos(angle);
  93. const float sin_a = std::sin(angle);
  94. const QVector2D offset = perp * cos_a - direction * half_width * sin_a;
  95. verts.push_back(center);
  96. verts.push_back(center + offset);
  97. }
  98. return verts;
  99. }
  100. inline auto generate_square_cap(const QVector2D &center,
  101. const QVector2D &direction, float half_width,
  102. bool is_start) -> std::vector<QVector2D> {
  103. const QVector2D perp = perp_ccw(direction) * half_width;
  104. const QVector2D extension =
  105. direction * half_width * (is_start ? -1.0F : 1.0F);
  106. std::vector<QVector2D> verts;
  107. verts.reserve(4);
  108. verts.push_back(center + perp);
  109. verts.push_back(center - perp);
  110. verts.push_back(center + perp + extension);
  111. verts.push_back(center - perp + extension);
  112. return verts;
  113. }
  114. inline auto
  115. build_stroke_mesh(const std::vector<QVector2D> &points,
  116. const StrokeMeshConfig &config) -> std::vector<QVector2D> {
  117. std::vector<QVector2D> result;
  118. if (points.size() < 2 || config.width <= 0.0F) {
  119. return result;
  120. }
  121. std::vector<QVector2D> cleaned;
  122. cleaned.reserve(points.size());
  123. for (const auto &pt : points) {
  124. if (cleaned.empty()) {
  125. cleaned.push_back(pt);
  126. continue;
  127. }
  128. const QVector2D delta = pt - cleaned.back();
  129. if (QVector2D::dotProduct(delta, delta) > 1e-10F) {
  130. cleaned.push_back(pt);
  131. }
  132. }
  133. if (cleaned.size() < 2) {
  134. return result;
  135. }
  136. const float half_width = config.width * 0.5F;
  137. result.reserve(cleaned.size() * 2 + 32);
  138. if (config.start_cap == CapStyle::Round) {
  139. const QVector2D start_dir = safe_normalize(cleaned[1] - cleaned[0]);
  140. auto cap_verts = generate_round_cap(cleaned[0], start_dir, half_width,
  141. config.cap_segments, true);
  142. result.insert(result.end(), cap_verts.begin(), cap_verts.end());
  143. } else if (config.start_cap == CapStyle::Square) {
  144. const QVector2D start_dir = safe_normalize(cleaned[1] - cleaned[0]);
  145. auto cap_verts =
  146. generate_square_cap(cleaned[0], start_dir, half_width, true);
  147. result.insert(result.end(), cap_verts.begin(), cap_verts.end());
  148. }
  149. for (size_t i = 0; i < cleaned.size(); ++i) {
  150. QVector2D offset;
  151. if (i == 0) {
  152. const QVector2D dir = safe_normalize(cleaned[1] - cleaned[0]);
  153. offset = perp_ccw(dir) * half_width;
  154. } else if (i + 1 == cleaned.size()) {
  155. const QVector2D dir = safe_normalize(cleaned[i] - cleaned[i - 1]);
  156. offset = perp_ccw(dir) * half_width;
  157. } else {
  158. QVector2D dir0 = safe_normalize(cleaned[i] - cleaned[i - 1]);
  159. QVector2D dir1 = safe_normalize(cleaned[i + 1] - cleaned[i]);
  160. if (dir0.isNull() && dir1.isNull()) {
  161. dir0 = QVector2D(1.0F, 0.0F);
  162. dir1 = QVector2D(1.0F, 0.0F);
  163. } else if (dir0.isNull()) {
  164. dir0 = dir1;
  165. } else if (dir1.isNull()) {
  166. dir1 = dir0;
  167. }
  168. const QVector2D n0 = perp_ccw(dir0);
  169. const QVector2D n1 = perp_ccw(dir1);
  170. QVector2D miter = safe_normalize(n0 + n1);
  171. if (miter.isNull()) {
  172. miter = n1;
  173. }
  174. float denom = QVector2D::dotProduct(miter, n1);
  175. if (std::abs(denom) < config.miter_params.min_denom) {
  176. denom = (denom >= 0.0F) ? config.miter_params.min_denom
  177. : -config.miter_params.min_denom;
  178. }
  179. float miter_len = half_width / denom;
  180. const float max_len = half_width * config.miter_params.max_miter_ratio;
  181. if (std::abs(miter_len) > max_len) {
  182. miter_len = (miter_len < 0.0F) ? -max_len : max_len;
  183. }
  184. offset = miter * miter_len;
  185. }
  186. result.push_back(cleaned[i] + offset);
  187. result.push_back(cleaned[i] - offset);
  188. }
  189. if (config.end_cap == CapStyle::Round) {
  190. const QVector2D end_dir =
  191. safe_normalize(cleaned.back() - cleaned[cleaned.size() - 2]);
  192. auto cap_verts = generate_round_cap(cleaned.back(), end_dir, half_width,
  193. config.cap_segments, false);
  194. result.insert(result.end(), cap_verts.begin(), cap_verts.end());
  195. } else if (config.end_cap == CapStyle::Square) {
  196. const QVector2D end_dir =
  197. safe_normalize(cleaned.back() - cleaned[cleaned.size() - 2]);
  198. auto cap_verts =
  199. generate_square_cap(cleaned.back(), end_dir, half_width, false);
  200. result.insert(result.end(), cap_verts.begin(), cap_verts.end());
  201. }
  202. return result;
  203. }
  204. struct StrokePass {
  205. QVector4D color;
  206. float width_multiplier;
  207. float z_offset;
  208. };
  209. namespace CartographicStyles {
  210. inline auto get_inked_route_passes(float base_width, int age_factor = 0)
  211. -> std::vector<StrokePass> {
  212. const float fade = 1.0F - static_cast<float>(age_factor) * 0.08F;
  213. const float fade_clamped = std::max(0.3F, fade);
  214. return {
  215. {{0.12F * fade_clamped, 0.09F * fade_clamped, 0.07F * fade_clamped,
  216. 0.65F * fade_clamped},
  217. 1.3F,
  218. 0.000F},
  219. {{0.18F * fade_clamped, 0.14F * fade_clamped, 0.10F * fade_clamped,
  220. 0.55F * fade_clamped},
  221. 1.05F,
  222. 0.001F},
  223. {{0.70F * fade_clamped, 0.58F * fade_clamped, 0.32F * fade_clamped,
  224. 0.65F * fade_clamped},
  225. 0.8F,
  226. 0.002F},
  227. {{0.62F * fade_clamped, 0.22F * fade_clamped, 0.18F * fade_clamped,
  228. 0.75F * fade_clamped},
  229. 0.6F,
  230. 0.003F}};
  231. }
  232. inline auto get_coastline_passes(float base_width) -> std::vector<StrokePass> {
  233. return {
  234. {{0.12F, 0.10F, 0.08F, 0.95F}, 1.8F, 0.000F},
  235. {{0.25F, 0.22F, 0.18F, 0.85F}, 1.4F, 0.001F},
  236. {{0.55F, 0.50F, 0.42F, 0.75F}, 1.0F, 0.002F}};
  237. }
  238. inline auto get_border_passes(float base_width) -> std::vector<StrokePass> {
  239. return {
  240. {{0.18F, 0.15F, 0.12F, 0.55F}, 1.6F, 0.000F},
  241. {{0.32F, 0.28F, 0.24F, 0.70F}, 1.0F, 0.001F}};
  242. }
  243. inline auto get_river_passes(float base_width) -> std::vector<StrokePass> {
  244. return {
  245. {{0.25F, 0.32F, 0.40F, 0.75F}, 1.6F, 0.000F},
  246. {{0.35F, 0.48F, 0.58F, 0.90F}, 1.0F, 0.001F},
  247. {{0.55F, 0.68F, 0.78F, 0.50F}, 0.4F, 0.002F}};
  248. }
  249. } // namespace CartographicStyles
  250. inline auto compute_normal_from_heights(float h_left, float h_right,
  251. float h_down, float h_up,
  252. float scale = 1.0F) -> QVector3D {
  253. const float dx = (h_right - h_left) * scale;
  254. const float dz = (h_up - h_down) * scale;
  255. QVector3D normal(-dx, 2.0F, -dz);
  256. normal.normalize();
  257. return normal;
  258. }
  259. inline auto hash_2d(float x, float y) -> float {
  260. const float h = std::sin(x * 12.9898F + y * 78.233F) * 43758.5453123F;
  261. return h - std::floor(h);
  262. }
  263. inline auto value_noise_2d(float x, float y) -> float {
  264. const float ix = std::floor(x);
  265. const float iy = std::floor(y);
  266. const float fx = x - ix;
  267. const float fy = y - iy;
  268. const float sx = fx * fx * (3.0F - 2.0F * fx);
  269. const float sy = fy * fy * (3.0F - 2.0F * fy);
  270. const float c00 = hash_2d(ix, iy);
  271. const float c10 = hash_2d(ix + 1.0F, iy);
  272. const float c01 = hash_2d(ix, iy + 1.0F);
  273. const float c11 = hash_2d(ix + 1.0F, iy + 1.0F);
  274. const float x0 = c00 * (1.0F - sx) + c10 * sx;
  275. const float x1 = c01 * (1.0F - sx) + c11 * sx;
  276. return x0 * (1.0F - sy) + x1 * sy;
  277. }
  278. inline auto fbm_noise_2d(float x, float y, int octaves = 4,
  279. float lacunarity = 2.0F,
  280. float persistence = 0.5F) -> float {
  281. float value = 0.0F;
  282. float amplitude = 1.0F;
  283. float frequency = 1.0F;
  284. float max_value = 0.0F;
  285. for (int i = 0; i < octaves; ++i) {
  286. value += amplitude * value_noise_2d(x * frequency, y * frequency);
  287. max_value += amplitude;
  288. amplitude *= persistence;
  289. frequency *= lacunarity;
  290. }
  291. return value / max_value;
  292. }
  293. inline auto compute_hillshade(const QVector3D &normal,
  294. const QVector3D &light_dir,
  295. float ambient = 0.3F) -> float {
  296. const float ndotl = QVector3D::dotProduct(normal, light_dir);
  297. return ambient + (1.0F - ambient) * std::max(0.0F, ndotl);
  298. }
  299. inline auto elevation_to_tint(float elevation, bool is_water) -> QVector4D {
  300. if (is_water) {
  301. const float depth_factor =
  302. 1.0F - std::min(1.0F, std::abs(elevation) * 2.0F);
  303. return QVector4D(0.6F * depth_factor + 0.2F, 0.7F * depth_factor + 0.2F,
  304. 0.85F * depth_factor + 0.15F, 1.0F);
  305. }
  306. if (elevation < 0.2F) {
  307. return QVector4D(0.95F, 1.0F, 0.92F, 1.0F);
  308. }
  309. if (elevation < 0.5F) {
  310. const float t = (elevation - 0.2F) / 0.3F;
  311. return QVector4D(1.0F, 0.98F - t * 0.05F, 0.95F - t * 0.08F, 1.0F);
  312. }
  313. const float t = (elevation - 0.5F) / 0.5F;
  314. return QVector4D(0.95F - t * 0.1F, 0.88F - t * 0.15F, 0.82F - t * 0.12F,
  315. 1.0F);
  316. }
  317. inline auto parchment_pattern(float u, float v, float scale = 8.0F) -> float {
  318. const float n1 = fbm_noise_2d(u * scale, v * scale, 3, 2.0F, 0.5F);
  319. const float n2 = fbm_noise_2d(u * scale * 2.5F + 100.0F,
  320. v * scale * 2.5F + 100.0F, 2, 2.0F, 0.4F);
  321. const float combined = n1 * 0.6F + n2 * 0.4F;
  322. return 0.85F + combined * 0.15F;
  323. }
  324. namespace CinematicCameraDefaults {
  325. inline constexpr float k_default_yaw = 185.0F;
  326. inline constexpr float k_default_pitch = 52.0F;
  327. inline constexpr float k_default_distance = 1.35F;
  328. struct RegionFocus {
  329. float u;
  330. float v;
  331. float distance;
  332. float pitch;
  333. float yaw;
  334. };
  335. inline constexpr RegionFocus k_focus_carthage = {0.35F, 0.55F, 1.0F, 48.0F,
  336. 200.0F};
  337. inline constexpr RegionFocus k_focus_rome = {0.55F, 0.35F, 0.9F, 50.0F, 175.0F};
  338. inline constexpr RegionFocus k_focus_spain = {0.18F, 0.42F, 1.1F, 45.0F,
  339. 195.0F};
  340. inline constexpr RegionFocus k_focus_alps = {0.52F, 0.28F, 0.85F, 55.0F,
  341. 180.0F};
  342. inline constexpr RegionFocus k_focus_sicily = {0.58F, 0.48F, 0.75F, 52.0F,
  343. 185.0F};
  344. } // namespace CinematicCameraDefaults
  345. enum class BadgeStyle { Standard, Seal, Banner, Shield, Medallion };
  346. struct MissionBadgeConfig {
  347. BadgeStyle style = BadgeStyle::Standard;
  348. QVector4D primary_color{0.75F, 0.18F, 0.12F, 1.0F};
  349. QVector4D secondary_color{0.95F, 0.85F, 0.45F, 1.0F};
  350. QVector4D border_color{0.15F, 0.10F, 0.08F, 1.0F};
  351. float size = 24.0F;
  352. float border_width = 2.0F;
  353. bool show_shadow = true;
  354. float shadow_offset = 2.0F;
  355. float shadow_opacity = 0.4F;
  356. };
  357. inline auto generate_shield_badge(const QVector2D &center, float size,
  358. int segments = 16) -> std::vector<QVector2D> {
  359. std::vector<QVector2D> verts;
  360. verts.reserve(static_cast<size_t>(segments * 2 + 4));
  361. const float w = size * 0.5F;
  362. const float h = size * 0.6F;
  363. const float pi = 3.14159265358979F;
  364. verts.push_back(center + QVector2D(-w, -h * 0.4F));
  365. verts.push_back(center + QVector2D(w, -h * 0.4F));
  366. for (int i = 0; i <= segments / 2; ++i) {
  367. const float t = static_cast<float>(i) / static_cast<float>(segments / 2);
  368. const float angle = pi * 0.5F * t;
  369. const float x = w * std::cos(angle);
  370. const float y = -h * 0.4F + h * 0.9F * std::sin(angle) + h * 0.5F * t * t;
  371. verts.push_back(center + QVector2D(x, y));
  372. }
  373. verts.push_back(center + QVector2D(0.0F, h * 0.6F));
  374. for (int i = segments / 2; i >= 0; --i) {
  375. const float t = static_cast<float>(i) / static_cast<float>(segments / 2);
  376. const float angle = pi * 0.5F * t;
  377. const float x = -w * std::cos(angle);
  378. const float y = -h * 0.4F + h * 0.9F * std::sin(angle) + h * 0.5F * t * t;
  379. verts.push_back(center + QVector2D(x, y));
  380. }
  381. return verts;
  382. }
  383. inline auto generate_banner_badge(const QVector2D &center, float size,
  384. int segments = 12) -> std::vector<QVector2D> {
  385. std::vector<QVector2D> verts;
  386. const float w = size * 0.4F;
  387. const float h = size * 0.7F;
  388. verts.push_back(center + QVector2D(-w, -h * 0.5F));
  389. verts.push_back(center + QVector2D(w, -h * 0.5F));
  390. verts.push_back(center + QVector2D(w, h * 0.3F));
  391. verts.push_back(center + QVector2D(0.0F, h * 0.5F));
  392. verts.push_back(center + QVector2D(-w, h * 0.3F));
  393. return verts;
  394. }
  395. inline auto
  396. generate_medallion_badge(const QVector2D &center, float size,
  397. int segments = 24) -> std::vector<QVector2D> {
  398. std::vector<QVector2D> verts;
  399. verts.reserve(static_cast<size_t>(segments + 1));
  400. const float radius = size * 0.5F;
  401. const float pi = 3.14159265358979F;
  402. for (int i = 0; i <= segments; ++i) {
  403. const float angle =
  404. 2.0F * pi * static_cast<float>(i) / static_cast<float>(segments);
  405. verts.push_back(
  406. center + QVector2D(radius * std::cos(angle), radius * std::sin(angle)));
  407. }
  408. return verts;
  409. }
  410. enum class CartographicSymbol { Mountain, City, Port, Fort, Temple };
  411. inline auto generate_mountain_icon(const QVector2D &center, float size,
  412. int peaks = 2) -> std::vector<QVector2D> {
  413. std::vector<QVector2D> verts;
  414. const float h = size * 0.5F;
  415. const float w = size * 0.3F;
  416. if (peaks == 1) {
  417. verts.push_back(center + QVector2D(-w, h * 0.3F));
  418. verts.push_back(center + QVector2D(0.0F, -h * 0.5F));
  419. verts.push_back(center + QVector2D(w, h * 0.3F));
  420. } else if (peaks == 2) {
  421. verts.push_back(center + QVector2D(-w * 1.5F, h * 0.3F));
  422. verts.push_back(center + QVector2D(-w * 0.5F, -h * 0.4F));
  423. verts.push_back(center + QVector2D(0.0F, h * 0.1F));
  424. verts.push_back(center + QVector2D(w * 0.5F, -h * 0.5F));
  425. verts.push_back(center + QVector2D(w * 1.5F, h * 0.3F));
  426. } else {
  427. verts.push_back(center + QVector2D(-w * 2.0F, h * 0.3F));
  428. verts.push_back(center + QVector2D(-w, -h * 0.35F));
  429. verts.push_back(center + QVector2D(-w * 0.3F, h * 0.1F));
  430. verts.push_back(center + QVector2D(0.0F, -h * 0.5F));
  431. verts.push_back(center + QVector2D(w * 0.3F, h * 0.0F));
  432. verts.push_back(center + QVector2D(w, -h * 0.4F));
  433. verts.push_back(center + QVector2D(w * 2.0F, h * 0.3F));
  434. }
  435. return verts;
  436. }
  437. inline auto generate_city_marker(const QVector2D &center, float size,
  438. int importance = 1) -> std::vector<QVector2D> {
  439. std::vector<QVector2D> verts;
  440. const float h = size * 0.5F;
  441. const float w = size * 0.2F;
  442. verts.push_back(center + QVector2D(-w * 2.0F, h * 0.3F));
  443. verts.push_back(center + QVector2D(w * 2.0F, h * 0.3F));
  444. if (importance >= 2) {
  445. verts.push_back(center + QVector2D(w * 2.0F, -h * 0.2F));
  446. verts.push_back(center + QVector2D(w * 1.5F, -h * 0.2F));
  447. verts.push_back(center + QVector2D(w * 1.5F, -h * 0.5F));
  448. verts.push_back(center + QVector2D(w * 0.5F, -h * 0.5F));
  449. verts.push_back(center + QVector2D(w * 0.5F, -h * 0.3F));
  450. verts.push_back(center + QVector2D(-w * 0.5F, -h * 0.3F));
  451. verts.push_back(center + QVector2D(-w * 0.5F, -h * 0.6F));
  452. verts.push_back(center + QVector2D(-w * 1.5F, -h * 0.6F));
  453. verts.push_back(center + QVector2D(-w * 1.5F, -h * 0.2F));
  454. verts.push_back(center + QVector2D(-w * 2.0F, -h * 0.2F));
  455. } else {
  456. verts.push_back(center + QVector2D(w * 2.0F, -h * 0.1F));
  457. verts.push_back(center + QVector2D(w, -h * 0.1F));
  458. verts.push_back(center + QVector2D(w, -h * 0.4F));
  459. verts.push_back(center + QVector2D(-w, -h * 0.4F));
  460. verts.push_back(center + QVector2D(-w, -h * 0.1F));
  461. verts.push_back(center + QVector2D(-w * 2.0F, -h * 0.1F));
  462. }
  463. return verts;
  464. }
  465. inline auto generate_anchor_icon(const QVector2D &center,
  466. float size) -> std::vector<QVector2D> {
  467. std::vector<QVector2D> verts;
  468. const float h = size * 0.5F;
  469. const float w = size * 0.4F;
  470. verts.push_back(center + QVector2D(0.0F, -h * 0.6F));
  471. verts.push_back(center + QVector2D(0.0F, h * 0.4F));
  472. const float ring_r = size * 0.12F;
  473. const int ring_segs = 8;
  474. const float pi = 3.14159265358979F;
  475. const QVector2D ring_center = center + QVector2D(0.0F, -h * 0.6F - ring_r);
  476. for (int i = 0; i <= ring_segs; ++i) {
  477. const float angle =
  478. 2.0F * pi * static_cast<float>(i) / static_cast<float>(ring_segs);
  479. verts.push_back(ring_center + QVector2D(ring_r * std::cos(angle),
  480. ring_r * std::sin(angle)));
  481. }
  482. verts.push_back(center + QVector2D(-w * 0.6F, -h * 0.2F));
  483. verts.push_back(center + QVector2D(w * 0.6F, -h * 0.2F));
  484. verts.push_back(center + QVector2D(-w, h * 0.1F));
  485. verts.push_back(center + QVector2D(0.0F, h * 0.4F));
  486. verts.push_back(center + QVector2D(w, h * 0.1F));
  487. return verts;
  488. }
  489. struct MediterraneanTerrainConfig {
  490. static constexpr float alps_u_min = 0.48F;
  491. static constexpr float alps_u_max = 0.58F;
  492. static constexpr float alps_v_min = 0.22F;
  493. static constexpr float alps_v_max = 0.32F;
  494. static constexpr float alps_height = 0.85F;
  495. static constexpr float pyrenees_u_min = 0.20F;
  496. static constexpr float pyrenees_u_max = 0.32F;
  497. static constexpr float pyrenees_v_min = 0.30F;
  498. static constexpr float pyrenees_v_max = 0.38F;
  499. static constexpr float pyrenees_height = 0.65F;
  500. static constexpr float apennines_u_min = 0.52F;
  501. static constexpr float apennines_u_max = 0.62F;
  502. static constexpr float apennines_v_min = 0.35F;
  503. static constexpr float apennines_v_max = 0.55F;
  504. static constexpr float apennines_height = 0.55F;
  505. static constexpr float atlas_u_min = 0.30F;
  506. static constexpr float atlas_u_max = 0.55F;
  507. static constexpr float atlas_v_min = 0.62F;
  508. static constexpr float atlas_v_max = 0.72F;
  509. static constexpr float atlas_height = 0.60F;
  510. static constexpr float sea_level = 0.0F;
  511. static constexpr float max_depth = -0.35F;
  512. };
  513. inline auto compute_mountain_contribution(float u, float v, float u_min,
  514. float u_max, float v_min, float v_max,
  515. float peak_height) -> float {
  516. if (u < u_min || u > u_max || v < v_min || v > v_max) {
  517. return 0.0F;
  518. }
  519. float dist_u =
  520. 1.0F - 2.0F * std::abs(u - (u_min + u_max) * 0.5F) / (u_max - u_min);
  521. float dist_v =
  522. 1.0F - 2.0F * std::abs(v - (v_min + v_max) * 0.5F) / (v_max - v_min);
  523. float falloff = dist_u * dist_v;
  524. falloff = falloff * falloff;
  525. return peak_height * falloff;
  526. }
  527. inline auto generate_terrain_height(float u, float v) -> float {
  528. using Config = MediterraneanTerrainConfig;
  529. float height = 0.05F;
  530. height += compute_mountain_contribution(
  531. u, v, Config::alps_u_min, Config::alps_u_max, Config::alps_v_min,
  532. Config::alps_v_max, Config::alps_height);
  533. height += compute_mountain_contribution(
  534. u, v, Config::pyrenees_u_min, Config::pyrenees_u_max,
  535. Config::pyrenees_v_min, Config::pyrenees_v_max, Config::pyrenees_height);
  536. height += compute_mountain_contribution(
  537. u, v, Config::apennines_u_min, Config::apennines_u_max,
  538. Config::apennines_v_min, Config::apennines_v_max,
  539. Config::apennines_height);
  540. height += compute_mountain_contribution(
  541. u, v, Config::atlas_u_min, Config::atlas_u_max, Config::atlas_v_min,
  542. Config::atlas_v_max, Config::atlas_height);
  543. float noise = fbm_noise_2d(u * 8.0F, v * 8.0F, 4, 2.0F, 0.5F);
  544. height += (noise - 0.5F) * 0.15F;
  545. return height;
  546. }
  547. inline auto compute_terrain_normal(float u, float v,
  548. float sample_dist = 0.01F) -> QVector3D {
  549. float h_left = generate_terrain_height(u - sample_dist, v);
  550. float h_right = generate_terrain_height(u + sample_dist, v);
  551. float h_down = generate_terrain_height(u, v - sample_dist);
  552. float h_up = generate_terrain_height(u, v + sample_dist);
  553. float dx = (h_right - h_left) / (2.0F * sample_dist);
  554. float dz = (h_up - h_down) / (2.0F * sample_dist);
  555. QVector3D normal(-dx, 1.0F, -dz);
  556. normal.normalize();
  557. return normal;
  558. }
  559. inline auto
  560. generate_terrain_mesh(int resolution = 64,
  561. float height_scale = 0.05F) -> std::vector<float> {
  562. std::vector<float> vertices;
  563. const int vertex_floats = 8;
  564. vertices.reserve(
  565. static_cast<size_t>(resolution * resolution * 6 * vertex_floats));
  566. const float step = 1.0F / static_cast<float>(resolution - 1);
  567. for (int y = 0; y < resolution - 1; ++y) {
  568. for (int x = 0; x < resolution - 1; ++x) {
  569. float u0 = static_cast<float>(x) * step;
  570. float v0 = static_cast<float>(y) * step;
  571. float u1 = static_cast<float>(x + 1) * step;
  572. float v1 = static_cast<float>(y + 1) * step;
  573. float h00 = generate_terrain_height(u0, v0) * height_scale;
  574. float h10 = generate_terrain_height(u1, v0) * height_scale;
  575. float h01 = generate_terrain_height(u0, v1) * height_scale;
  576. float h11 = generate_terrain_height(u1, v1) * height_scale;
  577. QVector3D n00 = compute_terrain_normal(u0, v0);
  578. QVector3D n10 = compute_terrain_normal(u1, v0);
  579. QVector3D n01 = compute_terrain_normal(u0, v1);
  580. QVector3D n11 = compute_terrain_normal(u1, v1);
  581. auto add_vertex = [&](float u, float v, float h, const QVector3D &n) {
  582. vertices.push_back(u);
  583. vertices.push_back(v);
  584. vertices.push_back(u);
  585. vertices.push_back(v);
  586. vertices.push_back(h);
  587. vertices.push_back(n.x());
  588. vertices.push_back(n.y());
  589. vertices.push_back(n.z());
  590. };
  591. add_vertex(u0, v0, h00, n00);
  592. add_vertex(u1, v0, h10, n10);
  593. add_vertex(u0, v1, h01, n01);
  594. add_vertex(u1, v0, h10, n10);
  595. add_vertex(u1, v1, h11, n11);
  596. add_vertex(u0, v1, h01, n01);
  597. }
  598. }
  599. return vertices;
  600. }
  601. struct HillshadeConfig {
  602. QVector3D light_direction{0.35F, 0.85F, 0.40F};
  603. float ambient = 0.25F;
  604. float intensity = 1.0F;
  605. float z_factor = 2.5F;
  606. };
  607. inline auto compute_hillshade_at(float u, float v,
  608. const HillshadeConfig &config) -> float {
  609. QVector3D normal = compute_terrain_normal(u, v, 0.005F);
  610. normal.setY(normal.y() * config.z_factor);
  611. normal.normalize();
  612. QVector3D light = config.light_direction.normalized();
  613. float shade = QVector3D::dotProduct(normal, light);
  614. shade = config.ambient + (1.0F - config.ambient) * std::max(0.0F, shade);
  615. return std::min(1.0F, shade * config.intensity);
  616. }
  617. inline auto
  618. generate_hillshade_texture(int width, int height,
  619. const HillshadeConfig &config = HillshadeConfig{})
  620. -> std::vector<unsigned char> {
  621. std::vector<unsigned char> pixels;
  622. pixels.reserve(static_cast<size_t>(width * height * 4));
  623. for (int y = 0; y < height; ++y) {
  624. for (int x = 0; x < width; ++x) {
  625. float u = static_cast<float>(x) / static_cast<float>(width - 1);
  626. float v = static_cast<float>(y) / static_cast<float>(height - 1);
  627. float shade = compute_hillshade_at(u, v, config);
  628. auto byte_val = static_cast<unsigned char>(shade * 255.0F);
  629. pixels.push_back(byte_val);
  630. pixels.push_back(byte_val);
  631. pixels.push_back(byte_val);
  632. pixels.push_back(255);
  633. }
  634. }
  635. return pixels;
  636. }
  637. struct GlyphMetrics {
  638. float advance;
  639. float bearing_x;
  640. float bearing_y;
  641. float width;
  642. float height;
  643. float uv_x;
  644. float uv_y;
  645. float uv_w;
  646. float uv_h;
  647. };
  648. struct LabelStyle {
  649. float font_size = 14.0F;
  650. QVector4D fill_color{0.18F, 0.14F, 0.10F, 1.0F};
  651. QVector4D stroke_color{0.95F, 0.92F, 0.88F, 0.85F};
  652. float stroke_width = 1.5F;
  653. bool use_small_caps = true;
  654. float letter_spacing = 0.05F;
  655. float line_height = 1.2F;
  656. };
  657. namespace LabelStyles {
  658. inline auto province_label() -> LabelStyle {
  659. return {.font_size = 12.0F,
  660. .fill_color = QVector4D(0.25F, 0.20F, 0.15F, 0.95F),
  661. .stroke_color = QVector4D(0.98F, 0.96F, 0.92F, 0.75F),
  662. .stroke_width = 1.2F,
  663. .use_small_caps = true,
  664. .letter_spacing = 0.08F,
  665. .line_height = 1.15F};
  666. }
  667. inline auto city_label() -> LabelStyle {
  668. return {.font_size = 10.0F,
  669. .fill_color = QVector4D(0.30F, 0.25F, 0.18F, 0.90F),
  670. .stroke_color = QVector4D(0.98F, 0.96F, 0.92F, 0.70F),
  671. .stroke_width = 1.0F,
  672. .use_small_caps = false,
  673. .letter_spacing = 0.03F,
  674. .line_height = 1.1F};
  675. }
  676. inline auto region_label() -> LabelStyle {
  677. return {.font_size = 16.0F,
  678. .fill_color = QVector4D(0.20F, 0.16F, 0.12F, 1.0F),
  679. .stroke_color = QVector4D(0.95F, 0.92F, 0.88F, 0.80F),
  680. .stroke_width = 2.0F,
  681. .use_small_caps = true,
  682. .letter_spacing = 0.12F,
  683. .line_height = 1.25F};
  684. }
  685. inline auto sea_label() -> LabelStyle {
  686. return {.font_size = 11.0F,
  687. .fill_color = QVector4D(0.25F, 0.38F, 0.50F, 0.85F),
  688. .stroke_color = QVector4D(0.92F, 0.95F, 0.98F, 0.65F),
  689. .stroke_width = 1.0F,
  690. .use_small_caps = true,
  691. .letter_spacing = 0.15F,
  692. .line_height = 1.2F};
  693. }
  694. } // namespace LabelStyles
  695. inline auto
  696. generate_label_quads(const QVector2D &position, const std::string &text,
  697. const LabelStyle &style,
  698. float base_font_size = 12.0F) -> std::vector<float> {
  699. std::vector<float> vertices;
  700. if (text.empty()) {
  701. return vertices;
  702. }
  703. const float scale = style.font_size / base_font_size;
  704. const float char_width = 0.006F * scale;
  705. const float char_height = 0.012F * scale;
  706. const float spacing = char_width * style.letter_spacing;
  707. const float total_width =
  708. static_cast<float>(text.length()) * (char_width + spacing);
  709. float x_offset = -total_width * 0.5F;
  710. for (size_t i = 0; i < text.length(); ++i) {
  711. char c = text[i];
  712. if (c == ' ') {
  713. x_offset += char_width * 0.5F;
  714. continue;
  715. }
  716. float atlas_u = static_cast<float>(c % 16) / 16.0F;
  717. float atlas_v = static_cast<float>(c / 16) / 16.0F;
  718. float atlas_w = 1.0F / 16.0F;
  719. float atlas_h = 1.0F / 16.0F;
  720. float x0 = position.x() + x_offset;
  721. float y0 = position.y() - char_height * 0.5F;
  722. float x1 = x0 + char_width;
  723. float y1 = y0 + char_height;
  724. vertices.insert(vertices.end(), {x0, y0, atlas_u, atlas_v, 0.0F, 0.0F});
  725. vertices.insert(vertices.end(),
  726. {x1, y0, atlas_u + atlas_w, atlas_v, 1.0F, 0.0F});
  727. vertices.insert(vertices.end(),
  728. {x0, y1, atlas_u, atlas_v + atlas_h, 0.0F, 1.0F});
  729. vertices.insert(vertices.end(),
  730. {x1, y0, atlas_u + atlas_w, atlas_v, 1.0F, 0.0F});
  731. vertices.insert(vertices.end(),
  732. {x1, y1, atlas_u + atlas_w, atlas_v + atlas_h, 1.0F, 1.0F});
  733. vertices.insert(vertices.end(),
  734. {x0, y1, atlas_u, atlas_v + atlas_h, 0.0F, 1.0F});
  735. x_offset += char_width + spacing;
  736. }
  737. return vertices;
  738. }
  739. inline auto compute_label_scale(float viewport_height, float camera_distance,
  740. float base_size = 12.0F) -> float {
  741. const float fov_rad = 0.7854F;
  742. const float view_height = 2.0F * camera_distance * std::tan(fov_rad * 0.5F);
  743. const float px_to_uv = view_height / viewport_height;
  744. return base_size * px_to_uv;
  745. }
  746. } // namespace CampaignMapRender