healer_renderer.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. #include "healer_renderer.h"
  2. #include "../../../../game/core/component.h"
  3. #include "../../../../game/core/entity.h"
  4. #include "../../../../game/systems/nation_id.h"
  5. #include "../../../equipment/equipment_registry.h"
  6. #include "../../../geom/math_utils.h"
  7. #include "../../../geom/transforms.h"
  8. #include "../../../gl/backend.h"
  9. #include "../../../gl/primitives.h"
  10. #include "../../../gl/render_constants.h"
  11. #include "../../../gl/shader.h"
  12. #include "../../../humanoid/humanoid_math.h"
  13. #include "../../../humanoid/humanoid_specs.h"
  14. #include "../../../humanoid/pose_controller.h"
  15. #include "../../../humanoid/rig.h"
  16. #include "../../../humanoid/style_palette.h"
  17. #include "../../../palette.h"
  18. #include "../../../scene_renderer.h"
  19. #include "../../../submitter.h"
  20. #include "../../registry.h"
  21. #include "../../renderer_constants.h"
  22. #include "healer_style.h"
  23. #include <QMatrix4x4>
  24. #include <QString>
  25. #include <QVector3D>
  26. #include <cmath>
  27. #include <cstdint>
  28. #include <numbers>
  29. #include <optional>
  30. #include <qmatrix4x4.h>
  31. #include <qstringliteral.h>
  32. #include <qvectornd.h>
  33. #include <string>
  34. #include <string_view>
  35. #include <unordered_map>
  36. using Render::Geom::cylinder_between;
  37. using Render::Geom::sphere_at;
  38. namespace Render::GL::Roman {
  39. namespace {
  40. constexpr std::string_view k_default_style_key = "default";
  41. auto style_registry() -> std::unordered_map<std::string, HealerStyleConfig> & {
  42. static std::unordered_map<std::string, HealerStyleConfig> styles;
  43. return styles;
  44. }
  45. void ensure_healer_styles_registered() {
  46. static const bool registered = []() {
  47. register_roman_healer_style();
  48. return true;
  49. }();
  50. (void)registered;
  51. }
  52. constexpr float k_team_mix_weight = 0.65F;
  53. constexpr float k_style_mix_weight = 0.35F;
  54. } // namespace
  55. void register_healer_style(const std::string &nation_id,
  56. const HealerStyleConfig &style) {
  57. style_registry()[nation_id] = style;
  58. }
  59. using Render::Geom::clamp01;
  60. using Render::Geom::clamp_f;
  61. using Render::GL::Humanoid::mix_palette_color;
  62. using Render::GL::Humanoid::saturate_color;
  63. class HealerRenderer : public HumanoidRendererBase {
  64. public:
  65. auto get_proportion_scaling() const -> QVector3D override {
  66. return {0.86F, 0.99F, 0.90F};
  67. }
  68. void get_variant(const DrawContext &ctx, uint32_t seed,
  69. HumanoidVariant &v) const override {
  70. QVector3D const team_tint = resolve_team_tint(ctx);
  71. v.palette = make_humanoid_palette(team_tint, seed);
  72. auto const &style = resolve_style(ctx);
  73. apply_palette_overrides(style, team_tint, v);
  74. }
  75. void customize_pose(const DrawContext &,
  76. const HumanoidAnimationContext &anim_ctx, uint32_t seed,
  77. HumanoidPose &pose) const override {
  78. using HP = HumanProportions;
  79. const AnimationInputs &anim = anim_ctx.inputs;
  80. HumanoidPoseController controller(pose, anim_ctx);
  81. float const arm_height_jitter = (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.03F;
  82. float const arm_asymmetry = (hash_01(seed ^ 0xDEF0U) - 0.5F) * 0.06F;
  83. if (anim.is_healing) {
  84. float const healing_time = anim.time * 2.5F;
  85. float const sway_phase = std::sin(healing_time);
  86. float const sway_phase_offset = std::sin(healing_time + 0.5F);
  87. float const base_arm_height = HP::SHOULDER_Y - 0.02F + arm_height_jitter;
  88. float const sway_height = 0.03F * sway_phase;
  89. float const target_dist =
  90. std::sqrt(anim.healing_target_dx * anim.healing_target_dx +
  91. anim.healing_target_dz * anim.healing_target_dz);
  92. float target_dir_x = 0.0F;
  93. float target_dir_z = 1.0F;
  94. if (target_dist > 0.01F) {
  95. target_dir_x = anim.healing_target_dx / target_dist;
  96. target_dir_z = anim.healing_target_dz / target_dist;
  97. }
  98. float const arm_spread = 0.18F + 0.02F * sway_phase_offset;
  99. float const forward_reach = 0.22F + 0.03F * std::sin(healing_time * 0.7F);
  100. QVector3D const heal_hand_l(-arm_spread + arm_asymmetry * 0.3F,
  101. base_arm_height + sway_height, forward_reach);
  102. QVector3D const heal_hand_r(arm_spread - arm_asymmetry * 0.3F,
  103. base_arm_height + sway_height + 0.01F,
  104. forward_reach * 0.95F);
  105. controller.place_hand_at(true, heal_hand_l);
  106. controller.place_hand_at(false, heal_hand_r);
  107. QVector3D const look_dir(target_dir_x, 0.0F, target_dir_z);
  108. QVector3D const head_focus =
  109. pose.head_pos +
  110. QVector3D(look_dir.x() * 0.18F, 0.0F, look_dir.z() * 0.45F);
  111. controller.look_at(head_focus);
  112. } else {
  113. float const forward_offset = 0.16F + (anim.is_moving ? 0.05F : 0.0F);
  114. float const hand_height = HP::WAIST_Y + 0.04F + arm_height_jitter;
  115. QVector3D const idle_hand_l(-0.16F + arm_asymmetry, hand_height,
  116. forward_offset);
  117. QVector3D const idle_hand_r(0.12F - arm_asymmetry * 0.6F,
  118. hand_height + 0.01F, forward_offset * 0.9F);
  119. controller.place_hand_at(true, idle_hand_l);
  120. controller.place_hand_at(false, idle_hand_r);
  121. }
  122. }
  123. void add_attachments(const DrawContext &ctx, const HumanoidVariant &v,
  124. const HumanoidPose &pose,
  125. const HumanoidAnimationContext &anim_ctx,
  126. ISubmitter &out) const override {}
  127. void draw_helmet(const DrawContext &ctx, const HumanoidVariant &v,
  128. const HumanoidPose &pose, ISubmitter &out) const override {
  129. if (!resolve_style(ctx).show_helmet) {
  130. return;
  131. }
  132. auto &registry = EquipmentRegistry::instance();
  133. auto helmet = registry.get(EquipmentCategory::Helmet, "roman_light");
  134. if (helmet) {
  135. HumanoidAnimationContext anim_ctx{};
  136. helmet->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
  137. }
  138. }
  139. void draw_armor(const DrawContext &ctx, const HumanoidVariant &v,
  140. const HumanoidPose &pose,
  141. const HumanoidAnimationContext &anim,
  142. ISubmitter &out) const override {
  143. draw_healer_tunic(ctx, v, pose, out);
  144. if (resolve_style(ctx).show_armor) {
  145. auto &registry = EquipmentRegistry::instance();
  146. auto armor = registry.get(EquipmentCategory::Armor, "roman_light_armor");
  147. if (armor) {
  148. armor->render(ctx, pose.body_frames, v.palette, anim, out);
  149. }
  150. }
  151. }
  152. void draw_healer_tunic(const DrawContext &ctx, const HumanoidVariant &v,
  153. const HumanoidPose &pose, ISubmitter &out) const {
  154. using HP = HumanProportions;
  155. const BodyFrames &frames = pose.body_frames;
  156. const AttachmentFrame &torso = frames.torso;
  157. const AttachmentFrame &waist = frames.waist;
  158. const AttachmentFrame &back = frames.back;
  159. const HealerStyleConfig &style = resolve_style(ctx);
  160. if (torso.radius <= 0.0F) {
  161. return;
  162. }
  163. QVector3D const tunic_white(0.96F, 0.95F, 0.92F);
  164. QVector3D const tunic_offwhite(0.93F, 0.91F, 0.86F);
  165. QVector3D const tunic_cream(0.89F, 0.86F, 0.80F);
  166. QVector3D const sash_red =
  167. style.cape_color.value_or(QVector3D(0.72F, 0.18F, 0.15F));
  168. QVector3D const trim_gold =
  169. saturate_color(v.palette.metal * 0.92F + QVector3D(0.05F, 0.04F, 0.0F));
  170. QVector3D const leather_brown = v.palette.leather;
  171. QVector3D const metal_bronze = v.palette.metal;
  172. const QVector3D &origin = torso.origin;
  173. const QVector3D &right = torso.right;
  174. const QVector3D &up = torso.up;
  175. const QVector3D &forward = torso.forward;
  176. float const torso_r = torso.radius * 1.05F;
  177. float const torso_depth =
  178. (torso.depth > 0.0F) ? torso.depth * 0.95F : torso.radius * 0.82F;
  179. float const y_shoulder = origin.y() + 0.030F;
  180. float const y_waist = waist.origin.y();
  181. float const y_robe_bottom = y_waist - 0.38F;
  182. constexpr int segments = 14;
  183. constexpr float pi = std::numbers::pi_v<float>;
  184. auto drawFabricRing = [&](float y_pos, float width, float depth,
  185. const QVector3D &color, float thickness) {
  186. for (int i = 0; i < segments; ++i) {
  187. float const angle1 = (static_cast<float>(i) / segments) * 2.0F * pi;
  188. float const angle2 = (static_cast<float>(i + 1) / segments) * 2.0F * pi;
  189. float const sin1 = std::sin(angle1);
  190. float const cos1 = std::cos(angle1);
  191. float const sin2 = std::sin(angle2);
  192. float const cos2 = std::cos(angle2);
  193. float const r1 = std::sqrt((width * width * sin1 * sin1) +
  194. (depth * depth * cos1 * cos1));
  195. float const r2 = std::sqrt((width * width * sin2 * sin2) +
  196. (depth * depth * cos2 * cos2));
  197. QVector3D const p1 = origin + right * (width * sin1) +
  198. forward * (depth * cos1) +
  199. up * (y_pos - origin.y());
  200. QVector3D const p2 = origin + right * (width * sin2) +
  201. forward * (depth * cos2) +
  202. up * (y_pos - origin.y());
  203. out.mesh(get_unit_cylinder(),
  204. cylinder_between(ctx.model, p1, p2, thickness), color, nullptr,
  205. 1.0F);
  206. }
  207. };
  208. auto drawTorsoSection = [&](float y_top, float y_bot, float width_top,
  209. float width_bot, float depth_top,
  210. float depth_bot, const QVector3D &color) {
  211. QVector3D const top_pos = origin + up * (y_top - origin.y());
  212. QVector3D const bot_pos = origin + up * (y_bot - origin.y());
  213. float const avg_r = (width_top + width_bot) * 0.5F;
  214. out.mesh(get_unit_cylinder(),
  215. cylinder_between(ctx.model, bot_pos, top_pos, avg_r), color,
  216. nullptr, 1.0F);
  217. };
  218. float const neck_y = y_shoulder + 0.04F;
  219. drawFabricRing(neck_y, torso_r * 0.72F, torso_depth * 0.64F, tunic_cream,
  220. 0.024F);
  221. drawFabricRing(y_shoulder + 0.05F, torso_r * 1.16F, torso_depth * 1.10F,
  222. tunic_white, 0.036F);
  223. drawFabricRing(y_shoulder + 0.010F, torso_r * 1.10F, torso_depth * 1.04F,
  224. tunic_white, 0.034F);
  225. drawTorsoSection(y_shoulder + 0.02F, y_shoulder - 0.10F, torso_r * 1.08F,
  226. torso_r * 1.02F, torso_depth * 1.06F, torso_depth * 1.00F,
  227. tunic_white);
  228. drawTorsoSection(y_shoulder - 0.10F, y_shoulder - 0.20F, torso_r * 1.02F,
  229. torso_r * 0.92F, torso_depth * 1.00F, torso_depth * 0.88F,
  230. tunic_offwhite);
  231. drawFabricRing(y_shoulder - 0.14F, torso_r * 0.98F, torso_depth * 0.92F,
  232. tunic_offwhite, 0.030F);
  233. drawTorsoSection(y_shoulder - 0.20F, y_waist + 0.02F, torso_r * 0.90F,
  234. torso_r * 0.82F, torso_depth * 0.86F, torso_depth * 0.78F,
  235. tunic_offwhite);
  236. float const sash_y = y_waist + 0.010F;
  237. QVector3D const sash_center = origin + up * (sash_y - origin.y());
  238. out.mesh(get_unit_cylinder(),
  239. cylinder_between(ctx.model, sash_center - up * 0.022F,
  240. sash_center + up * 0.022F, torso_r * 0.86F),
  241. sash_red, nullptr, 1.0F);
  242. out.mesh(get_unit_cylinder(),
  243. cylinder_between(ctx.model, sash_center + up * 0.020F,
  244. sash_center + up * 0.026F, torso_r * 0.88F),
  245. trim_gold, nullptr, 1.0F);
  246. out.mesh(get_unit_cylinder(),
  247. cylinder_between(ctx.model, sash_center - up * 0.026F,
  248. sash_center - up * 0.020F, torso_r * 0.88F),
  249. trim_gold, nullptr, 1.0F);
  250. if (style.show_cape) {
  251. float const cape_bottom_y =
  252. std::max(y_robe_bottom + 0.08F, y_waist - 0.20F);
  253. QVector3D const cape_color =
  254. saturate_color(sash_red * 0.95F + v.palette.cloth * 0.15F);
  255. QVector3D const left_top =
  256. frames.shoulder_l.origin + back.forward * 0.03F + up * 0.015F;
  257. QVector3D const right_top =
  258. frames.shoulder_r.origin + back.forward * 0.03F + up * 0.015F;
  259. QVector3D const left_bottom =
  260. left_top + up * (cape_bottom_y - left_top.y()) + back.forward * 0.05F;
  261. QVector3D const right_bottom = right_top +
  262. up * (cape_bottom_y - right_top.y()) +
  263. back.forward * 0.05F;
  264. out.mesh(get_unit_cylinder(),
  265. cylinder_between(ctx.model, left_top, right_top, 0.020F),
  266. cape_color, nullptr, 1.0F);
  267. out.mesh(get_unit_cylinder(),
  268. cylinder_between(ctx.model, left_top, left_bottom, 0.028F),
  269. cape_color, nullptr, 1.0F);
  270. out.mesh(get_unit_cylinder(),
  271. cylinder_between(ctx.model, right_top, right_bottom, 0.028F),
  272. cape_color, nullptr, 1.0F);
  273. out.mesh(get_unit_cylinder(),
  274. cylinder_between(ctx.model, left_bottom, right_bottom, 0.022F),
  275. cape_color * 0.94F, nullptr, 1.0F);
  276. QVector3D const cape_trim_top =
  277. (left_top + right_top) * 0.5F + back.forward * 0.01F;
  278. out.mesh(get_unit_sphere(),
  279. sphere_at(ctx.model, cape_trim_top, torso_r * 0.16F),
  280. trim_gold * 0.9F, nullptr, 1.0F);
  281. }
  282. QVector3D const emblem_center = origin + forward * (torso_depth * 0.90F) +
  283. up * ((y_shoulder - origin.y()) - 0.06F);
  284. float const cross_half = torso_r * 0.36F;
  285. float const cross_thickness = torso_r * 0.18F;
  286. QVector3D const cross_color = saturate_color(sash_red * 1.05F);
  287. out.mesh(get_unit_cylinder(),
  288. cylinder_between(ctx.model, emblem_center - right * cross_half,
  289. emblem_center + right * cross_half,
  290. cross_thickness),
  291. cross_color, nullptr, 1.0F);
  292. out.mesh(get_unit_cylinder(),
  293. cylinder_between(
  294. ctx.model, emblem_center - up * (cross_half * 1.1F),
  295. emblem_center + up * (cross_half * 1.1F), cross_thickness),
  296. cross_color, nullptr, 1.0F);
  297. float const robe_length = y_waist - y_robe_bottom;
  298. constexpr int skirt_layers = 10;
  299. for (int layer = 0; layer < skirt_layers; ++layer) {
  300. float const t =
  301. static_cast<float>(layer) / static_cast<float>(skirt_layers - 1);
  302. float const y = y_waist - 0.02F - t * robe_length;
  303. float const flare = 1.0F + t * 0.45F;
  304. float const width = torso_r * 0.88F * flare;
  305. float const depth = torso_depth * 0.82F * flare;
  306. QVector3D const layer_color =
  307. tunic_white * (1.0F - t * 0.12F) + tunic_cream * (t * 0.12F);
  308. float const thickness = 0.018F + t * 0.014F;
  309. drawFabricRing(y, width, depth, layer_color, thickness);
  310. }
  311. float const hem_y = y_robe_bottom + 0.01F;
  312. drawFabricRing(hem_y, torso_r * 0.88F * 1.45F, torso_depth * 0.82F * 1.45F,
  313. tunic_cream * 0.92F, 0.035F);
  314. drawFabricRing(hem_y - 0.012F, torso_r * 0.90F * 1.45F,
  315. torso_depth * 0.84F * 1.45F, trim_gold * 0.85F, 0.015F);
  316. auto draw_sleeve = [&](const QVector3D &shoulder_pos,
  317. const QVector3D &outward,
  318. const QVector3D &elbow_pos) {
  319. out.mesh(get_unit_sphere(),
  320. sphere_at(ctx.model, shoulder_pos + outward * 0.01F,
  321. HP::UPPER_ARM_R * 1.6F),
  322. tunic_white, nullptr, 1.0F);
  323. for (int i = 0; i < 5; ++i) {
  324. float const t = static_cast<float>(i) / 5.0F;
  325. QVector3D const sleeve_pos = shoulder_pos * (1.0F - t) + elbow_pos * t +
  326. outward * (0.01F - t * 0.005F);
  327. float const sleeve_r = HP::UPPER_ARM_R * (1.55F - t * 0.35F);
  328. QVector3D const sleeve_color = tunic_white * (1.0F - t * 0.06F);
  329. out.mesh(get_unit_sphere(), sphere_at(ctx.model, sleeve_pos, sleeve_r),
  330. sleeve_color, nullptr, 1.0F);
  331. }
  332. QVector3D const cuff_pos = elbow_pos + outward * 0.005F;
  333. out.mesh(get_unit_sphere(),
  334. sphere_at(ctx.model, cuff_pos, HP::UPPER_ARM_R * 1.25F),
  335. tunic_cream * 0.95F, nullptr, 1.0F);
  336. };
  337. draw_sleeve(frames.shoulder_l.origin, -right, pose.elbow_l);
  338. draw_sleeve(frames.shoulder_r.origin, right, pose.elbow_r);
  339. QVector3D const satchel_pos = origin + right * (torso_r * 0.75F) +
  340. up * (y_waist - 0.08F - origin.y()) +
  341. forward * (torso_depth * 0.15F);
  342. out.mesh(
  343. get_unit_cube(),
  344. [&]() {
  345. QMatrix4x4 m = ctx.model;
  346. m.translate(satchel_pos);
  347. m.scale(0.045F, 0.06F, 0.035F);
  348. return m;
  349. }(),
  350. leather_brown, nullptr, 1.0F);
  351. out.mesh(
  352. get_unit_cube(),
  353. [&]() {
  354. QMatrix4x4 m = ctx.model;
  355. m.translate(satchel_pos + up * 0.035F + forward * 0.01F);
  356. m.scale(0.048F, 0.015F, 0.038F);
  357. return m;
  358. }(),
  359. leather_brown * 0.85F, nullptr, 1.0F);
  360. QVector3D const clasp_pos = origin + right * (torso_r * 0.4F) +
  361. up * (y_shoulder - origin.y()) +
  362. forward * (torso_depth * 0.3F);
  363. out.mesh(get_unit_sphere(), sphere_at(ctx.model, clasp_pos, 0.022F),
  364. metal_bronze, nullptr, 1.0F);
  365. }
  366. private:
  367. auto
  368. resolve_style(const DrawContext &ctx) const -> const HealerStyleConfig & {
  369. ensure_healer_styles_registered();
  370. auto &styles = style_registry();
  371. std::string nation_id;
  372. if (ctx.entity != nullptr) {
  373. if (auto *unit =
  374. ctx.entity->get_component<Engine::Core::UnitComponent>()) {
  375. nation_id = Game::Systems::nation_id_to_string(unit->nation_id);
  376. }
  377. }
  378. if (!nation_id.empty()) {
  379. auto it = styles.find(nation_id);
  380. if (it != styles.end()) {
  381. return it->second;
  382. }
  383. }
  384. auto fallback = styles.find(std::string(k_default_style_key));
  385. if (fallback != styles.end()) {
  386. return fallback->second;
  387. }
  388. static const HealerStyleConfig default_style{};
  389. return default_style;
  390. }
  391. public:
  392. auto resolve_shader_key(const DrawContext &ctx) const -> QString {
  393. const HealerStyleConfig &style = resolve_style(ctx);
  394. if (!style.shader_id.empty()) {
  395. return QString::fromStdString(style.shader_id);
  396. }
  397. return QStringLiteral("healer");
  398. }
  399. private:
  400. void apply_palette_overrides(const HealerStyleConfig &style,
  401. const QVector3D &team_tint,
  402. HumanoidVariant &variant) const {
  403. auto apply_color = [&](const std::optional<QVector3D> &override_color,
  404. QVector3D &target) {
  405. target = mix_palette_color(target, override_color, team_tint,
  406. k_team_mix_weight, k_style_mix_weight);
  407. };
  408. apply_color(style.cloth_color, variant.palette.cloth);
  409. apply_color(style.leather_color, variant.palette.leather);
  410. apply_color(style.leather_dark_color, variant.palette.leather_dark);
  411. apply_color(style.metal_color, variant.palette.metal);
  412. apply_color(style.wood_color, variant.palette.wood);
  413. }
  414. };
  415. void register_healer_renderer(Render::GL::EntityRendererRegistry &registry) {
  416. ensure_healer_styles_registered();
  417. static HealerRenderer const renderer;
  418. registry.register_renderer(
  419. "troops/roman/healer", [](const DrawContext &ctx, ISubmitter &out) {
  420. static HealerRenderer const static_renderer;
  421. Shader *healer_shader = nullptr;
  422. if (ctx.backend != nullptr) {
  423. QString shader_key = static_renderer.resolve_shader_key(ctx);
  424. healer_shader = ctx.backend->shader(shader_key);
  425. if (healer_shader == nullptr) {
  426. healer_shader = ctx.backend->shader(QStringLiteral("healer"));
  427. }
  428. }
  429. auto *scene_renderer = dynamic_cast<Renderer *>(&out);
  430. if ((scene_renderer != nullptr) && (healer_shader != nullptr)) {
  431. scene_renderer->set_current_shader(healer_shader);
  432. }
  433. static_renderer.render(ctx, out);
  434. if (scene_renderer != nullptr) {
  435. scene_renderer->set_current_shader(nullptr);
  436. }
  437. });
  438. }
  439. } // namespace Render::GL::Roman