builder_renderer.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. #include "builder_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 "builder_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, BuilderStyleConfig> & {
  42. static std::unordered_map<std::string, BuilderStyleConfig> styles;
  43. return styles;
  44. }
  45. void ensure_builder_styles_registered() {
  46. static const bool registered = []() {
  47. register_roman_builder_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_builder_style(const std::string &nation_id,
  56. const BuilderStyleConfig &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 BuilderRenderer : public HumanoidRendererBase {
  64. public:
  65. friend void
  66. register_builder_renderer(Render::GL::EntityRendererRegistry &registry);
  67. auto get_proportion_scaling() const -> QVector3D override {
  68. return {1.05F, 0.98F, 1.02F};
  69. }
  70. void get_variant(const DrawContext &ctx, uint32_t seed,
  71. HumanoidVariant &v) const override {
  72. QVector3D const team_tint = resolve_team_tint(ctx);
  73. v.palette = make_humanoid_palette(team_tint, seed);
  74. auto const &style = resolve_style(ctx);
  75. apply_palette_overrides(style, team_tint, v);
  76. }
  77. void customize_pose(const DrawContext &,
  78. const HumanoidAnimationContext &anim_ctx, uint32_t seed,
  79. HumanoidPose &pose) const override {
  80. using HP = HumanProportions;
  81. const AnimationInputs &anim = anim_ctx.inputs;
  82. HumanoidPoseController controller(pose, anim_ctx);
  83. float const arm_jitter = (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.04F;
  84. float const asymmetry = (hash_01(seed ^ 0xDEF0U) - 0.5F) * 0.05F;
  85. if (anim.is_constructing) {
  86. uint32_t const pose_selector = seed % 100;
  87. float const phase_offset = float(seed % 100) * 0.0628F;
  88. float const cycle_speed = 2.0F + float(seed % 50) * 0.02F;
  89. float const swing_cycle =
  90. std::fmod(anim.time * cycle_speed + phase_offset, 1.0F);
  91. if (pose_selector < 40) {
  92. apply_hammering_pose(controller, swing_cycle, asymmetry, seed);
  93. } else if (pose_selector < 70) {
  94. apply_kneeling_work_pose(controller, swing_cycle, asymmetry, seed);
  95. } else if (pose_selector < 90) {
  96. apply_sawing_pose(controller, swing_cycle, asymmetry, seed);
  97. } else {
  98. apply_lifting_pose(controller, swing_cycle, asymmetry, seed);
  99. }
  100. return;
  101. }
  102. float const hammer_hand_forward = 0.22F + (anim.is_moving ? 0.03F : 0.0F);
  103. float const hammer_hand_height = HP::WAIST_Y + 0.08F + arm_jitter;
  104. QVector3D const hammer_hand(-0.10F + asymmetry, hammer_hand_height + 0.04F,
  105. hammer_hand_forward);
  106. QVector3D const rest_hand(0.24F - asymmetry * 0.5F,
  107. HP::WAIST_Y - 0.02F + arm_jitter * 0.5F, 0.08F);
  108. controller.place_hand_at(true, hammer_hand);
  109. controller.place_hand_at(false, rest_hand);
  110. }
  111. void add_attachments(const DrawContext &ctx, const HumanoidVariant &v,
  112. const HumanoidPose &pose,
  113. const HumanoidAnimationContext &anim_ctx,
  114. ISubmitter &out) const override {
  115. auto &registry = EquipmentRegistry::instance();
  116. auto work_apron =
  117. registry.get(EquipmentCategory::Armor, "work_apron_roman");
  118. if (work_apron) {
  119. work_apron->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
  120. }
  121. auto tool_belt = registry.get(EquipmentCategory::Armor, "tool_belt_roman");
  122. if (tool_belt) {
  123. tool_belt->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
  124. }
  125. auto arm_guards = registry.get(EquipmentCategory::Armor, "arm_guards");
  126. if (arm_guards) {
  127. arm_guards->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
  128. }
  129. draw_stone_hammer(ctx, v, pose, anim_ctx, out);
  130. }
  131. void draw_helmet(const DrawContext &ctx, const HumanoidVariant &v,
  132. const HumanoidPose &pose, ISubmitter &out) const override {
  133. auto &registry = EquipmentRegistry::instance();
  134. auto helmet = registry.get(EquipmentCategory::Helmet, "roman_light");
  135. if (helmet) {
  136. HumanoidAnimationContext anim_ctx{};
  137. helmet->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
  138. }
  139. }
  140. void draw_armor(const DrawContext &ctx, const HumanoidVariant &v,
  141. const HumanoidPose &pose,
  142. const HumanoidAnimationContext &anim,
  143. ISubmitter &out) const override {
  144. uint32_t const seed = reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU;
  145. draw_work_tunic(ctx, v, pose, seed, out);
  146. }
  147. private:
  148. static constexpr uint32_t KNEEL_SEED_OFFSET = 0x1234U;
  149. void apply_hammering_pose(HumanoidPoseController &controller,
  150. float swing_cycle, float asymmetry,
  151. uint32_t seed) const {
  152. using HP = HumanProportions;
  153. float swing_angle;
  154. float body_lean;
  155. float crouch_amount;
  156. if (swing_cycle < 0.3F) {
  157. float const t = swing_cycle / 0.3F;
  158. swing_angle = t * 0.95F;
  159. body_lean = -t * 0.10F;
  160. crouch_amount = 0.0F;
  161. } else if (swing_cycle < 0.5F) {
  162. float const t = (swing_cycle - 0.3F) / 0.2F;
  163. swing_angle = 0.95F - t * 1.5F;
  164. body_lean = -0.10F + t * 0.28F;
  165. crouch_amount = t * 0.08F;
  166. } else if (swing_cycle < 0.6F) {
  167. float const t = (swing_cycle - 0.5F) / 0.1F;
  168. swing_angle = -0.55F + t * 0.18F;
  169. body_lean = 0.18F - t * 0.06F;
  170. crouch_amount = 0.08F - t * 0.02F;
  171. } else {
  172. float const t = (swing_cycle - 0.6F) / 0.4F;
  173. swing_angle = -0.37F + t * 0.37F;
  174. body_lean = 0.12F * (1.0F - t);
  175. crouch_amount = 0.06F * (1.0F - t);
  176. }
  177. float const torso_y_offset = -crouch_amount;
  178. float const hammer_y = HP::SHOULDER_Y + 0.10F + swing_angle * 0.22F;
  179. float const hammer_forward =
  180. 0.18F + std::abs(swing_angle) * 0.16F + body_lean * 0.5F;
  181. float const hammer_down =
  182. swing_cycle > 0.4F && swing_cycle < 0.65F ? 0.10F : 0.0F;
  183. QVector3D const hammer_hand(-0.06F + asymmetry,
  184. hammer_y - hammer_down + torso_y_offset,
  185. hammer_forward);
  186. float const brace_y =
  187. HP::WAIST_Y + 0.12F + torso_y_offset - crouch_amount * 0.5F;
  188. float const brace_forward = 0.15F + body_lean * 0.3F;
  189. QVector3D const brace_hand(0.14F - asymmetry * 0.5F, brace_y,
  190. brace_forward);
  191. controller.place_hand_at(true, hammer_hand);
  192. controller.place_hand_at(false, brace_hand);
  193. }
  194. void apply_kneeling_work_pose(HumanoidPoseController &controller, float cycle,
  195. float asymmetry, uint32_t seed) const {
  196. using HP = HumanProportions;
  197. float const kneel_depth =
  198. 0.45F + (hash_01(seed ^ KNEEL_SEED_OFFSET) * 0.15F);
  199. controller.kneel(kneel_depth);
  200. float const work_cycle = std::sin(cycle * std::numbers::pi_v<float> * 2.0F);
  201. float const tool_y = HP::WAIST_Y * 0.3F + work_cycle * 0.08F;
  202. float const tool_x_offset = 0.05F + work_cycle * 0.04F;
  203. QVector3D const tool_hand(-tool_x_offset + asymmetry, tool_y, 0.25F);
  204. float const brace_x = 0.18F - asymmetry * 0.5F;
  205. QVector3D const brace_hand(brace_x, HP::WAIST_Y * 0.25F, 0.20F);
  206. controller.place_hand_at(true, tool_hand);
  207. controller.place_hand_at(false, brace_hand);
  208. }
  209. void apply_sawing_pose(HumanoidPoseController &controller, float cycle,
  210. float asymmetry, uint32_t seed) const {
  211. using HP = HumanProportions;
  212. controller.lean(QVector3D(0.0F, 0.0F, 1.0F), 0.12F);
  213. float const saw_offset =
  214. std::sin(cycle * std::numbers::pi_v<float> * 4.0F) * 0.12F;
  215. float const saw_y = HP::WAIST_Y + 0.15F;
  216. float const saw_z = 0.20F + saw_offset;
  217. QVector3D const left_hand(-0.08F + asymmetry, saw_y, saw_z);
  218. QVector3D const right_hand(0.08F - asymmetry, saw_y + 0.02F, saw_z);
  219. controller.place_hand_at(true, left_hand);
  220. controller.place_hand_at(false, right_hand);
  221. }
  222. void apply_lifting_pose(HumanoidPoseController &controller, float cycle,
  223. float asymmetry, uint32_t seed) const {
  224. using HP = HumanProportions;
  225. float lift_height;
  226. float crouch;
  227. if (cycle < 0.3F) {
  228. float const t = cycle / 0.3F;
  229. lift_height = HP::WAIST_Y * (1.0F - t * 0.5F);
  230. crouch = t * 0.20F;
  231. } else if (cycle < 0.6F) {
  232. float const t = (cycle - 0.3F) / 0.3F;
  233. lift_height =
  234. HP::WAIST_Y * 0.5F + t * (HP::SHOULDER_Y - HP::WAIST_Y * 0.5F);
  235. crouch = 0.20F * (1.0F - t);
  236. } else if (cycle < 0.8F) {
  237. lift_height = HP::SHOULDER_Y;
  238. crouch = 0.0F;
  239. } else {
  240. float const t = (cycle - 0.8F) / 0.2F;
  241. lift_height = HP::SHOULDER_Y * (1.0F - t * 0.3F);
  242. crouch = 0.0F;
  243. }
  244. QVector3D const left_hand(-0.12F + asymmetry, lift_height, 0.15F);
  245. QVector3D const right_hand(0.12F - asymmetry, lift_height, 0.15F);
  246. controller.place_hand_at(true, left_hand);
  247. controller.place_hand_at(false, right_hand);
  248. if (crouch > 0.0F) {
  249. controller.kneel(crouch);
  250. }
  251. }
  252. void draw_stone_hammer(const DrawContext &ctx, const HumanoidVariant &v,
  253. const HumanoidPose &pose,
  254. const HumanoidAnimationContext &anim_ctx,
  255. ISubmitter &out) const {
  256. QVector3D const wood_color = v.palette.wood;
  257. QVector3D const stone_color(0.55F, 0.52F, 0.48F);
  258. QVector3D const stone_dark(0.45F, 0.42F, 0.38F);
  259. QVector3D const hand = pose.hand_l;
  260. QVector3D const up(0.0F, 1.0F, 0.0F);
  261. QVector3D const forward(0.0F, 0.0F, 1.0F);
  262. QVector3D const right(1.0F, 0.0F, 0.0F);
  263. const AnimationInputs &anim = anim_ctx.inputs;
  264. QVector3D handle_axis;
  265. QVector3D head_axis;
  266. if (anim.is_constructing) {
  267. handle_axis = forward;
  268. head_axis = up;
  269. } else {
  270. handle_axis = up;
  271. head_axis = right;
  272. }
  273. float const handle_len = 0.32F;
  274. float const handle_r = 0.016F;
  275. QVector3D const handle_offset = anim.is_constructing
  276. ? (forward * 0.12F + up * 0.02F)
  277. : (up * 0.12F + forward * 0.02F);
  278. QVector3D const handle_top = hand + handle_offset;
  279. QVector3D const handle_bot = handle_top - handle_axis * handle_len;
  280. out.mesh(get_unit_cylinder(),
  281. cylinder_between(ctx.model, handle_bot, handle_top, handle_r),
  282. wood_color, nullptr, 1.0F);
  283. float const head_len = 0.10F;
  284. float const head_r = 0.030F;
  285. QVector3D const head_center = handle_top + handle_axis * 0.035F;
  286. out.mesh(
  287. get_unit_cylinder(),
  288. cylinder_between(ctx.model, head_center - head_axis * (head_len * 0.5F),
  289. head_center + head_axis * (head_len * 0.5F), head_r),
  290. stone_color, nullptr, 1.0F);
  291. out.mesh(get_unit_sphere(),
  292. sphere_at(ctx.model, head_center + head_axis * (head_len * 0.5F),
  293. head_r * 1.15F),
  294. stone_dark, nullptr, 1.0F);
  295. out.mesh(get_unit_sphere(),
  296. sphere_at(ctx.model, head_center - head_axis * (head_len * 0.5F),
  297. head_r * 0.9F),
  298. stone_color * 0.95F, nullptr, 1.0F);
  299. }
  300. void draw_work_tunic(const DrawContext &ctx, const HumanoidVariant &v,
  301. const HumanoidPose &pose, uint32_t seed,
  302. ISubmitter &out) const {
  303. using HP = HumanProportions;
  304. const BodyFrames &frames = pose.body_frames;
  305. const AttachmentFrame &torso = frames.torso;
  306. const AttachmentFrame &waist = frames.waist;
  307. if (torso.radius <= 0.0F) {
  308. return;
  309. }
  310. float const color_var = hash_01(seed ^ 0xABCU);
  311. QVector3D tunic_base;
  312. if (color_var < 0.4F) {
  313. tunic_base = QVector3D(0.65F, 0.52F, 0.38F);
  314. } else if (color_var < 0.7F) {
  315. tunic_base = QVector3D(0.58F, 0.48F, 0.35F);
  316. } else {
  317. tunic_base = QVector3D(0.72F, 0.62F, 0.48F);
  318. }
  319. QVector3D const tunic_dark = tunic_base * 0.85F;
  320. const QVector3D &origin = torso.origin;
  321. const QVector3D &right = torso.right;
  322. const QVector3D &up = torso.up;
  323. const QVector3D &forward = torso.forward;
  324. float const torso_r = torso.radius * 1.08F;
  325. float const torso_d =
  326. (torso.depth > 0.0F) ? torso.depth * 0.92F : torso.radius * 0.80F;
  327. float const y_shoulder = origin.y() + 0.032F;
  328. float const y_waist = waist.origin.y();
  329. float const y_hem = y_waist - 0.16F;
  330. constexpr int segs = 12;
  331. constexpr float pi = std::numbers::pi_v<float>;
  332. auto drawRing = [&](float y, float w, float d, const QVector3D &col,
  333. float th) {
  334. for (int i = 0; i < segs; ++i) {
  335. float a1 = (float(i) / segs) * 2.0F * pi;
  336. float a2 = (float(i + 1) / segs) * 2.0F * pi;
  337. QVector3D p1 = origin + right * (w * std::sin(a1)) +
  338. forward * (d * std::cos(a1)) + up * (y - origin.y());
  339. QVector3D p2 = origin + right * (w * std::sin(a2)) +
  340. forward * (d * std::cos(a2)) + up * (y - origin.y());
  341. out.mesh(get_unit_cylinder(), cylinder_between(ctx.model, p1, p2, th),
  342. col, nullptr, 1.0F);
  343. }
  344. };
  345. drawRing(y_shoulder + 0.04F, torso_r * 0.68F, torso_d * 0.60F, tunic_dark,
  346. 0.022F);
  347. drawRing(y_shoulder + 0.02F, torso_r * 1.08F, torso_d * 1.02F, tunic_base,
  348. 0.032F);
  349. for (int i = 0; i < 5; ++i) {
  350. float t = float(i) / 4.0F;
  351. float y = y_shoulder - 0.01F - t * (y_shoulder - y_waist - 0.03F);
  352. float w = torso_r * (1.04F - t * 0.14F);
  353. float d = torso_d * (0.98F - t * 0.10F);
  354. QVector3D col = tunic_base * (1.0F - t * 0.06F);
  355. drawRing(y, w, d, col, 0.026F - t * 0.004F);
  356. }
  357. for (int i = 0; i < 4; ++i) {
  358. float t = float(i) / 3.0F;
  359. float y = y_waist - 0.01F - t * (y_waist - y_hem);
  360. float flare = 1.0F + t * 0.18F;
  361. QVector3D col = tunic_base * (1.0F - t * 0.08F);
  362. drawRing(y, torso_r * 0.80F * flare, torso_d * 0.76F * flare, col,
  363. 0.018F + t * 0.006F);
  364. }
  365. auto drawSleeve = [&](const QVector3D &shoulder, const QVector3D &out_dir,
  366. const QVector3D &elbow) {
  367. for (int i = 0; i < 3; ++i) {
  368. float t = float(i) / 3.0F;
  369. QVector3D pos =
  370. shoulder * (1.0F - t) + elbow * t * 0.6F + out_dir * 0.008F;
  371. float r = HP::UPPER_ARM_R * (1.40F - t * 0.25F);
  372. out.mesh(get_unit_sphere(), sphere_at(ctx.model, pos, r),
  373. tunic_base * (1.0F - t * 0.04F), nullptr, 1.0F);
  374. }
  375. };
  376. drawSleeve(frames.shoulder_l.origin, -right, pose.elbow_l);
  377. drawSleeve(frames.shoulder_r.origin, right, pose.elbow_r);
  378. draw_extended_forearm(ctx, v, pose, out);
  379. }
  380. void draw_extended_forearm(const DrawContext &ctx, const HumanoidVariant &v,
  381. const HumanoidPose &pose, ISubmitter &out) const {
  382. QVector3D const skin_color = v.palette.skin;
  383. QVector3D const elbow_r = pose.elbow_r;
  384. QVector3D const hand_r = pose.hand_r;
  385. for (int i = 0; i < 4; ++i) {
  386. float t = 0.25F + float(i) * 0.20F;
  387. QVector3D pos = elbow_r * (1.0F - t) + hand_r * t;
  388. float r = 0.024F - float(i) * 0.002F;
  389. out.mesh(get_unit_sphere(), sphere_at(ctx.model, pos, r), skin_color,
  390. nullptr, 1.0F);
  391. }
  392. }
  393. private:
  394. auto
  395. resolve_style(const DrawContext &ctx) const -> const BuilderStyleConfig & {
  396. ensure_builder_styles_registered();
  397. auto &styles = style_registry();
  398. std::string nation_id;
  399. if (ctx.entity != nullptr) {
  400. if (auto *unit =
  401. ctx.entity->get_component<Engine::Core::UnitComponent>()) {
  402. nation_id = Game::Systems::nation_id_to_string(unit->nation_id);
  403. }
  404. }
  405. if (!nation_id.empty()) {
  406. auto it = styles.find(nation_id);
  407. if (it != styles.end()) {
  408. return it->second;
  409. }
  410. }
  411. auto fallback = styles.find(std::string(k_default_style_key));
  412. if (fallback != styles.end()) {
  413. return fallback->second;
  414. }
  415. static const BuilderStyleConfig default_style{};
  416. return default_style;
  417. }
  418. auto resolve_shader_key(const DrawContext &ctx) const -> QString {
  419. const BuilderStyleConfig &style = resolve_style(ctx);
  420. if (!style.shader_id.empty()) {
  421. return QString::fromStdString(style.shader_id);
  422. }
  423. return QStringLiteral("builder");
  424. }
  425. void apply_palette_overrides(const BuilderStyleConfig &style,
  426. const QVector3D &team_tint,
  427. HumanoidVariant &variant) const {
  428. auto apply = [&](const std::optional<QVector3D> &c, QVector3D &t) {
  429. t = mix_palette_color(t, c, team_tint, k_team_mix_weight,
  430. k_style_mix_weight);
  431. };
  432. apply(style.cloth_color, variant.palette.cloth);
  433. apply(style.leather_color, variant.palette.leather);
  434. apply(style.leather_dark_color, variant.palette.leather_dark);
  435. apply(style.metal_color, variant.palette.metal);
  436. apply(style.wood_color, variant.palette.wood);
  437. }
  438. };
  439. void register_builder_renderer(Render::GL::EntityRendererRegistry &registry) {
  440. ensure_builder_styles_registered();
  441. static BuilderRenderer const renderer;
  442. registry.register_renderer(
  443. "troops/roman/builder", [](const DrawContext &ctx, ISubmitter &out) {
  444. static BuilderRenderer const r;
  445. Shader *shader = nullptr;
  446. if (ctx.backend != nullptr) {
  447. QString key = r.resolve_shader_key(ctx);
  448. shader = ctx.backend->shader(key);
  449. if (!shader) {
  450. shader = ctx.backend->shader(QStringLiteral("builder"));
  451. }
  452. }
  453. auto *sr = dynamic_cast<Renderer *>(&out);
  454. if (sr && shader) {
  455. sr->set_current_shader(shader);
  456. }
  457. r.render(ctx, out);
  458. if (sr) {
  459. sr->set_current_shader(nullptr);
  460. }
  461. });
  462. }
  463. } // namespace Render::GL::Roman