horse_renderer.cpp 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386
  1. #include "horse_renderer.h"
  2. #include "../geom/math_utils.h"
  3. #include "../geom/transforms.h"
  4. #include "../gl/primitives.h"
  5. #include "../humanoid_base.h"
  6. #include <QMatrix4x4>
  7. #include <QRandomGenerator>
  8. #include <QVector3D>
  9. #include <algorithm>
  10. #include <cmath>
  11. #include <cstdint>
  12. #include <numbers>
  13. #include <qmatrix4x4.h>
  14. #include <qvectornd.h>
  15. namespace Render::GL {
  16. using Render::Geom::clamp01;
  17. using Render::Geom::coneFromTo;
  18. using Render::Geom::cylinderBetween;
  19. using Render::Geom::lerp;
  20. using Render::Geom::smoothstep;
  21. namespace {
  22. constexpr float kPi = std::numbers::pi_v<float>;
  23. constexpr int k_hash_shift_16 = 16;
  24. constexpr int k_hash_shift_15 = 15;
  25. constexpr uint32_t k_hash_mult_1 = 0x7Feb352dU;
  26. constexpr uint32_t k_hash_mult_2 = 0x846ca68bU;
  27. constexpr uint32_t k_hash_mask_24bit = 0xFFFFFF;
  28. constexpr float k_hash_divisor = 16777216.0F;
  29. constexpr float k_rgb_max = 255.0F;
  30. constexpr int k_rgb_shift_red = 16;
  31. constexpr int k_rgb_shift_green = 8;
  32. inline auto hash01(uint32_t x) -> float {
  33. x ^= x >> k_hash_shift_16;
  34. x *= k_hash_mult_1;
  35. x ^= x >> k_hash_shift_15;
  36. x *= k_hash_mult_2;
  37. x ^= x >> k_hash_shift_16;
  38. return (x & k_hash_mask_24bit) / k_hash_divisor;
  39. }
  40. inline auto randBetween(uint32_t seed, uint32_t salt, float minV,
  41. float maxV) -> float {
  42. const float t = hash01(seed ^ salt);
  43. return minV + (maxV - minV) * t;
  44. }
  45. inline auto saturate(float x) -> float {
  46. return std::min(1.0F, std::max(0.0F, x));
  47. }
  48. inline auto rotateAroundY(const QVector3D &v, float angle) -> QVector3D {
  49. float const s = std::sin(angle);
  50. float const c = std::cos(angle);
  51. return {v.x() * c + v.z() * s, v.y(), -v.x() * s + v.z() * c};
  52. }
  53. inline auto rotateAroundZ(const QVector3D &v, float angle) -> QVector3D {
  54. float const s = std::sin(angle);
  55. float const c = std::cos(angle);
  56. return {v.x() * c - v.y() * s, v.x() * s + v.y() * c, v.z()};
  57. }
  58. inline auto darken(const QVector3D &c, float k) -> QVector3D { return c * k; }
  59. inline auto lighten(const QVector3D &c, float k) -> QVector3D {
  60. return {saturate(c.x() * k), saturate(c.y() * k), saturate(c.z() * k)};
  61. }
  62. inline auto coatGradient(const QVector3D &coat, float verticalFactor,
  63. float longitudinalFactor, float seed) -> QVector3D {
  64. float const highlight = saturate(0.55F + verticalFactor * 0.35F -
  65. longitudinalFactor * 0.20F + seed * 0.08F);
  66. QVector3D const bright = lighten(coat, 1.08F);
  67. QVector3D const shadow = darken(coat, 0.86F);
  68. return shadow * (1.0F - highlight) + bright * highlight;
  69. }
  70. inline auto lerp3(const QVector3D &a, const QVector3D &b,
  71. float t) -> QVector3D {
  72. return {a.x() + (b.x() - a.x()) * t, a.y() + (b.y() - a.y()) * t,
  73. a.z() + (b.z() - a.z()) * t};
  74. }
  75. inline auto scaledSphere(const QMatrix4x4 &model, const QVector3D &center,
  76. const QVector3D &scale) -> QMatrix4x4 {
  77. QMatrix4x4 m = model;
  78. m.translate(center);
  79. m.scale(scale);
  80. return m;
  81. }
  82. inline void drawCylinder(ISubmitter &out, const QMatrix4x4 &model,
  83. const QVector3D &a, const QVector3D &b, float radius,
  84. const QVector3D &color, float alpha = 1.0F) {
  85. out.mesh(getUnitCylinder(), cylinderBetween(model, a, b, radius), color,
  86. nullptr, alpha);
  87. }
  88. inline void drawCone(ISubmitter &out, const QMatrix4x4 &model,
  89. const QVector3D &tip, const QVector3D &base, float radius,
  90. const QVector3D &color, float alpha = 1.0F) {
  91. out.mesh(getUnitCone(), coneFromTo(model, tip, base, radius), color, nullptr,
  92. alpha);
  93. }
  94. inline auto bezier(const QVector3D &p0, const QVector3D &p1,
  95. const QVector3D &p2, float t) -> QVector3D {
  96. float const u = 1.0F - t;
  97. return p0 * (u * u) + p1 * (2.0F * u * t) + p2 * (t * t);
  98. }
  99. inline auto colorHash(const QVector3D &c) -> uint32_t {
  100. auto const r = uint32_t(saturate(c.x()) * k_rgb_max);
  101. auto const g = uint32_t(saturate(c.y()) * k_rgb_max);
  102. auto const b = uint32_t(saturate(c.z()) * k_rgb_max);
  103. uint32_t v = (r << k_rgb_shift_red) | (g << k_rgb_shift_green) | b;
  104. v ^= v >> k_hash_shift_16;
  105. v *= k_hash_mult_1;
  106. v ^= v >> k_hash_shift_15;
  107. v *= k_hash_mult_2;
  108. v ^= v >> k_hash_shift_16;
  109. return v;
  110. }
  111. } // namespace
  112. auto makeHorseDimensions(uint32_t seed) -> HorseDimensions {
  113. HorseDimensions d{};
  114. d.bodyLength = randBetween(seed, 0x12U, 0.88F, 0.98F);
  115. d.bodyWidth = randBetween(seed, 0x34U, 0.18F, 0.22F);
  116. d.bodyHeight = randBetween(seed, 0x56U, 0.40F, 0.46F);
  117. d.barrel_centerY = randBetween(seed, 0x78U, 0.05F, 0.09F);
  118. d.neckLength = randBetween(seed, 0x9AU, 0.42F, 0.50F);
  119. d.neckRise = randBetween(seed, 0xBCU, 0.26F, 0.32F);
  120. d.headLength = randBetween(seed, 0xDEU, 0.28F, 0.34F);
  121. d.headWidth = randBetween(seed, 0xF1U, 0.14F, 0.17F);
  122. d.headHeight = randBetween(seed, 0x1357U, 0.18F, 0.22F);
  123. d.muzzleLength = randBetween(seed, 0x2468U, 0.13F, 0.16F);
  124. d.legLength = randBetween(seed, 0x369CU, 1.05F, 1.18F);
  125. d.hoofHeight = randBetween(seed, 0x48AEU, 0.080F, 0.095F);
  126. d.tailLength = randBetween(seed, 0x5ABCU, 0.38F, 0.48F);
  127. d.saddleThickness = randBetween(seed, 0x6CDEU, 0.035F, 0.045F);
  128. d.seatForwardOffset = randBetween(seed, 0x7531U, 0.010F, 0.035F);
  129. d.stirrupOut = d.bodyWidth * randBetween(seed, 0x8642U, 0.75F, 0.88F);
  130. d.stirrupDrop = randBetween(seed, 0x9753U, 0.28F, 0.32F);
  131. d.idleBobAmplitude = randBetween(seed, 0xA864U, 0.004F, 0.007F);
  132. d.moveBobAmplitude = randBetween(seed, 0xB975U, 0.024F, 0.032F);
  133. d.saddle_height = d.barrel_centerY + d.bodyHeight * 0.55F + d.saddleThickness;
  134. return d;
  135. }
  136. auto makeHorseVariant(uint32_t seed, const QVector3D &leatherBase,
  137. const QVector3D &clothBase) -> HorseVariant {
  138. HorseVariant v;
  139. float const coat_hue = hash01(seed ^ 0x23456U);
  140. if (coat_hue < 0.18F) {
  141. v.coatColor = QVector3D(0.70F, 0.68F, 0.63F);
  142. } else if (coat_hue < 0.38F) {
  143. v.coatColor = QVector3D(0.40F, 0.30F, 0.22F);
  144. } else if (coat_hue < 0.65F) {
  145. v.coatColor = QVector3D(0.28F, 0.22F, 0.19F);
  146. } else if (coat_hue < 0.85F) {
  147. v.coatColor = QVector3D(0.18F, 0.15F, 0.13F);
  148. } else {
  149. v.coatColor = QVector3D(0.48F, 0.42F, 0.39F);
  150. }
  151. float const blaze_chance = hash01(seed ^ 0x1122U);
  152. if (blaze_chance > 0.82F) {
  153. v.coatColor = lerp(v.coatColor, QVector3D(0.92F, 0.92F, 0.90F), 0.25F);
  154. }
  155. v.mane_color = lerp(v.coatColor, QVector3D(0.10F, 0.09F, 0.08F),
  156. randBetween(seed, 0x3344U, 0.55F, 0.85F));
  157. v.tail_color = lerp(v.mane_color, v.coatColor, 0.35F);
  158. v.muzzleColor = lerp(v.coatColor, QVector3D(0.18F, 0.14F, 0.12F), 0.65F);
  159. v.hoof_color =
  160. lerp(QVector3D(0.16F, 0.14F, 0.12F), QVector3D(0.40F, 0.35F, 0.32F),
  161. randBetween(seed, 0x5566U, 0.15F, 0.65F));
  162. float const leather_tone = randBetween(seed, 0x7788U, 0.78F, 0.96F);
  163. float const tack_tone = randBetween(seed, 0x88AAU, 0.58F, 0.78F);
  164. QVector3D const leather_tint = leatherBase * leather_tone;
  165. QVector3D tack_tint = leatherBase * tack_tone;
  166. if (blaze_chance > 0.90F) {
  167. tack_tint = lerp(tack_tint, QVector3D(0.18F, 0.19F, 0.22F), 0.25F);
  168. }
  169. v.saddleColor = leather_tint;
  170. v.tack_color = tack_tint;
  171. v.blanketColor = clothBase * randBetween(seed, 0x99B0U, 0.92F, 1.05F);
  172. return v;
  173. }
  174. auto makeHorseProfile(uint32_t seed, const QVector3D &leatherBase,
  175. const QVector3D &clothBase) -> HorseProfile {
  176. HorseProfile profile;
  177. profile.dims = makeHorseDimensions(seed);
  178. profile.variant = makeHorseVariant(seed, leatherBase, clothBase);
  179. profile.gait.cycleTime = randBetween(seed, 0xAA12U, 0.60F, 0.72F);
  180. profile.gait.frontLegPhase = randBetween(seed, 0xBB34U, 0.08F, 0.16F);
  181. float const diagonal_lead = randBetween(seed, 0xCC56U, 0.44F, 0.54F);
  182. profile.gait.rearLegPhase =
  183. std::fmod(profile.gait.frontLegPhase + diagonal_lead, 1.0F);
  184. profile.gait.strideSwing = randBetween(seed, 0xDD78U, 0.26F, 0.32F);
  185. profile.gait.strideLift = randBetween(seed, 0xEE9AU, 0.10F, 0.14F);
  186. return profile;
  187. }
  188. void HorseRenderer::render(const DrawContext &ctx, const AnimationInputs &anim,
  189. const HorseProfile &profile, ISubmitter &out) {
  190. const HorseDimensions &d = profile.dims;
  191. const HorseVariant &v = profile.variant;
  192. const HorseGait &g = profile.gait;
  193. float phase = 0.0F;
  194. float bob = 0.0F;
  195. if (anim.isMoving) {
  196. float const cycle = std::max(0.20F, g.cycleTime);
  197. phase = std::fmod(anim.time / cycle, 1.0F);
  198. bob = std::sin(phase * 2.0F * kPi) * d.moveBobAmplitude;
  199. } else {
  200. phase = std::fmod(anim.time * 0.25F, 1.0F);
  201. bob = std::sin(phase * 2.0F * kPi) * d.idleBobAmplitude;
  202. }
  203. float const head_nod = anim.isMoving
  204. ? std::sin((phase + 0.25F) * 2.0F * kPi) * 0.04F
  205. : std::sin(anim.time * 1.5F) * 0.01F;
  206. uint32_t const vhash = colorHash(v.coatColor);
  207. float const sock_chance_fl = hash01(vhash ^ 0x101U);
  208. float const sock_chance_fr = hash01(vhash ^ 0x202U);
  209. float const sock_chance_rl = hash01(vhash ^ 0x303U);
  210. float const sock_chance_rr = hash01(vhash ^ 0x404U);
  211. bool const has_blaze = hash01(vhash ^ 0x505U) > 0.82F;
  212. float rider_lean = hash01(vhash ^ 0x606U) * 0.12F - 0.06F;
  213. float rein_slack = hash01(vhash ^ 0x707U) * 0.08F + 0.02F;
  214. const float coat_seed_a = hash01(vhash ^ 0x701U);
  215. const float coat_seed_b = hash01(vhash ^ 0x702U);
  216. const float coat_seed_c = hash01(vhash ^ 0x703U);
  217. const float coat_seed_d = hash01(vhash ^ 0x704U);
  218. QVector3D const barrel_center(0.0F, d.barrel_centerY + bob, 0.0F);
  219. QVector3D const chest_center =
  220. barrel_center +
  221. QVector3D(0.0F, d.bodyHeight * 0.12F, d.bodyLength * 0.34F);
  222. QVector3D const rump_center =
  223. barrel_center +
  224. QVector3D(0.0F, d.bodyHeight * 0.08F, -d.bodyLength * 0.36F);
  225. QVector3D const belly_center =
  226. barrel_center +
  227. QVector3D(0.0F, -d.bodyHeight * 0.35F, -d.bodyLength * 0.05F);
  228. {
  229. QMatrix4x4 chest = ctx.model;
  230. chest.translate(chest_center);
  231. chest.scale(d.bodyWidth * 1.12F, d.bodyHeight * 0.95F,
  232. d.bodyLength * 0.36F);
  233. QVector3D const chest_color =
  234. coatGradient(v.coatColor, 0.75F, 0.20F, coat_seed_a);
  235. out.mesh(getUnitSphere(), chest, chest_color, nullptr, 1.0F);
  236. }
  237. {
  238. QMatrix4x4 withers = ctx.model;
  239. withers.translate(chest_center + QVector3D(0.0F, d.bodyHeight * 0.55F,
  240. -d.bodyLength * 0.03F));
  241. withers.scale(d.bodyWidth * 0.75F, d.bodyHeight * 0.35F,
  242. d.bodyLength * 0.18F);
  243. QVector3D const wither_color =
  244. coatGradient(v.coatColor, 0.88F, 0.35F, coat_seed_b);
  245. out.mesh(getUnitSphere(), withers, wither_color, nullptr, 1.0F);
  246. }
  247. {
  248. QMatrix4x4 belly = ctx.model;
  249. belly.translate(belly_center);
  250. belly.scale(d.bodyWidth * 0.98F, d.bodyHeight * 0.64F,
  251. d.bodyLength * 0.40F);
  252. QVector3D const belly_color =
  253. coatGradient(v.coatColor, 0.25F, -0.10F, coat_seed_c);
  254. out.mesh(getUnitSphere(), belly, belly_color, nullptr, 1.0F);
  255. }
  256. for (int i = 0; i < 2; ++i) {
  257. float const side = (i == 0) ? 1.0F : -1.0F;
  258. QMatrix4x4 ribs = ctx.model;
  259. ribs.translate(barrel_center + QVector3D(side * d.bodyWidth * 0.90F,
  260. -d.bodyHeight * 0.10F,
  261. -d.bodyLength * 0.05F));
  262. ribs.scale(d.bodyWidth * 0.38F, d.bodyHeight * 0.42F, d.bodyLength * 0.30F);
  263. QVector3D const rib_color =
  264. coatGradient(v.coatColor, 0.45F, 0.05F, coat_seed_d + side * 0.05F);
  265. out.mesh(getUnitSphere(), ribs, rib_color, nullptr, 1.0F);
  266. }
  267. {
  268. QMatrix4x4 rump = ctx.model;
  269. rump.translate(rump_center);
  270. rump.scale(d.bodyWidth * 1.18F, d.bodyHeight * 1.00F, d.bodyLength * 0.36F);
  271. QVector3D const rump_color =
  272. coatGradient(v.coatColor, 0.62F, -0.28F, coat_seed_a * 0.7F);
  273. out.mesh(getUnitSphere(), rump, rump_color, nullptr, 1.0F);
  274. }
  275. for (int i = 0; i < 2; ++i) {
  276. float const side = (i == 0) ? 1.0F : -1.0F;
  277. QMatrix4x4 hip = ctx.model;
  278. hip.translate(rump_center + QVector3D(side * d.bodyWidth * 0.95F,
  279. -d.bodyHeight * 0.10F,
  280. -d.bodyLength * 0.08F));
  281. hip.scale(d.bodyWidth * 0.45F, d.bodyHeight * 0.42F, d.bodyLength * 0.26F);
  282. QVector3D const hip_color =
  283. coatGradient(v.coatColor, 0.58F, -0.18F, coat_seed_b + side * 0.06F);
  284. out.mesh(getUnitSphere(), hip, hip_color, nullptr, 1.0F);
  285. QMatrix4x4 haunch = ctx.model;
  286. haunch.translate(rump_center + QVector3D(side * d.bodyWidth * 0.88F,
  287. d.bodyHeight * 0.24F,
  288. -d.bodyLength * 0.20F));
  289. haunch.scale(QVector3D(d.bodyWidth * 0.32F, d.bodyHeight * 0.28F,
  290. d.bodyLength * 0.18F));
  291. QVector3D const haunch_color =
  292. coatGradient(v.coatColor, 0.72F, -0.26F, coat_seed_c + side * 0.04F);
  293. out.mesh(getUnitSphere(), haunch, lighten(haunch_color, 1.02F), nullptr,
  294. 1.0F);
  295. }
  296. QVector3D withers_peak = chest_center + QVector3D(0.0F, d.bodyHeight * 0.62F,
  297. -d.bodyLength * 0.06F);
  298. QVector3D croup_peak = rump_center + QVector3D(0.0F, d.bodyHeight * 0.46F,
  299. -d.bodyLength * 0.18F);
  300. {
  301. QMatrix4x4 spine = ctx.model;
  302. spine.translate(lerp(withers_peak, croup_peak, 0.42F));
  303. spine.scale(QVector3D(d.bodyWidth * 0.50F, d.bodyHeight * 0.14F,
  304. d.bodyLength * 0.54F));
  305. QVector3D const spine_color =
  306. coatGradient(v.coatColor, 0.74F, -0.06F, coat_seed_d * 0.92F);
  307. out.mesh(getUnitSphere(), spine, spine_color, nullptr, 1.0F);
  308. }
  309. for (int i = 0; i < 2; ++i) {
  310. float const side = (i == 0) ? 1.0F : -1.0F;
  311. QVector3D const scapula_top =
  312. withers_peak + QVector3D(side * d.bodyWidth * 0.52F,
  313. d.bodyHeight * 0.08F, d.bodyLength * 0.06F);
  314. QVector3D const scapula_base =
  315. chest_center + QVector3D(side * d.bodyWidth * 0.70F,
  316. -d.bodyHeight * 0.02F, d.bodyLength * 0.06F);
  317. QVector3D const scapula_mid = lerp(scapula_top, scapula_base, 0.55F);
  318. drawCylinder(
  319. out, ctx.model, scapula_top, scapula_mid, d.bodyWidth * 0.18F,
  320. coatGradient(v.coatColor, 0.82F, 0.16F, coat_seed_a + side * 0.05F));
  321. QMatrix4x4 shoulder_cap = ctx.model;
  322. shoulder_cap.translate(scapula_base + QVector3D(0.0F, d.bodyHeight * 0.04F,
  323. d.bodyLength * 0.02F));
  324. shoulder_cap.scale(QVector3D(d.bodyWidth * 0.32F, d.bodyHeight * 0.24F,
  325. d.bodyLength * 0.18F));
  326. out.mesh(
  327. getUnitSphere(), shoulder_cap,
  328. coatGradient(v.coatColor, 0.66F, 0.12F, coat_seed_b + side * 0.07F),
  329. nullptr, 1.0F);
  330. }
  331. {
  332. QMatrix4x4 sternum = ctx.model;
  333. sternum.translate(barrel_center + QVector3D(0.0F, -d.bodyHeight * 0.40F,
  334. d.bodyLength * 0.28F));
  335. sternum.scale(QVector3D(d.bodyWidth * 0.50F, d.bodyHeight * 0.14F,
  336. d.bodyLength * 0.12F));
  337. out.mesh(getUnitSphere(), sternum,
  338. coatGradient(v.coatColor, 0.18F, 0.18F, coat_seed_a * 0.4F),
  339. nullptr, 1.0F);
  340. }
  341. QVector3D const neck_base =
  342. chest_center +
  343. QVector3D(0.0F, d.bodyHeight * 0.38F, d.bodyLength * 0.06F);
  344. QVector3D const neck_top =
  345. neck_base + QVector3D(0.0F, d.neckRise, d.neckLength);
  346. float const neck_radius = d.bodyWidth * 0.42F;
  347. QVector3D const neck_mid =
  348. lerp(neck_base, neck_top, 0.55F) +
  349. QVector3D(0.0F, d.bodyHeight * 0.02F, d.bodyLength * 0.02F);
  350. QVector3D const neck_color_base =
  351. coatGradient(v.coatColor, 0.78F, 0.12F, coat_seed_c * 0.6F);
  352. out.mesh(getUnitCylinder(),
  353. cylinderBetween(ctx.model, neck_base, neck_mid, neck_radius * 1.00F),
  354. neck_color_base, nullptr, 1.0F);
  355. out.mesh(getUnitCylinder(),
  356. cylinderBetween(ctx.model, neck_mid, neck_top, neck_radius * 0.86F),
  357. lighten(neck_color_base, 1.03F), nullptr, 1.0F);
  358. {
  359. QVector3D const jugular_start =
  360. lerp(neck_base, neck_top, 0.42F) + QVector3D(d.bodyWidth * 0.18F,
  361. -d.bodyHeight * 0.06F,
  362. d.bodyLength * 0.04F);
  363. QVector3D const jugular_end =
  364. jugular_start +
  365. QVector3D(0.0F, -d.bodyHeight * 0.24F, d.bodyLength * 0.06F);
  366. drawCylinder(out, ctx.model, jugular_start, jugular_end,
  367. neck_radius * 0.18F, lighten(neck_color_base, 1.08F), 0.85F);
  368. }
  369. const int mane_sections = 8;
  370. QVector3D const mane_color =
  371. lerp3(v.mane_color, QVector3D(0.12F, 0.09F, 0.08F), 0.35F);
  372. for (int i = 0; i < mane_sections; ++i) {
  373. float const t =
  374. static_cast<float>(i) / static_cast<float>(mane_sections - 1);
  375. QVector3D const spine = lerp(neck_base, neck_top, t) +
  376. QVector3D(0.0F, d.bodyHeight * 0.12F, 0.0F);
  377. float const length = lerp(0.14F, 0.08F, t) * d.bodyHeight * 1.4F;
  378. QVector3D const tip =
  379. spine + QVector3D(0.0F, length * 1.2F, 0.02F * length);
  380. drawCone(out, ctx.model, tip, spine, d.bodyWidth * lerp(0.25F, 0.12F, t),
  381. mane_color, 1.0F);
  382. }
  383. QVector3D const head_center =
  384. neck_top + QVector3D(0.0F, d.headHeight * (0.10F - head_nod * 0.15F),
  385. d.headLength * 0.40F);
  386. {
  387. QMatrix4x4 skull = ctx.model;
  388. skull.translate(head_center + QVector3D(0.0F, d.headHeight * 0.10F,
  389. -d.headLength * 0.10F));
  390. skull.scale(d.headWidth * 0.95F, d.headHeight * 0.90F,
  391. d.headLength * 0.80F);
  392. QVector3D const skull_color =
  393. coatGradient(v.coatColor, 0.82F, 0.30F, coat_seed_d * 0.8F);
  394. out.mesh(getUnitSphere(), skull, skull_color, nullptr, 1.0F);
  395. }
  396. for (int i = 0; i < 2; ++i) {
  397. float const side = (i == 0) ? 1.0F : -1.0F;
  398. QMatrix4x4 cheek = ctx.model;
  399. cheek.translate(head_center + QVector3D(side * d.headWidth * 0.55F,
  400. -d.headHeight * 0.15F, 0.0F));
  401. cheek.scale(d.headWidth * 0.45F, d.headHeight * 0.50F,
  402. d.headLength * 0.60F);
  403. QVector3D const cheek_color =
  404. coatGradient(v.coatColor, 0.70F, 0.18F, coat_seed_a * 0.9F);
  405. out.mesh(getUnitSphere(), cheek, cheek_color, nullptr, 1.0F);
  406. }
  407. QVector3D const muzzle_center =
  408. head_center +
  409. QVector3D(0.0F, -d.headHeight * 0.18F, d.headLength * 0.58F);
  410. {
  411. QMatrix4x4 muzzle = ctx.model;
  412. muzzle.translate(muzzle_center +
  413. QVector3D(0.0F, -d.headHeight * 0.05F, 0.0F));
  414. muzzle.scale(d.headWidth * 0.68F, d.headHeight * 0.60F,
  415. d.muzzleLength * 1.05F);
  416. out.mesh(getUnitSphere(), muzzle, v.muzzleColor, nullptr, 1.0F);
  417. }
  418. {
  419. QVector3D const nostril_base =
  420. muzzle_center +
  421. QVector3D(0.0F, -d.headHeight * 0.02F, d.muzzleLength * 0.60F);
  422. QVector3D const left_base =
  423. nostril_base + QVector3D(d.headWidth * 0.26F, 0.0F, 0.0F);
  424. QVector3D const right_base =
  425. nostril_base + QVector3D(-d.headWidth * 0.26F, 0.0F, 0.0F);
  426. QVector3D const inward =
  427. QVector3D(0.0F, -d.headHeight * 0.02F, d.muzzleLength * -0.30F);
  428. out.mesh(getUnitCone(),
  429. coneFromTo(ctx.model, left_base + inward, left_base,
  430. d.headWidth * 0.11F),
  431. darken(v.muzzleColor, 0.6F), nullptr, 1.0F);
  432. out.mesh(getUnitCone(),
  433. coneFromTo(ctx.model, right_base + inward, right_base,
  434. d.headWidth * 0.11F),
  435. darken(v.muzzleColor, 0.6F), nullptr, 1.0F);
  436. }
  437. float const ear_flick_l = std::sin(anim.time * 1.7F + 1.3F) * 0.15F;
  438. float const ear_flick_r = std::sin(anim.time * 1.9F + 2.1F) * -0.12F;
  439. QVector3D const ear_base_left =
  440. head_center + QVector3D(d.headWidth * 0.45F, d.headHeight * 0.42F,
  441. -d.headLength * 0.20F);
  442. QVector3D const ear_tip_left =
  443. ear_base_left +
  444. rotateAroundY(QVector3D(d.headWidth * 0.08F, d.headHeight * 0.42F,
  445. -d.headLength * 0.10F),
  446. ear_flick_l);
  447. QVector3D const ear_base_right =
  448. head_center + QVector3D(-d.headWidth * 0.45F, d.headHeight * 0.42F,
  449. -d.headLength * 0.20F);
  450. QVector3D const ear_tip_right =
  451. ear_base_right +
  452. rotateAroundY(QVector3D(-d.headWidth * 0.08F, d.headHeight * 0.42F,
  453. -d.headLength * 0.10F),
  454. ear_flick_r);
  455. out.mesh(
  456. getUnitCone(),
  457. coneFromTo(ctx.model, ear_tip_left, ear_base_left, d.headWidth * 0.11F),
  458. v.mane_color, nullptr, 1.0F);
  459. out.mesh(
  460. getUnitCone(),
  461. coneFromTo(ctx.model, ear_tip_right, ear_base_right, d.headWidth * 0.11F),
  462. v.mane_color, nullptr, 1.0F);
  463. QVector3D const eye_left =
  464. head_center + QVector3D(d.headWidth * 0.48F, d.headHeight * 0.10F,
  465. d.headLength * 0.05F);
  466. QVector3D const eye_right =
  467. head_center + QVector3D(-d.headWidth * 0.48F, d.headHeight * 0.10F,
  468. d.headLength * 0.05F);
  469. QVector3D eye_base_color(0.10F, 0.10F, 0.10F);
  470. auto draw_eye = [&](const QVector3D &pos) {
  471. {
  472. QMatrix4x4 eye = ctx.model;
  473. eye.translate(pos);
  474. eye.scale(d.headWidth * 0.14F);
  475. out.mesh(getUnitSphere(), eye, eye_base_color, nullptr, 1.0F);
  476. }
  477. {
  478. QMatrix4x4 pupil = ctx.model;
  479. pupil.translate(pos + QVector3D(0.0F, 0.0F, d.headWidth * 0.04F));
  480. pupil.scale(d.headWidth * 0.05F);
  481. out.mesh(getUnitSphere(), pupil, QVector3D(0.03F, 0.03F, 0.03F), nullptr,
  482. 1.0F);
  483. }
  484. {
  485. QMatrix4x4 spec = ctx.model;
  486. spec.translate(pos + QVector3D(d.headWidth * 0.03F, d.headWidth * 0.03F,
  487. d.headWidth * 0.03F));
  488. spec.scale(d.headWidth * 0.02F);
  489. out.mesh(getUnitSphere(), spec, QVector3D(0.95F, 0.95F, 0.95F), nullptr,
  490. 1.0F);
  491. }
  492. };
  493. draw_eye(eye_left);
  494. draw_eye(eye_right);
  495. if (has_blaze) {
  496. QMatrix4x4 blaze = ctx.model;
  497. blaze.translate(head_center + QVector3D(0.0F, d.headHeight * 0.15F,
  498. d.headLength * 0.10F));
  499. blaze.scale(d.headWidth * 0.22F, d.headHeight * 0.32F,
  500. d.headLength * 0.10F);
  501. out.mesh(getUnitSphere(), blaze, QVector3D(0.92F, 0.92F, 0.90F), nullptr,
  502. 1.0F);
  503. }
  504. QVector3D bridle_base = muzzle_center + QVector3D(0.0F, -d.headHeight * 0.05F,
  505. d.muzzleLength * 0.20F);
  506. QVector3D const cheek_anchor_left =
  507. head_center + QVector3D(d.headWidth * 0.55F, d.headHeight * 0.05F,
  508. -d.headLength * 0.05F);
  509. QVector3D const cheek_anchor_right =
  510. head_center + QVector3D(-d.headWidth * 0.55F, d.headHeight * 0.05F,
  511. -d.headLength * 0.05F);
  512. QVector3D const brow = head_center + QVector3D(0.0F, d.headHeight * 0.38F,
  513. -d.headLength * 0.28F);
  514. QVector3D const tack_color = lighten(v.tack_color, 0.9F);
  515. drawCylinder(out, ctx.model, bridle_base, cheek_anchor_left,
  516. d.headWidth * 0.07F, tack_color);
  517. drawCylinder(out, ctx.model, bridle_base, cheek_anchor_right,
  518. d.headWidth * 0.07F, tack_color);
  519. drawCylinder(out, ctx.model, cheek_anchor_left, brow, d.headWidth * 0.05F,
  520. tack_color);
  521. drawCylinder(out, ctx.model, cheek_anchor_right, brow, d.headWidth * 0.05F,
  522. tack_color);
  523. QVector3D const mane_root =
  524. neck_top + QVector3D(0.0F, d.headHeight * 0.20F, -d.headLength * 0.20F);
  525. constexpr int k_mane_segments = 12;
  526. constexpr float k_mane_segment_divisor = 11.0F;
  527. for (int i = 0; i < k_mane_segments; ++i) {
  528. float const t = i / k_mane_segment_divisor;
  529. QVector3D seg_start = lerp(mane_root, neck_base, t);
  530. seg_start.setY(seg_start.y() + (0.07F - t * 0.05F));
  531. float const sway =
  532. (anim.isMoving ? std::sin((phase + t * 0.15F) * 2.0F * kPi) * 0.04F
  533. : std::sin((anim.time * 0.8F + t * 2.3F)) * 0.02F);
  534. QVector3D const seg_end =
  535. seg_start + QVector3D(sway, 0.07F - t * 0.05F, -0.05F - t * 0.03F);
  536. out.mesh(getUnitCylinder(),
  537. cylinderBetween(ctx.model, seg_start, seg_end,
  538. d.headWidth * (0.10F * (1.0F - t * 0.4F))),
  539. v.mane_color * (0.98F + t * 0.05F), nullptr, 1.0F);
  540. }
  541. {
  542. QVector3D const forelock_base =
  543. head_center +
  544. QVector3D(0.0F, d.headHeight * 0.28F, -d.headLength * 0.18F);
  545. for (int i = 0; i < 3; ++i) {
  546. float const offset = (i - 1) * d.headWidth * 0.10F;
  547. QVector3D const strand_base =
  548. forelock_base + QVector3D(offset, 0.0F, 0.0F);
  549. QVector3D const strand_tip =
  550. strand_base +
  551. QVector3D(offset * 0.4F, -d.headHeight * 0.25F, d.headLength * 0.12F);
  552. drawCone(out, ctx.model, strand_tip, strand_base, d.headWidth * 0.10F,
  553. v.mane_color * (0.94F + 0.03F * i), 0.96F);
  554. }
  555. }
  556. QVector3D const tail_base =
  557. rump_center +
  558. QVector3D(0.0F, d.bodyHeight * 0.36F, -d.bodyLength * 0.48F);
  559. QVector3D const tail_ctrl =
  560. tail_base + QVector3D(0.0F, -d.tailLength * 0.20F, -d.tailLength * 0.28F);
  561. QVector3D const tail_end =
  562. tail_base + QVector3D(0.0F, -d.tailLength, -d.tailLength * 0.70F);
  563. QVector3D const tail_color = lerp3(v.tail_color, v.mane_color, 0.35F);
  564. QVector3D prev_tail = tail_base;
  565. for (int i = 1; i <= 8; ++i) {
  566. float const t = static_cast<float>(i) / 8.0F;
  567. QVector3D p = bezier(tail_base, tail_ctrl, tail_end, t);
  568. float const swing =
  569. (anim.isMoving ? std::sin((phase + t * 0.12F) * 2.0F * kPi)
  570. : std::sin((phase * 0.7F + t * 0.3F) * 2.0F * kPi)) *
  571. (0.04F + 0.015F * (1.0F - t));
  572. p.setX(p.x() + swing);
  573. float const radius = d.bodyWidth * (0.20F - 0.018F * i);
  574. drawCylinder(out, ctx.model, prev_tail, p, radius, tail_color);
  575. prev_tail = p;
  576. }
  577. {
  578. QMatrix4x4 tail_knot = ctx.model;
  579. tail_knot.translate(tail_base + QVector3D(0.0F, -d.bodyHeight * 0.06F,
  580. -d.bodyLength * 0.02F));
  581. tail_knot.scale(QVector3D(d.bodyWidth * 0.24F, d.bodyWidth * 0.18F,
  582. d.bodyWidth * 0.20F));
  583. out.mesh(getUnitSphere(), tail_knot, lighten(tail_color, 0.92F), nullptr,
  584. 1.0F);
  585. }
  586. for (int i = 0; i < 3; ++i) {
  587. float const spread = (i - 1) * d.bodyWidth * 0.14F;
  588. QVector3D const fan_base =
  589. tail_end +
  590. QVector3D(spread * 0.15F, -d.bodyWidth * 0.05F, -d.tailLength * 0.08F);
  591. QVector3D const fan_tip =
  592. fan_base +
  593. QVector3D(spread, -d.tailLength * 0.32F, -d.tailLength * 0.22F);
  594. drawCone(out, ctx.model, fan_tip, fan_base, d.bodyWidth * 0.24F,
  595. tail_color * (0.96F + 0.02F * i), 0.88F);
  596. }
  597. auto render_hoof = [&](const QVector3D &hoof_top,
  598. const QVector3D &hoof_bottom, float wallRadius,
  599. const QVector3D &hoof_color, bool is_rear) {
  600. QVector3D const wall_tint = lighten(hoof_color, is_rear ? 1.04F : 1.0F);
  601. out.mesh(getUnitCylinder(),
  602. cylinderBetween(ctx.model, hoof_top, hoof_bottom, wallRadius),
  603. wall_tint, nullptr, 1.0F);
  604. QVector3D const toe =
  605. hoof_bottom + QVector3D(0.0F, -d.hoofHeight * 0.14F, 0.0F);
  606. out.mesh(getUnitCone(),
  607. coneFromTo(ctx.model, toe, hoof_bottom, wallRadius * 0.90F),
  608. wall_tint * 0.96F, nullptr, 1.0F);
  609. QMatrix4x4 sole = ctx.model;
  610. sole.translate(lerp(hoof_top, hoof_bottom, 0.88F) +
  611. QVector3D(0.0F, -d.hoofHeight * 0.05F, 0.0F));
  612. sole.scale(
  613. QVector3D(wallRadius * 1.08F, wallRadius * 0.28F, wallRadius * 1.02F));
  614. out.mesh(getUnitSphere(), sole, lighten(hoof_color, 1.12F), nullptr, 1.0F);
  615. QMatrix4x4 coronet = ctx.model;
  616. coronet.translate(lerp(hoof_top, hoof_bottom, 0.12F));
  617. coronet.scale(
  618. QVector3D(wallRadius * 1.05F, wallRadius * 0.24F, wallRadius * 1.05F));
  619. out.mesh(getUnitSphere(), coronet, lighten(hoof_color, 1.06F), nullptr,
  620. 1.0F);
  621. };
  622. auto draw_leg = [&](const QVector3D &anchor, float lateralSign,
  623. float forwardBias, float phase_offset, float sockChance) {
  624. float const leg_phase = std::fmod(phase + phase_offset, 1.0F);
  625. float stride = 0.0F;
  626. float lift = 0.0F;
  627. if (anim.isMoving) {
  628. float const angle = leg_phase * 2.0F * kPi;
  629. stride = std::sin(angle) * g.strideSwing * 0.75F + forwardBias;
  630. float const lift_raw = std::sin(angle);
  631. lift = lift_raw > 0.0F ? lift_raw * g.strideLift
  632. : lift_raw * g.strideLift * 0.22F;
  633. } else {
  634. float const idle = std::sin(leg_phase * 2.0F * kPi);
  635. stride = idle * g.strideSwing * 0.06F + forwardBias;
  636. lift = idle * d.idleBobAmplitude * 2.0F;
  637. }
  638. bool const tighten_legs = anim.isMoving;
  639. float const shoulder_out = d.bodyWidth * (tighten_legs ? 0.44F : 0.58F);
  640. QVector3D shoulder = anchor + QVector3D(lateralSign * shoulder_out,
  641. 0.05F + lift * 0.05F, stride);
  642. bool const is_rear = (forwardBias < 0.0F);
  643. float const gallop_angle = leg_phase * 2.0F * kPi;
  644. float const hip_swing = anim.isMoving ? std::sin(gallop_angle) : 0.0F;
  645. float const lift_factor =
  646. anim.isMoving
  647. ? std::max(0.0F,
  648. std::sin(gallop_angle + (is_rear ? 0.35F : -0.25F)))
  649. : 0.0F;
  650. shoulder.setZ(shoulder.z() + hip_swing * (is_rear ? -0.12F : 0.10F));
  651. if (tighten_legs) {
  652. shoulder.setX(shoulder.x() - lateralSign * lift_factor * 0.05F);
  653. }
  654. float const thigh_length = d.legLength * (is_rear ? 0.62F : 0.56F);
  655. float const hip_pitch = hip_swing * (is_rear ? 0.62F : 0.50F);
  656. float const inward_lean =
  657. tighten_legs ? (-0.06F - lift_factor * 0.045F) : -0.012F;
  658. QVector3D thigh_dir(lateralSign * inward_lean, -std::cos(hip_pitch) * 0.90F,
  659. (is_rear ? -1.0F : 1.0F) * std::sin(hip_pitch) * 0.65F);
  660. if (thigh_dir.lengthSquared() > 1e-6F) {
  661. thigh_dir.normalize();
  662. }
  663. QVector3D knee = shoulder + thigh_dir * thigh_length;
  664. knee.setY(knee.y() + lift_factor * thigh_length * 0.28F);
  665. QVector3D girdle_top =
  666. (is_rear ? croup_peak : withers_peak) +
  667. QVector3D(lateralSign * d.bodyWidth * (is_rear ? 0.44F : 0.48F),
  668. is_rear ? -d.bodyHeight * 0.06F : d.bodyHeight * 0.04F,
  669. (is_rear ? -d.bodyLength * 0.06F : d.bodyLength * 0.05F));
  670. girdle_top.setZ(girdle_top.z() + hip_swing * (is_rear ? -0.08F : 0.05F));
  671. girdle_top.setX(girdle_top.x() - lateralSign * lift_factor * 0.03F);
  672. QVector3D const socket =
  673. shoulder +
  674. QVector3D(0.0F, d.bodyWidth * 0.12F,
  675. is_rear ? -d.bodyLength * 0.03F : d.bodyLength * 0.02F);
  676. drawCylinder(out, ctx.model, girdle_top, socket,
  677. d.bodyWidth * (is_rear ? 0.20F : 0.18F),
  678. coatGradient(v.coatColor, is_rear ? 0.70F : 0.80F,
  679. is_rear ? -0.20F : 0.22F,
  680. coat_seed_b + lateralSign * 0.03F));
  681. QMatrix4x4 socket_cap = ctx.model;
  682. socket_cap.translate(socket + QVector3D(0.0F, -d.bodyWidth * 0.04F,
  683. is_rear ? -d.bodyLength * 0.02F
  684. : d.bodyLength * 0.03F));
  685. socket_cap.scale(QVector3D(d.bodyWidth * (is_rear ? 0.36F : 0.32F),
  686. d.bodyWidth * 0.28F, d.bodyLength * 0.18F));
  687. out.mesh(getUnitSphere(), socket_cap,
  688. coatGradient(v.coatColor, is_rear ? 0.60F : 0.68F,
  689. is_rear ? -0.24F : 0.18F,
  690. coat_seed_c + lateralSign * 0.02F),
  691. nullptr, 1.0F);
  692. float const knee_flex =
  693. anim.isMoving
  694. ? clamp01(std::sin(gallop_angle + (is_rear ? 0.65F : -0.45F)) *
  695. 0.55F +
  696. 0.42F)
  697. : 0.32F;
  698. float const forearm_length = d.legLength * 0.30F;
  699. float const bend_cos = std::cos(knee_flex * kPi * 0.5F);
  700. float const bend_sin = std::sin(knee_flex * kPi * 0.5F);
  701. QVector3D forearm_dir(0.0F, -bend_cos,
  702. (is_rear ? -1.0F : 1.0F) * bend_sin * 0.85F);
  703. if (forearm_dir.lengthSquared() < 1e-6F) {
  704. forearm_dir = QVector3D(0.0F, -1.0F, 0.0F);
  705. } else {
  706. forearm_dir.normalize();
  707. }
  708. QVector3D const cannon = knee + forearm_dir * forearm_length;
  709. float const pastern_length = d.legLength * 0.12F;
  710. QVector3D const fetlock = cannon + QVector3D(0.0F, -pastern_length, 0.0F);
  711. float const hoof_pitch =
  712. anim.isMoving ? (-0.20F + std::sin(leg_phase * 2.0F * kPi +
  713. (is_rear ? 0.2F : -0.1F)) *
  714. 0.10F)
  715. : 0.0F;
  716. QVector3D const hoof_dir =
  717. rotateAroundZ(QVector3D(0.0F, -1.0F, 0.0F), hoof_pitch);
  718. QVector3D const hoof_top = fetlock;
  719. QVector3D const hoof_bottom = hoof_top + hoof_dir * d.hoofHeight;
  720. float const thigh_belly_r = d.bodyWidth * (is_rear ? 0.58F : 0.50F);
  721. float const knee_r = d.bodyWidth * (is_rear ? 0.22F : 0.20F);
  722. float const cannon_r = d.bodyWidth * 0.16F;
  723. float const pastern_r = d.bodyWidth * 0.11F;
  724. QVector3D const thigh_belly = shoulder + (knee - shoulder) * 0.62F;
  725. QVector3D const thigh_color = coatGradient(
  726. v.coatColor, is_rear ? 0.48F : 0.58F, is_rear ? -0.22F : 0.18F,
  727. coat_seed_a + lateralSign * 0.07F);
  728. out.mesh(getUnitCone(),
  729. coneFromTo(ctx.model, thigh_belly, shoulder, thigh_belly_r),
  730. thigh_color, nullptr, 1.0F);
  731. {
  732. QMatrix4x4 muscle = ctx.model;
  733. muscle.translate(thigh_belly +
  734. QVector3D(0.0F, 0.0F, is_rear ? -0.015F : 0.020F));
  735. muscle.scale(thigh_belly_r * QVector3D(1.05F, 0.85F, 0.92F));
  736. out.mesh(getUnitSphere(), muscle, lighten(thigh_color, 1.03F), nullptr,
  737. 1.0F);
  738. }
  739. QVector3D const knee_color = darken(thigh_color, 0.96F);
  740. out.mesh(getUnitCone(), coneFromTo(ctx.model, knee, thigh_belly, knee_r),
  741. knee_color, nullptr, 1.0F);
  742. {
  743. QMatrix4x4 joint = ctx.model;
  744. joint.translate(knee + QVector3D(0.0F, 0.0F, is_rear ? -0.028F : 0.034F));
  745. joint.scale(QVector3D(knee_r * 1.18F, knee_r * 1.06F, knee_r * 1.36F));
  746. out.mesh(getUnitSphere(), joint, darken(knee_color, 0.90F), nullptr,
  747. 1.0F);
  748. }
  749. out.mesh(getUnitCylinder(),
  750. cylinderBetween(ctx.model, knee, cannon, cannon_r),
  751. darken(thigh_color, 0.93F), nullptr, 1.0F);
  752. {
  753. QMatrix4x4 tendon = ctx.model;
  754. tendon.translate(
  755. lerp(knee, cannon, 0.55F) +
  756. QVector3D(0.0F, 0.0F,
  757. is_rear ? -cannon_r * 0.35F : cannon_r * 0.35F));
  758. tendon.scale(
  759. QVector3D(cannon_r * 0.45F, cannon_r * 0.95F, cannon_r * 0.55F));
  760. out.mesh(getUnitSphere(), tendon,
  761. darken(thigh_color, is_rear ? 0.88F : 0.90F), nullptr, 1.0F);
  762. }
  763. {
  764. QMatrix4x4 joint = ctx.model;
  765. joint.translate(fetlock);
  766. joint.scale(
  767. QVector3D(pastern_r * 1.12F, pastern_r * 1.05F, pastern_r * 1.26F));
  768. out.mesh(getUnitSphere(), joint, darken(thigh_color, 0.92F), nullptr,
  769. 1.0F);
  770. }
  771. float const sock =
  772. sockChance > 0.78F ? 1.0F : (sockChance > 0.58F ? 0.55F : 0.0F);
  773. QVector3D const distal_color =
  774. (sock > 0.0F) ? lighten(v.coatColor, 1.18F) : v.coatColor * 0.92F;
  775. float const t_sock = smoothstep(0.0F, 1.0F, sock);
  776. out.mesh(getUnitCylinder(),
  777. cylinderBetween(ctx.model, cannon, fetlock, pastern_r * 1.05F),
  778. lerp(v.coatColor * 0.94F, distal_color, t_sock * 0.8F), nullptr,
  779. 1.0F);
  780. QVector3D const hoof_color = v.hoof_color;
  781. render_hoof(hoof_top, hoof_bottom, pastern_r * 0.96F, hoof_color, is_rear);
  782. if (sock > 0.0F) {
  783. QVector3D const feather_tip = lerp(fetlock, hoof_top, 0.35F) +
  784. QVector3D(0.0F, -pastern_r * 0.60F, 0.0F);
  785. drawCone(out, ctx.model, feather_tip, fetlock, pastern_r * 0.85F,
  786. lerp(distal_color, v.coatColor, 0.25F), 0.85F);
  787. }
  788. };
  789. QVector3D const front_anchor =
  790. barrel_center +
  791. QVector3D(0.0F, d.bodyHeight * 0.05F, d.bodyLength * 0.28F);
  792. QVector3D const rear_anchor =
  793. barrel_center +
  794. QVector3D(0.0F, d.bodyHeight * 0.02F, -d.bodyLength * 0.32F);
  795. draw_leg(front_anchor, 1.0F, d.bodyLength * 0.30F, g.frontLegPhase,
  796. sock_chance_fl);
  797. draw_leg(front_anchor, -1.0F, d.bodyLength * 0.30F, g.frontLegPhase + 0.5F,
  798. sock_chance_fr);
  799. draw_leg(rear_anchor, 1.0F, -d.bodyLength * 0.28F, g.rearLegPhase,
  800. sock_chance_rl);
  801. draw_leg(rear_anchor, -1.0F, -d.bodyLength * 0.28F, g.rearLegPhase + 0.5F,
  802. sock_chance_rr);
  803. float const saddle_top = d.saddle_height;
  804. QVector3D saddle_center(0.0F, saddle_top - d.saddleThickness * 0.35F,
  805. -d.bodyLength * 0.05F + d.seatForwardOffset * 0.25F);
  806. {
  807. QMatrix4x4 saddle = ctx.model;
  808. saddle.translate(saddle_center);
  809. saddle.scale(d.bodyWidth * 1.10F, d.saddleThickness * 1.05F,
  810. d.bodyLength * 0.34F);
  811. out.mesh(getUnitSphere(), saddle, v.saddleColor, nullptr, 1.0F);
  812. }
  813. QVector3D const blanket_center =
  814. saddle_center + QVector3D(0.0F, -d.saddleThickness, 0.0F);
  815. {
  816. QMatrix4x4 blanket = ctx.model;
  817. blanket.translate(blanket_center);
  818. blanket.scale(d.bodyWidth * 1.26F, d.saddleThickness * 0.38F,
  819. d.bodyLength * 0.42F);
  820. out.mesh(getUnitSphere(), blanket, v.blanketColor * 1.02F, nullptr, 1.0F);
  821. }
  822. {
  823. QMatrix4x4 cantle = ctx.model;
  824. cantle.translate(saddle_center + QVector3D(0.0F, d.saddleThickness * 0.72F,
  825. -d.bodyLength * 0.12F));
  826. cantle.scale(QVector3D(d.bodyWidth * 0.52F, d.saddleThickness * 0.60F,
  827. d.bodyLength * 0.18F));
  828. out.mesh(getUnitSphere(), cantle, lighten(v.saddleColor, 1.05F), nullptr,
  829. 1.0F);
  830. }
  831. {
  832. QMatrix4x4 pommel = ctx.model;
  833. pommel.translate(saddle_center + QVector3D(0.0F, d.saddleThickness * 0.58F,
  834. d.bodyLength * 0.16F));
  835. pommel.scale(QVector3D(d.bodyWidth * 0.40F, d.saddleThickness * 0.48F,
  836. d.bodyLength * 0.14F));
  837. out.mesh(getUnitSphere(), pommel, darken(v.saddleColor, 0.92F), nullptr,
  838. 1.0F);
  839. }
  840. for (int i = 0; i < 6; ++i) {
  841. float const t = static_cast<float>(i) / 5.0F;
  842. QMatrix4x4 stitch = ctx.model;
  843. stitch.translate(blanket_center + QVector3D(d.bodyWidth * (t - 0.5F) * 1.1F,
  844. -d.saddleThickness * 0.35F,
  845. d.bodyLength * 0.28F));
  846. stitch.scale(QVector3D(d.bodyWidth * 0.05F, d.bodyWidth * 0.02F,
  847. d.bodyWidth * 0.12F));
  848. out.mesh(getUnitSphere(), stitch, v.blanketColor * 0.75F, nullptr, 0.9F);
  849. }
  850. for (int i = 0; i < 2; ++i) {
  851. float const side = (i == 0) ? 1.0F : -1.0F;
  852. QVector3D const strap_top =
  853. saddle_center + QVector3D(side * d.bodyWidth * 0.92F,
  854. d.saddleThickness * 0.32F,
  855. d.bodyLength * 0.02F);
  856. QVector3D const strap_bottom =
  857. strap_top +
  858. QVector3D(0.0F, -d.bodyHeight * 0.94F, -d.bodyLength * 0.06F);
  859. out.mesh(getUnitCylinder(),
  860. cylinderBetween(ctx.model, strap_top, strap_bottom,
  861. d.bodyWidth * 0.065F),
  862. v.tack_color * 0.94F, nullptr, 1.0F);
  863. QMatrix4x4 buckle = ctx.model;
  864. buckle.translate(lerp(strap_top, strap_bottom, 0.87F) +
  865. QVector3D(0.0F, 0.0F, -d.bodyLength * 0.02F));
  866. buckle.scale(QVector3D(d.bodyWidth * 0.16F, d.bodyWidth * 0.12F,
  867. d.bodyWidth * 0.05F));
  868. out.mesh(getUnitSphere(), buckle, QVector3D(0.42F, 0.39F, 0.35F), nullptr,
  869. 1.0F);
  870. }
  871. for (int i = 0; i < 2; ++i) {
  872. float const side = (i == 0) ? 1.0F : -1.0F;
  873. QVector3D const breast_anchor =
  874. chest_center + QVector3D(side * d.bodyWidth * 0.70F,
  875. -d.bodyHeight * 0.10F, d.bodyLength * 0.18F);
  876. QVector3D const breast_to_saddle =
  877. saddle_center + QVector3D(side * d.bodyWidth * 0.48F,
  878. -d.saddleThickness * 0.20F,
  879. d.bodyLength * 0.10F);
  880. out.mesh(getUnitCylinder(),
  881. cylinderBetween(ctx.model, breast_anchor, breast_to_saddle,
  882. d.bodyWidth * 0.055F),
  883. v.tack_color * 0.92F, nullptr, 1.0F);
  884. }
  885. QVector3D const stirrup_attach_left =
  886. saddle_center + QVector3D(d.bodyWidth * 0.92F, -d.saddleThickness * 0.10F,
  887. d.seatForwardOffset * 0.28F);
  888. QVector3D const stirrup_attach_right =
  889. saddle_center + QVector3D(-d.bodyWidth * 0.92F,
  890. -d.saddleThickness * 0.10F,
  891. d.seatForwardOffset * 0.28F);
  892. QVector3D stirrup_bottom_left =
  893. stirrup_attach_left + QVector3D(0.0F, -d.stirrupDrop, 0.0F);
  894. QVector3D stirrup_bottom_right =
  895. stirrup_attach_right + QVector3D(0.0F, -d.stirrupDrop, 0.0F);
  896. auto draw_rider = [&]() {
  897. QVector3D rider_coat(0.23F, 0.23F, 0.26F);
  898. QVector3D rider_cloth = lighten(v.blanketColor, 1.15F);
  899. QVector3D rider_skin(1.0F, 0.86F, 0.72F);
  900. QVector3D rider_leather = darken(v.saddleColor, 0.88F);
  901. QVector3D const rider_steel(0.72F, 0.73F, 0.78F);
  902. QVector3D pelvis_center =
  903. saddle_center + QVector3D(rider_lean * d.bodyWidth,
  904. d.saddleThickness * 0.68F,
  905. -d.bodyLength * 0.08F);
  906. QVector3D const spine_mid =
  907. pelvis_center + QVector3D(rider_lean * d.bodyWidth * 0.35F,
  908. d.bodyHeight * 0.32F, d.bodyLength * 0.02F);
  909. QVector3D torso_top =
  910. spine_mid + QVector3D(rider_lean * d.bodyWidth * 0.25F,
  911. d.bodyHeight * 0.28F, 0.03F);
  912. QVector3D const neck_base =
  913. torso_top + QVector3D(0.0F, d.bodyHeight * 0.10F, 0.02F);
  914. QMatrix4x4 pelvis = ctx.model;
  915. pelvis.translate(pelvis_center);
  916. pelvis.scale(QVector3D(d.bodyWidth * 0.52F, d.bodyWidth * 0.34F,
  917. d.bodyWidth * 0.48F));
  918. out.mesh(getUnitSphere(), pelvis, rider_coat * 0.92F, nullptr, 1.0F);
  919. drawCylinder(out, ctx.model, pelvis_center, spine_mid, d.bodyWidth * 0.36F,
  920. rider_coat * 0.96F);
  921. drawCylinder(out, ctx.model, spine_mid, torso_top, d.bodyWidth * 0.30F,
  922. rider_coat * 0.98F);
  923. {
  924. QMatrix4x4 chest = ctx.model;
  925. chest.translate(spine_mid + QVector3D(0.0F, d.bodyHeight * 0.20F, 0.0F));
  926. chest.scale(QVector3D(d.bodyWidth * 0.60F, d.bodyWidth * 0.46F,
  927. d.bodyWidth * 0.32F));
  928. out.mesh(getUnitSphere(), chest, rider_coat * 1.02F, nullptr, 1.0F);
  929. }
  930. for (int i = 0; i < 2; ++i) {
  931. float const side = (i == 0) ? 1.0F : -1.0F;
  932. QMatrix4x4 shoulder_pad = ctx.model;
  933. shoulder_pad.translate(torso_top + QVector3D(side * d.bodyWidth * 0.40F,
  934. -d.bodyWidth * 0.02F,
  935. 0.02F));
  936. shoulder_pad.scale(QVector3D(d.bodyWidth * 0.22F, d.bodyWidth * 0.16F,
  937. d.bodyWidth * 0.18F));
  938. out.mesh(getUnitSphere(), shoulder_pad, rider_cloth * 0.92F, nullptr,
  939. 1.0F);
  940. }
  941. QVector3D hand_left_target =
  942. bridle_base + QVector3D(d.bodyWidth * 0.08F, -d.headHeight * 0.06F,
  943. d.headLength * 0.22F);
  944. hand_left_target.setY(hand_left_target.y() - rein_slack * 0.45F);
  945. hand_left_target.setZ(hand_left_target.z() - rein_slack * 0.12F);
  946. QVector3D const sword_grip =
  947. pelvis_center + QVector3D(-d.bodyWidth * (0.54F - rider_lean * 0.20F),
  948. d.bodyHeight * 0.46F, d.bodyLength * 0.12F);
  949. QVector3D const hand_right_target =
  950. sword_grip + QVector3D(0.0F, -d.bodyWidth * 0.05F, 0.0F);
  951. auto draw_arm = [&](float side_sign, const QVector3D &handTarget,
  952. bool swordHand) {
  953. QVector3D shoulder =
  954. torso_top + QVector3D(side_sign * d.bodyWidth * 0.42F,
  955. -d.bodyWidth * 0.04F, d.bodyLength * 0.03F);
  956. if (swordHand) {
  957. shoulder.setZ(shoulder.z() - d.bodyLength * 0.06F);
  958. shoulder.setY(shoulder.y() + d.bodyWidth * 0.02F);
  959. } else {
  960. shoulder.setZ(shoulder.z() + rein_slack * 0.20F * side_sign);
  961. }
  962. QVector3D elbow =
  963. shoulder +
  964. QVector3D(side_sign * d.bodyWidth * (swordHand ? 0.20F : 0.14F),
  965. -d.bodyWidth * (swordHand ? 0.32F : 0.26F),
  966. d.bodyLength * (swordHand ? 0.02F : 0.10F));
  967. if (!swordHand) {
  968. elbow.setZ(elbow.z() + rein_slack * 0.12F * side_sign);
  969. } else {
  970. elbow.setX(elbow.x() + side_sign * d.bodyWidth * 0.02F);
  971. }
  972. QVector3D const wrist =
  973. handTarget +
  974. QVector3D(side_sign * d.bodyWidth * (swordHand ? 0.01F : 0.02F),
  975. -d.bodyWidth * 0.03F,
  976. -d.bodyLength * (swordHand ? -0.01F : 0.02F));
  977. drawCylinder(out, ctx.model, shoulder, elbow, d.bodyWidth * 0.11F,
  978. rider_cloth * (swordHand ? 0.98F : 0.96F));
  979. drawCylinder(out, ctx.model, elbow, wrist,
  980. d.bodyWidth * (swordHand ? 0.085F : 0.09F),
  981. rider_cloth * 0.90F);
  982. if (!swordHand) {
  983. drawCylinder(out, ctx.model, wrist, handTarget, d.bodyWidth * 0.075F,
  984. rider_leather * 0.85F);
  985. } else {
  986. drawCylinder(out, ctx.model, wrist, handTarget, d.bodyWidth * 0.070F,
  987. rider_leather * 0.92F);
  988. }
  989. QMatrix4x4 glove = ctx.model;
  990. QVector3D const glove_offset =
  991. swordHand ? QVector3D(0.0F, -d.bodyWidth * 0.04F, d.bodyWidth * 0.02F)
  992. : QVector3D(0.0F, -d.bodyWidth * 0.05F, 0.0F);
  993. glove.translate(handTarget + glove_offset);
  994. glove.scale(QVector3D(d.bodyWidth * 0.11F, d.bodyWidth * 0.14F,
  995. d.bodyWidth * 0.09F));
  996. QVector3D const glove_color =
  997. swordHand ? rider_leather * 0.96F : rider_skin * 0.96F;
  998. out.mesh(getUnitSphere(), glove, glove_color, nullptr, 1.0F);
  999. };
  1000. draw_arm(1.0F, hand_left_target, false);
  1001. draw_arm(-1.0F, hand_right_target, true);
  1002. QVector3D const helmet_top =
  1003. neck_base + QVector3D(0.0F, d.bodyHeight * 0.32F, 0.04F);
  1004. QMatrix4x4 neck = ctx.model;
  1005. neck.translate(lerp(torso_top, neck_base, 0.6F));
  1006. neck.scale(QVector3D(d.bodyWidth * 0.22F, d.bodyWidth * 0.24F,
  1007. d.bodyWidth * 0.20F));
  1008. out.mesh(getUnitSphere(), neck, rider_skin * 0.88F, nullptr, 1.0F);
  1009. QMatrix4x4 head = ctx.model;
  1010. head.translate(helmet_top + QVector3D(0.0F, -d.bodyWidth * 0.12F, 0.0F));
  1011. head.scale(d.bodyWidth * 0.30F);
  1012. out.mesh(getUnitSphere(), head, rider_skin * 0.95F, nullptr, 1.0F);
  1013. QMatrix4x4 helm = ctx.model;
  1014. helm.translate(helmet_top + QVector3D(0.0F, d.bodyWidth * 0.08F, 0.0F));
  1015. helm.scale(d.bodyWidth * 0.34F, d.bodyWidth * 0.18F, d.bodyWidth * 0.34F);
  1016. out.mesh(getUnitSphere(), helm, rider_cloth * 0.82F, nullptr, 1.0F);
  1017. QMatrix4x4 visor = ctx.model;
  1018. visor.translate(helmet_top +
  1019. QVector3D(0.0F, d.bodyWidth * 0.02F, d.bodyWidth * 0.15F));
  1020. visor.scale(QVector3D(d.bodyWidth * 0.32F, d.bodyWidth * 0.08F,
  1021. d.bodyWidth * 0.16F));
  1022. out.mesh(getUnitSphere(), visor, rider_coat * 0.75F, nullptr, 1.0F);
  1023. auto draw_leg = [&](float side_sign, const QVector3D &stirrupBottom) {
  1024. QVector3D const hip =
  1025. pelvis_center + QVector3D(side_sign * d.bodyWidth * 0.34F,
  1026. -d.bodyWidth * 0.01F, d.bodyWidth * 0.06F);
  1027. QVector3D const knee =
  1028. hip + QVector3D(side_sign * d.bodyWidth * 0.08F,
  1029. -d.stirrupDrop * 0.74F, d.bodyLength * 0.18F);
  1030. QVector3D const ankle =
  1031. stirrupBottom + QVector3D(side_sign * d.bodyWidth * 0.02F,
  1032. d.bodyWidth * 0.05F, d.bodyWidth * 0.05F);
  1033. QVector3D const toe =
  1034. ankle + QVector3D(side_sign * d.bodyWidth * 0.12F,
  1035. -d.bodyWidth * 0.04F, d.bodyWidth * 0.10F);
  1036. drawCylinder(out, ctx.model, hip, knee, d.bodyWidth * 0.12F,
  1037. rider_cloth * 0.96F);
  1038. drawCylinder(out, ctx.model, knee, ankle, d.bodyWidth * 0.095F,
  1039. rider_cloth * 0.90F);
  1040. QMatrix4x4 knee_cap = ctx.model;
  1041. knee_cap.translate(knee);
  1042. knee_cap.scale(QVector3D(d.bodyWidth * 0.12F, d.bodyWidth * 0.10F,
  1043. d.bodyWidth * 0.14F));
  1044. out.mesh(getUnitSphere(), knee_cap, rider_cloth * 0.86F, nullptr, 1.0F);
  1045. drawCylinder(out, ctx.model, ankle, toe, d.bodyWidth * 0.08F,
  1046. rider_coat * 0.75F);
  1047. QMatrix4x4 boot = ctx.model;
  1048. boot.translate(ankle + QVector3D(0.0F, -d.bodyWidth * 0.06F, 0.0F));
  1049. boot.scale(QVector3D(d.bodyWidth * 0.16F, d.bodyWidth * 0.14F,
  1050. d.bodyWidth * 0.20F));
  1051. out.mesh(getUnitSphere(), boot, rider_leather, nullptr, 1.0F);
  1052. QMatrix4x4 spur = ctx.model;
  1053. spur.translate(ankle + QVector3D(-side_sign * d.bodyWidth * 0.10F,
  1054. -d.bodyWidth * 0.01F,
  1055. -d.bodyWidth * 0.06F));
  1056. spur.scale(QVector3D(d.bodyWidth * 0.06F, d.bodyWidth * 0.06F,
  1057. d.bodyWidth * 0.08F));
  1058. out.mesh(getUnitSphere(), spur, QVector3D(0.62F, 0.62F, 0.64F), nullptr,
  1059. 1.0F);
  1060. };
  1061. draw_leg(1.0F, stirrup_bottom_left);
  1062. draw_leg(-1.0F, stirrup_bottom_right);
  1063. drawCylinder(out, ctx.model, hand_left_target,
  1064. bridle_base + QVector3D(0.0F, -d.headHeight * 0.02F, 0.0F),
  1065. d.bodyWidth * 0.038F, rider_leather * 0.75F, 0.90F);
  1066. QVector3D const sword_handle_top =
  1067. sword_grip + QVector3D(-d.bodyWidth * 0.02F, d.bodyHeight * 0.18F,
  1068. d.bodyLength * 0.04F);
  1069. QVector3D const sword_handle_bottom =
  1070. sword_grip +
  1071. QVector3D(0.0F, -d.bodyWidth * 0.08F, -d.bodyLength * 0.02F);
  1072. drawCylinder(out, ctx.model, sword_grip, sword_handle_top,
  1073. d.bodyWidth * 0.045F, rider_leather * 0.88F);
  1074. QMatrix4x4 pommel = ctx.model;
  1075. pommel.translate(sword_handle_bottom);
  1076. pommel.scale(d.bodyWidth * 0.12F);
  1077. out.mesh(getUnitSphere(), pommel, rider_leather * 0.75F, nullptr, 1.0F);
  1078. QVector3D const guard_center =
  1079. sword_handle_top + QVector3D(0.0F, d.bodyWidth * 0.015F, 0.0F);
  1080. QVector3D const guard_l =
  1081. guard_center + QVector3D(d.bodyWidth * 0.18F, d.bodyWidth * 0.03F,
  1082. -d.bodyWidth * 0.02F);
  1083. QVector3D const guard_r =
  1084. guard_center + QVector3D(-d.bodyWidth * 0.18F, d.bodyWidth * 0.03F,
  1085. -d.bodyWidth * 0.02F);
  1086. out.mesh(getUnitCylinder(),
  1087. cylinderBetween(ctx.model, guard_l, guard_r, d.bodyWidth * 0.020F),
  1088. rider_steel * 1.05F, nullptr, 1.0F);
  1089. QMatrix4x4 guard_core = ctx.model;
  1090. guard_core.translate(guard_center);
  1091. guard_core.scale(d.bodyWidth * 0.05F);
  1092. out.mesh(getUnitSphere(), guard_core, rider_steel * 1.08F, nullptr, 1.0F);
  1093. QVector3D const blade_base =
  1094. guard_center + QVector3D(-d.bodyWidth * 0.01F, d.bodyWidth * 0.02F,
  1095. d.bodyWidth * 0.01F);
  1096. QVector3D const blade_ctrl =
  1097. blade_base + QVector3D(-d.bodyWidth * 0.14F, d.bodyHeight * 0.55F,
  1098. d.bodyLength * 0.28F);
  1099. QVector3D const blade_tip =
  1100. blade_base + QVector3D(-d.bodyWidth * 0.06F, d.bodyHeight * 0.95F,
  1101. d.bodyLength * 0.36F);
  1102. QVector3D prev = blade_base;
  1103. const int blade_segments = 6;
  1104. for (int i = 1; i <= blade_segments; ++i) {
  1105. float const t =
  1106. static_cast<float>(i) / static_cast<float>(blade_segments);
  1107. QVector3D const p = bezier(blade_base, blade_ctrl, blade_tip, t);
  1108. float const radius = d.bodyWidth * lerp(0.060F, 0.020F, t);
  1109. QVector3D const blade_color = rider_steel * (1.08F - 0.10F * t) +
  1110. QVector3D(0.02F, 0.02F, 0.02F) * t;
  1111. out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, prev, p, radius),
  1112. blade_color, nullptr, 1.0F);
  1113. prev = p;
  1114. }
  1115. out.mesh(getUnitCone(),
  1116. coneFromTo(ctx.model,
  1117. blade_tip + QVector3D(-d.bodyWidth * 0.02F,
  1118. d.bodyWidth * 0.08F,
  1119. -d.bodyWidth * 0.02F),
  1120. blade_tip, d.bodyWidth * 0.020F),
  1121. rider_steel * 1.12F, nullptr, 1.0F);
  1122. };
  1123. draw_rider();
  1124. auto draw_stirrup = [&](const QVector3D &attach, const QVector3D &bottom) {
  1125. out.mesh(getUnitCylinder(),
  1126. cylinderBetween(ctx.model, attach, bottom, d.bodyWidth * 0.048F),
  1127. v.tack_color * 0.98F, nullptr, 1.0F);
  1128. QMatrix4x4 leather_loop = ctx.model;
  1129. leather_loop.translate(lerp(attach, bottom, 0.30F) +
  1130. QVector3D(0.0F, 0.0F, d.bodyWidth * 0.02F));
  1131. leather_loop.scale(QVector3D(d.bodyWidth * 0.18F, d.bodyWidth * 0.05F,
  1132. d.bodyWidth * 0.10F));
  1133. out.mesh(getUnitSphere(), leather_loop, v.tack_color * 0.92F, nullptr,
  1134. 1.0F);
  1135. QMatrix4x4 stirrup = ctx.model;
  1136. stirrup.translate(bottom + QVector3D(0.0F, -d.bodyWidth * 0.06F, 0.0F));
  1137. stirrup.scale(d.bodyWidth * 0.20F, d.bodyWidth * 0.07F,
  1138. d.bodyWidth * 0.16F);
  1139. out.mesh(getUnitSphere(), stirrup, QVector3D(0.66F, 0.65F, 0.62F), nullptr,
  1140. 1.0F);
  1141. };
  1142. draw_stirrup(stirrup_attach_left, stirrup_bottom_left);
  1143. draw_stirrup(stirrup_attach_right, stirrup_bottom_right);
  1144. QVector3D const cheek_left_top =
  1145. head_center + QVector3D(d.headWidth * 0.60F, -d.headHeight * 0.10F,
  1146. d.headLength * 0.25F);
  1147. QVector3D const cheek_left_bottom =
  1148. cheek_left_top + QVector3D(0.0F, -d.headHeight, -d.headLength * 0.12F);
  1149. QVector3D const cheek_right_top =
  1150. head_center + QVector3D(-d.headWidth * 0.60F, -d.headHeight * 0.10F,
  1151. d.headLength * 0.25F);
  1152. QVector3D const cheek_right_bottom =
  1153. cheek_right_top + QVector3D(0.0F, -d.headHeight, -d.headLength * 0.12F);
  1154. out.mesh(getUnitCylinder(),
  1155. cylinderBetween(ctx.model, cheek_left_top, cheek_left_bottom,
  1156. d.headWidth * 0.08F),
  1157. v.tack_color, nullptr, 1.0F);
  1158. out.mesh(getUnitCylinder(),
  1159. cylinderBetween(ctx.model, cheek_right_top, cheek_right_bottom,
  1160. d.headWidth * 0.08F),
  1161. v.tack_color, nullptr, 1.0F);
  1162. QVector3D const nose_band_front =
  1163. muzzle_center +
  1164. QVector3D(0.0F, d.headHeight * 0.02F, d.muzzleLength * 0.35F);
  1165. QVector3D const nose_band_left =
  1166. nose_band_front + QVector3D(d.headWidth * 0.55F, 0.0F, 0.0F);
  1167. QVector3D const nose_band_right =
  1168. nose_band_front + QVector3D(-d.headWidth * 0.55F, 0.0F, 0.0F);
  1169. out.mesh(getUnitCylinder(),
  1170. cylinderBetween(ctx.model, nose_band_left, nose_band_right,
  1171. d.headWidth * 0.08F),
  1172. v.tack_color * 0.92F, nullptr, 1.0F);
  1173. QVector3D const brow_band_front =
  1174. head_center + QVector3D(0.0F, d.headHeight * 0.28F, d.headLength * 0.15F);
  1175. QVector3D const brow_band_left =
  1176. brow_band_front + QVector3D(d.headWidth * 0.58F, 0.0F, 0.0F);
  1177. QVector3D const brow_band_right =
  1178. brow_band_front + QVector3D(-d.headWidth * 0.58F, 0.0F, 0.0F);
  1179. out.mesh(getUnitCylinder(),
  1180. cylinderBetween(ctx.model, brow_band_left, brow_band_right,
  1181. d.headWidth * 0.07F),
  1182. v.tack_color, nullptr, 1.0F);
  1183. QVector3D const bit_left =
  1184. muzzle_center + QVector3D(d.headWidth * 0.55F, -d.headHeight * 0.08F,
  1185. d.muzzleLength * 0.10F);
  1186. QVector3D const bit_right =
  1187. muzzle_center + QVector3D(-d.headWidth * 0.55F, -d.headHeight * 0.08F,
  1188. d.muzzleLength * 0.10F);
  1189. out.mesh(getUnitCylinder(),
  1190. cylinderBetween(ctx.model, bit_left, bit_right, d.headWidth * 0.05F),
  1191. QVector3D(0.55F, 0.55F, 0.55F), nullptr, 1.0F);
  1192. for (int i = 0; i < 2; ++i) {
  1193. float const side = (i == 0) ? 1.0F : -1.0F;
  1194. QVector3D const rein_start = (i == 0) ? bit_left : bit_right;
  1195. QVector3D const rein_end =
  1196. saddle_center + QVector3D(side * d.bodyWidth * 0.62F,
  1197. -d.saddleThickness * 0.32F,
  1198. d.seatForwardOffset * 0.10F);
  1199. QVector3D const mid =
  1200. lerp(rein_start, rein_end, 0.46F) +
  1201. QVector3D(0.0F, -d.bodyHeight * (0.08F + rein_slack), 0.0F);
  1202. out.mesh(getUnitCylinder(),
  1203. cylinderBetween(ctx.model, rein_start, mid, d.bodyWidth * 0.02F),
  1204. v.tack_color * 0.95F, nullptr, 1.0F);
  1205. out.mesh(getUnitCylinder(),
  1206. cylinderBetween(ctx.model, mid, rein_end, d.bodyWidth * 0.02F),
  1207. v.tack_color * 0.95F, nullptr, 1.0F);
  1208. }
  1209. }
  1210. } // namespace Render::GL