knight_renderer.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978
  1. #include "knight_renderer.h"
  2. #include "../../../../game/core/component.h"
  3. #include "../../../geom/math_utils.h"
  4. #include "../../../geom/transforms.h"
  5. #include "../../../gl/backend.h"
  6. #include "../../../gl/primitives.h"
  7. #include "../../../gl/shader.h"
  8. #include "../../../humanoid/rig.h"
  9. #include "../../../humanoid/style_palette.h"
  10. #include "../../../humanoid_math.h"
  11. #include "../../../humanoid_specs.h"
  12. #include "../../../palette.h"
  13. #include "../../../scene_renderer.h"
  14. #include "../../../submitter.h"
  15. #include "../../registry.h"
  16. #include "../../renderer_constants.h"
  17. #include "knight_style.h"
  18. #include <numbers>
  19. #include <qmatrix4x4.h>
  20. #include <qstringliteral.h>
  21. #include <qvectornd.h>
  22. #include <unordered_map>
  23. #include <QMatrix4x4>
  24. #include <QString>
  25. #include <QVector3D>
  26. #include <algorithm>
  27. #include <cmath>
  28. #include <cstdint>
  29. #include <optional>
  30. #include <string>
  31. #include <string_view>
  32. namespace Render::GL::Kingdom {
  33. namespace {
  34. constexpr std::string_view k_knight_default_style_key = "default";
  35. constexpr float k_knight_team_mix_weight = 0.6F;
  36. constexpr float k_knight_style_mix_weight = 0.4F;
  37. auto knight_style_registry()
  38. -> std::unordered_map<std::string, KnightStyleConfig> & {
  39. static std::unordered_map<std::string, KnightStyleConfig> styles;
  40. return styles;
  41. }
  42. void ensure_knight_styles_registered() {
  43. static const bool registered = []() {
  44. register_kingdom_knight_style();
  45. return true;
  46. }();
  47. (void)registered;
  48. }
  49. } // namespace
  50. void register_knight_style(const std::string &nation_id,
  51. const KnightStyleConfig &style) {
  52. knight_style_registry()[nation_id] = style;
  53. }
  54. using Render::Geom::clamp01;
  55. using Render::Geom::clampf;
  56. using Render::Geom::coneFromTo;
  57. using Render::Geom::cylinderBetween;
  58. using Render::Geom::easeInOutCubic;
  59. using Render::Geom::lerp;
  60. using Render::Geom::nlerp;
  61. using Render::Geom::smoothstep;
  62. using Render::Geom::sphereAt;
  63. using Render::GL::Humanoid::mix_palette_color;
  64. using Render::GL::Humanoid::saturate_color;
  65. struct KnightExtras {
  66. QVector3D metalColor;
  67. QVector3D shieldColor;
  68. QVector3D shieldTrimColor;
  69. float swordLength = 0.80F;
  70. float swordWidth = 0.065F;
  71. float shieldRadius = 0.18F;
  72. float shieldAspect = 1.0F;
  73. float guard_half_width = 0.12F;
  74. float handleRadius = 0.016F;
  75. float pommelRadius = 0.045F;
  76. float bladeRicasso = 0.16F;
  77. float bladeTaperBias = 0.65F;
  78. bool shieldCrossDecal = false;
  79. bool hasScabbard = true;
  80. };
  81. class KnightRenderer : public HumanoidRendererBase {
  82. public:
  83. auto getProportionScaling() const -> QVector3D override {
  84. return {1.40F, 1.05F, 1.10F};
  85. }
  86. private:
  87. mutable std::unordered_map<uint32_t, KnightExtras> m_extrasCache;
  88. public:
  89. void getVariant(const DrawContext &ctx, uint32_t seed,
  90. HumanoidVariant &v) const override {
  91. QVector3D const team_tint = resolveTeamTint(ctx);
  92. v.palette = makeHumanoidPalette(team_tint, seed);
  93. auto const &style = resolve_style(ctx);
  94. apply_palette_overrides(style, team_tint, v);
  95. }
  96. void customizePose(const DrawContext &,
  97. const HumanoidAnimationContext &anim_ctx, uint32_t seed,
  98. HumanoidPose &pose) const override {
  99. using HP = HumanProportions;
  100. const AnimationInputs &anim = anim_ctx.inputs;
  101. float const arm_height_jitter = (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.03F;
  102. float const arm_asymmetry = (hash_01(seed ^ 0xDEF0U) - 0.5F) * 0.04F;
  103. if (anim.is_attacking && anim.isMelee) {
  104. float const attack_phase =
  105. std::fmod(anim.time * KNIGHT_INV_ATTACK_CYCLE_TIME, 1.0F);
  106. QVector3D const rest_pos(0.20F, HP::SHOULDER_Y + 0.05F, 0.15F);
  107. QVector3D const prepare_pos(0.26F, HP::HEAD_TOP_Y + 0.18F, -0.06F);
  108. QVector3D const raised_pos(0.25F, HP::HEAD_TOP_Y + 0.22F, 0.02F);
  109. QVector3D const strike_pos(0.30F, HP::WAIST_Y - 0.05F, 0.50F);
  110. QVector3D const recover_pos(0.22F, HP::SHOULDER_Y + 0.02F, 0.22F);
  111. if (attack_phase < 0.18F) {
  112. float const t = easeInOutCubic(attack_phase / 0.18F);
  113. pose.hand_r = rest_pos * (1.0F - t) + prepare_pos * t;
  114. pose.handL =
  115. QVector3D(-0.21F, HP::SHOULDER_Y - 0.02F - 0.03F * t, 0.15F);
  116. } else if (attack_phase < 0.32F) {
  117. float const t = easeInOutCubic((attack_phase - 0.18F) / 0.14F);
  118. pose.hand_r = prepare_pos * (1.0F - t) + raised_pos * t;
  119. pose.handL = QVector3D(-0.21F, HP::SHOULDER_Y - 0.05F, 0.17F);
  120. } else if (attack_phase < 0.52F) {
  121. float t = (attack_phase - 0.32F) / 0.20F;
  122. t = t * t * t;
  123. pose.hand_r = raised_pos * (1.0F - t) + strike_pos * t;
  124. pose.handL =
  125. QVector3D(-0.21F, HP::SHOULDER_Y - 0.03F * (1.0F - 0.5F * t),
  126. 0.17F + 0.20F * t);
  127. } else if (attack_phase < 0.72F) {
  128. float const t = easeInOutCubic((attack_phase - 0.52F) / 0.20F);
  129. pose.hand_r = strike_pos * (1.0F - t) + recover_pos * t;
  130. pose.handL = QVector3D(-0.20F, HP::SHOULDER_Y - 0.015F * (1.0F - t),
  131. lerp(0.37F, 0.20F, t));
  132. } else {
  133. float const t = smoothstep(0.72F, 1.0F, attack_phase);
  134. pose.hand_r = recover_pos * (1.0F - t) + rest_pos * t;
  135. pose.handL = QVector3D(-0.20F - 0.02F * (1.0F - t),
  136. HP::SHOULDER_Y + arm_height_jitter * (1.0F - t),
  137. lerp(0.20F, 0.15F, t));
  138. }
  139. } else {
  140. pose.hand_r =
  141. QVector3D(0.30F + arm_asymmetry,
  142. HP::SHOULDER_Y - 0.02F + arm_height_jitter, 0.35F);
  143. pose.handL = QVector3D(-0.22F - 0.5F * arm_asymmetry,
  144. HP::SHOULDER_Y + 0.5F * arm_height_jitter, 0.18F);
  145. }
  146. }
  147. void addAttachments(const DrawContext &ctx, const HumanoidVariant &v,
  148. const HumanoidPose &pose,
  149. const HumanoidAnimationContext &anim_ctx,
  150. ISubmitter &out) const override {
  151. const AnimationInputs &anim = anim_ctx.inputs;
  152. uint32_t const seed = reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU;
  153. auto const &style = resolve_style(ctx);
  154. QVector3D const team_tint = resolveTeamTint(ctx);
  155. KnightExtras extras;
  156. auto it = m_extrasCache.find(seed);
  157. if (it != m_extrasCache.end()) {
  158. extras = it->second;
  159. } else {
  160. extras = computeKnightExtras(seed, v);
  161. apply_extras_overrides(style, team_tint, v, extras);
  162. m_extrasCache[seed] = extras;
  163. if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
  164. m_extrasCache.clear();
  165. }
  166. }
  167. apply_extras_overrides(style, team_tint, v, extras);
  168. bool const is_attacking = anim.is_attacking && anim.isMelee;
  169. float attack_phase = 0.0F;
  170. if (is_attacking) {
  171. attack_phase = std::fmod(anim.time * KNIGHT_INV_ATTACK_CYCLE_TIME, 1.0F);
  172. }
  173. drawSword(ctx, pose, v, extras, is_attacking, attack_phase, out);
  174. drawShield(ctx, pose, v, extras, out);
  175. if (!is_attacking && extras.hasScabbard) {
  176. drawScabbard(ctx, pose, v, extras, out);
  177. }
  178. }
  179. void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
  180. const HumanoidPose &pose, ISubmitter &out) const override {
  181. using HP = HumanProportions;
  182. auto ring = [&](const QVector3D &center, float r, float h,
  183. const QVector3D &col) {
  184. QVector3D const a = center + QVector3D(0, h * 0.5F, 0);
  185. QVector3D const b = center - QVector3D(0, h * 0.5F, 0);
  186. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r), col,
  187. nullptr, 1.0F);
  188. };
  189. QVector3D const steel_color =
  190. v.palette.metal * QVector3D(0.95F, 0.96F, 1.0F);
  191. float helm_r = pose.headR * 1.15F;
  192. QVector3D const helm_bot(0, pose.headPos.y() - pose.headR * 0.20F, 0);
  193. QVector3D const helm_top(0, pose.headPos.y() + pose.headR * 1.40F, 0);
  194. out.mesh(getUnitCylinder(),
  195. cylinderBetween(ctx.model, helm_bot, helm_top, helm_r),
  196. steel_color, nullptr, 1.0F);
  197. QVector3D const cap_top(0, pose.headPos.y() + pose.headR * 1.48F, 0);
  198. out.mesh(getUnitCylinder(),
  199. cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.98F),
  200. steel_color * 1.05F, nullptr, 1.0F);
  201. ring(QVector3D(0, pose.headPos.y() + pose.headR * 1.25F, 0), helm_r * 1.02F,
  202. 0.015F, steel_color * 1.08F);
  203. ring(QVector3D(0, pose.headPos.y() + pose.headR * 0.50F, 0), helm_r * 1.02F,
  204. 0.015F, steel_color * 1.08F);
  205. ring(QVector3D(0, pose.headPos.y() - pose.headR * 0.05F, 0), helm_r * 1.02F,
  206. 0.015F, steel_color * 1.08F);
  207. float const visor_y = pose.headPos.y() + pose.headR * 0.15F;
  208. float const visor_z = helm_r * 0.72F;
  209. QVector3D const visor_hl(-helm_r * 0.35F, visor_y, visor_z);
  210. QVector3D const visor_hr(helm_r * 0.35F, visor_y, visor_z);
  211. out.mesh(getUnitCylinder(),
  212. cylinderBetween(ctx.model, visor_hl, visor_hr, 0.012F),
  213. QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
  214. QVector3D const visor_vt(0, visor_y + helm_r * 0.25F, visor_z);
  215. QVector3D const visor_vb(0, visor_y - helm_r * 0.25F, visor_z);
  216. out.mesh(getUnitCylinder(),
  217. cylinderBetween(ctx.model, visor_vb, visor_vt, 0.012F),
  218. QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
  219. auto draw_breathing_hole = [&](float x, float y) {
  220. QVector3D const pos(x, pose.headPos.y() + y, helm_r * 0.70F);
  221. QMatrix4x4 m = ctx.model;
  222. m.translate(pos);
  223. m.scale(0.010F);
  224. out.mesh(getUnitSphere(), m, QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
  225. };
  226. for (int i = 0; i < 4; ++i) {
  227. draw_breathing_hole(helm_r * 0.50F, pose.headR * (0.05F - i * 0.10F));
  228. }
  229. for (int i = 0; i < 4; ++i) {
  230. draw_breathing_hole(-helm_r * 0.50F, pose.headR * (0.05F - i * 0.10F));
  231. }
  232. QVector3D const cross_center(0, pose.headPos.y() + pose.headR * 0.60F,
  233. helm_r * 0.75F);
  234. QVector3D const brass_color = v.palette.metal * QVector3D(1.3F, 1.1F, 0.7F);
  235. QVector3D const cross_h1 = cross_center + QVector3D(-0.04F, 0, 0);
  236. QVector3D const cross_h2 = cross_center + QVector3D(0.04F, 0, 0);
  237. out.mesh(getUnitCylinder(),
  238. cylinderBetween(ctx.model, cross_h1, cross_h2, 0.008F),
  239. brass_color, nullptr, 1.0F);
  240. QVector3D const cross_v1 = cross_center + QVector3D(0, -0.04F, 0);
  241. QVector3D const cross_v2 = cross_center + QVector3D(0, 0.04F, 0);
  242. out.mesh(getUnitCylinder(),
  243. cylinderBetween(ctx.model, cross_v1, cross_v2, 0.008F),
  244. brass_color, nullptr, 1.0F);
  245. }
  246. void draw_armorOverlay(const DrawContext &ctx, const HumanoidVariant &v,
  247. const HumanoidPose &pose, float y_top_cover,
  248. float torso_r, float, float upper_arm_r,
  249. const QVector3D &right_axis,
  250. ISubmitter &out) const override {
  251. using HP = HumanProportions;
  252. auto ring = [&](const QVector3D &center, float r, float h,
  253. const QVector3D &col) {
  254. QVector3D const a = center + QVector3D(0, h * 0.5F, 0);
  255. QVector3D const b = center - QVector3D(0, h * 0.5F, 0);
  256. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r), col,
  257. nullptr, 1.0F);
  258. };
  259. QVector3D steel_color = v.palette.metal * QVector3D(0.95F, 0.96F, 1.0F);
  260. QVector3D const dark_steel = steel_color * 0.85F;
  261. QVector3D brass_color = v.palette.metal * QVector3D(1.3F, 1.1F, 0.7F);
  262. QVector3D const bp_top(0, y_top_cover + 0.02F, 0);
  263. QVector3D const bp_mid(0, (y_top_cover + HP::WAIST_Y) * 0.5F + 0.04F, 0);
  264. QVector3D const bp_bot(0, HP::WAIST_Y + 0.06F, 0);
  265. float const r_chest = torso_r * 1.18F;
  266. float const r_waist = torso_r * 1.14F;
  267. out.mesh(getUnitCylinder(),
  268. cylinderBetween(ctx.model, bp_top, bp_mid, r_chest), steel_color,
  269. nullptr, 1.0F);
  270. QVector3D const bp_mid_low(0, (bp_mid.y() + bp_bot.y()) * 0.5F, 0);
  271. out.mesh(getUnitCylinder(),
  272. cylinderBetween(ctx.model, bp_mid, bp_mid_low, r_chest * 0.98F),
  273. steel_color * 0.99F, nullptr, 1.0F);
  274. out.mesh(getUnitCone(), coneFromTo(ctx.model, bp_bot, bp_mid_low, r_waist),
  275. steel_color * 0.98F, nullptr, 1.0F);
  276. auto draw_rivet = [&](const QVector3D &pos) {
  277. QMatrix4x4 m = ctx.model;
  278. m.translate(pos);
  279. m.scale(0.012F);
  280. out.mesh(getUnitSphere(), m, brass_color, nullptr, 1.0F);
  281. };
  282. for (int i = 0; i < 8; ++i) {
  283. float const angle = (i / 8.0F) * 2.0F * std::numbers::pi_v<float>;
  284. float const x = r_chest * std::sin(angle) * 0.95F;
  285. float const z = r_chest * std::cos(angle) * 0.95F;
  286. draw_rivet(QVector3D(x, bp_mid.y() + 0.08F, z));
  287. }
  288. auto draw_pauldron = [&](const QVector3D &shoulder,
  289. const QVector3D &outward) {
  290. for (int i = 0; i < 4; ++i) {
  291. float const seg_y = shoulder.y() + 0.04F - i * 0.045F;
  292. float const seg_r = upper_arm_r * (2.5F - i * 0.12F);
  293. QVector3D seg_pos = shoulder + outward * (0.02F + i * 0.008F);
  294. seg_pos.setY(seg_y);
  295. out.mesh(getUnitSphere(), sphereAt(ctx.model, seg_pos, seg_r),
  296. i == 0 ? steel_color * 1.05F
  297. : steel_color * (1.0F - i * 0.03F),
  298. nullptr, 1.0F);
  299. if (i < 3) {
  300. draw_rivet(seg_pos + QVector3D(0, 0.015F, 0.03F));
  301. }
  302. }
  303. };
  304. draw_pauldron(pose.shoulderL, -right_axis);
  305. draw_pauldron(pose.shoulderR, right_axis);
  306. auto draw_arm_plate = [&](const QVector3D &shoulder,
  307. const QVector3D &elbow) {
  308. QVector3D dir = (elbow - shoulder);
  309. float const len = dir.length();
  310. if (len < 1e-5F) {
  311. return;
  312. }
  313. dir /= len;
  314. for (int i = 0; i < 3; ++i) {
  315. float const t0 = 0.10F + i * 0.25F;
  316. float const t1 = t0 + 0.22F;
  317. QVector3D const a = shoulder + dir * (t0 * len);
  318. QVector3D const b = shoulder + dir * (t1 * len);
  319. float const r = upper_arm_r * (1.32F - i * 0.04F);
  320. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r),
  321. steel_color * (0.98F - i * 0.02F), nullptr, 1.0F);
  322. if (i < 2) {
  323. draw_rivet(b);
  324. }
  325. }
  326. };
  327. draw_arm_plate(pose.shoulderL, pose.elbowL);
  328. draw_arm_plate(pose.shoulderR, pose.elbowR);
  329. for (int i = 0; i < 4; ++i) {
  330. float const y0 = HP::WAIST_Y + 0.04F - i * 0.038F;
  331. float const y1 = y0 - 0.032F;
  332. float const r0 = r_waist * (1.06F + i * 0.025F);
  333. out.mesh(
  334. getUnitCone(),
  335. coneFromTo(ctx.model, QVector3D(0, y0, 0), QVector3D(0, y1, 0), r0),
  336. steel_color * (0.96F - i * 0.02F), nullptr, 1.0F);
  337. if (i < 3) {
  338. draw_rivet(QVector3D(r0 * 0.90F, y0 - 0.016F, 0));
  339. }
  340. }
  341. QVector3D const gorget_top(0, y_top_cover + 0.025F, 0);
  342. QVector3D const gorget_bot(0, y_top_cover - 0.012F, 0);
  343. out.mesh(getUnitCylinder(),
  344. cylinderBetween(ctx.model, gorget_bot, gorget_top,
  345. HP::NECK_RADIUS * 2.6F),
  346. steel_color * 1.08F, nullptr, 1.0F);
  347. ring(gorget_top, HP::NECK_RADIUS * 2.62F, 0.010F, brass_color);
  348. }
  349. void drawShoulderDecorations(const DrawContext &ctx, const HumanoidVariant &v,
  350. const HumanoidPose &pose, float y_top_cover,
  351. float y_neck, const QVector3D &right_axis,
  352. ISubmitter &out) const override {
  353. using HP = HumanProportions;
  354. QVector3D brass_color = v.palette.metal * QVector3D(1.3F, 1.1F, 0.7F);
  355. QVector3D const chainmail_color =
  356. v.palette.metal * QVector3D(0.85F, 0.88F, 0.92F);
  357. QVector3D mantling_color = v.palette.cloth;
  358. for (int i = 0; i < 5; ++i) {
  359. float const y = y_neck - i * 0.022F;
  360. float const r = HP::NECK_RADIUS * (1.85F + i * 0.08F);
  361. QVector3D const ring_pos(0, y, 0);
  362. QVector3D const a = ring_pos + QVector3D(0, 0.010F, 0);
  363. QVector3D const b = ring_pos - QVector3D(0, 0.010F, 0);
  364. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r),
  365. chainmail_color * (1.0F - i * 0.04F), nullptr, 1.0F);
  366. }
  367. QVector3D const helm_top(0, HP::HEAD_TOP_Y - HP::HEAD_RADIUS * 0.15F, 0);
  368. QMatrix4x4 crest_base = ctx.model;
  369. crest_base.translate(helm_top);
  370. crest_base.scale(0.025F, 0.015F, 0.025F);
  371. out.mesh(getUnitSphere(), crest_base, brass_color * 1.2F, nullptr, 1.0F);
  372. auto draw_stud = [&](const QVector3D &pos) {
  373. QMatrix4x4 m = ctx.model;
  374. m.translate(pos);
  375. m.scale(0.008F);
  376. out.mesh(getUnitSphere(), m, brass_color * 1.3F, nullptr, 1.0F);
  377. };
  378. draw_stud(helm_top + QVector3D(0.020F, 0, 0.020F));
  379. draw_stud(helm_top + QVector3D(-0.020F, 0, 0.020F));
  380. draw_stud(helm_top + QVector3D(0.020F, 0, -0.020F));
  381. draw_stud(helm_top + QVector3D(-0.020F, 0, -0.020F));
  382. auto draw_mantling = [&](const QVector3D &startPos,
  383. const QVector3D &direction) {
  384. QVector3D current_pos = startPos;
  385. for (int i = 0; i < 4; ++i) {
  386. float const seg_len = 0.035F - i * 0.005F;
  387. float const seg_r = 0.020F - i * 0.003F;
  388. QVector3D next_pos = current_pos + direction * seg_len;
  389. next_pos.setY(next_pos.y() - 0.025F);
  390. out.mesh(getUnitCylinder(),
  391. cylinderBetween(ctx.model, current_pos, next_pos, seg_r),
  392. mantling_color * (1.1F - i * 0.06F), nullptr, 1.0F);
  393. current_pos = next_pos;
  394. }
  395. };
  396. QVector3D const mantling_start(0, HP::CHIN_Y + HP::HEAD_RADIUS * 0.25F, 0);
  397. draw_mantling(mantling_start + right_axis * HP::HEAD_RADIUS * 0.95F,
  398. right_axis * 0.5F + QVector3D(0, -0.1F, -0.3F));
  399. draw_mantling(mantling_start - right_axis * HP::HEAD_RADIUS * 0.95F,
  400. -right_axis * 0.5F + QVector3D(0, -0.1F, -0.3F));
  401. auto draw_pauldron_rivet = [&](const QVector3D &shoulder,
  402. const QVector3D &outward) {
  403. for (int i = 0; i < 3; ++i) {
  404. float const seg_y = shoulder.y() + 0.025F - i * 0.045F;
  405. QVector3D rivet_pos = shoulder + outward * (0.04F + i * 0.008F);
  406. rivet_pos.setY(seg_y);
  407. draw_stud(rivet_pos);
  408. }
  409. };
  410. draw_pauldron_rivet(pose.shoulderL, -right_axis);
  411. draw_pauldron_rivet(pose.shoulderR, right_axis);
  412. QVector3D const gorget_top(0, y_top_cover + 0.045F, 0);
  413. for (int i = 0; i < 6; ++i) {
  414. float const angle = (i / 6.0F) * 2.0F * std::numbers::pi_v<float>;
  415. float const x = HP::NECK_RADIUS * 2.58F * std::sin(angle);
  416. float const z = HP::NECK_RADIUS * 2.58F * std::cos(angle);
  417. draw_stud(gorget_top + QVector3D(x, 0, z));
  418. }
  419. QVector3D const belt_center(0, HP::WAIST_Y + 0.03F,
  420. HP::TORSO_BOT_R * 1.15F);
  421. QMatrix4x4 buckle = ctx.model;
  422. buckle.translate(belt_center);
  423. buckle.scale(0.035F, 0.025F, 0.012F);
  424. out.mesh(getUnitSphere(), buckle, brass_color * 1.25F, nullptr, 1.0F);
  425. QVector3D const buckle_h1 = belt_center + QVector3D(-0.025F, 0, 0.005F);
  426. QVector3D const buckle_h2 = belt_center + QVector3D(0.025F, 0, 0.005F);
  427. out.mesh(getUnitCylinder(),
  428. cylinderBetween(ctx.model, buckle_h1, buckle_h2, 0.006F),
  429. brass_color * 1.4F, nullptr, 1.0F);
  430. QVector3D const buckle_v1 = belt_center + QVector3D(0, -0.018F, 0.005F);
  431. QVector3D const buckle_v2 = belt_center + QVector3D(0, 0.018F, 0.005F);
  432. out.mesh(getUnitCylinder(),
  433. cylinderBetween(ctx.model, buckle_v1, buckle_v2, 0.006F),
  434. brass_color * 1.4F, nullptr, 1.0F);
  435. }
  436. private:
  437. static auto computeKnightExtras(uint32_t seed,
  438. const HumanoidVariant &v) -> KnightExtras {
  439. KnightExtras e;
  440. e.metalColor = QVector3D(0.72F, 0.73F, 0.78F);
  441. float const shield_hue = hash_01(seed ^ 0x12345U);
  442. if (shield_hue < 0.45F) {
  443. e.shieldColor = v.palette.cloth * 1.10F;
  444. } else if (shield_hue < 0.90F) {
  445. e.shieldColor = v.palette.leather * 1.25F;
  446. } else {
  447. e.shieldColor = e.metalColor * 0.95F;
  448. }
  449. e.swordLength = 0.80F + (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.16F;
  450. e.swordWidth = 0.060F + (hash_01(seed ^ 0x7777U) - 0.5F) * 0.010F;
  451. e.shieldRadius = 0.16F + (hash_01(seed ^ 0xDEF0U) - 0.5F) * 0.04F;
  452. e.guard_half_width = 0.120F + (hash_01(seed ^ 0x3456U) - 0.5F) * 0.020F;
  453. e.handleRadius = 0.016F + (hash_01(seed ^ 0x88AAU) - 0.5F) * 0.003F;
  454. e.pommelRadius = 0.045F + (hash_01(seed ^ 0x19C3U) - 0.5F) * 0.006F;
  455. e.bladeRicasso =
  456. clampf(0.14F + (hash_01(seed ^ 0xBEEFU) - 0.5F) * 0.04F, 0.10F, 0.20F);
  457. e.bladeTaperBias = clamp01(0.6F + (hash_01(seed ^ 0xFACEU) - 0.5F) * 0.2F);
  458. e.shieldCrossDecal = (hash_01(seed ^ 0xA11CU) > 0.55F);
  459. e.hasScabbard = (hash_01(seed ^ 0x5CABU) > 0.15F);
  460. e.shieldTrimColor = e.metalColor * 0.95F;
  461. e.shieldAspect = 1.0F;
  462. return e;
  463. }
  464. static void drawSword(const DrawContext &ctx, const HumanoidPose &pose,
  465. const HumanoidVariant &v, const KnightExtras &extras,
  466. bool is_attacking, float attack_phase,
  467. ISubmitter &out) {
  468. QVector3D const grip_pos = pose.hand_r;
  469. constexpr float k_sword_yaw_deg = 25.0F;
  470. QMatrix4x4 yaw_m;
  471. yaw_m.rotate(k_sword_yaw_deg, 0.0F, 1.0F, 0.0F);
  472. QVector3D upish = yaw_m.map(QVector3D(0.05F, 1.0F, 0.15F));
  473. QVector3D midish = yaw_m.map(QVector3D(0.08F, 0.20F, 1.0F));
  474. QVector3D downish = yaw_m.map(QVector3D(0.10F, -1.0F, 0.25F));
  475. if (upish.lengthSquared() > 1e-6F) {
  476. upish.normalize();
  477. }
  478. if (midish.lengthSquared() > 1e-6F) {
  479. midish.normalize();
  480. }
  481. if (downish.lengthSquared() > 1e-6F) {
  482. downish.normalize();
  483. }
  484. QVector3D sword_dir = upish;
  485. if (is_attacking) {
  486. if (attack_phase < 0.18F) {
  487. float const t = easeInOutCubic(attack_phase / 0.18F);
  488. sword_dir = nlerp(upish, upish, t);
  489. } else if (attack_phase < 0.32F) {
  490. float const t = easeInOutCubic((attack_phase - 0.18F) / 0.14F);
  491. sword_dir = nlerp(upish, midish, t * 0.35F);
  492. } else if (attack_phase < 0.52F) {
  493. float t = (attack_phase - 0.32F) / 0.20F;
  494. t = t * t * t;
  495. if (t < 0.5F) {
  496. float const u = t / 0.5F;
  497. sword_dir = nlerp(upish, midish, u);
  498. } else {
  499. float const u = (t - 0.5F) / 0.5F;
  500. sword_dir = nlerp(midish, downish, u);
  501. }
  502. } else if (attack_phase < 0.72F) {
  503. float const t = easeInOutCubic((attack_phase - 0.52F) / 0.20F);
  504. sword_dir = nlerp(downish, midish, t);
  505. } else {
  506. float const t = smoothstep(0.72F, 1.0F, attack_phase);
  507. sword_dir = nlerp(midish, upish, t);
  508. }
  509. }
  510. QVector3D const handle_end = grip_pos - sword_dir * 0.10F;
  511. QVector3D const blade_base = grip_pos;
  512. QVector3D const blade_tip = grip_pos + sword_dir * extras.swordLength;
  513. out.mesh(
  514. getUnitCylinder(),
  515. cylinderBetween(ctx.model, handle_end, blade_base, extras.handleRadius),
  516. v.palette.leather, nullptr, 1.0F);
  517. QVector3D const guard_center = blade_base;
  518. float const gw = extras.guard_half_width;
  519. QVector3D guard_right =
  520. QVector3D::crossProduct(QVector3D(0, 1, 0), sword_dir);
  521. if (guard_right.lengthSquared() < 1e-6F) {
  522. guard_right = QVector3D::crossProduct(QVector3D(1, 0, 0), sword_dir);
  523. }
  524. guard_right.normalize();
  525. QVector3D const guard_l = guard_center - guard_right * gw;
  526. QVector3D const guard_r = guard_center + guard_right * gw;
  527. out.mesh(getUnitCylinder(),
  528. cylinderBetween(ctx.model, guard_l, guard_r, 0.014F),
  529. extras.metalColor, nullptr, 1.0F);
  530. QMatrix4x4 gl = ctx.model;
  531. gl.translate(guard_l);
  532. gl.scale(0.018F);
  533. out.mesh(getUnitSphere(), gl, extras.metalColor, nullptr, 1.0F);
  534. QMatrix4x4 gr = ctx.model;
  535. gr.translate(guard_r);
  536. gr.scale(0.018F);
  537. out.mesh(getUnitSphere(), gr, extras.metalColor, nullptr, 1.0F);
  538. float const l = extras.swordLength;
  539. float const base_w = extras.swordWidth;
  540. float blade_thickness = base_w * 0.15F;
  541. float const ricasso_len = clampf(extras.bladeRicasso, 0.10F, l * 0.30F);
  542. QVector3D const ricasso_end = blade_base + sword_dir * ricasso_len;
  543. float const mid_w = base_w * 0.95F;
  544. float const tip_w = base_w * 0.28F;
  545. float const tip_start_dist = lerp(ricasso_len, l, 0.70F);
  546. QVector3D const tip_start = blade_base + sword_dir * tip_start_dist;
  547. auto draw_flat_section = [&](const QVector3D &start, const QVector3D &end,
  548. float width, const QVector3D &color) {
  549. QVector3D right = QVector3D::crossProduct(sword_dir, QVector3D(0, 1, 0));
  550. if (right.lengthSquared() < 0.001F) {
  551. right = QVector3D::crossProduct(sword_dir, QVector3D(1, 0, 0));
  552. }
  553. right.normalize();
  554. float const offset = width * 0.33F;
  555. out.mesh(getUnitCylinder(),
  556. cylinderBetween(ctx.model, start, end, blade_thickness), color,
  557. nullptr, 1.0F);
  558. out.mesh(getUnitCylinder(),
  559. cylinderBetween(ctx.model, start + right * offset,
  560. end + right * offset, blade_thickness * 0.8F),
  561. color * 0.92F, nullptr, 1.0F);
  562. out.mesh(getUnitCylinder(),
  563. cylinderBetween(ctx.model, start - right * offset,
  564. end - right * offset, blade_thickness * 0.8F),
  565. color * 0.92F, nullptr, 1.0F);
  566. };
  567. draw_flat_section(blade_base, ricasso_end, base_w, extras.metalColor);
  568. draw_flat_section(ricasso_end, tip_start, mid_w, extras.metalColor);
  569. int const tip_segments = 3;
  570. for (int i = 0; i < tip_segments; ++i) {
  571. float const t0 = (float)i / tip_segments;
  572. float const t1 = (float)(i + 1) / tip_segments;
  573. QVector3D const seg_start =
  574. tip_start + sword_dir * ((blade_tip - tip_start).length() * t0);
  575. QVector3D const seg_end =
  576. tip_start + sword_dir * ((blade_tip - tip_start).length() * t1);
  577. float const w = lerp(mid_w, tip_w, t1);
  578. out.mesh(getUnitCylinder(),
  579. cylinderBetween(ctx.model, seg_start, seg_end, blade_thickness),
  580. extras.metalColor * (1.0F - i * 0.03F), nullptr, 1.0F);
  581. }
  582. QVector3D const fuller_start =
  583. blade_base + sword_dir * (ricasso_len + 0.02F);
  584. QVector3D const fuller_end =
  585. blade_base + sword_dir * (tip_start_dist - 0.06F);
  586. out.mesh(getUnitCylinder(),
  587. cylinderBetween(ctx.model, fuller_start, fuller_end,
  588. blade_thickness * 0.6F),
  589. extras.metalColor * 0.65F, nullptr, 1.0F);
  590. QVector3D const pommel = handle_end - sword_dir * 0.02F;
  591. QMatrix4x4 pommel_mat = ctx.model;
  592. pommel_mat.translate(pommel);
  593. pommel_mat.scale(extras.pommelRadius);
  594. out.mesh(getUnitSphere(), pommel_mat, extras.metalColor, nullptr, 1.0F);
  595. if (is_attacking && attack_phase >= 0.32F && attack_phase < 0.56F) {
  596. float const t = (attack_phase - 0.32F) / 0.24F;
  597. float const alpha = clamp01(0.35F * (1.0F - t));
  598. QVector3D const trail_start = blade_base - sword_dir * 0.05F;
  599. QVector3D const trail_end = blade_base - sword_dir * (0.28F + 0.15F * t);
  600. out.mesh(getUnitCone(),
  601. coneFromTo(ctx.model, trail_end, trail_start, base_w * 0.9F),
  602. extras.metalColor * 0.9F, nullptr, alpha);
  603. }
  604. }
  605. static void drawShield(const DrawContext &ctx, const HumanoidPose &pose,
  606. const HumanoidVariant &v, const KnightExtras &extras,
  607. ISubmitter &out) {
  608. constexpr float k_scale_factor = 2.5F;
  609. constexpr float k_shield_yaw_degrees = -70.0F;
  610. QMatrix4x4 rot;
  611. rot.rotate(k_shield_yaw_degrees, 0.0F, 1.0F, 0.0F);
  612. const QVector3D n = rot.map(QVector3D(0.0F, 0.0F, 1.0F));
  613. const QVector3D axis_x = rot.map(QVector3D(1.0F, 0.0F, 0.0F));
  614. const QVector3D axis_y = rot.map(QVector3D(0.0F, 1.0F, 0.0F));
  615. float const base_extent = extras.shieldRadius * k_scale_factor;
  616. float const shield_width = base_extent;
  617. float const shield_height = base_extent * extras.shieldAspect;
  618. float const min_extent = std::min(shield_width, shield_height);
  619. QVector3D shield_center = pose.handL + axis_x * (-shield_width * 0.35F) +
  620. axis_y * (-0.05F) + n * (0.06F);
  621. const float plate_half = 0.0015F;
  622. const float plate_full = plate_half * 2.0F;
  623. {
  624. QMatrix4x4 m = ctx.model;
  625. m.translate(shield_center + n * plate_half);
  626. m.rotate(k_shield_yaw_degrees, 0.0F, 1.0F, 0.0F);
  627. m.scale(shield_width, shield_height, plate_full);
  628. out.mesh(getUnitCylinder(), m, extras.shieldColor, nullptr, 1.0F);
  629. }
  630. {
  631. QMatrix4x4 m = ctx.model;
  632. m.translate(shield_center - n * plate_half);
  633. m.rotate(k_shield_yaw_degrees, 0.0F, 1.0F, 0.0F);
  634. m.scale(shield_width * 0.985F, shield_height * 0.985F, plate_full);
  635. out.mesh(getUnitCylinder(), m, v.palette.leather * 0.8F, nullptr, 1.0F);
  636. }
  637. auto draw_ring_rotated = [&](float width, float height, float thickness,
  638. const QVector3D &color) {
  639. constexpr int k_segments = 18;
  640. for (int i = 0; i < k_segments; ++i) {
  641. float const a0 =
  642. (float)i / k_segments * 2.0F * std::numbers::pi_v<float>;
  643. float const a1 =
  644. (float)(i + 1) / k_segments * 2.0F * std::numbers::pi_v<float>;
  645. QVector3D const v0(width * std::cos(a0), height * std::sin(a0), 0.0F);
  646. QVector3D const v1(width * std::cos(a1), height * std::sin(a1), 0.0F);
  647. QVector3D const p0 = shield_center + rot.map(v0);
  648. QVector3D const p1 = shield_center + rot.map(v1);
  649. out.mesh(getUnitCylinder(),
  650. cylinderBetween(ctx.model, p0, p1, thickness), color, nullptr,
  651. 1.0F);
  652. }
  653. };
  654. draw_ring_rotated(shield_width, shield_height, min_extent * 0.010F,
  655. extras.shieldTrimColor * 0.95F);
  656. draw_ring_rotated(shield_width * 0.72F, shield_height * 0.72F,
  657. min_extent * 0.006F, v.palette.leather * 0.90F);
  658. {
  659. QMatrix4x4 m = ctx.model;
  660. m.translate(shield_center + n * (0.02F * k_scale_factor));
  661. m.scale(0.045F * k_scale_factor);
  662. out.mesh(getUnitSphere(), m, extras.metalColor, nullptr, 1.0F);
  663. }
  664. {
  665. QVector3D const grip_a = shield_center - axis_x * 0.035F - n * 0.030F;
  666. QVector3D const grip_b = shield_center + axis_x * 0.035F - n * 0.030F;
  667. out.mesh(getUnitCylinder(),
  668. cylinderBetween(ctx.model, grip_a, grip_b, 0.010F),
  669. v.palette.leather, nullptr, 1.0F);
  670. }
  671. if (extras.shieldCrossDecal) {
  672. QVector3D const center_front =
  673. shield_center + n * (plate_full * 0.5F + 0.0015F);
  674. float const bar_radius = min_extent * 0.10F;
  675. QVector3D const top = center_front + axis_y * (shield_height * 0.90F);
  676. QVector3D const bot = center_front - axis_y * (shield_height * 0.90F);
  677. out.mesh(getUnitCylinder(),
  678. cylinderBetween(ctx.model, top, bot, bar_radius),
  679. extras.shieldTrimColor, nullptr, 1.0F);
  680. QVector3D const left = center_front - axis_x * (shield_width * 0.90F);
  681. QVector3D const right = center_front + axis_x * (shield_width * 0.90F);
  682. out.mesh(getUnitCylinder(),
  683. cylinderBetween(ctx.model, left, right, bar_radius),
  684. extras.shieldTrimColor, nullptr, 1.0F);
  685. }
  686. }
  687. static void drawScabbard(const DrawContext &ctx, const HumanoidPose &,
  688. const HumanoidVariant &v, const KnightExtras &extras,
  689. ISubmitter &out) {
  690. using HP = HumanProportions;
  691. QVector3D const hip(0.10F, HP::WAIST_Y - 0.04F, -0.02F);
  692. QVector3D const tip = hip + QVector3D(-0.05F, -0.22F, -0.12F);
  693. float const sheath_r = extras.swordWidth * 0.85F;
  694. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, hip, tip, sheath_r),
  695. v.palette.leather * 0.9F, nullptr, 1.0F);
  696. out.mesh(getUnitCone(),
  697. coneFromTo(ctx.model, tip, tip + QVector3D(-0.02F, -0.02F, -0.02F),
  698. sheath_r),
  699. extras.metalColor, nullptr, 1.0F);
  700. QVector3D const strap_a = hip + QVector3D(0.00F, 0.03F, 0.00F);
  701. QVector3D const belt = QVector3D(0.12F, HP::WAIST_Y + 0.01F, 0.02F);
  702. out.mesh(getUnitCylinder(),
  703. cylinderBetween(ctx.model, strap_a, belt, 0.006F),
  704. v.palette.leather, nullptr, 1.0F);
  705. }
  706. auto
  707. resolve_style(const DrawContext &ctx) const -> const KnightStyleConfig & {
  708. ensure_knight_styles_registered();
  709. auto &styles = knight_style_registry();
  710. std::string nation_id;
  711. if (ctx.entity != nullptr) {
  712. if (auto *unit =
  713. ctx.entity->getComponent<Engine::Core::UnitComponent>()) {
  714. nation_id = unit->nation_id;
  715. }
  716. }
  717. if (!nation_id.empty()) {
  718. auto it = styles.find(nation_id);
  719. if (it != styles.end()) {
  720. return it->second;
  721. }
  722. }
  723. auto it_default = styles.find(std::string(k_knight_default_style_key));
  724. if (it_default != styles.end()) {
  725. return it_default->second;
  726. }
  727. static const KnightStyleConfig k_empty{};
  728. return k_empty;
  729. }
  730. public:
  731. auto resolve_shader_key(const DrawContext &ctx) const -> QString {
  732. const KnightStyleConfig &style = resolve_style(ctx);
  733. if (!style.shader_id.empty()) {
  734. return QString::fromStdString(style.shader_id);
  735. }
  736. return QStringLiteral("knight");
  737. }
  738. private:
  739. void apply_palette_overrides(const KnightStyleConfig &style,
  740. const QVector3D &team_tint,
  741. HumanoidVariant &variant) const {
  742. auto apply_color = [&](const std::optional<QVector3D> &override_color,
  743. QVector3D &target) {
  744. target = mix_palette_color(target, override_color, team_tint,
  745. k_knight_team_mix_weight,
  746. k_knight_style_mix_weight);
  747. };
  748. apply_color(style.cloth_color, variant.palette.cloth);
  749. apply_color(style.leather_color, variant.palette.leather);
  750. apply_color(style.leather_dark_color, variant.palette.leatherDark);
  751. apply_color(style.metal_color, variant.palette.metal);
  752. }
  753. void apply_extras_overrides(const KnightStyleConfig &style,
  754. const QVector3D &team_tint,
  755. const HumanoidVariant &variant,
  756. KnightExtras &extras) const {
  757. extras.metalColor = saturate_color(variant.palette.metal);
  758. extras.shieldColor = saturate_color(extras.shieldColor);
  759. extras.shieldTrimColor = saturate_color(extras.shieldTrimColor);
  760. auto apply_shield_color =
  761. [&](const std::optional<QVector3D> &override_color, QVector3D &target) {
  762. target = mix_palette_color(target, override_color, team_tint,
  763. k_knight_team_mix_weight,
  764. k_knight_style_mix_weight);
  765. };
  766. apply_shield_color(style.shield_color, extras.shieldColor);
  767. apply_shield_color(style.shield_trim_color, extras.shieldTrimColor);
  768. if (style.shield_radius_scale) {
  769. extras.shieldRadius =
  770. std::max(0.10F, extras.shieldRadius * *style.shield_radius_scale);
  771. }
  772. if (style.shield_aspect_ratio) {
  773. extras.shieldAspect = std::max(0.40F, *style.shield_aspect_ratio);
  774. }
  775. if (style.has_scabbard) {
  776. extras.hasScabbard = *style.has_scabbard;
  777. }
  778. if (style.shield_cross_decal) {
  779. extras.shieldCrossDecal = *style.shield_cross_decal;
  780. }
  781. }
  782. };
  783. void registerKnightRenderer(Render::GL::EntityRendererRegistry &registry) {
  784. ensure_knight_styles_registered();
  785. static KnightRenderer const renderer;
  786. registry.registerRenderer(
  787. "troops/kingdom/swordsman", [](const DrawContext &ctx, ISubmitter &out) {
  788. static KnightRenderer const static_renderer;
  789. Shader *knight_shader = nullptr;
  790. if (ctx.backend != nullptr) {
  791. QString shader_key = static_renderer.resolve_shader_key(ctx);
  792. knight_shader = ctx.backend->shader(shader_key);
  793. if (knight_shader == nullptr) {
  794. knight_shader = ctx.backend->shader(QStringLiteral("knight"));
  795. }
  796. }
  797. auto *scene_renderer = dynamic_cast<Renderer *>(&out);
  798. if ((scene_renderer != nullptr) && (knight_shader != nullptr)) {
  799. scene_renderer->setCurrentShader(knight_shader);
  800. }
  801. static_renderer.render(ctx, out);
  802. if (scene_renderer != nullptr) {
  803. scene_renderer->setCurrentShader(nullptr);
  804. }
  805. });
  806. registry.registerRenderer(
  807. "troops/kingdom/knight", [](const DrawContext &ctx, ISubmitter &out) {
  808. static KnightRenderer const static_renderer;
  809. Shader *knight_shader = nullptr;
  810. if (ctx.backend != nullptr) {
  811. knight_shader = ctx.backend->shader(QStringLiteral("knight"));
  812. }
  813. auto *scene_renderer = dynamic_cast<Renderer *>(&out);
  814. if ((scene_renderer != nullptr) && (knight_shader != nullptr)) {
  815. scene_renderer->setCurrentShader(knight_shader);
  816. }
  817. static_renderer.render(ctx, out);
  818. if (scene_renderer != nullptr) {
  819. scene_renderer->setCurrentShader(nullptr);
  820. }
  821. });
  822. }
  823. } // namespace Render::GL::Kingdom