rig.cpp 73 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043
  1. #include "rig.h"
  2. #include "../../game/core/component.h"
  3. #include "../../game/core/entity.h"
  4. #include "../../game/core/world.h"
  5. #include "../../game/map/terrain_service.h"
  6. #include "../../game/systems/nation_id.h"
  7. #include "../../game/units/spawn_type.h"
  8. #include "../../game/units/troop_config.h"
  9. #include "../../game/visuals/team_colors.h"
  10. #include "../entity/registry.h"
  11. #include "../geom/math_utils.h"
  12. #include "../geom/transforms.h"
  13. #include "../gl/backend.h"
  14. #include "../gl/camera.h"
  15. #include "../gl/humanoid/animation/animation_inputs.h"
  16. #include "../gl/humanoid/animation/gait.h"
  17. #include "../gl/humanoid/humanoid_constants.h"
  18. #include "../gl/primitives.h"
  19. #include "../gl/render_constants.h"
  20. #include "../gl/resources.h"
  21. #include "../palette.h"
  22. #include "../scene_renderer.h"
  23. #include "../submitter.h"
  24. #include "../visibility_budget.h"
  25. #include "formation_calculator.h"
  26. #include "humanoid_math.h"
  27. #include "pose_controller.h"
  28. #include <QMatrix4x4>
  29. #include <QVector2D>
  30. #include <QVector4D>
  31. #include <QtMath>
  32. #include <algorithm>
  33. #include <cmath>
  34. #include <cstdint>
  35. #include <functional>
  36. #include <limits>
  37. #include <numbers>
  38. #include <unordered_map>
  39. #include <vector>
  40. namespace Render::GL {
  41. using namespace Render::GL::Geometry;
  42. using Render::Geom::capsule_between;
  43. using Render::Geom::cone_from_to;
  44. using Render::Geom::cylinder_between;
  45. using Render::Geom::sphere_at;
  46. namespace {
  47. constexpr float k_shadow_size_infantry = 0.16F;
  48. constexpr float k_shadow_size_mounted = 0.35F;
  49. constexpr float k_run_extra_foot_lift = 0.08F;
  50. constexpr float k_run_stride_enhancement = 0.15F;
  51. struct CachedPoseEntry {
  52. HumanoidPose pose;
  53. VariationParams variation;
  54. uint32_t frame_number{0};
  55. bool was_moving{false};
  56. };
  57. using PoseCacheKey = uint64_t;
  58. static std::unordered_map<PoseCacheKey, CachedPoseEntry> s_pose_cache;
  59. static uint32_t s_current_frame = 0;
  60. constexpr uint32_t k_pose_cache_max_age = 300;
  61. inline auto make_pose_cache_key(uintptr_t entity_ptr,
  62. int soldier_idx) -> PoseCacheKey {
  63. return (static_cast<uint64_t>(entity_ptr) << 16) |
  64. static_cast<uint64_t>(soldier_idx & 0xFFFF);
  65. }
  66. static HumanoidRenderStats s_render_stats;
  67. constexpr float k_shadow_ground_offset = 0.02F;
  68. constexpr float k_shadow_base_alpha = 0.24F;
  69. constexpr QVector3D k_shadow_light_dir(0.4F, 1.0F, 0.25F);
  70. constexpr float k_temporal_skip_distance_reduced = 35.0F;
  71. constexpr float k_temporal_skip_distance_minimal = 45.0F;
  72. constexpr uint32_t k_temporal_skip_period_reduced = 2;
  73. constexpr uint32_t k_temporal_skip_period_minimal = 3;
  74. inline auto should_render_temporal(uint32_t frame, uint32_t seed,
  75. uint32_t period) -> bool {
  76. if (period <= 1) {
  77. return true;
  78. }
  79. return ((frame + seed) % period) == 0U;
  80. }
  81. } // namespace
  82. void advance_pose_cache_frame() {
  83. ++s_current_frame;
  84. if ((s_current_frame & 0x1FF) == 0) {
  85. auto it = s_pose_cache.begin();
  86. while (it != s_pose_cache.end()) {
  87. if (s_current_frame - it->second.frame_number >
  88. k_pose_cache_max_age * 2) {
  89. it = s_pose_cache.erase(it);
  90. } else {
  91. ++it;
  92. }
  93. }
  94. }
  95. }
  96. auto torso_mesh_without_bottom_cap() -> Mesh * {
  97. static std::unique_ptr<Mesh> s_mesh;
  98. if (s_mesh != nullptr) {
  99. return s_mesh.get();
  100. }
  101. Mesh *base = get_unit_torso();
  102. if (base == nullptr) {
  103. return nullptr;
  104. }
  105. auto filtered = base->clone_with_filtered_indices(
  106. [](unsigned int a, unsigned int b, unsigned int c,
  107. const std::vector<Vertex> &verts) -> bool {
  108. auto sample = [&](unsigned int idx) -> QVector3D {
  109. return {verts[idx].position[0], verts[idx].position[1],
  110. verts[idx].position[2]};
  111. };
  112. QVector3D pa = sample(a);
  113. QVector3D pb = sample(b);
  114. QVector3D pc = sample(c);
  115. float min_y = std::min({pa.y(), pb.y(), pc.y()});
  116. float max_y = std::max({pa.y(), pb.y(), pc.y()});
  117. QVector3D n(
  118. verts[a].normal[0] + verts[b].normal[0] + verts[c].normal[0],
  119. verts[a].normal[1] + verts[b].normal[1] + verts[c].normal[1],
  120. verts[a].normal[2] + verts[b].normal[2] + verts[c].normal[2]);
  121. if (n.lengthSquared() > 0.0F) {
  122. n.normalize();
  123. }
  124. constexpr float k_band_height = 0.02F;
  125. constexpr float k_bottom_threshold = 0.45F;
  126. bool is_flat = (max_y - min_y) < k_band_height;
  127. bool is_at_bottom = min_y > k_bottom_threshold;
  128. bool facing_down = (n.y() > 0.35F);
  129. return is_flat && is_at_bottom && facing_down;
  130. });
  131. s_mesh =
  132. (filtered != nullptr) ? std::move(filtered) : std::unique_ptr<Mesh>(base);
  133. return s_mesh.get();
  134. }
  135. auto HumanoidRendererBase::frame_local_position(
  136. const AttachmentFrame &frame, const QVector3D &local) -> QVector3D {
  137. float const lx = local.x() * frame.radius;
  138. float const ly = local.y() * frame.radius;
  139. float const lz = local.z() * frame.radius;
  140. return frame.origin + frame.right * lx + frame.up * ly + frame.forward * lz;
  141. }
  142. auto HumanoidRendererBase::make_frame_local_transform(
  143. const QMatrix4x4 &parent, const AttachmentFrame &frame,
  144. const QVector3D &local_offset, float uniform_scale) -> QMatrix4x4 {
  145. float scale = frame.radius * uniform_scale;
  146. if (scale == 0.0F) {
  147. scale = uniform_scale;
  148. }
  149. QVector3D const origin = frame_local_position(frame, local_offset);
  150. QMatrix4x4 local;
  151. local.setColumn(0, QVector4D(frame.right * scale, 0.0F));
  152. local.setColumn(1, QVector4D(frame.up * scale, 0.0F));
  153. local.setColumn(2, QVector4D(frame.forward * scale, 0.0F));
  154. local.setColumn(3, QVector4D(origin, 1.0F));
  155. return parent * local;
  156. }
  157. auto HumanoidRendererBase::head_local_position(
  158. const HeadFrame &frame, const QVector3D &local) -> QVector3D {
  159. return frame_local_position(frame, local);
  160. }
  161. auto HumanoidRendererBase::make_head_local_transform(
  162. const QMatrix4x4 &parent, const HeadFrame &frame,
  163. const QVector3D &local_offset, float uniform_scale) -> QMatrix4x4 {
  164. return make_frame_local_transform(parent, frame, local_offset, uniform_scale);
  165. }
  166. void HumanoidRendererBase::get_variant(const DrawContext &ctx, uint32_t seed,
  167. HumanoidVariant &v) const {
  168. QVector3D const team_tint = resolve_team_tint(ctx);
  169. v.palette = make_humanoid_palette(team_tint, seed);
  170. }
  171. void HumanoidRendererBase::customize_pose(const DrawContext &,
  172. const HumanoidAnimationContext &,
  173. uint32_t, HumanoidPose &) const {}
  174. void HumanoidRendererBase::add_attachments(const DrawContext &,
  175. const HumanoidVariant &,
  176. const HumanoidPose &,
  177. const HumanoidAnimationContext &,
  178. ISubmitter &) const {}
  179. auto HumanoidRendererBase::resolve_entity_ground_offset(
  180. const DrawContext &, Engine::Core::UnitComponent *unit_comp,
  181. Engine::Core::TransformComponent *transform_comp) const -> float {
  182. (void)unit_comp;
  183. (void)transform_comp;
  184. return 0.0F;
  185. }
  186. auto HumanoidRendererBase::resolve_team_tint(const DrawContext &ctx)
  187. -> QVector3D {
  188. QVector3D tunic(0.8F, 0.9F, 1.0F);
  189. Engine::Core::UnitComponent *unit = nullptr;
  190. Engine::Core::RenderableComponent *rc = nullptr;
  191. if (ctx.entity != nullptr) {
  192. unit = ctx.entity->get_component<Engine::Core::UnitComponent>();
  193. rc = ctx.entity->get_component<Engine::Core::RenderableComponent>();
  194. }
  195. if ((unit != nullptr) && unit->owner_id > 0) {
  196. tunic = Game::Visuals::team_colorForOwner(unit->owner_id);
  197. } else if (rc != nullptr) {
  198. tunic = QVector3D(rc->color[0], rc->color[1], rc->color[2]);
  199. }
  200. return tunic;
  201. }
  202. auto HumanoidRendererBase::resolve_formation(const DrawContext &ctx)
  203. -> FormationParams {
  204. FormationParams params{};
  205. params.individuals_per_unit = 1;
  206. params.max_per_row = 1;
  207. params.spacing = 0.75F;
  208. if (ctx.entity != nullptr) {
  209. auto *unit = ctx.entity->get_component<Engine::Core::UnitComponent>();
  210. if (unit != nullptr) {
  211. params.individuals_per_unit =
  212. Game::Units::TroopConfig::instance().getIndividualsPerUnit(
  213. unit->spawn_type);
  214. params.max_per_row =
  215. Game::Units::TroopConfig::instance().getMaxUnitsPerRow(
  216. unit->spawn_type);
  217. if (unit->spawn_type == Game::Units::SpawnType::MountedKnight) {
  218. params.spacing = 1.05F;
  219. }
  220. }
  221. }
  222. return params;
  223. }
  224. void HumanoidRendererBase::compute_locomotion_pose(
  225. uint32_t seed, float time, bool is_moving, const VariationParams &variation,
  226. HumanoidPose &pose) {
  227. using HP = HumanProportions;
  228. float const h_scale = variation.height_scale;
  229. pose.head_pos = QVector3D(0.0F, HP::HEAD_CENTER_Y * h_scale, 0.0F);
  230. pose.head_r = HP::HEAD_RADIUS * h_scale;
  231. pose.neck_base = QVector3D(0.0F, HP::NECK_BASE_Y * h_scale, 0.0F);
  232. float const b_scale = variation.bulk_scale;
  233. float const s_width = variation.stance_width;
  234. float const half_shoulder_span = 0.5F * HP::SHOULDER_WIDTH * b_scale;
  235. pose.shoulder_l =
  236. QVector3D(-half_shoulder_span, HP::SHOULDER_Y * h_scale, 0.0F);
  237. pose.shoulder_r =
  238. QVector3D(half_shoulder_span, HP::SHOULDER_Y * h_scale, 0.0F);
  239. pose.pelvis_pos = QVector3D(0.0F, HP::WAIST_Y * h_scale, 0.0F);
  240. float const rest_stride = 0.06F + (variation.arm_swing_amp - 1.0F) * 0.045F;
  241. float const foot_x_span = HP::SHOULDER_WIDTH * 0.62F * s_width;
  242. pose.foot_y_offset = HP::FOOT_Y_OFFSET_DEFAULT;
  243. pose.foot_l =
  244. QVector3D(-foot_x_span, HP::GROUND_Y + pose.foot_y_offset, rest_stride);
  245. pose.foot_r =
  246. QVector3D(foot_x_span, HP::GROUND_Y + pose.foot_y_offset, -rest_stride);
  247. pose.shoulder_l.setY(pose.shoulder_l.y() + variation.shoulder_tilt);
  248. pose.shoulder_r.setY(pose.shoulder_r.y() - variation.shoulder_tilt);
  249. float const slouch_offset = variation.posture_slump * 0.15F;
  250. pose.shoulder_l.setZ(pose.shoulder_l.z() + slouch_offset);
  251. pose.shoulder_r.setZ(pose.shoulder_r.z() + slouch_offset);
  252. float const foot_inward_jitter = (hash_01(seed ^ 0x5678U) - 0.5F) * 0.02F;
  253. float const foot_forward_jitter = (hash_01(seed ^ 0x9ABCU) - 0.5F) * 0.035F;
  254. pose.foot_l.setX(pose.foot_l.x() + foot_inward_jitter);
  255. pose.foot_r.setX(pose.foot_r.x() - foot_inward_jitter);
  256. pose.foot_l.setZ(pose.foot_l.z() + foot_forward_jitter);
  257. pose.foot_r.setZ(pose.foot_r.z() - foot_forward_jitter);
  258. float const arm_height_jitter = (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.03F;
  259. float const arm_asymmetry = (hash_01(seed ^ 0xDEF0U) - 0.5F) * 0.04F;
  260. pose.hand_l =
  261. QVector3D(-0.05F + arm_asymmetry,
  262. HP::SHOULDER_Y * h_scale + 0.05F + arm_height_jitter, 0.55F);
  263. pose.hand_r = QVector3D(
  264. 0.15F - arm_asymmetry * 0.5F,
  265. HP::SHOULDER_Y * h_scale + 0.15F + arm_height_jitter * 0.8F, 0.20F);
  266. if (is_moving) {
  267. float const walk_cycle_time = 0.8F / variation.walk_speed_mult;
  268. float const walk_phase = std::fmod(time * (1.0F / walk_cycle_time), 1.0F);
  269. float const left_phase = walk_phase;
  270. float const right_phase = std::fmod(walk_phase + 0.5F, 1.0F);
  271. const float ground_y = HP::GROUND_Y;
  272. const float stride_length = 0.35F * variation.arm_swing_amp;
  273. float const bob_phase = walk_phase * 2.0F;
  274. float const vertical_bob =
  275. std::sin(bob_phase * std::numbers::pi_v<float>) * 0.018F;
  276. float const hip_sway_amount = 0.002F;
  277. float const sway_raw =
  278. std::sin(walk_phase * 2.0F * std::numbers::pi_v<float>);
  279. float const hip_sway = sway_raw * hip_sway_amount;
  280. float const torso_sway_z = 0.0F;
  281. auto animate_foot = [ground_y, &pose, stride_length](QVector3D &foot,
  282. float phase) {
  283. float const lift_raw = std::sin(phase * 2.0F * std::numbers::pi_v<float>);
  284. float lift = 0.0F;
  285. if (lift_raw > 0.0F) {
  286. float const lift_phase =
  287. phase < 0.5F ? phase * 2.0F : (1.0F - phase) * 2.0F;
  288. float const ease_t =
  289. lift_phase * lift_phase * (3.0F - 2.0F * lift_phase);
  290. lift = lift_raw * ease_t;
  291. foot.setY(ground_y + pose.foot_y_offset + lift * 0.15F);
  292. } else {
  293. foot.setY(ground_y + pose.foot_y_offset);
  294. }
  295. float const stride_phase =
  296. (phase - 0.25F) * 2.0F * std::numbers::pi_v<float>;
  297. foot.setZ(foot.z() + std::sin(stride_phase) * stride_length);
  298. };
  299. animate_foot(pose.foot_l, left_phase);
  300. animate_foot(pose.foot_r, right_phase);
  301. pose.pelvis_pos.setY(pose.pelvis_pos.y() + vertical_bob);
  302. pose.shoulder_l.setY(pose.shoulder_l.y() + vertical_bob);
  303. pose.shoulder_r.setY(pose.shoulder_r.y() + vertical_bob);
  304. pose.neck_base.setY(pose.neck_base.y() + vertical_bob);
  305. pose.head_pos.setY(pose.head_pos.y() + vertical_bob);
  306. pose.pelvis_pos.setX(pose.pelvis_pos.x() + hip_sway);
  307. pose.shoulder_l.setZ(pose.shoulder_l.z() + torso_sway_z);
  308. pose.shoulder_r.setZ(pose.shoulder_r.z() + torso_sway_z);
  309. pose.neck_base.setZ(pose.neck_base.z() + torso_sway_z * 0.7F);
  310. pose.head_pos.setZ(pose.head_pos.z() + torso_sway_z * 0.5F);
  311. float const arm_swing_amp = 0.04F * variation.arm_swing_amp;
  312. float const arm_phase_offset = 0.15F;
  313. constexpr float max_arm_displacement = 0.06F;
  314. float const left_swing_raw = std::sin((left_phase + arm_phase_offset) *
  315. 2.0F * std::numbers::pi_v<float>);
  316. float const left_arm_swing =
  317. std::clamp(left_swing_raw * arm_swing_amp, -max_arm_displacement,
  318. max_arm_displacement);
  319. pose.hand_l.setZ(pose.hand_l.z() - left_arm_swing);
  320. float const right_swing_raw = std::sin((right_phase + arm_phase_offset) *
  321. 2.0F * std::numbers::pi_v<float>);
  322. float const right_arm_swing =
  323. std::clamp(right_swing_raw * arm_swing_amp, -max_arm_displacement,
  324. max_arm_displacement);
  325. pose.hand_r.setZ(pose.hand_r.z() - right_arm_swing);
  326. auto clamp_hand_reach = [&](const QVector3D &shoulder, QVector3D &hand) {
  327. float const max_reach =
  328. (HP::UPPER_ARM_LEN + HP::FORE_ARM_LEN) * h_scale * 0.98F;
  329. QVector3D diff = hand - shoulder;
  330. float const len = diff.length();
  331. if (len > max_reach && len > 1e-6F) {
  332. hand = shoulder + diff * (max_reach / len);
  333. }
  334. };
  335. clamp_hand_reach(pose.shoulder_l, pose.hand_l);
  336. clamp_hand_reach(pose.shoulder_r, pose.hand_r);
  337. }
  338. QVector3D const hip_l =
  339. pose.pelvis_pos +
  340. QVector3D(-HP::HIP_LATERAL_OFFSET, HP::HIP_VERTICAL_OFFSET, 0.0F);
  341. QVector3D const hip_r =
  342. pose.pelvis_pos +
  343. QVector3D(HP::HIP_LATERAL_OFFSET, HP::HIP_VERTICAL_OFFSET, 0.0F);
  344. auto solve_leg = [&](const QVector3D &hip, const QVector3D &foot,
  345. bool is_left) -> QVector3D {
  346. QVector3D hip_to_foot = foot - hip;
  347. float const distance = hip_to_foot.length();
  348. if (distance < HP::EPSILON_SMALL) {
  349. return hip;
  350. }
  351. float const upper_len = HP::UPPER_LEG_LEN * h_scale;
  352. float const lower_len = HP::LOWER_LEG_LEN * h_scale;
  353. float const reach = upper_len + lower_len;
  354. float const min_reach =
  355. std::max(std::abs(upper_len - lower_len) + 1e-4F, 1e-3F);
  356. float const max_reach = std::max(reach - 1e-4F, min_reach + 1e-4F);
  357. float const clamped_dist = std::clamp(distance, min_reach, max_reach);
  358. QVector3D const dir = hip_to_foot / distance;
  359. float cos_theta = (upper_len * upper_len + clamped_dist * clamped_dist -
  360. lower_len * lower_len) /
  361. (2.0F * upper_len * clamped_dist);
  362. cos_theta = std::clamp(cos_theta, -1.0F, 1.0F);
  363. float const sin_theta =
  364. std::sqrt(std::max(0.0F, 1.0F - cos_theta * cos_theta));
  365. QVector3D bend_pref = is_left ? QVector3D(-0.24F, 0.0F, 0.95F)
  366. : QVector3D(0.24F, 0.0F, 0.95F);
  367. bend_pref.normalize();
  368. QVector3D bend_axis =
  369. bend_pref - dir * QVector3D::dotProduct(dir, bend_pref);
  370. if (bend_axis.lengthSquared() < 1e-6F) {
  371. bend_axis = QVector3D::crossProduct(dir, QVector3D(0.0F, 1.0F, 0.0F));
  372. if (bend_axis.lengthSquared() < 1e-6F) {
  373. bend_axis = QVector3D::crossProduct(dir, QVector3D(1.0F, 0.0F, 0.0F));
  374. }
  375. }
  376. bend_axis.normalize();
  377. QVector3D const knee = hip + dir * (cos_theta * upper_len) +
  378. bend_axis * (sin_theta * upper_len);
  379. float const knee_floor = HP::GROUND_Y + pose.foot_y_offset * 0.5F;
  380. if (knee.y() < knee_floor) {
  381. QVector3D adjusted = knee;
  382. adjusted.setY(knee_floor);
  383. return adjusted;
  384. }
  385. return knee;
  386. };
  387. pose.knee_l = solve_leg(hip_l, pose.foot_l, true);
  388. pose.knee_r = solve_leg(hip_r, pose.foot_r, false);
  389. QVector3D right_axis = pose.shoulder_r - pose.shoulder_l;
  390. right_axis.setY(0.0F);
  391. if (right_axis.lengthSquared() < 1e-8F) {
  392. right_axis = QVector3D(1, 0, 0);
  393. }
  394. right_axis.normalize();
  395. if (right_axis.x() < 0.0F) {
  396. right_axis = -right_axis;
  397. }
  398. QVector3D const outward_l = -right_axis;
  399. QVector3D const outward_r = right_axis;
  400. pose.elbow_l = elbow_bend_torso(pose.shoulder_l, pose.hand_l, outward_l,
  401. 0.45F, 0.15F, -0.08F, +1.0F);
  402. pose.elbow_r = elbow_bend_torso(pose.shoulder_r, pose.hand_r, outward_r,
  403. 0.48F, 0.12F, 0.02F, +1.0F);
  404. }
  405. void HumanoidRendererBase::draw_common_body(const DrawContext &ctx,
  406. const HumanoidVariant &v,
  407. HumanoidPose &pose,
  408. ISubmitter &out) const {
  409. using HP = HumanProportions;
  410. QVector3D const scaling = get_proportion_scaling();
  411. float const width_scale = scaling.x();
  412. float const height_scale = scaling.y();
  413. float const torso_scale = get_torso_scale();
  414. float const head_scale = 1.0F;
  415. QVector3D right_axis = pose.shoulder_r - pose.shoulder_l;
  416. right_axis.setY(0.0F);
  417. right_axis.setZ(0.0F);
  418. if (right_axis.lengthSquared() < 1e-8F) {
  419. right_axis = QVector3D(1.0F, 0.0F, 0.0F);
  420. }
  421. right_axis.normalize();
  422. if (right_axis.x() < 0.0F) {
  423. right_axis = -right_axis;
  424. }
  425. QVector3D const up_axis(0.0F, 1.0F, 0.0F);
  426. QVector3D forward_axis = QVector3D::crossProduct(right_axis, up_axis);
  427. if (forward_axis.lengthSquared() < 1e-8F) {
  428. forward_axis = QVector3D(0.0F, 0.0F, 1.0F);
  429. }
  430. forward_axis.normalize();
  431. QVector3D const shoulder_mid = (pose.shoulder_l + pose.shoulder_r) * 0.5F;
  432. const float y_shoulder = shoulder_mid.y();
  433. const float y_neck = pose.neck_base.y();
  434. const float shoulder_half_span =
  435. 0.5F * std::abs(pose.shoulder_r.x() - pose.shoulder_l.x());
  436. const float torso_r_base =
  437. std::max(HP::TORSO_TOP_R, shoulder_half_span * 0.95F);
  438. const float torso_r = torso_r_base * torso_scale;
  439. float const depth_scale = scaling.z();
  440. const float torso_depth_factor =
  441. std::clamp(HP::TORSO_DEPTH_FACTOR_BASE + (depth_scale - 1.0F) * 0.20F,
  442. HP::TORSO_DEPTH_FACTOR_MIN, HP::TORSO_DEPTH_FACTOR_MAX);
  443. float torso_depth = torso_r * torso_depth_factor;
  444. const float y_top_cover =
  445. std::max(y_shoulder + 0.00F, y_neck + HP::TORSO_TOP_COVER_OFFSET);
  446. const float upper_arm_r = HP::UPPER_ARM_R * width_scale;
  447. const float fore_arm_r = HP::FORE_ARM_R * width_scale;
  448. const float joint_r = HP::HAND_RADIUS * width_scale * 1.05F;
  449. const float hand_r = HP::HAND_RADIUS * width_scale * 0.95F;
  450. const float leg_joint_r = HP::LOWER_LEG_R * width_scale * 0.95F;
  451. const float thigh_r = HP::UPPER_LEG_R * width_scale;
  452. const float shin_r = HP::LOWER_LEG_R * width_scale;
  453. const float foot_radius = shin_r * 1.10F;
  454. float const torso_x = (shoulder_mid.x() + pose.pelvis_pos.x()) * 0.5F;
  455. constexpr float torso_z = 0.0F;
  456. QVector3D const tunic_top{torso_x, y_top_cover - 0.006F, torso_z};
  457. QVector3D const tunic_bot{torso_x, pose.pelvis_pos.y() - 0.05F, torso_z};
  458. QMatrix4x4 torso_transform =
  459. cylinder_between(ctx.model, tunic_top, tunic_bot, 1.0F);
  460. torso_transform.scale(torso_r, 1.0F, torso_depth);
  461. Mesh *torso_mesh = torso_mesh_without_bottom_cap();
  462. if (torso_mesh != nullptr) {
  463. out.mesh(torso_mesh, torso_transform, v.palette.cloth, nullptr, 1.0F);
  464. }
  465. float const head_r = pose.head_r;
  466. QVector3D head_up;
  467. QVector3D head_right;
  468. QVector3D head_forward;
  469. if (pose.head_frame.radius > 0.001F) {
  470. head_up = pose.head_frame.up;
  471. head_right = pose.head_frame.right;
  472. head_forward = pose.head_frame.forward;
  473. } else {
  474. head_up = pose.head_pos - pose.neck_base;
  475. if (head_up.lengthSquared() < 1e-8F) {
  476. head_up = up_axis;
  477. } else {
  478. head_up.normalize();
  479. }
  480. head_right =
  481. right_axis - head_up * QVector3D::dotProduct(right_axis, head_up);
  482. if (head_right.lengthSquared() < 1e-8F) {
  483. head_right = QVector3D::crossProduct(head_up, forward_axis);
  484. if (head_right.lengthSquared() < 1e-8F) {
  485. head_right = QVector3D(1.0F, 0.0F, 0.0F);
  486. }
  487. }
  488. head_right.normalize();
  489. if (QVector3D::dotProduct(head_right, right_axis) < 0.0F) {
  490. head_right = -head_right;
  491. }
  492. head_forward = QVector3D::crossProduct(head_right, head_up);
  493. if (head_forward.lengthSquared() < 1e-8F) {
  494. head_forward = forward_axis;
  495. } else {
  496. head_forward.normalize();
  497. }
  498. if (QVector3D::dotProduct(head_forward, forward_axis) < 0.0F) {
  499. head_right = -head_right;
  500. head_forward = -head_forward;
  501. }
  502. }
  503. QVector3D const chin_pos = pose.head_pos - head_up * head_r;
  504. out.mesh(get_unit_cylinder(),
  505. cylinder_between(ctx.model, pose.neck_base, chin_pos,
  506. HP::NECK_RADIUS * width_scale),
  507. v.palette.skin * 0.9F, nullptr, 1.0F);
  508. QMatrix4x4 head_rot;
  509. head_rot.setColumn(0, QVector4D(head_right, 0.0F));
  510. head_rot.setColumn(1, QVector4D(head_up, 0.0F));
  511. head_rot.setColumn(2, QVector4D(head_forward, 0.0F));
  512. head_rot.setColumn(3, QVector4D(0.0F, 0.0F, 0.0F, 1.0F));
  513. QMatrix4x4 head_transform = ctx.model;
  514. head_transform.translate(pose.head_pos);
  515. head_transform = head_transform * head_rot;
  516. head_transform.scale(head_r);
  517. out.mesh(get_unit_sphere(), head_transform, v.palette.skin, nullptr, 1.0F);
  518. pose.head_frame.origin = pose.head_pos;
  519. pose.head_frame.right = head_right;
  520. pose.head_frame.up = head_up;
  521. pose.head_frame.forward = head_forward;
  522. pose.head_frame.radius = head_r;
  523. pose.body_frames.head = pose.head_frame;
  524. QVector3D const torso_center = QVector3D(
  525. (shoulder_mid.x() + pose.pelvis_pos.x()) * 0.5F, y_shoulder, 0.0F);
  526. pose.body_frames.torso.origin = torso_center;
  527. pose.body_frames.torso.right = right_axis;
  528. pose.body_frames.torso.up = up_axis;
  529. pose.body_frames.torso.forward = forward_axis;
  530. pose.body_frames.torso.radius = torso_r;
  531. pose.body_frames.torso.depth = torso_depth;
  532. pose.body_frames.back.origin = torso_center - forward_axis * torso_depth;
  533. pose.body_frames.back.right = right_axis;
  534. pose.body_frames.back.up = up_axis;
  535. pose.body_frames.back.forward = -forward_axis;
  536. pose.body_frames.back.radius = torso_r * 0.75F;
  537. pose.body_frames.back.depth = torso_depth * 0.90F;
  538. pose.body_frames.waist.origin = pose.pelvis_pos;
  539. pose.body_frames.waist.right = right_axis;
  540. pose.body_frames.waist.up = up_axis;
  541. pose.body_frames.waist.forward = forward_axis;
  542. pose.body_frames.waist.radius = torso_r * 0.80F;
  543. pose.body_frames.waist.depth = torso_depth * 0.72F;
  544. QVector3D shoulder_up = (pose.shoulder_l - pose.pelvis_pos).normalized();
  545. QVector3D shoulder_forward_l =
  546. QVector3D::crossProduct(-right_axis, shoulder_up);
  547. if (shoulder_forward_l.lengthSquared() < 1e-8F) {
  548. shoulder_forward_l = forward_axis;
  549. } else {
  550. shoulder_forward_l.normalize();
  551. }
  552. pose.body_frames.shoulder_l.origin = pose.shoulder_l;
  553. pose.body_frames.shoulder_l.right = -right_axis;
  554. pose.body_frames.shoulder_l.up = shoulder_up;
  555. pose.body_frames.shoulder_l.forward = shoulder_forward_l;
  556. pose.body_frames.shoulder_l.radius = upper_arm_r;
  557. QVector3D shoulder_forward_r =
  558. QVector3D::crossProduct(right_axis, shoulder_up);
  559. if (shoulder_forward_r.lengthSquared() < 1e-8F) {
  560. shoulder_forward_r = forward_axis;
  561. } else {
  562. shoulder_forward_r.normalize();
  563. }
  564. pose.body_frames.shoulder_r.origin = pose.shoulder_r;
  565. pose.body_frames.shoulder_r.right = right_axis;
  566. pose.body_frames.shoulder_r.up = shoulder_up;
  567. pose.body_frames.shoulder_r.forward = shoulder_forward_r;
  568. pose.body_frames.shoulder_r.radius = upper_arm_r;
  569. QVector3D hand_up_l = (pose.hand_l - pose.elbow_l);
  570. if (hand_up_l.lengthSquared() > 1e-8F) {
  571. hand_up_l.normalize();
  572. } else {
  573. hand_up_l = up_axis;
  574. }
  575. QVector3D hand_forward_l = QVector3D::crossProduct(-right_axis, hand_up_l);
  576. if (hand_forward_l.lengthSquared() < 1e-8F) {
  577. hand_forward_l = forward_axis;
  578. } else {
  579. hand_forward_l.normalize();
  580. }
  581. pose.body_frames.hand_l.origin = pose.hand_l;
  582. pose.body_frames.hand_l.right = -right_axis;
  583. pose.body_frames.hand_l.up = hand_up_l;
  584. pose.body_frames.hand_l.forward = hand_forward_l;
  585. pose.body_frames.hand_l.radius = hand_r;
  586. QVector3D hand_up_r = (pose.hand_r - pose.elbow_r);
  587. if (hand_up_r.lengthSquared() > 1e-8F) {
  588. hand_up_r.normalize();
  589. } else {
  590. hand_up_r = up_axis;
  591. }
  592. QVector3D hand_forward_r = QVector3D::crossProduct(right_axis, hand_up_r);
  593. if (hand_forward_r.lengthSquared() < 1e-8F) {
  594. hand_forward_r = forward_axis;
  595. } else {
  596. hand_forward_r.normalize();
  597. }
  598. pose.body_frames.hand_r.origin = pose.hand_r;
  599. pose.body_frames.hand_r.right = right_axis;
  600. pose.body_frames.hand_r.up = hand_up_r;
  601. pose.body_frames.hand_r.forward = hand_forward_r;
  602. pose.body_frames.hand_r.radius = hand_r;
  603. QVector3D foot_up_l = up_axis;
  604. QVector3D foot_forward_l = forward_axis - right_axis * 0.12F;
  605. if (foot_forward_l.lengthSquared() > 1e-8F) {
  606. foot_forward_l.normalize();
  607. } else {
  608. foot_forward_l = forward_axis;
  609. }
  610. pose.body_frames.foot_l.origin = pose.foot_l;
  611. pose.body_frames.foot_l.right = -right_axis;
  612. pose.body_frames.foot_l.up = foot_up_l;
  613. pose.body_frames.foot_l.forward = foot_forward_l;
  614. pose.body_frames.foot_l.radius = foot_radius;
  615. QVector3D foot_forward_r = forward_axis + right_axis * 0.12F;
  616. if (foot_forward_r.lengthSquared() > 1e-8F) {
  617. foot_forward_r.normalize();
  618. } else {
  619. foot_forward_r = forward_axis;
  620. }
  621. pose.body_frames.foot_r.origin = pose.foot_r;
  622. pose.body_frames.foot_r.right = right_axis;
  623. pose.body_frames.foot_r.up = foot_up_l;
  624. pose.body_frames.foot_r.forward = foot_forward_r;
  625. pose.body_frames.foot_r.radius = foot_radius;
  626. auto compute_shin_frame = [&](const QVector3D &ankle, const QVector3D &knee,
  627. float right_sign) -> AttachmentFrame {
  628. AttachmentFrame shin{};
  629. shin.origin = ankle;
  630. QVector3D shin_dir = knee - ankle;
  631. float shin_len = shin_dir.length();
  632. if (shin_len > 1e-6F) {
  633. shin.up = shin_dir / shin_len;
  634. } else {
  635. shin.up = up_axis;
  636. }
  637. QVector3D shin_forward = forward_axis;
  638. shin_forward =
  639. shin_forward - shin.up * QVector3D::dotProduct(shin_forward, shin.up);
  640. if (shin_forward.lengthSquared() > 1e-6F) {
  641. shin_forward.normalize();
  642. } else {
  643. shin_forward = forward_axis;
  644. }
  645. shin.forward = shin_forward;
  646. shin.right = QVector3D::crossProduct(shin.up, shin.forward) * right_sign;
  647. shin.radius = HP::LOWER_LEG_R;
  648. return shin;
  649. };
  650. pose.body_frames.shin_l = compute_shin_frame(pose.foot_l, pose.knee_l, -1.0F);
  651. pose.body_frames.shin_r = compute_shin_frame(pose.foot_r, pose.knee_r, 1.0F);
  652. QVector3D const iris = QVector3D(0.10F, 0.10F, 0.12F);
  653. auto eye_position = [&](float lateral) {
  654. QVector3D const local(lateral, 0.12F, 0.92F);
  655. QVector3D world = frame_local_position(pose.body_frames.head, local);
  656. world +=
  657. pose.body_frames.head.forward * (pose.body_frames.head.radius * 0.02F);
  658. return world;
  659. };
  660. QVector3D const left_eye_world = eye_position(-0.32F);
  661. QVector3D const right_eye_world = eye_position(0.32F);
  662. float const eye_radius = pose.body_frames.head.radius * 0.17F;
  663. out.mesh(get_unit_sphere(), sphere_at(ctx.model, left_eye_world, eye_radius),
  664. iris, nullptr, 1.0F);
  665. out.mesh(get_unit_sphere(), sphere_at(ctx.model, right_eye_world, eye_radius),
  666. iris, nullptr, 1.0F);
  667. out.mesh(
  668. get_unit_cylinder(),
  669. cylinder_between(ctx.model, pose.shoulder_l, pose.elbow_l, upper_arm_r),
  670. v.palette.cloth, nullptr, 1.0F);
  671. out.mesh(get_unit_sphere(), sphere_at(ctx.model, pose.elbow_l, joint_r),
  672. v.palette.cloth * 0.95F, nullptr, 1.0F);
  673. out.mesh(get_unit_cylinder(),
  674. cylinder_between(ctx.model, pose.elbow_l, pose.hand_l, fore_arm_r),
  675. v.palette.skin * 0.95F, nullptr, 1.0F);
  676. out.mesh(get_unit_sphere(), sphere_at(ctx.model, pose.hand_l, hand_r),
  677. v.palette.leather_dark * 0.92F, nullptr, 1.0F);
  678. out.mesh(
  679. get_unit_cylinder(),
  680. cylinder_between(ctx.model, pose.shoulder_r, pose.elbow_r, upper_arm_r),
  681. v.palette.cloth, nullptr, 1.0F);
  682. out.mesh(get_unit_sphere(), sphere_at(ctx.model, pose.elbow_r, joint_r),
  683. v.palette.cloth * 0.95F, nullptr, 1.0F);
  684. out.mesh(get_unit_cylinder(),
  685. cylinder_between(ctx.model, pose.elbow_r, pose.hand_r, fore_arm_r),
  686. v.palette.skin * 0.95F, nullptr, 1.0F);
  687. out.mesh(get_unit_sphere(), sphere_at(ctx.model, pose.hand_r, hand_r),
  688. v.palette.leather_dark * 0.92F, nullptr, 1.0F);
  689. QVector3D const hip_l =
  690. pose.pelvis_pos +
  691. QVector3D(-HP::HIP_LATERAL_OFFSET, HP::HIP_VERTICAL_OFFSET, 0.0F);
  692. QVector3D const hip_r =
  693. pose.pelvis_pos +
  694. QVector3D(HP::HIP_LATERAL_OFFSET, HP::HIP_VERTICAL_OFFSET, 0.0F);
  695. out.mesh(get_unit_cylinder(),
  696. cylinder_between(ctx.model, hip_l, pose.knee_l, thigh_r),
  697. v.palette.cloth * 0.92F, nullptr, 1.0F);
  698. out.mesh(get_unit_sphere(), sphere_at(ctx.model, pose.knee_l, leg_joint_r),
  699. v.palette.cloth * 0.90F, nullptr, 1.0F);
  700. out.mesh(get_unit_cylinder(),
  701. cylinder_between(ctx.model, pose.knee_l, pose.foot_l, shin_r),
  702. v.palette.leather * 0.95F, nullptr, 1.0F);
  703. out.mesh(get_unit_cylinder(),
  704. cylinder_between(ctx.model, hip_r, pose.knee_r, thigh_r),
  705. v.palette.cloth * 0.92F, nullptr, 1.0F);
  706. out.mesh(get_unit_sphere(), sphere_at(ctx.model, pose.knee_r, leg_joint_r),
  707. v.palette.cloth * 0.90F, nullptr, 1.0F);
  708. out.mesh(get_unit_cylinder(),
  709. cylinder_between(ctx.model, pose.knee_r, pose.foot_r, shin_r),
  710. v.palette.leather * 0.95F, nullptr, 1.0F);
  711. auto draw_foot = [&](const QVector3D &ankle, bool is_left) {
  712. QVector3D lateral = is_left ? -right_axis : right_axis;
  713. QVector3D foot_forward =
  714. forward_axis + lateral * (is_left ? -0.12F : 0.12F);
  715. if (foot_forward.lengthSquared() < 1e-6F) {
  716. foot_forward = forward_axis;
  717. }
  718. foot_forward.normalize();
  719. float const heel_span = foot_radius * 3.50F;
  720. float const toe_span = foot_radius * 5.50F;
  721. float const sole_y = HP::GROUND_Y;
  722. QVector3D ankle_ground = ankle;
  723. ankle_ground.setY(sole_y);
  724. QVector3D heel = ankle_ground - foot_forward * heel_span;
  725. QVector3D toe = ankle_ground + foot_forward * toe_span;
  726. heel.setY(sole_y);
  727. toe.setY(sole_y);
  728. QMatrix4x4 foot_mat = capsule_between(ctx.model, heel, toe, foot_radius);
  729. float const width_at_heel = 1.2F;
  730. float const width_at_toe = 2.5F;
  731. float const height_scale = 0.26F;
  732. float const depth_scale = 1.0F;
  733. QMatrix4x4 scale_mat;
  734. scale_mat.setToIdentity();
  735. scale_mat.scale((width_at_heel + width_at_toe) * 0.5F, height_scale,
  736. depth_scale);
  737. QMatrix4x4 shear_mat;
  738. shear_mat.setToIdentity();
  739. shear_mat(0, 2) = (width_at_toe - width_at_heel) * 0.5F;
  740. foot_mat = foot_mat * scale_mat * shear_mat;
  741. out.mesh(get_unit_capsule(), foot_mat, v.palette.leather_dark * 0.92F,
  742. nullptr, 1.0F);
  743. };
  744. draw_foot(pose.foot_l, true);
  745. draw_foot(pose.foot_r, false);
  746. draw_armor_overlay(ctx, v, pose, y_top_cover, torso_r, shoulder_half_span,
  747. upper_arm_r, right_axis, out);
  748. draw_shoulder_decorations(ctx, v, pose, y_top_cover, pose.neck_base.y(),
  749. right_axis, out);
  750. draw_helmet(ctx, v, pose, out);
  751. }
  752. void HumanoidRendererBase::draw_armor_overlay(
  753. const DrawContext &, const HumanoidVariant &, const HumanoidPose &, float,
  754. float, float, float, const QVector3D &, ISubmitter &) const {}
  755. void HumanoidRendererBase::draw_armor(const DrawContext &,
  756. const HumanoidVariant &,
  757. const HumanoidPose &,
  758. const HumanoidAnimationContext &,
  759. ISubmitter &) const {}
  760. void HumanoidRendererBase::draw_shoulder_decorations(
  761. const DrawContext &, const HumanoidVariant &, const HumanoidPose &, float,
  762. float, const QVector3D &, ISubmitter &) const {}
  763. void HumanoidRendererBase::draw_helmet(const DrawContext &,
  764. const HumanoidVariant &,
  765. const HumanoidPose &,
  766. ISubmitter &) const {}
  767. void HumanoidRendererBase::draw_facial_hair(const DrawContext &ctx,
  768. const HumanoidVariant &v,
  769. const HumanoidPose &pose,
  770. ISubmitter &out) const {
  771. const FacialHairParams &fh = v.facial_hair;
  772. if (fh.style == FacialHairStyle::None || fh.coverage < 0.01F) {
  773. return;
  774. }
  775. const auto &gfx_settings = Render::GraphicsSettings::instance();
  776. if (!gfx_settings.features().enable_facial_hair) {
  777. return;
  778. }
  779. constexpr float k_facial_hair_max_distance = 25.0F;
  780. if (ctx.camera != nullptr) {
  781. QVector3D const soldier_world_pos =
  782. ctx.model.map(QVector3D(0.0F, 0.0F, 0.0F));
  783. float const distance =
  784. (soldier_world_pos - ctx.camera->get_position()).length();
  785. if (distance > k_facial_hair_max_distance) {
  786. ++s_render_stats.facial_hair_skipped_distance;
  787. return;
  788. }
  789. }
  790. const AttachmentFrame &frame = pose.body_frames.head;
  791. float const head_r = frame.radius;
  792. if (head_r <= 0.0F) {
  793. return;
  794. }
  795. auto saturate = [](const QVector3D &c) -> QVector3D {
  796. return {std::clamp(c.x(), 0.0F, 1.0F), std::clamp(c.y(), 0.0F, 1.0F),
  797. std::clamp(c.z(), 0.0F, 1.0F)};
  798. };
  799. QVector3D hair_color = fh.color * (1.0F - fh.greyness) +
  800. QVector3D(0.75F, 0.75F, 0.75F) * fh.greyness;
  801. QVector3D hair_dark = hair_color * 0.80F;
  802. QVector3D const hair_root = hair_dark * 0.95F;
  803. QVector3D const hair_tip = hair_color * 1.08F;
  804. float const chin_y = -head_r * 0.95F;
  805. float const mouth_y = -head_r * 0.18F;
  806. float const jaw_z = head_r * 0.68F;
  807. float const chin_norm = chin_y / head_r;
  808. float const mouth_norm = mouth_y / head_r;
  809. float const jaw_forward_norm = jaw_z / head_r;
  810. uint32_t rand_state = 0x9E3779B9U;
  811. if (ctx.entity != nullptr) {
  812. uintptr_t ptr = reinterpret_cast<uintptr_t>(ctx.entity);
  813. rand_state ^= static_cast<uint32_t>(ptr & 0xFFFFFFFFU);
  814. rand_state ^= static_cast<uint32_t>((ptr >> 32) & 0xFFFFFFFFU);
  815. }
  816. rand_state ^= static_cast<uint32_t>(fh.length * 9973.0F);
  817. rand_state ^= static_cast<uint32_t>(fh.thickness * 6151.0F);
  818. rand_state ^= static_cast<uint32_t>(fh.coverage * 4099.0F);
  819. auto random01 = [&]() -> float {
  820. rand_state = rand_state * 1664525U + 1013904223U;
  821. return hash_01(rand_state);
  822. };
  823. auto jitter = [&](float amplitude) -> float {
  824. return (random01() - 0.5F) * 2.0F * amplitude;
  825. };
  826. float const beard_forward_tilt_norm = 0.32F;
  827. auto place_strands = [&](int rows, int segments, float jaw_span,
  828. float row_spacing_norm, float base_length_norm,
  829. float length_variation, float forward_bias_norm,
  830. float base_radius_norm) {
  831. if (rows <= 0 || segments <= 0) {
  832. return;
  833. }
  834. const float phi_half_range = std::max(0.35F, jaw_span * 0.5F);
  835. const float base_y_norm = chin_norm + 0.10F;
  836. for (int row = 0; row < rows; ++row) {
  837. float const row_t = (rows > 1) ? float(row) / float(rows - 1) : 0.0F;
  838. float const target_y_norm =
  839. std::clamp(base_y_norm + row_t * row_spacing_norm, -0.92F, 0.10F);
  840. float const plane_radius =
  841. std::sqrt(std::max(0.02F, 1.0F - target_y_norm * target_y_norm));
  842. for (int seg = 0; seg < segments; ++seg) {
  843. float const seg_t =
  844. (segments > 1) ? float(seg) / float(segments - 1) : 0.5F;
  845. float const base_phi = (seg_t - 0.5F) * jaw_span;
  846. float const phi = std::clamp(base_phi + jitter(0.25F), -phi_half_range,
  847. phi_half_range);
  848. float const coverage_falloff =
  849. 1.0F - std::abs(phi) / std::max(0.001F, phi_half_range);
  850. float const keep_prob = std::clamp(
  851. fh.coverage * (0.75F + coverage_falloff * 0.35F), 0.05F, 1.0F);
  852. if (random01() > keep_prob) {
  853. continue;
  854. }
  855. float const wrap_scale = 0.80F + (1.0F - row_t) * 0.20F;
  856. float lateral_norm = plane_radius * std::sin(phi) * wrap_scale;
  857. float forward_norm = plane_radius * std::cos(phi);
  858. lateral_norm += jitter(0.06F);
  859. forward_norm += jitter(0.08F);
  860. float const y_norm = target_y_norm + jitter(0.05F);
  861. QVector3D surface_dir(lateral_norm, y_norm,
  862. forward_norm *
  863. (0.75F + forward_bias_norm * 0.45F) +
  864. (forward_bias_norm - 0.05F));
  865. float const dir_len = surface_dir.length();
  866. if (dir_len < 1e-4F) {
  867. continue;
  868. }
  869. surface_dir /= dir_len;
  870. float const shell = 1.02F + jitter(0.03F);
  871. QVector3D const root = frame_local_position(frame, surface_dir * shell);
  872. QVector3D local_dir(jitter(0.15F),
  873. -(0.55F + row_t * 0.30F) + jitter(0.10F),
  874. forward_bias_norm + beard_forward_tilt_norm +
  875. row_t * 0.20F + jitter(0.12F));
  876. QVector3D strand_dir =
  877. frame.right * local_dir.x() + frame.up * local_dir.y() +
  878. frame.forward * local_dir.z() - surface_dir * 0.25F;
  879. if (strand_dir.lengthSquared() < 1e-6F) {
  880. continue;
  881. }
  882. strand_dir.normalize();
  883. float const strand_length = base_length_norm * fh.length *
  884. (1.0F + length_variation * jitter(0.5F)) *
  885. (1.0F + row_t * 0.25F);
  886. if (strand_length < 0.05F) {
  887. continue;
  888. }
  889. QVector3D const tip = root + strand_dir * (head_r * strand_length);
  890. float const base_radius =
  891. std::max(head_r * base_radius_norm * fh.thickness *
  892. (0.7F + coverage_falloff * 0.35F),
  893. head_r * 0.010F);
  894. float const mid_radius = base_radius * 0.55F;
  895. float const color_jitter = 0.85F + random01() * 0.30F;
  896. QVector3D const root_color = saturate(hair_root * color_jitter);
  897. QVector3D const tip_color = saturate(hair_tip * color_jitter);
  898. QMatrix4x4 base_blob = sphere_at(ctx.model, root, base_radius * 0.95F);
  899. out.mesh(get_unit_sphere(), base_blob, root_color, nullptr, 1.0F);
  900. QVector3D const mid = root + (tip - root) * 0.40F;
  901. out.mesh(get_unit_cylinder(),
  902. cylinder_between(ctx.model, root, mid, base_radius),
  903. root_color, nullptr, 1.0F);
  904. out.mesh(get_unit_cone(), cone_from_to(ctx.model, mid, tip, mid_radius),
  905. tip_color, nullptr, 1.0F);
  906. }
  907. }
  908. };
  909. auto place_mustache = [&](int segments, float base_length_norm,
  910. float upward_bias_norm) {
  911. if (segments <= 0) {
  912. return;
  913. }
  914. float const mustache_y_norm = mouth_norm + upward_bias_norm - 0.04F;
  915. float const phi_half_range = 0.55F;
  916. for (int side = -1; side <= 1; side += 2) {
  917. for (int seg = 0; seg < segments; ++seg) {
  918. float const t =
  919. (segments > 1) ? float(seg) / float(segments - 1) : 0.5F;
  920. float const base_phi = (t - 0.5F) * (phi_half_range * 2.0F);
  921. float const phi = std::clamp(base_phi + jitter(0.18F), -phi_half_range,
  922. phi_half_range);
  923. float const plane_radius = std::sqrt(
  924. std::max(0.02F, 1.0F - mustache_y_norm * mustache_y_norm));
  925. float lateral_norm = plane_radius * std::sin(phi);
  926. float forward_norm = plane_radius * std::cos(phi);
  927. lateral_norm += jitter(0.04F);
  928. forward_norm += jitter(0.05F);
  929. if (random01() > fh.coverage) {
  930. continue;
  931. }
  932. QVector3D surface_dir(lateral_norm, mustache_y_norm + jitter(0.03F),
  933. forward_norm * 0.85F + 0.20F);
  934. float const dir_len = surface_dir.length();
  935. if (dir_len < 1e-4F) {
  936. continue;
  937. }
  938. surface_dir /= dir_len;
  939. QVector3D const root =
  940. frame_local_position(frame, surface_dir * (1.01F + jitter(0.02F)));
  941. QVector3D const dir_local(side * (0.55F + jitter(0.12F)), jitter(0.06F),
  942. 0.34F + jitter(0.08F));
  943. QVector3D strand_dir =
  944. frame.right * dir_local.x() + frame.up * dir_local.y() +
  945. frame.forward * dir_local.z() - surface_dir * 0.20F;
  946. if (strand_dir.lengthSquared() < 1e-6F) {
  947. continue;
  948. }
  949. strand_dir.normalize();
  950. float const strand_length =
  951. base_length_norm * fh.length * (1.0F + jitter(0.35F));
  952. QVector3D const tip = root + strand_dir * (head_r * strand_length);
  953. float const base_radius =
  954. std::max(head_r * 0.028F * fh.thickness, head_r * 0.0065F);
  955. float const mid_radius = base_radius * 0.45F;
  956. float const color_jitter = 0.92F + random01() * 0.18F;
  957. QVector3D const root_color =
  958. saturate(hair_root * (color_jitter * 0.95F));
  959. QVector3D const tip_color = saturate(hair_tip * (color_jitter * 1.02F));
  960. out.mesh(get_unit_sphere(),
  961. sphere_at(ctx.model, root, base_radius * 0.7F), root_color,
  962. nullptr, 1.0F);
  963. QVector3D const mid = root + (tip - root) * 0.5F;
  964. out.mesh(get_unit_cylinder(),
  965. cylinder_between(ctx.model, root, mid, base_radius * 0.85F),
  966. root_color, nullptr, 1.0F);
  967. out.mesh(get_unit_cone(), cone_from_to(ctx.model, mid, tip, mid_radius),
  968. tip_color, nullptr, 1.0F);
  969. }
  970. }
  971. };
  972. switch (fh.style) {
  973. case FacialHairStyle::Stubble: {
  974. place_strands(1, 11, 2.0F, 0.12F, 0.28F, 0.30F, 0.08F, 0.035F);
  975. break;
  976. }
  977. case FacialHairStyle::ShortBeard: {
  978. place_strands(3, 14, 2.6F, 0.18F, 0.58F, 0.38F, 0.12F, 0.055F);
  979. break;
  980. }
  981. case FacialHairStyle::FullBeard:
  982. case FacialHairStyle::LongBeard: {
  983. bool const is_long = (fh.style == FacialHairStyle::LongBeard);
  984. if (is_long) {
  985. place_strands(5, 20, 3.0F, 0.24F, 1.00F, 0.48F, 0.18F, 0.060F);
  986. } else {
  987. place_strands(4, 18, 2.8F, 0.22F, 0.85F, 0.42F, 0.16F, 0.058F);
  988. }
  989. break;
  990. }
  991. case FacialHairStyle::Goatee: {
  992. place_strands(2, 8, 1.8F, 0.16F, 0.70F, 0.34F, 0.14F, 0.055F);
  993. break;
  994. }
  995. case FacialHairStyle::Mustache: {
  996. place_mustache(5, 0.32F, 0.05F);
  997. break;
  998. }
  999. case FacialHairStyle::MustacheAndBeard: {
  1000. FacialHairParams mustache_only = fh;
  1001. mustache_only.style = FacialHairStyle::Mustache;
  1002. FacialHairParams beard_only = fh;
  1003. beard_only.style = FacialHairStyle::ShortBeard;
  1004. HumanoidVariant v_mustache = v;
  1005. v_mustache.facial_hair = mustache_only;
  1006. draw_facial_hair(ctx, v_mustache, pose, out);
  1007. HumanoidVariant v_beard = v;
  1008. v_beard.facial_hair = beard_only;
  1009. draw_facial_hair(ctx, v_beard, pose, out);
  1010. break;
  1011. }
  1012. case FacialHairStyle::None:
  1013. default:
  1014. break;
  1015. }
  1016. }
  1017. void HumanoidRendererBase::draw_simplified_body(const DrawContext &ctx,
  1018. const HumanoidVariant &v,
  1019. HumanoidPose &pose,
  1020. ISubmitter &out) const {
  1021. using HP = HumanProportions;
  1022. QVector3D const scaling = get_proportion_scaling();
  1023. float const width_scale = scaling.x();
  1024. float const height_scale = scaling.y();
  1025. float const torso_scale = get_torso_scale();
  1026. QVector3D right_axis = pose.shoulder_r - pose.shoulder_l;
  1027. right_axis.setY(0.0F);
  1028. right_axis.setZ(0.0F);
  1029. if (right_axis.lengthSquared() < HP::EPSILON_VECTOR) {
  1030. right_axis = QVector3D(1.0F, 0.0F, 0.0F);
  1031. }
  1032. right_axis.normalize();
  1033. if (right_axis.x() < 0.0F) {
  1034. right_axis = -right_axis;
  1035. }
  1036. QVector3D const up_axis(0.0F, 1.0F, 0.0F);
  1037. QVector3D forward_axis = QVector3D::crossProduct(right_axis, up_axis);
  1038. if (forward_axis.lengthSquared() < HP::EPSILON_VECTOR) {
  1039. forward_axis = QVector3D(0.0F, 0.0F, 1.0F);
  1040. }
  1041. forward_axis.normalize();
  1042. QVector3D const shoulder_mid = (pose.shoulder_l + pose.shoulder_r) * 0.5F;
  1043. const float y_shoulder = shoulder_mid.y();
  1044. const float y_neck = pose.neck_base.y();
  1045. const float shoulder_half_span =
  1046. 0.5F * std::abs(pose.shoulder_r.x() - pose.shoulder_l.x());
  1047. const float torso_r_base =
  1048. std::max(HP::TORSO_TOP_R, shoulder_half_span * 0.95F);
  1049. const float torso_r = torso_r_base * torso_scale;
  1050. float const depth_scale = scaling.z();
  1051. const float torso_depth_factor =
  1052. std::clamp(HP::TORSO_DEPTH_FACTOR_BASE + (depth_scale - 1.0F) * 0.20F,
  1053. HP::TORSO_DEPTH_FACTOR_MIN, HP::TORSO_DEPTH_FACTOR_MAX);
  1054. float torso_depth = torso_r * torso_depth_factor;
  1055. const float y_top_cover =
  1056. std::max(y_shoulder + 0.00F, y_neck + HP::TORSO_TOP_COVER_OFFSET);
  1057. const float upper_arm_r = HP::UPPER_ARM_R * width_scale;
  1058. const float fore_arm_r = HP::FORE_ARM_R * width_scale;
  1059. const float thigh_r = HP::UPPER_LEG_R * width_scale;
  1060. const float shin_r = HP::LOWER_LEG_R * width_scale;
  1061. float const torso_x = (shoulder_mid.x() + pose.pelvis_pos.x()) * 0.5F;
  1062. constexpr float torso_z = 0.0F;
  1063. QVector3D const tunic_top{torso_x, y_top_cover - 0.006F, torso_z};
  1064. QVector3D const tunic_bot{torso_x, pose.pelvis_pos.y() - 0.05F, torso_z};
  1065. QMatrix4x4 torso_transform =
  1066. cylinder_between(ctx.model, tunic_top, tunic_bot, 1.0F);
  1067. torso_transform.scale(torso_r, 1.0F, torso_depth);
  1068. Mesh *torso_mesh = torso_mesh_without_bottom_cap();
  1069. if (torso_mesh != nullptr) {
  1070. out.mesh(torso_mesh, torso_transform, v.palette.cloth, nullptr, 1.0F);
  1071. }
  1072. float const head_r = pose.head_r;
  1073. QMatrix4x4 head_transform = ctx.model;
  1074. head_transform.translate(pose.head_pos);
  1075. head_transform.scale(head_r);
  1076. out.mesh(get_unit_sphere(), head_transform, v.palette.skin, nullptr, 1.0F);
  1077. out.mesh(get_unit_cylinder(),
  1078. cylinder_between(ctx.model, pose.shoulder_l, pose.hand_l,
  1079. (upper_arm_r + fore_arm_r) * 0.5F),
  1080. v.palette.cloth, nullptr, 1.0F);
  1081. out.mesh(get_unit_cylinder(),
  1082. cylinder_between(ctx.model, pose.shoulder_r, pose.hand_r,
  1083. (upper_arm_r + fore_arm_r) * 0.5F),
  1084. v.palette.cloth, nullptr, 1.0F);
  1085. QVector3D const hip_l =
  1086. pose.pelvis_pos +
  1087. QVector3D(-HP::HIP_LATERAL_OFFSET, HP::HIP_VERTICAL_OFFSET, 0.0F);
  1088. QVector3D const hip_r =
  1089. pose.pelvis_pos +
  1090. QVector3D(HP::HIP_LATERAL_OFFSET, HP::HIP_VERTICAL_OFFSET, 0.0F);
  1091. out.mesh(get_unit_cylinder(),
  1092. cylinder_between(ctx.model, hip_l, pose.foot_l,
  1093. (thigh_r + shin_r) * 0.5F),
  1094. v.palette.cloth * 0.92F, nullptr, 1.0F);
  1095. out.mesh(get_unit_cylinder(),
  1096. cylinder_between(ctx.model, hip_r, pose.foot_r,
  1097. (thigh_r + shin_r) * 0.5F),
  1098. v.palette.cloth * 0.92F, nullptr, 1.0F);
  1099. }
  1100. void HumanoidRendererBase::draw_minimal_body(const DrawContext &ctx,
  1101. const HumanoidVariant &v,
  1102. const HumanoidPose &pose,
  1103. ISubmitter &out) const {
  1104. using HP = HumanProportions;
  1105. QVector3D const top = pose.head_pos + QVector3D(0.0F, pose.head_r, 0.0F);
  1106. QVector3D const bot = (pose.foot_l + pose.foot_r) * 0.5F;
  1107. float const body_radius = HP::TORSO_TOP_R * get_torso_scale();
  1108. out.mesh(get_unit_capsule(),
  1109. capsule_between(ctx.model, top, bot, body_radius), v.palette.cloth,
  1110. nullptr, 1.0F);
  1111. }
  1112. void HumanoidRendererBase::render(const DrawContext &ctx,
  1113. ISubmitter &out) const {
  1114. FormationParams const formation = resolve_formation(ctx);
  1115. AnimationInputs const anim = sample_anim_state(ctx);
  1116. Engine::Core::UnitComponent *unit_comp = nullptr;
  1117. if (ctx.entity != nullptr) {
  1118. unit_comp = ctx.entity->get_component<Engine::Core::UnitComponent>();
  1119. }
  1120. Engine::Core::MovementComponent *movement_comp = nullptr;
  1121. Engine::Core::TransformComponent *transform_comp = nullptr;
  1122. if (ctx.entity != nullptr) {
  1123. movement_comp =
  1124. ctx.entity->get_component<Engine::Core::MovementComponent>();
  1125. transform_comp =
  1126. ctx.entity->get_component<Engine::Core::TransformComponent>();
  1127. }
  1128. float entity_ground_offset =
  1129. resolve_entity_ground_offset(ctx, unit_comp, transform_comp);
  1130. uint32_t seed = 0U;
  1131. if (unit_comp != nullptr) {
  1132. seed ^= uint32_t(unit_comp->owner_id * 2654435761U);
  1133. }
  1134. if (ctx.entity != nullptr) {
  1135. seed ^= uint32_t(reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU);
  1136. }
  1137. const int rows =
  1138. (formation.individuals_per_unit + formation.max_per_row - 1) /
  1139. formation.max_per_row;
  1140. const int cols = formation.max_per_row;
  1141. bool is_mounted_spawn = false;
  1142. if (unit_comp != nullptr) {
  1143. using Game::Units::SpawnType;
  1144. auto const st = unit_comp->spawn_type;
  1145. is_mounted_spawn =
  1146. (st == SpawnType::MountedKnight || st == SpawnType::HorseArcher ||
  1147. st == SpawnType::HorseSpearman);
  1148. }
  1149. int visible_count = rows * cols;
  1150. if (unit_comp != nullptr) {
  1151. int const mh = std::max(1, unit_comp->max_health);
  1152. float const ratio = std::clamp(unit_comp->health / float(mh), 0.0F, 1.0F);
  1153. visible_count = std::max(1, (int)std::ceil(ratio * float(rows * cols)));
  1154. }
  1155. HumanoidVariant variant;
  1156. get_variant(ctx, seed, variant);
  1157. if (!m_proportion_scale_cached) {
  1158. m_cached_proportion_scale = get_proportion_scaling();
  1159. m_proportion_scale_cached = true;
  1160. }
  1161. const QVector3D prop_scale = m_cached_proportion_scale;
  1162. const float height_scale = prop_scale.y();
  1163. const bool needs_height_scaling = std::abs(height_scale - 1.0F) > 0.001F;
  1164. const QMatrix4x4 k_identity_matrix;
  1165. const IFormationCalculator *formation_calculator = nullptr;
  1166. {
  1167. using Nation = FormationCalculatorFactory::Nation;
  1168. using UnitCategory = FormationCalculatorFactory::UnitCategory;
  1169. Nation nation = Nation::Roman;
  1170. if (unit_comp != nullptr) {
  1171. if (unit_comp->nation_id == Game::Systems::NationID::Carthage) {
  1172. nation = Nation::Carthage;
  1173. }
  1174. }
  1175. UnitCategory category =
  1176. is_mounted_spawn ? UnitCategory::Cavalry : UnitCategory::Infantry;
  1177. if (unit_comp != nullptr &&
  1178. unit_comp->spawn_type == Game::Units::SpawnType::Builder &&
  1179. anim.is_constructing) {
  1180. category = UnitCategory::BuilderConstruction;
  1181. }
  1182. formation_calculator =
  1183. FormationCalculatorFactory::getCalculator(nation, category);
  1184. }
  1185. auto fast_random = [](uint32_t &state) -> float {
  1186. state = state * 1664525U + 1013904223U;
  1187. return float(state & 0x7FFFFFU) / float(0x7FFFFFU);
  1188. };
  1189. s_render_stats.soldiers_total += visible_count;
  1190. for (int idx = 0; idx < visible_count; ++idx) {
  1191. int const r = idx / cols;
  1192. int const c = idx % cols;
  1193. FormationOffset const formation_offset =
  1194. formation_calculator->calculate_offset(idx, r, c, rows, cols,
  1195. formation.spacing, seed);
  1196. float offset_x = formation_offset.offset_x;
  1197. float offset_z = formation_offset.offset_z;
  1198. uint32_t const inst_seed = seed ^ uint32_t(idx * 9176U);
  1199. uint32_t rng_state = inst_seed;
  1200. float const vertical_jitter = (fast_random(rng_state) - 0.5F) * 0.03F;
  1201. float const yaw_offset = (fast_random(rng_state) - 0.5F) * 5.0F;
  1202. float const phase_offset = fast_random(rng_state) * 0.25F;
  1203. if (anim.is_attacking && anim.is_melee) {
  1204. float const combat_jitter_x =
  1205. (fast_random(rng_state) - 0.5F) * formation.spacing * 0.4F;
  1206. float const combat_jitter_z =
  1207. (fast_random(rng_state) - 0.5F) * formation.spacing * 0.3F;
  1208. float const sway_time = anim.time + phase_offset * 2.0F;
  1209. float const sway_x = std::sin(sway_time * 1.5F) * 0.05F;
  1210. float const sway_z = std::cos(sway_time * 1.2F) * 0.04F;
  1211. offset_x += combat_jitter_x + sway_x;
  1212. offset_z += combat_jitter_z + sway_z;
  1213. }
  1214. float applied_vertical_jitter = vertical_jitter;
  1215. float applied_yaw_offset = yaw_offset;
  1216. if (anim.is_attacking && anim.is_melee) {
  1217. float const combat_yaw = (fast_random(rng_state) - 0.5F) * 15.0F;
  1218. applied_yaw_offset += combat_yaw;
  1219. }
  1220. QMatrix4x4 inst_model;
  1221. float applied_yaw = applied_yaw_offset;
  1222. if (transform_comp != nullptr) {
  1223. applied_yaw = transform_comp->rotation.y + applied_yaw_offset;
  1224. QMatrix4x4 m = k_identity_matrix;
  1225. m.translate(transform_comp->position.x, transform_comp->position.y,
  1226. transform_comp->position.z);
  1227. m.rotate(applied_yaw, 0.0F, 1.0F, 0.0F);
  1228. m.scale(transform_comp->scale.x, transform_comp->scale.y,
  1229. transform_comp->scale.z);
  1230. m.translate(offset_x, applied_vertical_jitter, offset_z);
  1231. if (entity_ground_offset != 0.0F) {
  1232. m.translate(0.0F, -entity_ground_offset, 0.0F);
  1233. }
  1234. inst_model = m;
  1235. } else {
  1236. inst_model = ctx.model;
  1237. inst_model.rotate(applied_yaw, 0.0F, 1.0F, 0.0F);
  1238. inst_model.translate(offset_x, applied_vertical_jitter, offset_z);
  1239. if (entity_ground_offset != 0.0F) {
  1240. inst_model.translate(0.0F, -entity_ground_offset, 0.0F);
  1241. }
  1242. }
  1243. QVector3D const soldier_world_pos =
  1244. inst_model.map(QVector3D(0.0F, 0.0F, 0.0F));
  1245. constexpr float k_soldier_cull_radius = 0.6F;
  1246. if (ctx.camera != nullptr &&
  1247. !ctx.camera->is_in_frustum(soldier_world_pos, k_soldier_cull_radius)) {
  1248. ++s_render_stats.soldiers_skipped_frustum;
  1249. continue;
  1250. }
  1251. HumanoidLOD soldier_lod = HumanoidLOD::Full;
  1252. float soldier_distance = 0.0F;
  1253. if (ctx.camera != nullptr) {
  1254. soldier_distance =
  1255. (soldier_world_pos - ctx.camera->get_position()).length();
  1256. soldier_lod = calculate_humanoid_lod(soldier_distance);
  1257. if (soldier_lod == HumanoidLOD::Billboard) {
  1258. ++s_render_stats.soldiers_skipped_lod;
  1259. continue;
  1260. }
  1261. soldier_lod =
  1262. Render::VisibilityBudgetTracker::instance().request_humanoid_lod(
  1263. soldier_lod);
  1264. }
  1265. if (soldier_distance > 0.0F) {
  1266. if (soldier_lod == HumanoidLOD::Reduced &&
  1267. soldier_distance > k_temporal_skip_distance_reduced) {
  1268. if (!should_render_temporal(s_current_frame, inst_seed,
  1269. k_temporal_skip_period_reduced)) {
  1270. ++s_render_stats.soldiers_skipped_temporal;
  1271. continue;
  1272. }
  1273. }
  1274. if (soldier_lod == HumanoidLOD::Minimal &&
  1275. soldier_distance > k_temporal_skip_distance_minimal) {
  1276. if (!should_render_temporal(s_current_frame, inst_seed,
  1277. k_temporal_skip_period_minimal)) {
  1278. ++s_render_stats.soldiers_skipped_temporal;
  1279. continue;
  1280. }
  1281. }
  1282. }
  1283. ++s_render_stats.soldiers_rendered;
  1284. DrawContext inst_ctx{ctx.resources, ctx.entity, ctx.world, inst_model};
  1285. inst_ctx.selected = ctx.selected;
  1286. inst_ctx.hovered = ctx.hovered;
  1287. inst_ctx.animation_time = ctx.animation_time;
  1288. inst_ctx.renderer_id = ctx.renderer_id;
  1289. inst_ctx.backend = ctx.backend;
  1290. inst_ctx.camera = ctx.camera;
  1291. VariationParams variation = VariationParams::fromSeed(inst_seed);
  1292. adjust_variation(inst_ctx, inst_seed, variation);
  1293. float const combined_height_scale = height_scale * variation.height_scale;
  1294. if (needs_height_scaling ||
  1295. std::abs(variation.height_scale - 1.0F) > 0.001F) {
  1296. QMatrix4x4 scale_matrix;
  1297. scale_matrix.scale(variation.bulk_scale, combined_height_scale, 1.0F);
  1298. inst_ctx.model = inst_ctx.model * scale_matrix;
  1299. }
  1300. HumanoidPose pose;
  1301. bool used_cached_pose = false;
  1302. PoseCacheKey cache_key =
  1303. make_pose_cache_key(reinterpret_cast<uintptr_t>(ctx.entity), idx);
  1304. auto cache_it = s_pose_cache.find(cache_key);
  1305. if (!anim.is_moving && cache_it != s_pose_cache.end()) {
  1306. const CachedPoseEntry &cached = cache_it->second;
  1307. if (!cached.was_moving &&
  1308. s_current_frame - cached.frame_number < k_pose_cache_max_age) {
  1309. pose = cached.pose;
  1310. used_cached_pose = true;
  1311. ++s_render_stats.poses_cached;
  1312. }
  1313. }
  1314. if (!used_cached_pose) {
  1315. compute_locomotion_pose(inst_seed, anim.time + phase_offset,
  1316. anim.is_moving, variation, pose);
  1317. ++s_render_stats.poses_computed;
  1318. CachedPoseEntry &entry = s_pose_cache[cache_key];
  1319. entry.pose = pose;
  1320. entry.variation = variation;
  1321. entry.frame_number = s_current_frame;
  1322. entry.was_moving = anim.is_moving;
  1323. }
  1324. HumanoidAnimationContext anim_ctx{};
  1325. anim_ctx.inputs = anim;
  1326. anim_ctx.variation = variation;
  1327. anim_ctx.formation = formation;
  1328. anim_ctx.jitter_seed = phase_offset;
  1329. anim_ctx.instance_position =
  1330. inst_ctx.model.map(QVector3D(0.0F, 0.0F, 0.0F));
  1331. float yaw_rad = qDegreesToRadians(applied_yaw);
  1332. QVector3D forward(std::sin(yaw_rad), 0.0F, std::cos(yaw_rad));
  1333. if (forward.lengthSquared() > 1e-8F) {
  1334. forward.normalize();
  1335. } else {
  1336. forward = QVector3D(0.0F, 0.0F, 1.0F);
  1337. }
  1338. QVector3D up(0.0F, 1.0F, 0.0F);
  1339. QVector3D right = QVector3D::crossProduct(up, forward);
  1340. if (right.lengthSquared() > 1e-8F) {
  1341. right.normalize();
  1342. } else {
  1343. right = QVector3D(1.0F, 0.0F, 0.0F);
  1344. }
  1345. anim_ctx.entity_forward = forward;
  1346. anim_ctx.entity_right = right;
  1347. anim_ctx.entity_up = up;
  1348. anim_ctx.locomotion_direction = forward;
  1349. anim_ctx.yaw_degrees = applied_yaw;
  1350. anim_ctx.yaw_radians = yaw_rad;
  1351. anim_ctx.has_movement_target = false;
  1352. anim_ctx.move_speed = 0.0F;
  1353. anim_ctx.movement_target = QVector3D(0.0F, 0.0F, 0.0F);
  1354. if (movement_comp != nullptr) {
  1355. QVector3D velocity(movement_comp->vx, 0.0F, movement_comp->vz);
  1356. float speed = velocity.length();
  1357. anim_ctx.move_speed = speed;
  1358. if (speed > 1e-4F) {
  1359. anim_ctx.locomotion_direction = velocity.normalized();
  1360. }
  1361. anim_ctx.has_movement_target = movement_comp->has_target;
  1362. anim_ctx.movement_target =
  1363. QVector3D(movement_comp->target_x, 0.0F, movement_comp->target_y);
  1364. }
  1365. anim_ctx.locomotion_velocity =
  1366. anim_ctx.locomotion_direction * anim_ctx.move_speed;
  1367. anim_ctx.motion_state = classify_motion_state(anim, anim_ctx.move_speed);
  1368. anim_ctx.gait.state = anim_ctx.motion_state;
  1369. anim_ctx.gait.speed = anim_ctx.move_speed;
  1370. anim_ctx.gait.velocity = anim_ctx.locomotion_velocity;
  1371. anim_ctx.gait.has_target = anim_ctx.has_movement_target;
  1372. anim_ctx.gait.is_airborne = false;
  1373. float reference_speed = (anim_ctx.gait.state == HumanoidMotionState::Run)
  1374. ? k_reference_run_speed
  1375. : k_reference_walk_speed;
  1376. if (reference_speed > 0.0001F) {
  1377. anim_ctx.gait.normalized_speed =
  1378. std::clamp(anim_ctx.gait.speed / reference_speed, 0.0F, 1.0F);
  1379. } else {
  1380. anim_ctx.gait.normalized_speed = 0.0F;
  1381. }
  1382. if (!anim.is_moving) {
  1383. anim_ctx.gait.normalized_speed = 0.0F;
  1384. }
  1385. if (anim.is_moving) {
  1386. float stride_base = 0.8F;
  1387. if (anim_ctx.motion_state == HumanoidMotionState::Run) {
  1388. stride_base = 0.45F;
  1389. }
  1390. float const base_cycle =
  1391. stride_base / std::max(0.1F, variation.walk_speed_mult);
  1392. anim_ctx.locomotion_cycle_time = base_cycle;
  1393. anim_ctx.locomotion_phase = std::fmod(
  1394. (anim.time + phase_offset) / std::max(0.001F, base_cycle), 1.0F);
  1395. anim_ctx.gait.cycle_time = anim_ctx.locomotion_cycle_time;
  1396. anim_ctx.gait.cycle_phase = anim_ctx.locomotion_phase;
  1397. anim_ctx.gait.stride_distance =
  1398. anim_ctx.gait.speed * anim_ctx.gait.cycle_time;
  1399. } else {
  1400. anim_ctx.locomotion_cycle_time = 0.0F;
  1401. anim_ctx.locomotion_phase = 0.0F;
  1402. anim_ctx.gait.cycle_time = 0.0F;
  1403. anim_ctx.gait.cycle_phase = 0.0F;
  1404. anim_ctx.gait.stride_distance = 0.0F;
  1405. }
  1406. if (anim.is_attacking) {
  1407. float const attack_offset = phase_offset * 1.5F;
  1408. anim_ctx.attack_phase = std::fmod(anim.time + attack_offset, 1.0F);
  1409. anim_ctx.inputs.attack_variant = static_cast<std::uint8_t>(inst_seed % 3);
  1410. }
  1411. customize_pose(inst_ctx, anim_ctx, inst_seed, pose);
  1412. if (!anim.is_moving && !anim.is_attacking) {
  1413. HumanoidPoseController pose_ctrl(pose, anim_ctx);
  1414. pose_ctrl.apply_micro_idle(anim.time + phase_offset, inst_seed);
  1415. if (visible_count > 0) {
  1416. auto hash_u32 = [](std::uint32_t x) -> std::uint32_t {
  1417. x ^= x >> 16;
  1418. x *= 0x7feb352dU;
  1419. x ^= x >> 15;
  1420. x *= 0x846ca68bU;
  1421. x ^= x >> 16;
  1422. return x;
  1423. };
  1424. constexpr float kAmbientAnimDuration = 6.0F;
  1425. constexpr float kUnitCycleBase = 15.0F;
  1426. constexpr float kUnitCycleRange = 10.0F;
  1427. float const unit_cycle_period =
  1428. kUnitCycleBase +
  1429. static_cast<float>(seed % 1000U) / (1000.0F / kUnitCycleRange);
  1430. float const unit_time_in_cycle =
  1431. std::fmod(anim.time, std::max(0.001F, unit_cycle_period));
  1432. std::uint32_t const unit_cycle_number = static_cast<std::uint32_t>(
  1433. anim.time / std::max(0.001F, unit_cycle_period));
  1434. int const max_slots = std::min(2, visible_count);
  1435. std::uint32_t const cycle_rng =
  1436. hash_u32(seed ^ (unit_cycle_number * 0x9e3779b9U));
  1437. int const slot_count =
  1438. (max_slots <= 0)
  1439. ? 0
  1440. : (1 + static_cast<int>(cycle_rng %
  1441. static_cast<std::uint32_t>(max_slots)));
  1442. int const idx_a =
  1443. static_cast<int>(hash_u32(cycle_rng ^ 0xA341316CU) %
  1444. static_cast<std::uint32_t>(visible_count));
  1445. int idx_b = static_cast<int>(hash_u32(cycle_rng ^ 0xC8013EA4U) %
  1446. static_cast<std::uint32_t>(visible_count));
  1447. if (visible_count > 1 && idx_b == idx_a) {
  1448. idx_b = (idx_a + 1 +
  1449. static_cast<int>(cycle_rng % static_cast<std::uint32_t>(
  1450. visible_count - 1))) %
  1451. visible_count;
  1452. }
  1453. auto pick_idle_type = [&](std::uint32_t v) -> AmbientIdleType {
  1454. std::uint32_t const roll = v % 100U;
  1455. if (roll < 18U) {
  1456. return AmbientIdleType::SitDown;
  1457. }
  1458. if (roll < 36U) {
  1459. return AmbientIdleType::ShiftWeight;
  1460. }
  1461. if (roll < 52U) {
  1462. return AmbientIdleType::ShuffleFeet;
  1463. }
  1464. if (roll < 66U) {
  1465. return AmbientIdleType::TapFoot;
  1466. }
  1467. if (roll < 78U) {
  1468. return AmbientIdleType::StepInPlace;
  1469. }
  1470. if (roll < 90U) {
  1471. return AmbientIdleType::BendKnee;
  1472. }
  1473. if (roll < 98U) {
  1474. return AmbientIdleType::RaiseWeapon;
  1475. }
  1476. return AmbientIdleType::Jump;
  1477. };
  1478. AmbientIdleType const unit_idle_type =
  1479. pick_idle_type(hash_u32(cycle_rng ^ 0x3C6EF372U));
  1480. if (slot_count > 0 && unit_time_in_cycle <= kAmbientAnimDuration) {
  1481. bool const is_active =
  1482. (idx == idx_a) || (slot_count > 1 && idx == idx_b);
  1483. if (is_active) {
  1484. float const phase = unit_time_in_cycle / kAmbientAnimDuration;
  1485. pose_ctrl.apply_ambient_idle_explicit(unit_idle_type, phase);
  1486. }
  1487. }
  1488. }
  1489. }
  1490. if (anim_ctx.motion_state == HumanoidMotionState::Run) {
  1491. float const run_lean = 0.10F;
  1492. pose.pelvis_pos.setZ(pose.pelvis_pos.z() - 0.05F);
  1493. pose.shoulder_l.setZ(pose.shoulder_l.z() + run_lean);
  1494. pose.shoulder_r.setZ(pose.shoulder_r.z() + run_lean);
  1495. pose.neck_base.setZ(pose.neck_base.z() + run_lean * 0.7F);
  1496. pose.head_pos.setZ(pose.head_pos.z() + run_lean * 0.5F);
  1497. pose.pelvis_pos.setY(pose.pelvis_pos.y() - 0.03F);
  1498. pose.shoulder_l.setY(pose.shoulder_l.y() - 0.04F);
  1499. pose.shoulder_r.setY(pose.shoulder_r.y() - 0.04F);
  1500. float const run_stride_mult = 1.5F;
  1501. float const phase = anim_ctx.locomotion_phase;
  1502. float const left_phase = phase;
  1503. float const right_phase = std::fmod(phase + 0.5F, 1.0F);
  1504. auto enhance_run_foot = [&](QVector3D &foot, float foot_phase) {
  1505. float const lift_raw =
  1506. std::sin(foot_phase * 2.0F * std::numbers::pi_v<float>);
  1507. if (lift_raw > 0.0F) {
  1508. float const extra_lift = lift_raw * k_run_extra_foot_lift;
  1509. foot.setY(foot.y() + extra_lift);
  1510. float const stride_enhance = std::sin((foot_phase - 0.25F) * 2.0F *
  1511. std::numbers::pi_v<float>) *
  1512. k_run_stride_enhancement;
  1513. foot.setZ(foot.z() + stride_enhance);
  1514. }
  1515. };
  1516. enhance_run_foot(pose.foot_l, left_phase);
  1517. enhance_run_foot(pose.foot_r, right_phase);
  1518. float const run_arm_swing = 0.06F;
  1519. constexpr float max_run_arm_displacement = 0.08F;
  1520. float const left_swing_raw =
  1521. std::sin((left_phase + 0.1F) * 2.0F * std::numbers::pi_v<float>);
  1522. float const left_arm_phase =
  1523. std::clamp(left_swing_raw * run_arm_swing, -max_run_arm_displacement,
  1524. max_run_arm_displacement);
  1525. float const right_swing_raw =
  1526. std::sin((right_phase + 0.1F) * 2.0F * std::numbers::pi_v<float>);
  1527. float const right_arm_phase =
  1528. std::clamp(right_swing_raw * run_arm_swing, -max_run_arm_displacement,
  1529. max_run_arm_displacement);
  1530. pose.hand_l.setZ(pose.hand_l.z() - left_arm_phase);
  1531. pose.hand_r.setZ(pose.hand_r.z() - right_arm_phase);
  1532. pose.hand_l.setY(pose.hand_l.y() + 0.02F);
  1533. pose.hand_r.setY(pose.hand_r.y() + 0.02F);
  1534. {
  1535. using HP = HumanProportions;
  1536. float const h_scale = variation.height_scale;
  1537. float const max_reach =
  1538. (HP::UPPER_ARM_LEN + HP::FORE_ARM_LEN) * h_scale * 0.98F;
  1539. auto clamp_hand = [&](const QVector3D &shoulder, QVector3D &hand) {
  1540. QVector3D diff = hand - shoulder;
  1541. float const len = diff.length();
  1542. if (len > max_reach && len > 1e-6F) {
  1543. hand = shoulder + diff * (max_reach / len);
  1544. }
  1545. };
  1546. clamp_hand(pose.shoulder_l, pose.hand_l);
  1547. clamp_hand(pose.shoulder_r, pose.hand_r);
  1548. QVector3D right_axis = pose.shoulder_r - pose.shoulder_l;
  1549. right_axis.setY(0.0F);
  1550. if (right_axis.lengthSquared() < 1e-8F) {
  1551. right_axis = QVector3D(1.0F, 0.0F, 0.0F);
  1552. }
  1553. right_axis.normalize();
  1554. if (right_axis.x() < 0.0F) {
  1555. right_axis = -right_axis;
  1556. }
  1557. QVector3D const outward_l = -right_axis;
  1558. QVector3D const outward_r = right_axis;
  1559. pose.elbow_l = elbow_bend_torso(pose.shoulder_l, pose.hand_l, outward_l,
  1560. 0.45F, 0.15F, -0.08F, +1.0F);
  1561. pose.elbow_r = elbow_bend_torso(pose.shoulder_r, pose.hand_r, outward_r,
  1562. 0.48F, 0.12F, 0.02F, +1.0F);
  1563. }
  1564. float const hip_rotation_raw =
  1565. std::sin(phase * 2.0F * std::numbers::pi_v<float>);
  1566. float const hip_rotation = hip_rotation_raw * 0.003F;
  1567. pose.pelvis_pos.setX(pose.pelvis_pos.x() + hip_rotation);
  1568. float const torso_sway_z = 0.0F;
  1569. pose.shoulder_l.setZ(pose.shoulder_l.z() + torso_sway_z);
  1570. pose.shoulder_r.setZ(pose.shoulder_r.z() + torso_sway_z);
  1571. pose.neck_base.setZ(pose.neck_base.z() + torso_sway_z * 0.7F);
  1572. pose.head_pos.setZ(pose.head_pos.z() + torso_sway_z * 0.5F);
  1573. if (pose.head_frame.radius > 0.001F) {
  1574. QVector3D head_up = pose.head_pos - pose.neck_base;
  1575. if (head_up.lengthSquared() < 1e-8F) {
  1576. head_up = pose.head_frame.up;
  1577. } else {
  1578. head_up.normalize();
  1579. }
  1580. QVector3D head_right =
  1581. pose.head_frame.right -
  1582. head_up * QVector3D::dotProduct(pose.head_frame.right, head_up);
  1583. if (head_right.lengthSquared() < 1e-8F) {
  1584. head_right =
  1585. QVector3D::crossProduct(head_up, anim_ctx.entity_forward);
  1586. if (head_right.lengthSquared() < 1e-8F) {
  1587. head_right = QVector3D(1.0F, 0.0F, 0.0F);
  1588. }
  1589. }
  1590. head_right.normalize();
  1591. QVector3D head_forward =
  1592. QVector3D::crossProduct(head_right, head_up).normalized();
  1593. pose.head_frame.origin = pose.head_pos;
  1594. pose.head_frame.up = head_up;
  1595. pose.head_frame.right = head_right;
  1596. pose.head_frame.forward = head_forward;
  1597. pose.body_frames.head = pose.head_frame;
  1598. }
  1599. }
  1600. const auto &gfx_settings = Render::GraphicsSettings::instance();
  1601. const bool should_render_shadow =
  1602. gfx_settings.shadows_enabled() &&
  1603. (soldier_lod == HumanoidLOD::Full ||
  1604. soldier_lod == HumanoidLOD::Reduced) &&
  1605. soldier_distance < gfx_settings.shadow_max_distance();
  1606. if (should_render_shadow && inst_ctx.backend != nullptr &&
  1607. inst_ctx.resources != nullptr) {
  1608. auto *shadow_shader =
  1609. inst_ctx.backend->shader(QStringLiteral("troop_shadow"));
  1610. auto *quad_mesh = inst_ctx.resources->quad();
  1611. if (shadow_shader != nullptr && quad_mesh != nullptr) {
  1612. float const shadow_size =
  1613. is_mounted_spawn ? k_shadow_size_mounted : k_shadow_size_infantry;
  1614. float depth_boost = 1.0F;
  1615. float width_boost = 1.0F;
  1616. if (unit_comp != nullptr) {
  1617. using Game::Units::SpawnType;
  1618. switch (unit_comp->spawn_type) {
  1619. case SpawnType::Spearman:
  1620. depth_boost = 1.8F;
  1621. width_boost = 0.95F;
  1622. break;
  1623. case SpawnType::HorseSpearman:
  1624. depth_boost = 2.1F;
  1625. width_boost = 1.05F;
  1626. break;
  1627. case SpawnType::Archer:
  1628. case SpawnType::HorseArcher:
  1629. depth_boost = 1.2F;
  1630. width_boost = 0.95F;
  1631. break;
  1632. default:
  1633. break;
  1634. }
  1635. }
  1636. float const shadow_width =
  1637. shadow_size * (is_mounted_spawn ? 1.05F : 1.0F) * width_boost;
  1638. float const shadow_depth =
  1639. shadow_size * (is_mounted_spawn ? 1.30F : 1.10F) * depth_boost;
  1640. auto &terrain_service = Game::Map::TerrainService::instance();
  1641. if (terrain_service.is_initialized()) {
  1642. QVector3D const inst_pos =
  1643. inst_ctx.model.map(QVector3D(0.0F, 0.0F, 0.0F));
  1644. float const shadow_y =
  1645. terrain_service.get_terrain_height(inst_pos.x(), inst_pos.z());
  1646. QVector3D light_dir = k_shadow_light_dir.normalized();
  1647. QVector2D light_dir_xz(light_dir.x(), light_dir.z());
  1648. if (light_dir_xz.lengthSquared() < 1e-6F) {
  1649. light_dir_xz = QVector2D(0.0F, 1.0F);
  1650. } else {
  1651. light_dir_xz.normalize();
  1652. }
  1653. QVector2D const shadow_dir = -light_dir_xz;
  1654. QVector2D dir_for_use = shadow_dir;
  1655. if (dir_for_use.lengthSquared() < 1e-6F) {
  1656. dir_for_use = QVector2D(0.0F, 1.0F);
  1657. } else {
  1658. dir_for_use.normalize();
  1659. }
  1660. float const shadow_offset = shadow_depth * 1.25F;
  1661. QVector2D const offset_2d = dir_for_use * shadow_offset;
  1662. float const light_yaw_deg = qRadiansToDegrees(
  1663. std::atan2(double(dir_for_use.x()), double(dir_for_use.y())));
  1664. QMatrix4x4 shadow_model;
  1665. shadow_model.translate(inst_pos.x() + offset_2d.x(),
  1666. shadow_y + k_shadow_ground_offset,
  1667. inst_pos.z() + offset_2d.y());
  1668. shadow_model.rotate(light_yaw_deg, 0.0F, 1.0F, 0.0F);
  1669. shadow_model.rotate(-90.0F, 1.0F, 0.0F, 0.0F);
  1670. shadow_model.scale(shadow_width, shadow_depth, 1.0F);
  1671. if (auto *renderer = dynamic_cast<Renderer *>(&out)) {
  1672. Shader *previous_shader = renderer->get_current_shader();
  1673. renderer->set_current_shader(shadow_shader);
  1674. shadow_shader->set_uniform(QStringLiteral("u_lightDir"),
  1675. dir_for_use);
  1676. out.mesh(quad_mesh, shadow_model, QVector3D(0.0F, 0.0F, 0.0F),
  1677. nullptr, k_shadow_base_alpha, 0);
  1678. renderer->set_current_shader(previous_shader);
  1679. }
  1680. }
  1681. }
  1682. }
  1683. switch (soldier_lod) {
  1684. case HumanoidLOD::Full:
  1685. ++s_render_stats.lod_full;
  1686. draw_common_body(inst_ctx, variant, pose, out);
  1687. draw_facial_hair(inst_ctx, variant, pose, out);
  1688. draw_armor(inst_ctx, variant, pose, anim_ctx, out);
  1689. add_attachments(inst_ctx, variant, pose, anim_ctx, out);
  1690. break;
  1691. case HumanoidLOD::Reduced:
  1692. ++s_render_stats.lod_reduced;
  1693. draw_simplified_body(inst_ctx, variant, pose, out);
  1694. draw_armor(inst_ctx, variant, pose, anim_ctx, out);
  1695. add_attachments(inst_ctx, variant, pose, anim_ctx, out);
  1696. break;
  1697. case HumanoidLOD::Minimal:
  1698. ++s_render_stats.lod_minimal;
  1699. draw_minimal_body(inst_ctx, variant, pose, out);
  1700. break;
  1701. case HumanoidLOD::Billboard:
  1702. break;
  1703. }
  1704. }
  1705. }
  1706. auto get_humanoid_render_stats() -> const HumanoidRenderStats & {
  1707. return s_render_stats;
  1708. }
  1709. void reset_humanoid_render_stats() { s_render_stats.reset(); }
  1710. } // namespace Render::GL