knight_renderer.cpp 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. #include "knight_renderer.h"
  2. #include "../../game/core/component.h"
  3. #include "../../game/core/entity.h"
  4. #include "../../game/core/world.h"
  5. #include "../../game/units/troop_config.h"
  6. #include "../../game/visuals/team_colors.h"
  7. #include "../geom/math_utils.h"
  8. #include "../geom/transforms.h"
  9. #include "../gl/backend.h"
  10. #include "../gl/mesh.h"
  11. #include "../gl/primitives.h"
  12. #include "../gl/shader.h"
  13. #include "../humanoid_base.h"
  14. #include "../humanoid_math.h"
  15. #include "../humanoid_specs.h"
  16. #include "../palette.h"
  17. #include "../scene_renderer.h"
  18. #include "../submitter.h"
  19. #include "registry.h"
  20. #include <unordered_map>
  21. #include <QMatrix4x4>
  22. #include <QString>
  23. #include <QVector3D>
  24. #include <algorithm>
  25. #include <cmath>
  26. #include <cstdint>
  27. namespace Render::GL {
  28. using Render::Geom::clamp01;
  29. using Render::Geom::clampf;
  30. using Render::Geom::coneFromTo;
  31. using Render::Geom::cylinderBetween;
  32. using Render::Geom::sphereAt;
  33. static constexpr std::size_t MAX_EXTRAS_CACHE_SIZE = 10000;
  34. static inline float easeInOutCubic(float t) {
  35. t = clamp01(t);
  36. return t < 0.5f ? 4.0f * t * t * t
  37. : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) / 2.0f;
  38. }
  39. static inline float smoothstep(float a, float b, float x) {
  40. x = clamp01((x - a) / (b - a));
  41. return x * x * (3.0f - 2.0f * x);
  42. }
  43. static inline float lerp(float a, float b, float t) {
  44. return a * (1.0f - t) + b * t;
  45. }
  46. static inline QVector3D nlerp(const QVector3D &a, const QVector3D &b, float t) {
  47. QVector3D v = a * (1.0f - t) + b * t;
  48. if (v.lengthSquared() > 1e-6f)
  49. v.normalize();
  50. return v;
  51. }
  52. struct KnightExtras {
  53. QVector3D metalColor;
  54. QVector3D shieldColor;
  55. float swordLength = 0.80f;
  56. float swordWidth = 0.065f;
  57. float shieldRadius = 0.18f;
  58. float guardHalfWidth = 0.12f;
  59. float handleRadius = 0.016f;
  60. float pommelRadius = 0.045f;
  61. float bladeRicasso = 0.16f;
  62. float bladeTaperBias = 0.65f;
  63. bool shieldCrossDecal = false;
  64. bool hasScabbard = true;
  65. };
  66. class KnightRenderer : public HumanoidRendererBase {
  67. public:
  68. QVector3D getProportionScaling() const override {
  69. return QVector3D(1.40f, 1.05f, 1.10f);
  70. }
  71. private:
  72. mutable std::unordered_map<uint32_t, KnightExtras> m_extrasCache;
  73. public:
  74. void getVariant(const DrawContext &ctx, uint32_t seed,
  75. HumanoidVariant &v) const override {
  76. QVector3D teamTint = resolveTeamTint(ctx);
  77. v.palette = makeHumanoidPalette(teamTint, seed);
  78. }
  79. void customizePose(const DrawContext &ctx, const AnimationInputs &anim,
  80. uint32_t seed, HumanoidPose &pose) const override {
  81. using HP = HumanProportions;
  82. float armHeightJitter = (hash01(seed ^ 0xABCDu) - 0.5f) * 0.03f;
  83. float armAsymmetry = (hash01(seed ^ 0xDEF0u) - 0.5f) * 0.04f;
  84. if (anim.isAttacking && anim.isMelee) {
  85. const float attackCycleTime = 0.6f;
  86. float attackPhase = std::fmod(anim.time * (1.0f / attackCycleTime), 1.0f);
  87. QVector3D restPos(0.20f, HP::SHOULDER_Y + 0.05f, 0.15f);
  88. QVector3D preparePos(0.26f, HP::HEAD_TOP_Y + 0.18f, -0.06f);
  89. QVector3D raisedPos(0.25f, HP::HEAD_TOP_Y + 0.22f, 0.02f);
  90. QVector3D strikePos(0.30f, HP::WAIST_Y - 0.05f, 0.50f);
  91. QVector3D recoverPos(0.22f, HP::SHOULDER_Y + 0.02f, 0.22f);
  92. if (attackPhase < 0.18f) {
  93. float t = easeInOutCubic(attackPhase / 0.18f);
  94. pose.handR = restPos * (1.0f - t) + preparePos * t;
  95. pose.handL =
  96. QVector3D(-0.21f, HP::SHOULDER_Y - 0.02f - 0.03f * t, 0.15f);
  97. } else if (attackPhase < 0.32f) {
  98. float t = easeInOutCubic((attackPhase - 0.18f) / 0.14f);
  99. pose.handR = preparePos * (1.0f - t) + raisedPos * t;
  100. pose.handL = QVector3D(-0.21f, HP::SHOULDER_Y - 0.05f, 0.17f);
  101. } else if (attackPhase < 0.52f) {
  102. float t = (attackPhase - 0.32f) / 0.20f;
  103. t = t * t * t;
  104. pose.handR = raisedPos * (1.0f - t) + strikePos * t;
  105. pose.handL =
  106. QVector3D(-0.21f, HP::SHOULDER_Y - 0.03f * (1.0f - 0.5f * t),
  107. 0.17f + 0.20f * t);
  108. } else if (attackPhase < 0.72f) {
  109. float t = easeInOutCubic((attackPhase - 0.52f) / 0.20f);
  110. pose.handR = strikePos * (1.0f - t) + recoverPos * t;
  111. pose.handL = QVector3D(-0.20f, HP::SHOULDER_Y - 0.015f * (1.0f - t),
  112. lerp(0.37f, 0.20f, t));
  113. } else {
  114. float t = smoothstep(0.72f, 1.0f, attackPhase);
  115. pose.handR = recoverPos * (1.0f - t) + restPos * t;
  116. pose.handL = QVector3D(-0.20f - 0.02f * (1.0f - t),
  117. HP::SHOULDER_Y + armHeightJitter * (1.0f - t),
  118. lerp(0.20f, 0.15f, t));
  119. }
  120. } else {
  121. pose.handR = QVector3D(0.30f + armAsymmetry,
  122. HP::SHOULDER_Y - 0.02f + armHeightJitter, 0.35f);
  123. pose.handL = QVector3D(-0.22f - 0.5f * armAsymmetry,
  124. HP::SHOULDER_Y + 0.5f * armHeightJitter, 0.18f);
  125. }
  126. }
  127. void addAttachments(const DrawContext &ctx, const HumanoidVariant &v,
  128. const HumanoidPose &pose, const AnimationInputs &anim,
  129. ISubmitter &out) const override {
  130. uint32_t seed = reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFu;
  131. KnightExtras extras;
  132. auto it = m_extrasCache.find(seed);
  133. if (it != m_extrasCache.end()) {
  134. extras = it->second;
  135. } else {
  136. extras = computeKnightExtras(seed, v);
  137. m_extrasCache[seed] = extras;
  138. if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
  139. m_extrasCache.clear();
  140. }
  141. }
  142. bool isAttacking = anim.isAttacking && anim.isMelee;
  143. float attackPhase = 0.0f;
  144. if (isAttacking) {
  145. float attackCycleTime = 0.6f;
  146. attackPhase = std::fmod(anim.time * (1.0f / attackCycleTime), 1.0f);
  147. }
  148. drawSword(ctx, pose, v, extras, isAttacking, attackPhase, out);
  149. drawShield(ctx, pose, v, extras, out);
  150. if (!isAttacking && extras.hasScabbard) {
  151. drawScabbard(ctx, pose, v, extras, out);
  152. }
  153. }
  154. void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
  155. const HumanoidPose &pose, ISubmitter &out) const override {
  156. using HP = HumanProportions;
  157. auto ring = [&](const QVector3D &center, float r, float h,
  158. const QVector3D &col) {
  159. QVector3D a = center + QVector3D(0, h * 0.5f, 0);
  160. QVector3D b = center - QVector3D(0, h * 0.5f, 0);
  161. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r), col,
  162. nullptr, 1.0f);
  163. };
  164. QVector3D steelColor = v.palette.metal * QVector3D(0.95f, 0.96f, 1.0f);
  165. float helmR = pose.headR * 1.15f;
  166. QVector3D helmBot(0, pose.headPos.y() - pose.headR * 0.20f, 0);
  167. QVector3D helmTop(0, pose.headPos.y() + pose.headR * 1.40f, 0);
  168. out.mesh(getUnitCylinder(),
  169. cylinderBetween(ctx.model, helmBot, helmTop, helmR), steelColor,
  170. nullptr, 1.0f);
  171. QVector3D capTop(0, pose.headPos.y() + pose.headR * 1.48f, 0);
  172. out.mesh(getUnitCylinder(),
  173. cylinderBetween(ctx.model, helmTop, capTop, helmR * 0.98f),
  174. steelColor * 1.05f, nullptr, 1.0f);
  175. ring(QVector3D(0, pose.headPos.y() + pose.headR * 1.25f, 0), helmR * 1.02f,
  176. 0.015f, steelColor * 1.08f);
  177. ring(QVector3D(0, pose.headPos.y() + pose.headR * 0.50f, 0), helmR * 1.02f,
  178. 0.015f, steelColor * 1.08f);
  179. ring(QVector3D(0, pose.headPos.y() - pose.headR * 0.05f, 0), helmR * 1.02f,
  180. 0.015f, steelColor * 1.08f);
  181. float visorY = pose.headPos.y() + pose.headR * 0.15f;
  182. float visorZ = helmR * 0.72f;
  183. QVector3D visorHL(-helmR * 0.35f, visorY, visorZ);
  184. QVector3D visorHR(helmR * 0.35f, visorY, visorZ);
  185. out.mesh(getUnitCylinder(),
  186. cylinderBetween(ctx.model, visorHL, visorHR, 0.012f),
  187. QVector3D(0.1f, 0.1f, 0.1f), nullptr, 1.0f);
  188. QVector3D visorVT(0, visorY + helmR * 0.25f, visorZ);
  189. QVector3D visorVB(0, visorY - helmR * 0.25f, visorZ);
  190. out.mesh(getUnitCylinder(),
  191. cylinderBetween(ctx.model, visorVB, visorVT, 0.012f),
  192. QVector3D(0.1f, 0.1f, 0.1f), nullptr, 1.0f);
  193. auto drawBreathingHole = [&](float x, float y) {
  194. QVector3D pos(x, pose.headPos.y() + y, helmR * 0.70f);
  195. QMatrix4x4 m = ctx.model;
  196. m.translate(pos);
  197. m.scale(0.010f);
  198. out.mesh(getUnitSphere(), m, QVector3D(0.1f, 0.1f, 0.1f), nullptr, 1.0f);
  199. };
  200. for (int i = 0; i < 4; ++i) {
  201. drawBreathingHole(helmR * 0.50f, pose.headR * (0.05f - i * 0.10f));
  202. }
  203. for (int i = 0; i < 4; ++i) {
  204. drawBreathingHole(-helmR * 0.50f, pose.headR * (0.05f - i * 0.10f));
  205. }
  206. QVector3D crossCenter(0, pose.headPos.y() + pose.headR * 0.60f,
  207. helmR * 0.75f);
  208. QVector3D brassColor = v.palette.metal * QVector3D(1.3f, 1.1f, 0.7f);
  209. QVector3D crossH1 = crossCenter + QVector3D(-0.04f, 0, 0);
  210. QVector3D crossH2 = crossCenter + QVector3D(0.04f, 0, 0);
  211. out.mesh(getUnitCylinder(),
  212. cylinderBetween(ctx.model, crossH1, crossH2, 0.008f), brassColor,
  213. nullptr, 1.0f);
  214. QVector3D crossV1 = crossCenter + QVector3D(0, -0.04f, 0);
  215. QVector3D crossV2 = crossCenter + QVector3D(0, 0.04f, 0);
  216. out.mesh(getUnitCylinder(),
  217. cylinderBetween(ctx.model, crossV1, crossV2, 0.008f), brassColor,
  218. nullptr, 1.0f);
  219. }
  220. void drawArmorOverlay(const DrawContext &ctx, const HumanoidVariant &v,
  221. const HumanoidPose &pose, float yTopCover, float torsoR,
  222. float shoulderHalfSpan, float upperArmR,
  223. const QVector3D &rightAxis,
  224. ISubmitter &out) const override {
  225. using HP = HumanProportions;
  226. auto ring = [&](const QVector3D &center, float r, float h,
  227. const QVector3D &col) {
  228. QVector3D a = center + QVector3D(0, h * 0.5f, 0);
  229. QVector3D b = center - QVector3D(0, h * 0.5f, 0);
  230. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r), col,
  231. nullptr, 1.0f);
  232. };
  233. QVector3D steelColor = v.palette.metal * QVector3D(0.95f, 0.96f, 1.0f);
  234. QVector3D darkSteel = steelColor * 0.85f;
  235. QVector3D brassColor = v.palette.metal * QVector3D(1.3f, 1.1f, 0.7f);
  236. QVector3D bpTop(0, yTopCover + 0.02f, 0);
  237. QVector3D bpMid(0, (yTopCover + HP::WAIST_Y) * 0.5f + 0.04f, 0);
  238. QVector3D bpBot(0, HP::WAIST_Y + 0.06f, 0);
  239. float rChest = torsoR * 1.18f;
  240. float rWaist = torsoR * 1.14f;
  241. out.mesh(getUnitCylinder(),
  242. cylinderBetween(ctx.model, bpTop, bpMid, rChest), steelColor,
  243. nullptr, 1.0f);
  244. QVector3D bpMidLow(0, (bpMid.y() + bpBot.y()) * 0.5f, 0);
  245. out.mesh(getUnitCylinder(),
  246. cylinderBetween(ctx.model, bpMid, bpMidLow, rChest * 0.98f),
  247. steelColor * 0.99f, nullptr, 1.0f);
  248. out.mesh(getUnitCone(), coneFromTo(ctx.model, bpBot, bpMidLow, rWaist),
  249. steelColor * 0.98f, nullptr, 1.0f);
  250. auto drawRivet = [&](const QVector3D &pos) {
  251. QMatrix4x4 m = ctx.model;
  252. m.translate(pos);
  253. m.scale(0.012f);
  254. out.mesh(getUnitSphere(), m, brassColor, nullptr, 1.0f);
  255. };
  256. for (int i = 0; i < 8; ++i) {
  257. float angle = (i / 8.0f) * 2.0f * 3.14159265f;
  258. float x = rChest * std::sin(angle) * 0.95f;
  259. float z = rChest * std::cos(angle) * 0.95f;
  260. drawRivet(QVector3D(x, bpMid.y() + 0.08f, z));
  261. }
  262. auto drawPauldron = [&](const QVector3D &shoulder,
  263. const QVector3D &outward) {
  264. for (int i = 0; i < 4; ++i) {
  265. float segY = shoulder.y() + 0.04f - i * 0.045f;
  266. float segR = upperArmR * (2.5f - i * 0.12f);
  267. QVector3D segPos = shoulder + outward * (0.02f + i * 0.008f);
  268. segPos.setY(segY);
  269. out.mesh(getUnitSphere(), sphereAt(ctx.model, segPos, segR),
  270. i == 0 ? steelColor * 1.05f : steelColor * (1.0f - i * 0.03f),
  271. nullptr, 1.0f);
  272. if (i < 3) {
  273. drawRivet(segPos + QVector3D(0, 0.015f, 0.03f));
  274. }
  275. }
  276. };
  277. drawPauldron(pose.shoulderL, -rightAxis);
  278. drawPauldron(pose.shoulderR, rightAxis);
  279. auto drawArmPlate = [&](const QVector3D &shoulder, const QVector3D &elbow) {
  280. QVector3D dir = (elbow - shoulder);
  281. float len = dir.length();
  282. if (len < 1e-5f)
  283. return;
  284. dir /= len;
  285. for (int i = 0; i < 3; ++i) {
  286. float t0 = 0.10f + i * 0.25f;
  287. float t1 = t0 + 0.22f;
  288. QVector3D a = shoulder + dir * (t0 * len);
  289. QVector3D b = shoulder + dir * (t1 * len);
  290. float r = upperArmR * (1.32f - i * 0.04f);
  291. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r),
  292. steelColor * (0.98f - i * 0.02f), nullptr, 1.0f);
  293. if (i < 2) {
  294. drawRivet(b);
  295. }
  296. }
  297. };
  298. drawArmPlate(pose.shoulderL, pose.elbowL);
  299. drawArmPlate(pose.shoulderR, pose.elbowR);
  300. for (int i = 0; i < 4; ++i) {
  301. float y0 = HP::WAIST_Y + 0.04f - i * 0.038f;
  302. float y1 = y0 - 0.032f;
  303. float r0 = rWaist * (1.06f + i * 0.025f);
  304. out.mesh(
  305. getUnitCone(),
  306. coneFromTo(ctx.model, QVector3D(0, y0, 0), QVector3D(0, y1, 0), r0),
  307. steelColor * (0.96f - i * 0.02f), nullptr, 1.0f);
  308. if (i < 3) {
  309. drawRivet(QVector3D(r0 * 0.90f, y0 - 0.016f, 0));
  310. }
  311. }
  312. QVector3D gorgetTop(0, yTopCover + 0.025f, 0);
  313. QVector3D gorgetBot(0, yTopCover - 0.012f, 0);
  314. out.mesh(getUnitCylinder(),
  315. cylinderBetween(ctx.model, gorgetBot, gorgetTop,
  316. HP::NECK_RADIUS * 2.6f),
  317. steelColor * 1.08f, nullptr, 1.0f);
  318. ring(gorgetTop, HP::NECK_RADIUS * 2.62f, 0.010f, brassColor);
  319. }
  320. void drawShoulderDecorations(const DrawContext &ctx, const HumanoidVariant &v,
  321. const HumanoidPose &pose, float yTopCover,
  322. float yNeck, const QVector3D &rightAxis,
  323. ISubmitter &out) const override {
  324. using HP = HumanProportions;
  325. QVector3D brassColor = v.palette.metal * QVector3D(1.3f, 1.1f, 0.7f);
  326. QVector3D chainmailColor = v.palette.metal * QVector3D(0.85f, 0.88f, 0.92f);
  327. QVector3D mantlingColor = v.palette.cloth;
  328. for (int i = 0; i < 5; ++i) {
  329. float y = yNeck - i * 0.022f;
  330. float r = HP::NECK_RADIUS * (1.85f + i * 0.08f);
  331. QVector3D ringPos(0, y, 0);
  332. QVector3D a = ringPos + QVector3D(0, 0.010f, 0);
  333. QVector3D b = ringPos - QVector3D(0, 0.010f, 0);
  334. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r),
  335. chainmailColor * (1.0f - i * 0.04f), nullptr, 1.0f);
  336. }
  337. QVector3D helmTop(0, HP::HEAD_TOP_Y - HP::HEAD_RADIUS * 0.15f, 0);
  338. QMatrix4x4 crestBase = ctx.model;
  339. crestBase.translate(helmTop);
  340. crestBase.scale(0.025f, 0.015f, 0.025f);
  341. out.mesh(getUnitSphere(), crestBase, brassColor * 1.2f, nullptr, 1.0f);
  342. auto drawStud = [&](const QVector3D &pos) {
  343. QMatrix4x4 m = ctx.model;
  344. m.translate(pos);
  345. m.scale(0.008f);
  346. out.mesh(getUnitSphere(), m, brassColor * 1.3f, nullptr, 1.0f);
  347. };
  348. drawStud(helmTop + QVector3D(0.020f, 0, 0.020f));
  349. drawStud(helmTop + QVector3D(-0.020f, 0, 0.020f));
  350. drawStud(helmTop + QVector3D(0.020f, 0, -0.020f));
  351. drawStud(helmTop + QVector3D(-0.020f, 0, -0.020f));
  352. auto drawMantling = [&](const QVector3D &startPos,
  353. const QVector3D &direction) {
  354. QVector3D currentPos = startPos;
  355. for (int i = 0; i < 4; ++i) {
  356. float segLen = 0.035f - i * 0.005f;
  357. float segR = 0.020f - i * 0.003f;
  358. QVector3D nextPos = currentPos + direction * segLen;
  359. nextPos.setY(nextPos.y() - 0.025f);
  360. out.mesh(getUnitCylinder(),
  361. cylinderBetween(ctx.model, currentPos, nextPos, segR),
  362. mantlingColor * (1.1f - i * 0.06f), nullptr, 1.0f);
  363. currentPos = nextPos;
  364. }
  365. };
  366. QVector3D mantlingStart(0, HP::CHIN_Y + HP::HEAD_RADIUS * 0.25f, 0);
  367. drawMantling(mantlingStart + rightAxis * HP::HEAD_RADIUS * 0.95f,
  368. rightAxis * 0.5f + QVector3D(0, -0.1f, -0.3f));
  369. drawMantling(mantlingStart - rightAxis * HP::HEAD_RADIUS * 0.95f,
  370. -rightAxis * 0.5f + QVector3D(0, -0.1f, -0.3f));
  371. auto drawPauldronRivet = [&](const QVector3D &shoulder,
  372. const QVector3D &outward) {
  373. for (int i = 0; i < 3; ++i) {
  374. float segY = shoulder.y() + 0.025f - i * 0.045f;
  375. QVector3D rivetPos = shoulder + outward * (0.04f + i * 0.008f);
  376. rivetPos.setY(segY);
  377. drawStud(rivetPos);
  378. }
  379. };
  380. drawPauldronRivet(pose.shoulderL, -rightAxis);
  381. drawPauldronRivet(pose.shoulderR, rightAxis);
  382. QVector3D gorgetTop(0, yTopCover + 0.045f, 0);
  383. for (int i = 0; i < 6; ++i) {
  384. float angle = (i / 6.0f) * 2.0f * 3.14159265f;
  385. float x = HP::NECK_RADIUS * 2.58f * std::sin(angle);
  386. float z = HP::NECK_RADIUS * 2.58f * std::cos(angle);
  387. drawStud(gorgetTop + QVector3D(x, 0, z));
  388. }
  389. QVector3D beltCenter(0, HP::WAIST_Y + 0.03f, HP::TORSO_BOT_R * 1.15f);
  390. QMatrix4x4 buckle = ctx.model;
  391. buckle.translate(beltCenter);
  392. buckle.scale(0.035f, 0.025f, 0.012f);
  393. out.mesh(getUnitSphere(), buckle, brassColor * 1.25f, nullptr, 1.0f);
  394. QVector3D buckleH1 = beltCenter + QVector3D(-0.025f, 0, 0.005f);
  395. QVector3D buckleH2 = beltCenter + QVector3D(0.025f, 0, 0.005f);
  396. out.mesh(getUnitCylinder(),
  397. cylinderBetween(ctx.model, buckleH1, buckleH2, 0.006f),
  398. brassColor * 1.4f, nullptr, 1.0f);
  399. QVector3D buckleV1 = beltCenter + QVector3D(0, -0.018f, 0.005f);
  400. QVector3D buckleV2 = beltCenter + QVector3D(0, 0.018f, 0.005f);
  401. out.mesh(getUnitCylinder(),
  402. cylinderBetween(ctx.model, buckleV1, buckleV2, 0.006f),
  403. brassColor * 1.4f, nullptr, 1.0f);
  404. }
  405. private:
  406. static KnightExtras computeKnightExtras(uint32_t seed,
  407. const HumanoidVariant &v) {
  408. KnightExtras e;
  409. e.metalColor = QVector3D(0.72f, 0.73f, 0.78f);
  410. float shieldHue = hash01(seed ^ 0x12345u);
  411. if (shieldHue < 0.45f) {
  412. e.shieldColor = v.palette.cloth * 1.10f;
  413. } else if (shieldHue < 0.90f) {
  414. e.shieldColor = v.palette.leather * 1.25f;
  415. } else {
  416. e.shieldColor = e.metalColor * 0.95f;
  417. }
  418. e.swordLength = 0.80f + (hash01(seed ^ 0xABCDu) - 0.5f) * 0.16f;
  419. e.swordWidth = 0.060f + (hash01(seed ^ 0x7777u) - 0.5f) * 0.010f;
  420. e.shieldRadius = 0.16f + (hash01(seed ^ 0xDEF0u) - 0.5f) * 0.04f;
  421. e.guardHalfWidth = 0.120f + (hash01(seed ^ 0x3456u) - 0.5f) * 0.020f;
  422. e.handleRadius = 0.016f + (hash01(seed ^ 0x88AAu) - 0.5f) * 0.003f;
  423. e.pommelRadius = 0.045f + (hash01(seed ^ 0x19C3u) - 0.5f) * 0.006f;
  424. e.bladeRicasso =
  425. clampf(0.14f + (hash01(seed ^ 0xBEEFu) - 0.5f) * 0.04f, 0.10f, 0.20f);
  426. e.bladeTaperBias = clamp01(0.6f + (hash01(seed ^ 0xFACEu) - 0.5f) * 0.2f);
  427. e.shieldCrossDecal = (hash01(seed ^ 0xA11Cu) > 0.55f);
  428. e.hasScabbard = (hash01(seed ^ 0x5CABu) > 0.15f);
  429. return e;
  430. }
  431. static void drawSword(const DrawContext &ctx, const HumanoidPose &pose,
  432. const HumanoidVariant &v, const KnightExtras &extras,
  433. bool isAttacking, float attackPhase, ISubmitter &out) {
  434. QVector3D gripPos = pose.handR;
  435. constexpr float kSwordYawDeg = 25.0f;
  436. QMatrix4x4 yawM;
  437. yawM.rotate(kSwordYawDeg, 0.0f, 1.0f, 0.0f);
  438. QVector3D upish = yawM.map(QVector3D(0.05f, 1.0f, 0.15f));
  439. QVector3D midish = yawM.map(QVector3D(0.08f, 0.20f, 1.0f));
  440. QVector3D downish = yawM.map(QVector3D(0.10f, -1.0f, 0.25f));
  441. if (upish.lengthSquared() > 1e-6f)
  442. upish.normalize();
  443. if (midish.lengthSquared() > 1e-6f)
  444. midish.normalize();
  445. if (downish.lengthSquared() > 1e-6f)
  446. downish.normalize();
  447. QVector3D swordDir = upish;
  448. if (isAttacking) {
  449. if (attackPhase < 0.18f) {
  450. float t = easeInOutCubic(attackPhase / 0.18f);
  451. swordDir = nlerp(upish, upish, t);
  452. } else if (attackPhase < 0.32f) {
  453. float t = easeInOutCubic((attackPhase - 0.18f) / 0.14f);
  454. swordDir = nlerp(upish, midish, t * 0.35f);
  455. } else if (attackPhase < 0.52f) {
  456. float t = (attackPhase - 0.32f) / 0.20f;
  457. t = t * t * t;
  458. if (t < 0.5f) {
  459. float u = t / 0.5f;
  460. swordDir = nlerp(upish, midish, u);
  461. } else {
  462. float u = (t - 0.5f) / 0.5f;
  463. swordDir = nlerp(midish, downish, u);
  464. }
  465. } else if (attackPhase < 0.72f) {
  466. float t = easeInOutCubic((attackPhase - 0.52f) / 0.20f);
  467. swordDir = nlerp(downish, midish, t);
  468. } else {
  469. float t = smoothstep(0.72f, 1.0f, attackPhase);
  470. swordDir = nlerp(midish, upish, t);
  471. }
  472. }
  473. QVector3D handleEnd = gripPos - swordDir * 0.10f;
  474. QVector3D bladeBase = gripPos;
  475. QVector3D bladeTip = gripPos + swordDir * extras.swordLength;
  476. out.mesh(
  477. getUnitCylinder(),
  478. cylinderBetween(ctx.model, handleEnd, bladeBase, extras.handleRadius),
  479. v.palette.leather, nullptr, 1.0f);
  480. QVector3D guardCenter = bladeBase;
  481. float gw = extras.guardHalfWidth;
  482. QVector3D guardRight =
  483. QVector3D::crossProduct(QVector3D(0, 1, 0), swordDir);
  484. if (guardRight.lengthSquared() < 1e-6f)
  485. guardRight = QVector3D::crossProduct(QVector3D(1, 0, 0), swordDir);
  486. guardRight.normalize();
  487. QVector3D guardL = guardCenter - guardRight * gw;
  488. QVector3D guardR = guardCenter + guardRight * gw;
  489. out.mesh(getUnitCylinder(),
  490. cylinderBetween(ctx.model, guardL, guardR, 0.014f),
  491. extras.metalColor, nullptr, 1.0f);
  492. QMatrix4x4 gl = ctx.model;
  493. gl.translate(guardL);
  494. gl.scale(0.018f);
  495. out.mesh(getUnitSphere(), gl, extras.metalColor, nullptr, 1.0f);
  496. QMatrix4x4 gr = ctx.model;
  497. gr.translate(guardR);
  498. gr.scale(0.018f);
  499. out.mesh(getUnitSphere(), gr, extras.metalColor, nullptr, 1.0f);
  500. float L = extras.swordLength;
  501. float baseW = extras.swordWidth;
  502. float bladeThickness = baseW * 0.15f;
  503. float ricassoLen = clampf(extras.bladeRicasso, 0.10f, L * 0.30f);
  504. QVector3D ricassoEnd = bladeBase + swordDir * ricassoLen;
  505. float midW = baseW * 0.95f;
  506. float tipW = baseW * 0.28f;
  507. float tipStartDist = lerp(ricassoLen, L, 0.70f);
  508. QVector3D tipStart = bladeBase + swordDir * tipStartDist;
  509. auto drawFlatSection = [&](const QVector3D &start, const QVector3D &end,
  510. float width, const QVector3D &color) {
  511. QVector3D right = QVector3D::crossProduct(swordDir, QVector3D(0, 1, 0));
  512. if (right.lengthSquared() < 0.001f) {
  513. right = QVector3D::crossProduct(swordDir, QVector3D(1, 0, 0));
  514. }
  515. right.normalize();
  516. float offset = width * 0.33f;
  517. out.mesh(getUnitCylinder(),
  518. cylinderBetween(ctx.model, start, end, bladeThickness), color,
  519. nullptr, 1.0f);
  520. out.mesh(getUnitCylinder(),
  521. cylinderBetween(ctx.model, start + right * offset,
  522. end + right * offset, bladeThickness * 0.8f),
  523. color * 0.92f, nullptr, 1.0f);
  524. out.mesh(getUnitCylinder(),
  525. cylinderBetween(ctx.model, start - right * offset,
  526. end - right * offset, bladeThickness * 0.8f),
  527. color * 0.92f, nullptr, 1.0f);
  528. };
  529. drawFlatSection(bladeBase, ricassoEnd, baseW, extras.metalColor);
  530. drawFlatSection(ricassoEnd, tipStart, midW, extras.metalColor);
  531. int tipSegments = 3;
  532. for (int i = 0; i < tipSegments; ++i) {
  533. float t0 = (float)i / tipSegments;
  534. float t1 = (float)(i + 1) / tipSegments;
  535. QVector3D segStart =
  536. tipStart + swordDir * ((bladeTip - tipStart).length() * t0);
  537. QVector3D segEnd =
  538. tipStart + swordDir * ((bladeTip - tipStart).length() * t1);
  539. float w = lerp(midW, tipW, t1);
  540. out.mesh(getUnitCylinder(),
  541. cylinderBetween(ctx.model, segStart, segEnd, bladeThickness),
  542. extras.metalColor * (1.0f - i * 0.03f), nullptr, 1.0f);
  543. }
  544. QVector3D fullerStart = bladeBase + swordDir * (ricassoLen + 0.02f);
  545. QVector3D fullerEnd = bladeBase + swordDir * (tipStartDist - 0.06f);
  546. out.mesh(getUnitCylinder(),
  547. cylinderBetween(ctx.model, fullerStart, fullerEnd,
  548. bladeThickness * 0.6f),
  549. extras.metalColor * 0.65f, nullptr, 1.0f);
  550. QVector3D pommel = handleEnd - swordDir * 0.02f;
  551. QMatrix4x4 pommelMat = ctx.model;
  552. pommelMat.translate(pommel);
  553. pommelMat.scale(extras.pommelRadius);
  554. out.mesh(getUnitSphere(), pommelMat, extras.metalColor, nullptr, 1.0f);
  555. if (isAttacking && attackPhase >= 0.32f && attackPhase < 0.56f) {
  556. float t = (attackPhase - 0.32f) / 0.24f;
  557. float alpha = clamp01(0.35f * (1.0f - t));
  558. QVector3D trailStart = bladeBase - swordDir * 0.05f;
  559. QVector3D trailEnd = bladeBase - swordDir * (0.28f + 0.15f * t);
  560. out.mesh(getUnitCone(),
  561. coneFromTo(ctx.model, trailEnd, trailStart, baseW * 0.9f),
  562. extras.metalColor * 0.9f, nullptr, alpha);
  563. }
  564. }
  565. static void drawShieldDecal(const DrawContext &ctx, const QVector3D &center,
  566. float radius, const QVector3D &,
  567. const HumanoidVariant &v, ISubmitter &out) {
  568. QVector3D accent = v.palette.cloth * 1.2f;
  569. float barR = radius * 0.10f;
  570. QVector3D top = center + QVector3D(0.0f, radius * 0.95f, 0.0f);
  571. QVector3D bot = center - QVector3D(0.0f, radius * 0.95f, 0.0f);
  572. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, top, bot, barR),
  573. accent, nullptr, 1.0f);
  574. QVector3D left = center + QVector3D(-radius * 0.95f, 0.0f, 0.0f);
  575. QVector3D right = center + QVector3D(radius * 0.95f, 0.0f, 0.0f);
  576. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, left, right, barR),
  577. accent, nullptr, 1.0f);
  578. }
  579. static void drawShieldRing(const DrawContext &ctx, const QVector3D &center,
  580. float radius, float thickness,
  581. const QVector3D &color, ISubmitter &out) {
  582. const int segments = 12;
  583. for (int i = 0; i < segments; ++i) {
  584. float a0 = (float)i / segments * 2.0f * 3.14159265f;
  585. float a1 = (float)(i + 1) / segments * 2.0f * 3.14159265f;
  586. QVector3D p0(center.x() + radius * std::cos(a0),
  587. center.y() + radius * std::sin(a0), center.z());
  588. QVector3D p1(center.x() + radius * std::cos(a1),
  589. center.y() + radius * std::sin(a1), center.z());
  590. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, p0, p1, thickness),
  591. color, nullptr, 1.0f);
  592. }
  593. }
  594. static void drawShield(const DrawContext &ctx, const HumanoidPose &pose,
  595. const HumanoidVariant &v, const KnightExtras &extras,
  596. ISubmitter &out) {
  597. const float scaleFactor = 2.5f;
  598. const float R = extras.shieldRadius * scaleFactor;
  599. const float yawDeg = -70.0f;
  600. QMatrix4x4 rot;
  601. rot.rotate(yawDeg, 0.0f, 1.0f, 0.0f);
  602. const QVector3D n = rot.map(QVector3D(0.0f, 0.0f, 1.0f));
  603. const QVector3D axisX = rot.map(QVector3D(1.0f, 0.0f, 0.0f));
  604. const QVector3D axisY = rot.map(QVector3D(0.0f, 1.0f, 0.0f));
  605. QVector3D shieldCenter =
  606. pose.handL + axisX * (-R * 0.35f) + axisY * (-0.05f) + n * (0.06f);
  607. const float plateHalf = 0.0015f;
  608. const float plateFull = plateHalf * 2.0f;
  609. {
  610. QMatrix4x4 m = ctx.model;
  611. m.translate(shieldCenter + n * plateHalf);
  612. m.rotate(yawDeg, 0.0f, 1.0f, 0.0f);
  613. m.scale(R, R, plateFull);
  614. out.mesh(getUnitCylinder(), m, extras.shieldColor, nullptr, 1.0f);
  615. }
  616. {
  617. QMatrix4x4 m = ctx.model;
  618. m.translate(shieldCenter - n * plateHalf);
  619. m.rotate(yawDeg, 0.0f, 1.0f, 0.0f);
  620. m.scale(R * 0.985f, R * 0.985f, plateFull);
  621. out.mesh(getUnitCylinder(), m, v.palette.leather * 0.8f, nullptr, 1.0f);
  622. }
  623. auto drawRingRotated = [&](float radius, float thickness,
  624. const QVector3D &color) {
  625. const int segments = 16;
  626. for (int i = 0; i < segments; ++i) {
  627. float a0 = (float)i / segments * 2.0f * 3.14159265f;
  628. float a1 = (float)(i + 1) / segments * 2.0f * 3.14159265f;
  629. QVector3D v0 =
  630. QVector3D(radius * std::cos(a0), radius * std::sin(a0), 0.0f);
  631. QVector3D v1 =
  632. QVector3D(radius * std::cos(a1), radius * std::sin(a1), 0.0f);
  633. QVector3D p0 = shieldCenter + rot.map(v0);
  634. QVector3D p1 = shieldCenter + rot.map(v1);
  635. out.mesh(getUnitCylinder(),
  636. cylinderBetween(ctx.model, p0, p1, thickness), color, nullptr,
  637. 1.0f);
  638. }
  639. };
  640. drawRingRotated(R, 0.010f * scaleFactor, extras.metalColor * 0.95f);
  641. drawRingRotated(R * 0.72f, 0.006f * scaleFactor, v.palette.leather * 0.90f);
  642. {
  643. QMatrix4x4 m = ctx.model;
  644. m.translate(shieldCenter + n * (0.02f * scaleFactor));
  645. m.scale(0.045f * scaleFactor);
  646. out.mesh(getUnitSphere(), m, extras.metalColor, nullptr, 1.0f);
  647. }
  648. {
  649. QVector3D gripA = shieldCenter - axisX * 0.035f - n * 0.030f;
  650. QVector3D gripB = shieldCenter + axisX * 0.035f - n * 0.030f;
  651. out.mesh(getUnitCylinder(),
  652. cylinderBetween(ctx.model, gripA, gripB, 0.010f),
  653. v.palette.leather, nullptr, 1.0f);
  654. }
  655. if (extras.shieldCrossDecal && (extras.shieldColor != extras.metalColor)) {
  656. float decalR = R * 0.85f;
  657. float barR = decalR * 0.10f;
  658. QVector3D centerFront = shieldCenter + n * (plateFull * 0.5f + 0.0015f);
  659. QVector3D top = centerFront + axisY * (decalR * 0.95f);
  660. QVector3D bot = centerFront - axisY * (decalR * 0.95f);
  661. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, top, bot, barR),
  662. v.palette.cloth * 1.2f, nullptr, 1.0f);
  663. QVector3D left = centerFront - axisX * (decalR * 0.95f);
  664. QVector3D right = centerFront + axisX * (decalR * 0.95f);
  665. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, left, right, barR),
  666. v.palette.cloth * 1.2f, nullptr, 1.0f);
  667. }
  668. }
  669. static void drawScabbard(const DrawContext &ctx, const HumanoidPose &,
  670. const HumanoidVariant &v, const KnightExtras &extras,
  671. ISubmitter &out) {
  672. using HP = HumanProportions;
  673. QVector3D hip(0.10f, HP::WAIST_Y - 0.04f, -0.02f);
  674. QVector3D tip = hip + QVector3D(-0.05f, -0.22f, -0.12f);
  675. float sheathR = extras.swordWidth * 0.85f;
  676. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, hip, tip, sheathR),
  677. v.palette.leather * 0.9f, nullptr, 1.0f);
  678. out.mesh(getUnitCone(),
  679. coneFromTo(ctx.model, tip, tip + QVector3D(-0.02f, -0.02f, -0.02f),
  680. sheathR),
  681. extras.metalColor, nullptr, 1.0f);
  682. QVector3D strapA = hip + QVector3D(0.00f, 0.03f, 0.00f);
  683. QVector3D belt = QVector3D(0.12f, HP::WAIST_Y + 0.01f, 0.02f);
  684. out.mesh(getUnitCylinder(),
  685. cylinderBetween(ctx.model, strapA, belt, 0.006f),
  686. v.palette.leather, nullptr, 1.0f);
  687. }
  688. };
  689. void registerKnightRenderer(Render::GL::EntityRendererRegistry &registry) {
  690. static KnightRenderer renderer;
  691. registry.registerRenderer(
  692. "knight", [](const DrawContext &ctx, ISubmitter &out) {
  693. static KnightRenderer staticRenderer;
  694. Shader *knightShader = nullptr;
  695. if (ctx.backend) {
  696. knightShader = ctx.backend->shader(QStringLiteral("knight"));
  697. }
  698. Renderer *sceneRenderer = dynamic_cast<Renderer *>(&out);
  699. if (sceneRenderer && knightShader) {
  700. sceneRenderer->setCurrentShader(knightShader);
  701. }
  702. staticRenderer.render(ctx, out);
  703. if (sceneRenderer) {
  704. sceneRenderer->setCurrentShader(nullptr);
  705. }
  706. });
  707. }
  708. } // namespace Render::GL