horse_renderer.cpp 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. #include "horse_renderer.h"
  2. #include "../geom/math_utils.h"
  3. #include "../geom/transforms.h"
  4. #include "../gl/mesh.h"
  5. #include "../gl/primitives.h"
  6. #include "../humanoid_base.h"
  7. #include "../palette.h"
  8. #include <QMatrix4x4>
  9. #include <QRandomGenerator>
  10. #include <QVector3D>
  11. #include <algorithm>
  12. #include <cmath>
  13. #include <cstdint>
  14. namespace Render::GL {
  15. using Render::Geom::clamp01;
  16. using Render::Geom::coneFromTo;
  17. using Render::Geom::cylinderBetween;
  18. namespace {
  19. constexpr float kPi = 3.14159265358979323846f;
  20. inline float hash01(uint32_t x) {
  21. x ^= x >> 16;
  22. x *= 0x7feb352dU;
  23. x ^= x >> 15;
  24. x *= 0x846ca68bU;
  25. x ^= x >> 16;
  26. return (x & 0xFFFFFF) / float(0x1000000);
  27. }
  28. inline float randBetween(uint32_t seed, uint32_t salt, float minV, float maxV) {
  29. const float t = hash01(seed ^ salt);
  30. return minV + (maxV - minV) * t;
  31. }
  32. inline QVector3D lerpVec(const QVector3D &a, const QVector3D &b, float t) {
  33. return a * (1.0f - t) + b * t;
  34. }
  35. inline float saturate(float x) { return std::min(1.0f, std::max(0.0f, x)); }
  36. inline float smoothstep(float e0, float e1, float x) {
  37. float t = saturate((x - e0) / (e1 - e0));
  38. return t * t * (3.0f - 2.0f * t);
  39. }
  40. inline QVector3D rotateAroundY(const QVector3D &v, float angle) {
  41. float s = std::sin(angle), c = std::cos(angle);
  42. return QVector3D(v.x() * c + v.z() * s, v.y(), -v.x() * s + v.z() * c);
  43. }
  44. inline QVector3D rotateAroundZ(const QVector3D &v, float angle) {
  45. float s = std::sin(angle), c = std::cos(angle);
  46. return QVector3D(v.x() * c - v.y() * s, v.x() * s + v.y() * c, v.z());
  47. }
  48. inline QVector3D darken(const QVector3D &c, float k) { return c * k; }
  49. inline QVector3D lighten(const QVector3D &c, float k) {
  50. return QVector3D(saturate(c.x() * k), saturate(c.y() * k),
  51. saturate(c.z() * k));
  52. }
  53. inline uint32_t colorHash(const QVector3D &c) {
  54. uint32_t r = uint32_t(saturate(c.x()) * 255.0f);
  55. uint32_t g = uint32_t(saturate(c.y()) * 255.0f);
  56. uint32_t b = uint32_t(saturate(c.z()) * 255.0f);
  57. uint32_t v = (r << 16) | (g << 8) | b;
  58. v ^= v >> 16;
  59. v *= 0x7feb352dU;
  60. v ^= v >> 15;
  61. v *= 0x846ca68bU;
  62. v ^= v >> 16;
  63. return v;
  64. }
  65. } // namespace
  66. HorseDimensions makeHorseDimensions(uint32_t seed) {
  67. HorseDimensions d;
  68. d.bodyLength = randBetween(seed, 0x12u, 0.74f, 0.84f);
  69. d.bodyWidth = randBetween(seed, 0x34u, 0.17f, 0.20f);
  70. d.bodyHeight = randBetween(seed, 0x56u, 0.33f, 0.37f);
  71. d.barrelCenterY = randBetween(seed, 0x78u, 0.02f, 0.08f);
  72. d.neckLength = randBetween(seed, 0x9Au, 0.32f, 0.37f);
  73. d.neckRise = randBetween(seed, 0xBCu, 0.20f, 0.26f);
  74. d.headLength = randBetween(seed, 0xDEu, 0.24f, 0.28f);
  75. d.headWidth = randBetween(seed, 0xF1u, 0.13f, 0.16f);
  76. d.headHeight = randBetween(seed, 0x1357u, 0.16f, 0.19f);
  77. d.muzzleLength = randBetween(seed, 0x2468u, 0.11f, 0.14f);
  78. d.legLength = randBetween(seed, 0x369Cu, 0.87f, 0.99f);
  79. d.hoofHeight = randBetween(seed, 0x48AEu, 0.070f, 0.080f);
  80. d.tailLength = randBetween(seed, 0x5ABCu, 0.30f, 0.36f);
  81. d.saddleThickness = randBetween(seed, 0x6CDEu, 0.040f, 0.052f);
  82. d.seatForwardOffset = randBetween(seed, 0x7531u, 0.020f, 0.050f);
  83. d.stirrupOut = d.bodyWidth * randBetween(seed, 0x8642u, 0.88f, 0.98f);
  84. d.stirrupDrop = randBetween(seed, 0x9753u, 0.23f, 0.27f);
  85. d.idleBobAmplitude = randBetween(seed, 0xA864u, 0.005f, 0.008f);
  86. d.moveBobAmplitude = randBetween(seed, 0xB975u, 0.020f, 0.028f);
  87. d.saddleHeight = d.barrelCenterY + d.bodyHeight * 0.55f + d.saddleThickness;
  88. return d;
  89. }
  90. HorseVariant makeHorseVariant(uint32_t seed, const QVector3D &leatherBase,
  91. const QVector3D &clothBase) {
  92. HorseVariant v;
  93. float coatHue = hash01(seed ^ 0x23456u);
  94. if (coatHue < 0.18f) {
  95. v.coatColor = QVector3D(0.70f, 0.68f, 0.63f);
  96. } else if (coatHue < 0.38f) {
  97. v.coatColor = QVector3D(0.40f, 0.30f, 0.22f);
  98. } else if (coatHue < 0.65f) {
  99. v.coatColor = QVector3D(0.28f, 0.22f, 0.19f);
  100. } else if (coatHue < 0.85f) {
  101. v.coatColor = QVector3D(0.18f, 0.15f, 0.13f);
  102. } else {
  103. v.coatColor = QVector3D(0.48f, 0.42f, 0.39f);
  104. }
  105. float blazeChance = hash01(seed ^ 0x1122u);
  106. if (blazeChance > 0.82f) {
  107. v.coatColor = lerpVec(v.coatColor, QVector3D(0.92f, 0.92f, 0.90f), 0.25f);
  108. }
  109. v.maneColor = lerpVec(v.coatColor, QVector3D(0.10f, 0.09f, 0.08f),
  110. randBetween(seed, 0x3344u, 0.55f, 0.85f));
  111. v.tailColor = lerpVec(v.maneColor, v.coatColor, 0.35f);
  112. v.muzzleColor = lerpVec(v.coatColor, QVector3D(0.18f, 0.14f, 0.12f), 0.65f);
  113. v.hoofColor =
  114. lerpVec(QVector3D(0.16f, 0.14f, 0.12f), QVector3D(0.40f, 0.35f, 0.32f),
  115. randBetween(seed, 0x5566u, 0.15f, 0.65f));
  116. float leatherTone = randBetween(seed, 0x7788u, 0.78f, 0.96f);
  117. float tackTone = randBetween(seed, 0x88AAu, 0.58f, 0.78f);
  118. QVector3D leatherTint = leatherBase * leatherTone;
  119. QVector3D tackTint = leatherBase * tackTone;
  120. if (blazeChance > 0.90f) {
  121. tackTint = lerpVec(tackTint, QVector3D(0.18f, 0.19f, 0.22f), 0.25f);
  122. }
  123. v.saddleColor = leatherTint;
  124. v.tackColor = tackTint;
  125. v.blanketColor = clothBase * randBetween(seed, 0x99B0u, 0.92f, 1.05f);
  126. return v;
  127. }
  128. HorseProfile makeHorseProfile(uint32_t seed, const QVector3D &leatherBase,
  129. const QVector3D &clothBase) {
  130. HorseProfile profile;
  131. profile.dims = makeHorseDimensions(seed);
  132. profile.variant = makeHorseVariant(seed, leatherBase, clothBase);
  133. profile.gait.cycleTime = randBetween(seed, 0xAA12u, 0.50f, 0.58f);
  134. profile.gait.frontLegPhase = randBetween(seed, 0xBB34u, 0.10f, 0.18f);
  135. float diagonalLead = randBetween(seed, 0xCC56u, 0.48f, 0.56f);
  136. profile.gait.rearLegPhase =
  137. std::fmod(profile.gait.frontLegPhase + diagonalLead, 1.0f);
  138. profile.gait.strideSwing = randBetween(seed, 0xDD78u, 0.18f, 0.24f);
  139. profile.gait.strideLift = randBetween(seed, 0xEE9Au, 0.11f, 0.15f);
  140. return profile;
  141. }
  142. void HorseRenderer::render(const DrawContext &ctx, const AnimationInputs &anim,
  143. const HorseProfile &profile, ISubmitter &out) const {
  144. const HorseDimensions &d = profile.dims;
  145. const HorseVariant &v = profile.variant;
  146. const HorseGait &g = profile.gait;
  147. float phase = 0.0f;
  148. float bob = 0.0f;
  149. if (anim.isMoving) {
  150. float cycle = std::max(0.20f, g.cycleTime);
  151. phase = std::fmod(anim.time / cycle, 1.0f);
  152. bob = std::sin(phase * 2.0f * kPi) * d.moveBobAmplitude;
  153. } else {
  154. phase = std::fmod(anim.time * 0.25f, 1.0f);
  155. bob = std::sin(phase * 2.0f * kPi) * d.idleBobAmplitude;
  156. }
  157. float headNod = anim.isMoving ? std::sin((phase + 0.25f) * 2.0f * kPi) * 0.04f
  158. : std::sin(anim.time * 1.5f) * 0.01f;
  159. uint32_t vhash = colorHash(v.coatColor);
  160. float sockChanceFL = hash01(vhash ^ 0x101u);
  161. float sockChanceFR = hash01(vhash ^ 0x202u);
  162. float sockChanceRL = hash01(vhash ^ 0x303u);
  163. float sockChanceRR = hash01(vhash ^ 0x404u);
  164. bool hasBlaze = hash01(vhash ^ 0x505u) > 0.82f;
  165. QVector3D barrelCenter(0.0f, d.barrelCenterY + bob, 0.0f);
  166. QVector3D chestCenter = barrelCenter + QVector3D(0.0f, d.bodyHeight * 0.12f,
  167. d.bodyLength * 0.34f);
  168. QVector3D rumpCenter = barrelCenter + QVector3D(0.0f, d.bodyHeight * 0.08f,
  169. -d.bodyLength * 0.36f);
  170. QVector3D bellyCenter = barrelCenter + QVector3D(0.0f, -d.bodyHeight * 0.35f,
  171. -d.bodyLength * 0.05f);
  172. {
  173. QMatrix4x4 chest = ctx.model;
  174. chest.translate(chestCenter);
  175. chest.scale(d.bodyWidth * 1.12f, d.bodyHeight * 0.95f,
  176. d.bodyLength * 0.36f);
  177. out.mesh(getUnitSphere(), chest, v.coatColor * 1.03f, nullptr, 1.0f);
  178. }
  179. {
  180. QMatrix4x4 withers = ctx.model;
  181. withers.translate(chestCenter + QVector3D(0.0f, d.bodyHeight * 0.55f,
  182. -d.bodyLength * 0.03f));
  183. withers.scale(d.bodyWidth * 0.75f, d.bodyHeight * 0.35f,
  184. d.bodyLength * 0.18f);
  185. out.mesh(getUnitSphere(), withers, v.coatColor * 0.96f, nullptr, 1.0f);
  186. }
  187. {
  188. QMatrix4x4 belly = ctx.model;
  189. belly.translate(bellyCenter);
  190. belly.scale(d.bodyWidth * 0.98f, d.bodyHeight * 0.64f,
  191. d.bodyLength * 0.40f);
  192. out.mesh(getUnitSphere(), belly, v.coatColor * 1.06f, nullptr, 1.0f);
  193. }
  194. for (int i = 0; i < 2; ++i) {
  195. float side = (i == 0) ? 1.0f : -1.0f;
  196. QMatrix4x4 ribs = ctx.model;
  197. ribs.translate(barrelCenter + QVector3D(side * d.bodyWidth * 0.90f,
  198. -d.bodyHeight * 0.10f,
  199. -d.bodyLength * 0.05f));
  200. ribs.scale(d.bodyWidth * 0.38f, d.bodyHeight * 0.42f, d.bodyLength * 0.30f);
  201. out.mesh(getUnitSphere(), ribs, v.coatColor * 1.00f, nullptr, 1.0f);
  202. }
  203. {
  204. QMatrix4x4 rump = ctx.model;
  205. rump.translate(rumpCenter);
  206. rump.scale(d.bodyWidth * 1.18f, d.bodyHeight * 1.00f, d.bodyLength * 0.36f);
  207. out.mesh(getUnitSphere(), rump, v.coatColor * 0.98f, nullptr, 1.0f);
  208. }
  209. for (int i = 0; i < 2; ++i) {
  210. float side = (i == 0) ? 1.0f : -1.0f;
  211. QMatrix4x4 hip = ctx.model;
  212. hip.translate(rumpCenter + QVector3D(side * d.bodyWidth * 0.95f,
  213. -d.bodyHeight * 0.10f,
  214. -d.bodyLength * 0.08f));
  215. hip.scale(d.bodyWidth * 0.45f, d.bodyHeight * 0.42f, d.bodyLength * 0.26f);
  216. out.mesh(getUnitSphere(), hip, v.coatColor * 0.99f, nullptr, 1.0f);
  217. }
  218. QVector3D neckBase =
  219. chestCenter + QVector3D(0.0f, d.bodyHeight * 0.38f, d.bodyLength * 0.06f);
  220. QVector3D neckTop = neckBase + QVector3D(0.0f, d.neckRise, d.neckLength);
  221. float neckRadius = d.bodyWidth * 0.42f;
  222. QVector3D neckMid =
  223. lerpVec(neckBase, neckTop, 0.55f) +
  224. QVector3D(0.0f, d.bodyHeight * 0.02f, d.bodyLength * 0.02f);
  225. out.mesh(getUnitCylinder(),
  226. cylinderBetween(ctx.model, neckBase, neckMid, neckRadius * 1.00f),
  227. v.coatColor * 1.03f, nullptr, 1.0f);
  228. out.mesh(getUnitCylinder(),
  229. cylinderBetween(ctx.model, neckMid, neckTop, neckRadius * 0.86f),
  230. v.coatColor * 1.04f, nullptr, 1.0f);
  231. QVector3D headCenter =
  232. neckTop + QVector3D(0.0f, d.headHeight * (0.10f - headNod * 0.15f),
  233. d.headLength * 0.40f);
  234. {
  235. QMatrix4x4 skull = ctx.model;
  236. skull.translate(headCenter + QVector3D(0.0f, d.headHeight * 0.10f,
  237. -d.headLength * 0.10f));
  238. skull.scale(d.headWidth * 0.95f, d.headHeight * 0.90f,
  239. d.headLength * 0.80f);
  240. out.mesh(getUnitSphere(), skull, v.coatColor * 1.05f, nullptr, 1.0f);
  241. }
  242. for (int i = 0; i < 2; ++i) {
  243. float side = (i == 0) ? 1.0f : -1.0f;
  244. QMatrix4x4 cheek = ctx.model;
  245. cheek.translate(headCenter + QVector3D(side * d.headWidth * 0.55f,
  246. -d.headHeight * 0.15f, 0.0f));
  247. cheek.scale(d.headWidth * 0.45f, d.headHeight * 0.50f,
  248. d.headLength * 0.60f);
  249. out.mesh(getUnitSphere(), cheek, v.coatColor * 1.02f, nullptr, 1.0f);
  250. }
  251. QVector3D muzzleCenter =
  252. headCenter + QVector3D(0.0f, -d.headHeight * 0.18f, d.headLength * 0.58f);
  253. {
  254. QMatrix4x4 muzzle = ctx.model;
  255. muzzle.translate(muzzleCenter +
  256. QVector3D(0.0f, -d.headHeight * 0.05f, 0.0f));
  257. muzzle.scale(d.headWidth * 0.68f, d.headHeight * 0.60f,
  258. d.muzzleLength * 1.05f);
  259. out.mesh(getUnitSphere(), muzzle, v.muzzleColor, nullptr, 1.0f);
  260. }
  261. {
  262. QVector3D nostrilBase =
  263. muzzleCenter +
  264. QVector3D(0.0f, -d.headHeight * 0.02f, d.muzzleLength * 0.60f);
  265. QVector3D leftBase =
  266. nostrilBase + QVector3D(d.headWidth * 0.26f, 0.0f, 0.0f);
  267. QVector3D rightBase =
  268. nostrilBase + QVector3D(-d.headWidth * 0.26f, 0.0f, 0.0f);
  269. QVector3D inward =
  270. QVector3D(0.0f, -d.headHeight * 0.02f, d.muzzleLength * -0.30f);
  271. out.mesh(
  272. getUnitCone(),
  273. coneFromTo(ctx.model, leftBase + inward, leftBase, d.headWidth * 0.11f),
  274. darken(v.muzzleColor, 0.6f), nullptr, 1.0f);
  275. out.mesh(getUnitCone(),
  276. coneFromTo(ctx.model, rightBase + inward, rightBase,
  277. d.headWidth * 0.11f),
  278. darken(v.muzzleColor, 0.6f), nullptr, 1.0f);
  279. }
  280. float earFlickL = std::sin(anim.time * 1.7f + 1.3f) * 0.15f;
  281. float earFlickR = std::sin(anim.time * 1.9f + 2.1f) * -0.12f;
  282. QVector3D earBaseLeft =
  283. headCenter + QVector3D(d.headWidth * 0.45f, d.headHeight * 0.42f,
  284. -d.headLength * 0.20f);
  285. QVector3D earTipLeft =
  286. earBaseLeft +
  287. rotateAroundY(QVector3D(d.headWidth * 0.08f, d.headHeight * 0.42f,
  288. -d.headLength * 0.10f),
  289. earFlickL);
  290. QVector3D earBaseRight =
  291. headCenter + QVector3D(-d.headWidth * 0.45f, d.headHeight * 0.42f,
  292. -d.headLength * 0.20f);
  293. QVector3D earTipRight =
  294. earBaseRight +
  295. rotateAroundY(QVector3D(-d.headWidth * 0.08f, d.headHeight * 0.42f,
  296. -d.headLength * 0.10f),
  297. earFlickR);
  298. out.mesh(getUnitCone(),
  299. coneFromTo(ctx.model, earTipLeft, earBaseLeft, d.headWidth * 0.11f),
  300. v.maneColor, nullptr, 1.0f);
  301. out.mesh(
  302. getUnitCone(),
  303. coneFromTo(ctx.model, earTipRight, earBaseRight, d.headWidth * 0.11f),
  304. v.maneColor, nullptr, 1.0f);
  305. QVector3D eyeLeft =
  306. headCenter + QVector3D(d.headWidth * 0.48f, d.headHeight * 0.10f,
  307. d.headLength * 0.05f);
  308. QVector3D eyeRight =
  309. headCenter + QVector3D(-d.headWidth * 0.48f, d.headHeight * 0.10f,
  310. d.headLength * 0.05f);
  311. QVector3D eyeBaseColor(0.10f, 0.10f, 0.10f);
  312. auto drawEye = [&](const QVector3D &pos) {
  313. {
  314. QMatrix4x4 eye = ctx.model;
  315. eye.translate(pos);
  316. eye.scale(d.headWidth * 0.14f);
  317. out.mesh(getUnitSphere(), eye, eyeBaseColor, nullptr, 1.0f);
  318. }
  319. {
  320. QMatrix4x4 pupil = ctx.model;
  321. pupil.translate(pos + QVector3D(0.0f, 0.0f, d.headWidth * 0.04f));
  322. pupil.scale(d.headWidth * 0.05f);
  323. out.mesh(getUnitSphere(), pupil, QVector3D(0.03f, 0.03f, 0.03f), nullptr,
  324. 1.0f);
  325. }
  326. {
  327. QMatrix4x4 spec = ctx.model;
  328. spec.translate(pos + QVector3D(d.headWidth * 0.03f, d.headWidth * 0.03f,
  329. d.headWidth * 0.03f));
  330. spec.scale(d.headWidth * 0.02f);
  331. out.mesh(getUnitSphere(), spec, QVector3D(0.95f, 0.95f, 0.95f), nullptr,
  332. 1.0f);
  333. }
  334. };
  335. drawEye(eyeLeft);
  336. drawEye(eyeRight);
  337. if (hasBlaze) {
  338. QMatrix4x4 blaze = ctx.model;
  339. blaze.translate(headCenter + QVector3D(0.0f, d.headHeight * 0.15f,
  340. d.headLength * 0.10f));
  341. blaze.scale(d.headWidth * 0.22f, d.headHeight * 0.32f,
  342. d.headLength * 0.10f);
  343. out.mesh(getUnitSphere(), blaze, QVector3D(0.92f, 0.92f, 0.90f), nullptr,
  344. 1.0f);
  345. }
  346. QVector3D maneRoot =
  347. neckTop + QVector3D(0.0f, d.headHeight * 0.20f, -d.headLength * 0.20f);
  348. for (int i = 0; i < 12; ++i) {
  349. float t = i / 11.0f;
  350. QVector3D segStart = lerpVec(maneRoot, neckBase, t);
  351. segStart.setY(segStart.y() + (0.07f - t * 0.05f));
  352. float sway =
  353. (anim.isMoving ? std::sin((phase + t * 0.15f) * 2.0f * kPi) * 0.04f
  354. : std::sin((anim.time * 0.8f + t * 2.3f)) * 0.02f);
  355. QVector3D segEnd =
  356. segStart + QVector3D(sway, 0.07f - t * 0.05f, -0.05f - t * 0.03f);
  357. out.mesh(getUnitCylinder(),
  358. cylinderBetween(ctx.model, segStart, segEnd,
  359. d.headWidth * (0.10f * (1.0f - t * 0.4f))),
  360. v.maneColor * (0.98f + t * 0.05f), nullptr, 1.0f);
  361. }
  362. QVector3D tailBase =
  363. rumpCenter + QVector3D(0.0f, d.bodyHeight * 0.36f, -d.bodyLength * 0.48f);
  364. for (int i = 0; i < 8; ++i) {
  365. float t = i / 8.0f;
  366. float swing =
  367. (anim.isMoving
  368. ? std::sin((phase + t * 0.10f) * 2.0f * kPi) *
  369. (0.05f + 0.02f * (1.0f - t))
  370. : std::sin((phase * 0.7f + t * 0.20f) * 2.0f * kPi) * 0.04f);
  371. QVector3D segStart =
  372. tailBase + QVector3D(swing, -i * 0.06f, -t * d.tailLength * 0.65f);
  373. QVector3D segEnd = segStart + QVector3D(swing * 0.6f, -0.08f - t * 0.05f,
  374. -d.tailLength * 0.16f);
  375. float radius = d.bodyWidth * (0.22f - 0.025f * i);
  376. out.mesh(getUnitCylinder(),
  377. cylinderBetween(ctx.model, segStart, segEnd, radius),
  378. v.tailColor * (0.95f - 0.05f * i), nullptr, 1.0f);
  379. }
  380. auto drawLeg = [&](const QVector3D &anchor, float lateralSign,
  381. float forwardBias, float phaseOffset, float sockChance) {
  382. float legPhase = std::fmod(phase + phaseOffset, 1.0f);
  383. float stride = 0.0f;
  384. float lift = 0.0f;
  385. if (anim.isMoving) {
  386. float angle = legPhase * 2.0f * kPi;
  387. stride = std::sin(angle) * g.strideSwing + forwardBias;
  388. float liftRaw = std::sin(angle);
  389. lift = liftRaw > 0.0f ? liftRaw * g.strideLift
  390. : liftRaw * g.strideLift * 0.22f;
  391. } else {
  392. float idle = std::sin(legPhase * 2.0f * kPi);
  393. stride = idle * g.strideSwing * 0.06f + forwardBias;
  394. lift = idle * d.idleBobAmplitude * 2.0f;
  395. }
  396. bool tightenLegs = anim.isMoving;
  397. float shoulderOut = d.bodyWidth * (tightenLegs ? 0.48f : 0.58f);
  398. QVector3D shoulder = anchor + QVector3D(lateralSign * shoulderOut,
  399. 0.05f + lift * 0.05f, stride);
  400. bool isRear = (forwardBias < 0.0f);
  401. float gallopAngle = legPhase * 2.0f * kPi;
  402. float hipSwing = anim.isMoving ? std::sin(gallopAngle) : 0.0f;
  403. float liftFactor =
  404. anim.isMoving
  405. ? std::max(0.0f, std::sin(gallopAngle + (isRear ? 0.35f : -0.25f)))
  406. : 0.0f;
  407. shoulder.setZ(shoulder.z() + hipSwing * (isRear ? -0.12f : 0.10f));
  408. if (tightenLegs)
  409. shoulder.setX(shoulder.x() - lateralSign * liftFactor * 0.05f);
  410. float thighLength = d.legLength * 0.55f;
  411. float hipPitch = hipSwing * (isRear ? 0.70f : 0.55f);
  412. float inwardLean = tightenLegs ? (-0.06f - liftFactor * 0.04f) : -0.015f;
  413. QVector3D thighDir(lateralSign * inwardLean, -std::cos(hipPitch) * 0.90f,
  414. (isRear ? -1.0f : 1.0f) * std::sin(hipPitch) * 0.65f);
  415. if (thighDir.lengthSquared() > 1e-6f)
  416. thighDir.normalize();
  417. QVector3D knee = shoulder + thighDir * thighLength;
  418. knee.setY(knee.y() + liftFactor * thighLength * 0.28f);
  419. float kneeFlex =
  420. anim.isMoving
  421. ? clamp01(std::sin(gallopAngle + (isRear ? 0.65f : -0.45f)) * 0.7f +
  422. 0.45f)
  423. : 0.35f;
  424. float forearmLength = d.legLength * 0.30f;
  425. float bendCos = std::cos(kneeFlex * kPi * 0.5f);
  426. float bendSin = std::sin(kneeFlex * kPi * 0.5f);
  427. QVector3D forearmDir(0.0f, -bendCos,
  428. (isRear ? -1.0f : 1.0f) * bendSin * 0.85f);
  429. if (forearmDir.lengthSquared() < 1e-6f)
  430. forearmDir = QVector3D(0.0f, -1.0f, 0.0f);
  431. else
  432. forearmDir.normalize();
  433. QVector3D cannon = knee + forearmDir * forearmLength;
  434. float pasternLength = d.legLength * 0.12f;
  435. QVector3D fetlock = cannon + QVector3D(0.0f, -pasternLength, 0.0f);
  436. float hoofPitch = anim.isMoving
  437. ? (-0.20f + std::sin(legPhase * 2.0f * kPi +
  438. (isRear ? 0.2f : -0.1f)) *
  439. 0.10f)
  440. : 0.0f;
  441. QVector3D hoofDir = rotateAroundZ(QVector3D(0.0f, -1.0f, 0.0f), hoofPitch);
  442. QVector3D hoofTop = fetlock;
  443. QVector3D hoofBottom = hoofTop + hoofDir * d.hoofHeight;
  444. float shoulderR = d.bodyWidth * (isRear ? 0.48f : 0.42f);
  445. float thighBellyR = d.bodyWidth * (isRear ? 0.60f : 0.54f);
  446. float kneeR = d.bodyWidth * 0.26f;
  447. float cannonR = d.bodyWidth * 0.19f;
  448. float pasternR = d.bodyWidth * 0.14f;
  449. QVector3D thighBelly = shoulder + (knee - shoulder) * 0.62f;
  450. out.mesh(getUnitCone(),
  451. coneFromTo(ctx.model, thighBelly, shoulder, thighBellyR),
  452. v.coatColor * 1.01f, nullptr, 1.0f);
  453. {
  454. QMatrix4x4 muscle = ctx.model;
  455. muscle.translate(thighBelly +
  456. QVector3D(0.0f, 0.0f, isRear ? -0.015f : 0.020f));
  457. muscle.scale(thighBellyR * QVector3D(1.05f, 0.85f, 0.92f));
  458. out.mesh(getUnitSphere(), muscle, v.coatColor * 1.04f, nullptr, 1.0f);
  459. }
  460. out.mesh(getUnitCone(), coneFromTo(ctx.model, knee, thighBelly, kneeR),
  461. v.coatColor * 0.98f, nullptr, 1.0f);
  462. {
  463. QMatrix4x4 joint = ctx.model;
  464. joint.translate(knee + QVector3D(0.0f, 0.0f, isRear ? -0.025f : 0.030f));
  465. joint.scale(QVector3D(kneeR * 1.22f, kneeR * 1.08f, kneeR * 1.40f));
  466. out.mesh(getUnitSphere(), joint, v.coatColor * 0.95f, nullptr, 1.0f);
  467. }
  468. out.mesh(getUnitCylinder(),
  469. cylinderBetween(ctx.model, knee, cannon, cannonR),
  470. v.coatColor * 0.94f, nullptr, 1.0f);
  471. {
  472. QMatrix4x4 joint = ctx.model;
  473. joint.translate(fetlock);
  474. joint.scale(pasternR * 1.18f);
  475. out.mesh(getUnitSphere(), joint, v.coatColor * 0.95f, nullptr, 1.0f);
  476. }
  477. float sock =
  478. sockChance > 0.78f ? 1.0f : (sockChance > 0.58f ? 0.55f : 0.0f);
  479. QVector3D distalColor =
  480. (sock > 0.0f) ? lighten(v.coatColor, 1.18f) : v.coatColor * 0.92f;
  481. float tSock = smoothstep(0.0f, 1.0f, sock);
  482. out.mesh(getUnitCylinder(),
  483. cylinderBetween(ctx.model, cannon, fetlock, pasternR * 1.05f),
  484. lerpVec(v.coatColor * 0.94f, distalColor, tSock * 0.8f), nullptr,
  485. 1.0f);
  486. QVector3D hoofColor = v.hoofColor;
  487. out.mesh(getUnitCylinder(),
  488. cylinderBetween(ctx.model, hoofTop, hoofBottom, pasternR * 0.95f),
  489. hoofColor, nullptr, 1.0f);
  490. {
  491. QVector3D toe = hoofBottom + QVector3D(0.0f, -d.hoofHeight * 0.15f, 0.0f);
  492. out.mesh(getUnitCone(),
  493. coneFromTo(ctx.model, toe, hoofBottom, pasternR * 0.88f),
  494. hoofColor * 0.96f, nullptr, 1.0f);
  495. }
  496. };
  497. QVector3D frontAnchor = barrelCenter + QVector3D(0.0f, d.bodyHeight * 0.05f,
  498. d.bodyLength * 0.28f);
  499. QVector3D rearAnchor = barrelCenter + QVector3D(0.0f, d.bodyHeight * 0.02f,
  500. -d.bodyLength * 0.32f);
  501. drawLeg(frontAnchor, 1.0f, d.bodyLength * 0.30f, g.frontLegPhase,
  502. sockChanceFL);
  503. drawLeg(frontAnchor, -1.0f, d.bodyLength * 0.30f, g.frontLegPhase + 0.5f,
  504. sockChanceFR);
  505. drawLeg(rearAnchor, 1.0f, -d.bodyLength * 0.28f, g.rearLegPhase,
  506. sockChanceRL);
  507. drawLeg(rearAnchor, -1.0f, -d.bodyLength * 0.28f, g.rearLegPhase + 0.5f,
  508. sockChanceRR);
  509. float saddleTop = d.saddleHeight;
  510. QVector3D saddleCenter(0.0f, saddleTop - d.saddleThickness * 0.3f,
  511. d.seatForwardOffset * 0.4f);
  512. {
  513. QMatrix4x4 saddle = ctx.model;
  514. saddle.translate(saddleCenter);
  515. saddle.scale(d.bodyWidth * 1.08f, d.saddleThickness, d.bodyLength * 0.32f);
  516. out.mesh(getUnitSphere(), saddle, v.saddleColor, nullptr, 1.0f);
  517. }
  518. QVector3D blanketCenter =
  519. saddleCenter + QVector3D(0.0f, -d.saddleThickness, 0.0f);
  520. {
  521. QMatrix4x4 blanket = ctx.model;
  522. blanket.translate(blanketCenter);
  523. blanket.scale(d.bodyWidth * 1.24f, d.saddleThickness * 0.35f,
  524. d.bodyLength * 0.40f);
  525. out.mesh(getUnitSphere(), blanket, v.blanketColor * 1.02f, nullptr, 1.0f);
  526. }
  527. for (int i = 0; i < 2; ++i) {
  528. float side = (i == 0) ? 1.0f : -1.0f;
  529. QVector3D strapTop =
  530. saddleCenter +
  531. QVector3D(side * d.bodyWidth * 0.92f, d.saddleThickness * 0.35f, 0.0f);
  532. QVector3D strapBottom =
  533. strapTop + QVector3D(0.0f, -d.bodyHeight * 0.95f, 0.02f);
  534. out.mesh(
  535. getUnitCylinder(),
  536. cylinderBetween(ctx.model, strapTop, strapBottom, d.bodyWidth * 0.07f),
  537. v.tackColor * 0.94f, nullptr, 1.0f);
  538. }
  539. auto drawStirrup = [&](float sideSign) {
  540. QVector3D stirrupAttach =
  541. saddleCenter + QVector3D(sideSign * d.bodyWidth * 0.92f,
  542. -d.saddleThickness * 0.1f,
  543. d.seatForwardOffset * 0.3f);
  544. QVector3D stirrupBottom =
  545. stirrupAttach + QVector3D(0.0f, -d.stirrupDrop, 0.0f);
  546. out.mesh(getUnitCylinder(),
  547. cylinderBetween(ctx.model, stirrupAttach, stirrupBottom,
  548. d.bodyWidth * 0.05f),
  549. v.tackColor, nullptr, 1.0f);
  550. QMatrix4x4 stirrup = ctx.model;
  551. stirrup.translate(stirrupBottom +
  552. QVector3D(0.0f, -d.bodyWidth * 0.06f, 0.0f));
  553. stirrup.scale(d.bodyWidth * 0.20f, d.bodyWidth * 0.07f,
  554. d.bodyWidth * 0.16f);
  555. out.mesh(getUnitSphere(), stirrup, QVector3D(0.66f, 0.65f, 0.62f), nullptr,
  556. 1.0f);
  557. };
  558. drawStirrup(1.0f);
  559. drawStirrup(-1.0f);
  560. QVector3D cheekLeftTop =
  561. headCenter + QVector3D(d.headWidth * 0.60f, -d.headHeight * 0.10f,
  562. d.headLength * 0.25f);
  563. QVector3D cheekLeftBottom =
  564. cheekLeftTop + QVector3D(0.0f, -d.headHeight, -d.headLength * 0.12f);
  565. QVector3D cheekRightTop =
  566. headCenter + QVector3D(-d.headWidth * 0.60f, -d.headHeight * 0.10f,
  567. d.headLength * 0.25f);
  568. QVector3D cheekRightBottom =
  569. cheekRightTop + QVector3D(0.0f, -d.headHeight, -d.headLength * 0.12f);
  570. out.mesh(getUnitCylinder(),
  571. cylinderBetween(ctx.model, cheekLeftTop, cheekLeftBottom,
  572. d.headWidth * 0.08f),
  573. v.tackColor, nullptr, 1.0f);
  574. out.mesh(getUnitCylinder(),
  575. cylinderBetween(ctx.model, cheekRightTop, cheekRightBottom,
  576. d.headWidth * 0.08f),
  577. v.tackColor, nullptr, 1.0f);
  578. QVector3D noseBandFront = muzzleCenter + QVector3D(0.0f, d.headHeight * 0.02f,
  579. d.muzzleLength * 0.35f);
  580. QVector3D noseBandLeft =
  581. noseBandFront + QVector3D(d.headWidth * 0.55f, 0.0f, 0.0f);
  582. QVector3D noseBandRight =
  583. noseBandFront + QVector3D(-d.headWidth * 0.55f, 0.0f, 0.0f);
  584. out.mesh(getUnitCylinder(),
  585. cylinderBetween(ctx.model, noseBandLeft, noseBandRight,
  586. d.headWidth * 0.08f),
  587. v.tackColor * 0.92f, nullptr, 1.0f);
  588. QVector3D browBandFront =
  589. headCenter + QVector3D(0.0f, d.headHeight * 0.28f, d.headLength * 0.15f);
  590. QVector3D browBandLeft =
  591. browBandFront + QVector3D(d.headWidth * 0.58f, 0.0f, 0.0f);
  592. QVector3D browBandRight =
  593. browBandFront + QVector3D(-d.headWidth * 0.58f, 0.0f, 0.0f);
  594. out.mesh(getUnitCylinder(),
  595. cylinderBetween(ctx.model, browBandLeft, browBandRight,
  596. d.headWidth * 0.07f),
  597. v.tackColor, nullptr, 1.0f);
  598. QVector3D bitLeft =
  599. muzzleCenter + QVector3D(d.headWidth * 0.55f, -d.headHeight * 0.08f,
  600. d.muzzleLength * 0.10f);
  601. QVector3D bitRight =
  602. muzzleCenter + QVector3D(-d.headWidth * 0.55f, -d.headHeight * 0.08f,
  603. d.muzzleLength * 0.10f);
  604. out.mesh(getUnitCylinder(),
  605. cylinderBetween(ctx.model, bitLeft, bitRight, d.headWidth * 0.05f),
  606. QVector3D(0.55f, 0.55f, 0.55f), nullptr, 1.0f);
  607. for (int i = 0; i < 2; ++i) {
  608. float side = (i == 0) ? 1.0f : -1.0f;
  609. QVector3D reinStart = (i == 0) ? bitLeft : bitRight;
  610. QVector3D reinEnd = saddleCenter + QVector3D(side * d.bodyWidth * 0.65f,
  611. -d.saddleThickness * 0.3f,
  612. d.seatForwardOffset * 0.15f);
  613. QVector3D mid = lerpVec(reinStart, reinEnd, 0.5f) +
  614. QVector3D(0.0f, -d.bodyHeight * 0.10f, 0.0f);
  615. out.mesh(getUnitCylinder(),
  616. cylinderBetween(ctx.model, reinStart, mid, d.bodyWidth * 0.02f),
  617. v.tackColor * 0.95f, nullptr, 1.0f);
  618. out.mesh(getUnitCylinder(),
  619. cylinderBetween(ctx.model, mid, reinEnd, d.bodyWidth * 0.02f),
  620. v.tackColor * 0.95f, nullptr, 1.0f);
  621. }
  622. }
  623. } // namespace Render::GL