Browse Source

Merge pull request #415 from djeada/copilot/migrate-helmet-rendering

Migrate helmet rendering to equipment system for all nations
Adam Djellouli 1 month ago
parent
commit
e31f6ffb61
28 changed files with 1192 additions and 1038 deletions
  1. 6 0
      render/CMakeLists.txt
  2. 13 126
      render/entity/nations/carthage/archer_renderer.cpp
  3. 7 100
      render/entity/nations/carthage/horse_swordsman_renderer.cpp
  4. 7 50
      render/entity/nations/carthage/spearman_renderer.cpp
  5. 7 90
      render/entity/nations/carthage/swordsman_renderer.cpp
  6. 6 96
      render/entity/nations/kingdom/archer_renderer.cpp
  7. 7 94
      render/entity/nations/kingdom/horse_swordsman_renderer.cpp
  8. 7 52
      render/entity/nations/kingdom/spearman_renderer.cpp
  9. 7 92
      render/entity/nations/kingdom/swordsman_renderer.cpp
  10. 12 109
      render/entity/nations/roman/archer_renderer.cpp
  11. 7 94
      render/entity/nations/roman/horse_swordsman_renderer.cpp
  12. 7 48
      render/entity/nations/roman/spearman_renderer.cpp
  13. 7 87
      render/entity/nations/roman/swordsman_renderer.cpp
  14. 60 0
      render/equipment/helmets/headwrap.cpp
  15. 19 0
      render/equipment/helmets/headwrap.h
  16. 139 0
      render/equipment/helmets/kingdom_heavy_helmet.cpp
  17. 21 0
      render/equipment/helmets/kingdom_heavy_helmet.h
  18. 88 0
      render/equipment/helmets/kingdom_light_helmet.cpp
  19. 21 0
      render/equipment/helmets/kingdom_light_helmet.h
  20. 121 0
      render/equipment/helmets/montefortino_helmet.cpp
  21. 19 0
      render/equipment/helmets/montefortino_helmet.h
  22. 193 0
      render/equipment/helmets/roman_heavy_helmet.cpp
  23. 23 0
      render/equipment/helmets/roman_heavy_helmet.h
  24. 123 0
      render/equipment/helmets/roman_light_helmet.cpp
  25. 22 0
      render/equipment/helmets/roman_light_helmet.h
  26. 46 0
      render/equipment/register_equipment.cpp
  27. 1 0
      tests/CMakeLists.txt
  28. 196 0
      tests/render/helmet_renderers_test.cpp

+ 6 - 0
render/CMakeLists.txt

@@ -73,6 +73,12 @@ add_library(render_gl STATIC
     equipment/register_equipment.cpp
     equipment/weapons/bow_renderer.cpp
     equipment/weapons/quiver_renderer.cpp
+    equipment/helmets/montefortino_helmet.cpp
+    equipment/helmets/headwrap.cpp
+    equipment/helmets/roman_heavy_helmet.cpp
+    equipment/helmets/roman_light_helmet.cpp
+    equipment/helmets/kingdom_heavy_helmet.cpp
+    equipment/helmets/kingdom_light_helmet.cpp
 )
 
 target_include_directories(render_gl PUBLIC .)

+ 13 - 126
render/entity/nations/carthage/archer_renderer.cpp

@@ -402,103 +402,25 @@ public:
     using HP = HumanProportions;
 
     auto const &style = resolve_style(ctx);
+    auto &registry = EquipmentRegistry::instance();
+    HumanoidAnimationContext anim_ctx{};
+
     if (!style.show_helmet) {
       if (style.attachment_profile == std::string(k_attachment_headwrap)) {
-        draw_headwrap(ctx, v, pose, out);
+        // Use headwrap helmet from equipment registry
+        auto headwrap = registry.get(EquipmentCategory::Helmet, "headwrap");
+        if (headwrap) {
+          headwrap->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
+        }
       }
       return;
     }
 
-    auto draw_montefortino = [&](const QVector3D &base_metal) {
-      const AttachmentFrame &head = pose.bodyFrames.head;
-      float const head_r = head.radius;
-      if (head_r <= 0.0F) {
-        return;
-      }
-
-      auto headPoint = [&](const QVector3D &norm) -> QVector3D {
-        return frameLocalPosition(head, norm);
-      };
-
-      auto headTransform = [&](const QVector3D &norm,
-                               float scale) -> QMatrix4x4 {
-        return makeFrameLocalTransform(ctx.model, head, norm, scale);
-      };
-
-      QVector3D bronze =
-          saturate_color(base_metal * QVector3D(1.22F, 1.04F, 0.70F));
-      QVector3D patina =
-          saturate_color(bronze * QVector3D(0.88F, 0.96F, 0.92F));
-      QVector3D tinned_highlight =
-          saturate_color(bronze * QVector3D(1.12F, 1.08F, 1.04F));
-      QVector3D leather_band = saturate_color(v.palette.leatherDark *
-                                              QVector3D(1.10F, 0.96F, 0.80F));
-
-      auto draw_leather_cap = [&]() {
-        QVector3D leather_brown = saturate_color(
-            v.palette.leatherDark * QVector3D(1.15F, 0.95F, 0.78F));
-        QVector3D leather_dark =
-            saturate_color(leather_brown * QVector3D(0.85F, 0.88F, 0.92F));
-        QVector3D bronze_stud =
-            saturate_color(v.palette.metal * QVector3D(1.20F, 1.02F, 0.70F));
-
-        QMatrix4x4 cap_transform =
-            headTransform(QVector3D(0.0F, 0.70F, 0.0F), 1.0F);
-        cap_transform.scale(0.92F, 0.55F, 0.88F);
-        out.mesh(getUnitSphere(), cap_transform, leather_brown, nullptr, 1.0F);
-
-        QVector3D const band_top = headPoint(QVector3D(0.0F, 0.20F, 0.0F));
-        QVector3D const band_bot = headPoint(QVector3D(0.0F, 0.15F, 0.0F));
-
-        out.mesh(getUnitCylinder(),
-                 cylinderBetween(ctx.model, band_bot, band_top, head_r * 1.02F),
-                 leather_dark, nullptr, 1.0F);
-
-        auto draw_stud = [&](float angle) {
-          QVector3D const stud_pos = headPoint(QVector3D(
-              std::sin(angle) * 1.03F, 0.175F, std::cos(angle) * 1.03F));
-          out.mesh(getUnitSphere(),
-                   sphereAt(ctx.model, stud_pos, head_r * 0.012F), bronze_stud,
-                   nullptr, 1.0F);
-        };
-
-        for (int i = 0; i < 4; ++i) {
-          float const angle = (i / 4.0F) * 2.0F * std::numbers::pi_v<float>;
-          draw_stud(angle);
-        }
-      };
-
-      draw_leather_cap();
-
-      QMatrix4x4 top_knob = headTransform(QVector3D(0.0F, 0.88F, 0.0F), 0.18F);
-      out.mesh(getUnitSphere(), top_knob, tinned_highlight, nullptr, 1.0F);
-
-      QVector3D const brow_top = headPoint(QVector3D(0.0F, 0.55F, 0.0F));
-      QVector3D const brow_bot = headPoint(QVector3D(0.0F, 0.42F, 0.0F));
-      QMatrix4x4 brow =
-          cylinderBetween(ctx.model, brow_bot, brow_top, head_r * 1.20F);
-      brow.scale(1.04F, 1.0F, 0.86F);
-      out.mesh(getUnitCylinder(), brow, leather_band, nullptr, 1.0F);
-
-      QVector3D const rim_upper = headPoint(QVector3D(0.0F, 0.40F, 0.0F));
-      QVector3D const rim_lower = headPoint(QVector3D(0.0F, 0.30F, 0.0F));
-      QMatrix4x4 rim =
-          cylinderBetween(ctx.model, rim_lower, rim_upper, head_r * 1.30F);
-      rim.scale(1.06F, 1.0F, 0.90F);
-      out.mesh(getUnitCylinder(), rim, bronze * QVector3D(0.94F, 0.92F, 0.88F),
-               nullptr, 1.0F);
-
-      QVector3D const crest_front = headPoint(QVector3D(0.0F, 0.92F, 0.82F));
-      QVector3D const crest_back = headPoint(QVector3D(0.0F, 0.92F, -0.90F));
-      QMatrix4x4 crest =
-          cylinderBetween(ctx.model, crest_back, crest_front, head_r * 0.14F);
-      crest.scale(0.54F, 1.0F, 1.0F);
-      out.mesh(getUnitCylinder(), crest,
-               tinned_highlight * QVector3D(0.94F, 0.96F, 1.02F), nullptr,
-               1.0F);
-    };
-
-    draw_montefortino(v.palette.metal);
+    // Use montefortino helmet from equipment registry
+    auto helmet = registry.get(EquipmentCategory::Helmet, "montefortino");
+    if (helmet) {
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
+    }
   }
 
   void draw_armorOverlay(const DrawContext &ctx, const HumanoidVariant &v,
@@ -658,41 +580,6 @@ private:
     apply_color(style.metal_color, variant.palette.metal);
     apply_color(style.wood_color, variant.palette.wood);
   }
-
-  void draw_headwrap(const DrawContext &ctx, const HumanoidVariant &v,
-                     const HumanoidPose &pose, ISubmitter &out) const {
-    QVector3D const cloth_color =
-        saturate_color(v.palette.cloth * QVector3D(0.9F, 1.05F, 1.05F));
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
-    }
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    QVector3D const band_top = headPoint(QVector3D(0.0F, 0.70F, 0.0F));
-    QVector3D const band_bot = headPoint(QVector3D(0.0F, 0.30F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, band_bot, band_top, head_r * 1.08F),
-             cloth_color, nullptr, 1.0F);
-
-    QVector3D const knot_center = headPoint(QVector3D(0.10F, 0.60F, 0.72F));
-    QMatrix4x4 knot_m = ctx.model;
-    knot_m.translate(knot_center);
-    knot_m.scale(head_r * 0.32F);
-    out.mesh(getUnitSphere(), knot_m, cloth_color * 1.05F, nullptr, 1.0F);
-
-    QVector3D const tail_top = knot_center + head.right * (-0.08F) +
-                               head.up * (-0.05F) + head.forward * (-0.06F);
-    QVector3D const tail_bot = tail_top + head.right * 0.02F +
-                               head.up * (-0.28F) + head.forward * (-0.08F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, tail_top, tail_bot, head_r * 0.28F),
-             cloth_color * QVector3D(0.92F, 0.98F, 1.05F), nullptr, 1.0F);
-  }
 };
 
 void registerArcherRenderer(Render::GL::EntityRendererRegistry &registry) {

+ 7 - 100
render/entity/nations/carthage/horse_swordsman_renderer.cpp

@@ -2,6 +2,7 @@
 #include "../../../../game/core/component.h"
 #include "../../../../game/core/entity.h"
 #include "../../../../game/systems/nation_id.h"
+#include "../../../equipment/equipment_registry.h"
 #include "../../../geom/math_utils.h"
 #include "../../../geom/transforms.h"
 #include "../../../gl/backend.h"
@@ -274,106 +275,12 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
-    }
-
-    const QVector3D steel_color = v.palette.metal * STEEL_TINT;
-    float const helm_r = head_r * 1.15F;
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    auto ring = [&](float y_offset, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.015F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, a, b, helm_r * 1.02F), col, nullptr,
-               1.0F);
-    };
-
-    QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.20F, 0.0F));
-    QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.40F, 0.0F));
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_bot, helm_top, helm_r),
-             steel_color, nullptr, 1.0F);
-
-    QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.98F),
-             steel_color * 1.05F, nullptr, 1.0F);
-
-    const QVector3D ring_color = steel_color * 1.08F;
-    ring(1.25F, ring_color);
-    ring(0.50F, ring_color);
-    ring(-0.05F, ring_color);
-
-    float const visor_forward = helm_r * 0.72F;
-    QVector3D const visor_center =
-        headPoint(QVector3D(0.0F, 0.15F, visor_forward / head_r));
-    QVector3D const lateral = head.right * (helm_r * 0.35F);
-    QVector3D const vertical = head.up * (helm_r * 0.25F);
-    static const QVector3D visor_color(0.1F, 0.1F, 0.1F);
-
-    QVector3D const visor_hl = visor_center - lateral;
-    QVector3D const visor_hr = visor_center + lateral;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_hl, visor_hr, head_r * 0.012F),
-             visor_color, nullptr, 1.0F);
-
-    QVector3D const visor_vt = visor_center + vertical;
-    QVector3D const visor_vb = visor_center - vertical;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_vb, visor_vt, head_r * 0.012F),
-             visor_color, nullptr, 1.0F);
-
-    auto draw_breathing_hole = [&](float x_scale, float y_offset) {
-      QVector3D const pos =
-          headPoint(QVector3D(x_scale * (helm_r / head_r), y_offset / head_r,
-                              visor_forward / head_r * 0.97F));
-      QMatrix4x4 m = ctx.model;
-      m.translate(pos);
-      m.scale(0.010F);
-      out.mesh(getUnitSphere(), m, QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-    };
-
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(+0.50F, 0.05F - i * 0.10F);
-    }
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(-0.50F, 0.05F - i * 0.10F);
-    }
-
-    QVector3D const plume_base = headPoint(QVector3D(0.0F, 1.50F, 0.0F));
-    const QVector3D brass_color = v.palette.metal * BRASS_TINT;
-
-    QMatrix4x4 plume = ctx.model;
-    plume.translate(plume_base);
-    plume.scale(0.030F, 0.015F, 0.030F);
-    out.mesh(getUnitSphere(), plume, brass_color * 1.2F, nullptr, 1.0F);
-
-    QVector3D const plume_forward = head.forward * -0.020F;
-    QVector3D const plume_up = head.up;
-
-    for (int i = 0; i < 5; ++i) {
-      float const offset = i * 0.025F;
-      QVector3D const base =
-          plume_base + plume_forward + head.right * (offset * 0.5F);
-      QVector3D const feather_start = base + plume_up * 0.005F;
-      QVector3D const feather_end = feather_start +
-                                    plume_up * (0.15F - i * 0.015F) +
-                                    head.forward * (-0.08F + offset * 0.3F);
-
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, feather_start, feather_end,
-                               head_r * 0.008F),
-               v.palette.cloth * (1.1F - i * 0.05F), nullptr, 1.0F);
+    // Use montefortino helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "montefortino");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
   }
 

+ 7 - 50
render/entity/nations/carthage/spearman_renderer.cpp

@@ -1,6 +1,7 @@
 #include "spearman_renderer.h"
 #include "../../../../game/core/component.h"
 #include "../../../../game/systems/nation_id.h"
+#include "../../../equipment/equipment_registry.h"
 #include "../../../geom/math_utils.h"
 #include "../../../geom/transforms.h"
 #include "../../../gl/backend.h"
@@ -263,56 +264,12 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
-    }
-
-    const QVector3D iron_color = v.palette.metal * IRON_TINT;
-    float const helm_r = head_r * 1.12F;
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.15F, 0.0F));
-    QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.25F, 0.0F));
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_bot, helm_top, helm_r), iron_color,
-             nullptr, 1.0F);
-
-    QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.32F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.96F),
-             iron_color * 1.04F, nullptr, 1.0F);
-
-    auto ring = [&](float y_offset, float radius_scale, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.012F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, a, b, helm_r * radius_scale), col,
-               nullptr, 1.0F);
-    };
-
-    ring(0.95F, 1.01F, iron_color * 1.06F);
-    ring(-0.02F, 1.01F, iron_color * 1.06F);
-
-    float const visor_forward = helm_r * 0.68F;
-
-    for (int i = 0; i < 3; ++i) {
-      float const y_offset = 0.10F + (0.18F - i * 0.12F);
-      QVector3D const center =
-          headPoint(QVector3D(0.0F, y_offset, visor_forward / head_r));
-      QVector3D const lateral = head.right * (helm_r * 0.30F);
-      QVector3D const visor_l = center - lateral;
-      QVector3D const visor_r = center + lateral;
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, visor_l, visor_r, head_r * 0.010F),
-               DARK_METAL, nullptr, 1.0F);
+    // Use montefortino helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "montefortino");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
   }
 

+ 7 - 90
render/entity/nations/carthage/swordsman_renderer.cpp

@@ -1,6 +1,7 @@
 #include "swordsman_renderer.h"
 #include "../../../../game/core/component.h"
 #include "../../../../game/systems/nation_id.h"
+#include "../../../equipment/equipment_registry.h"
 #include "../../../geom/math_utils.h"
 #include "../../../geom/transforms.h"
 #include "../../../gl/backend.h"
@@ -212,97 +213,13 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
+    // Use montefortino helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "montefortino");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
-
-    QVector3D const steel_color =
-        v.palette.metal * QVector3D(0.95F, 0.96F, 1.0F);
-    float const helm_r = head_r * 1.15F;
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    auto ring = [&](float y_offset, float radius_scale, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.015F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, a, b, helm_r * radius_scale), col,
-               nullptr, 1.0F);
-    };
-
-    QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.20F, 0.0F));
-    QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.40F, 0.0F));
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_bot, helm_top, helm_r),
-             steel_color, nullptr, 1.0F);
-
-    QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.98F),
-             steel_color * 1.05F, nullptr, 1.0F);
-
-    ring(1.25F, 1.02F, steel_color * 1.08F);
-    ring(0.50F, 1.02F, steel_color * 1.08F);
-    ring(-0.05F, 1.02F, steel_color * 1.08F);
-
-    float const visor_forward = helm_r * 0.72F;
-    float const visor_y = 0.15F;
-
-    QVector3D const visor_center =
-        headPoint(QVector3D(0.0F, visor_y, visor_forward / head_r));
-    QVector3D const lateral = head.right * (helm_r * 0.35F);
-    QVector3D const vertical = head.up * (helm_r * 0.25F);
-
-    QVector3D const visor_hl = visor_center - lateral;
-    QVector3D const visor_hr = visor_center + lateral;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_hl, visor_hr, head_r * 0.012F),
-             QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-
-    QVector3D const visor_vt = visor_center + vertical;
-    QVector3D const visor_vb = visor_center - vertical;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_vb, visor_vt, head_r * 0.012F),
-             QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-
-    auto draw_breathing_hole = [&](float x_scale, float y_offset) {
-      QVector3D const pos =
-          headPoint(QVector3D(x_scale * (helm_r / head_r), y_offset / head_r,
-                              visor_forward / head_r * 0.97F));
-      QMatrix4x4 m = ctx.model;
-      m.translate(pos);
-      m.scale(0.010F);
-      out.mesh(getUnitSphere(), m, QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-    };
-
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(+0.50F, 0.05F - i * 0.10F);
-    }
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(-0.50F, 0.05F - i * 0.10F);
-    }
-
-    QVector3D const brass_color = v.palette.metal * QVector3D(1.3F, 1.1F, 0.7F);
-    QVector3D const cross_center =
-        headPoint(QVector3D(0.0F, 0.60F, (helm_r * 0.75F) / head_r));
-    QVector3D const cross_h1 = cross_center - head.right * 0.04F;
-    QVector3D const cross_h2 = cross_center + head.right * 0.04F;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, cross_h1, cross_h2, head_r * 0.008F),
-             brass_color, nullptr, 1.0F);
-
-    QVector3D const cross_v1 = cross_center - head.up * 0.04F;
-    QVector3D const cross_v2 = cross_center + head.up * 0.04F;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, cross_v1, cross_v2, head_r * 0.008F),
-             brass_color, nullptr, 1.0F);
   }
 
   void draw_armorOverlay(const DrawContext &ctx, const HumanoidVariant &v,

+ 6 - 96
render/entity/nations/kingdom/archer_renderer.cpp

@@ -322,68 +322,13 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    using HP = HumanProportions;
-
-    auto const &style = resolve_style(ctx);
-    if (!style.show_helmet) {
-      if (style.attachment_profile == std::string(k_attachment_headwrap)) {
-        draw_headwrap(ctx, v, pose, out);
-      }
-      return;
-    }
-
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
+    // Use Kingdom Light helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "kingdom_light");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
-
-    QVector3D const steel_color =
-        v.palette.metal * QVector3D(0.88F, 0.90F, 0.95F);
-    QVector3D const steel_dark = steel_color * 0.82F;
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    float const bowl_scale = 1.06F;
-    QVector3D const bowl_top = headPoint(QVector3D(0.0F, 1.10F, 0.0F));
-    QVector3D const bowl_bot = headPoint(QVector3D(0.0F, 0.15F, 0.0F));
-    float const bowl_r = head_r * bowl_scale;
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, bowl_bot, bowl_top, bowl_r),
-             steel_color, nullptr, 1.0F);
-
-    QMatrix4x4 cap_m = ctx.model;
-    cap_m.translate(bowl_top);
-    cap_m.scale(bowl_r * 0.92F, head_r * 0.28F, bowl_r * 0.92F);
-    out.mesh(getUnitSphere(), cap_m, steel_color * 1.05F, nullptr, 1.0F);
-
-    QVector3D const brim_top = headPoint(QVector3D(0.0F, 0.18F, 0.0F));
-    QVector3D const brim_bot = headPoint(QVector3D(0.0F, 0.08F, 0.0F));
-    float const brim_r = head_r * 1.42F;
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, brim_bot, brim_top, brim_r), steel_dark,
-             nullptr, 1.0F);
-
-    auto ring = [&](float y_offset, float radius_scale, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.010F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, a, b, head_r * radius_scale), col,
-               nullptr, 1.0F);
-    };
-
-    ring(0.13F, 1.42F * 1.01F, steel_color);
-
-    QMatrix4x4 rivet_m = ctx.model;
-    rivet_m.translate(headPoint(QVector3D(0.0F, 1.15F, 0.0F)));
-    rivet_m.scale(0.015F);
-    out.mesh(getUnitSphere(), rivet_m, steel_color * 1.15F, nullptr, 1.0F);
   }
 
   void draw_armorOverlay(const DrawContext &ctx, const HumanoidVariant &v,
@@ -590,41 +535,6 @@ private:
     apply_color(style.metal_color, variant.palette.metal);
     apply_color(style.wood_color, variant.palette.wood);
   }
-
-  void draw_headwrap(const DrawContext &ctx, const HumanoidVariant &v,
-                     const HumanoidPose &pose, ISubmitter &out) const {
-    QVector3D const cloth_color =
-        saturate_color(v.palette.cloth * QVector3D(0.9F, 1.05F, 1.05F));
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
-    }
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    QVector3D const band_top = headPoint(QVector3D(0.0F, 0.70F, 0.0F));
-    QVector3D const band_bot = headPoint(QVector3D(0.0F, 0.30F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, band_bot, band_top, head_r * 1.08F),
-             cloth_color, nullptr, 1.0F);
-
-    QVector3D const knot_center = headPoint(QVector3D(0.10F, 0.60F, 0.72F));
-    QMatrix4x4 knot_m = ctx.model;
-    knot_m.translate(knot_center);
-    knot_m.scale(head_r * 0.32F);
-    out.mesh(getUnitSphere(), knot_m, cloth_color * 1.05F, nullptr, 1.0F);
-
-    QVector3D const tail_top = knot_center + head.right * (-0.08F) +
-                               head.up * (-0.05F) + head.forward * (-0.06F);
-    QVector3D const tail_bot = tail_top + head.right * 0.02F +
-                               head.up * (-0.28F) + head.forward * (-0.08F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, tail_top, tail_bot, head_r * 0.28F),
-             cloth_color * QVector3D(0.92F, 0.98F, 1.05F), nullptr, 1.0F);
-  }
 };
 
 void registerArcherRenderer(Render::GL::EntityRendererRegistry &registry) {

+ 7 - 94
render/entity/nations/kingdom/horse_swordsman_renderer.cpp

@@ -28,6 +28,7 @@
 #include <algorithm>
 #include <cmath>
 #include <cstdint>
+#include "../../../equipment/equipment_registry.h"
 
 namespace Render::GL::Kingdom {
 
@@ -274,100 +275,12 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    using HP = HumanProportions;
-
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
-    }
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    const QVector3D steel_color = v.palette.metal * STEEL_TINT;
-
-    float helm_r = head_r * 1.15F;
-
-    auto ring = [&](float y_offset, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.015F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, helm_r * 1.02F), col,
-               nullptr, 1.0F);
-    };
-
-    QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.20F, 0.0F));
-    QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.40F, 0.0F));
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_bot, helm_top, helm_r),
-             steel_color, nullptr, 1.0F);
-
-    QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.98F),
-             steel_color * 1.05F, nullptr, 1.0F);
-
-    const QVector3D ring_color = steel_color * 1.08F;
-    ring(1.25F, ring_color);
-    ring(0.50F, ring_color);
-    ring(-0.05F, ring_color);
-
-    const float visor_y_offset = 0.15F;
-    const float visor_z_norm = 0.72F * helm_r / head_r;
-    static const QVector3D visor_color(0.1F, 0.1F, 0.1F);
-
-    QVector3D const visor_center = headPoint(QVector3D(0.0F, visor_y_offset, visor_z_norm));
-    QVector3D const visor_hl = visor_center - head.right * (helm_r * 0.35F);
-    QVector3D const visor_hr = visor_center + head.right * (helm_r * 0.35F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_hl, visor_hr, 0.012F),
-             visor_color, nullptr, 1.0F);
-
-    QVector3D const visor_vt = visor_center + head.up * (helm_r * 0.25F);
-    QVector3D const visor_vb = visor_center - head.up * (helm_r * 0.25F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_vb, visor_vt, 0.012F),
-             visor_color, nullptr, 1.0F);
-
-    auto draw_breathing_hole = [&](float x_sign, float y_offset) {
-      QVector3D const pos = headPoint(QVector3D(x_sign * 0.50F * helm_r / head_r, y_offset, 0.70F * helm_r / head_r));
-      QMatrix4x4 m = ctx.model;
-      m.translate(pos);
-      m.scale(0.010F);
-      out.mesh(getUnitSphere(), m, QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-    };
-
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(1.0F, 0.05F - i * 0.10F);
-    }
-
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(-1.0F, 0.05F - i * 0.10F);
-    }
-
-    const QVector3D plume_base = headPoint(QVector3D(0.0F, 1.50F, 0.0F));
-    const QVector3D brass_color = v.palette.metal * BRASS_TINT;
-
-    QMatrix4x4 plume = ctx.model;
-    plume.translate(plume_base);
-    plume.scale(0.030F, 0.015F, 0.030F);
-    out.mesh(getUnitSphere(), plume, brass_color * 1.2F, nullptr, 1.0F);
-
-    for (int i = 0; i < 5; ++i) {
-      float const offset = i * 0.025F;
-      QVector3D const feather_start =
-          plume_base + QVector3D(0, 0.005F, -0.020F + offset * 0.5F);
-      QVector3D const feather_end =
-          feather_start +
-          QVector3D(0, 0.15F - i * 0.015F, -0.08F + offset * 0.3F);
-
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, feather_start, feather_end, 0.008F),
-               v.palette.cloth * (1.1F - i * 0.05F), nullptr, 1.0F);
+    // Use Kingdom Heavy helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "kingdom_heavy");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
   }
 

+ 7 - 52
render/entity/nations/kingdom/spearman_renderer.cpp

@@ -29,6 +29,7 @@
 #include <string>
 #include <string_view>
 #include <unordered_map>
+#include "../../../equipment/equipment_registry.h"
 
 namespace Render::GL::Kingdom {
 
@@ -263,58 +264,12 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    using HP = HumanProportions;
-
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
-    }
-
-    QVector3D const iron_color = v.palette.metal * IRON_TINT;
-    float const helm_r = head_r * 1.12F;
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.15F, 0.0F));
-    QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.25F, 0.0F));
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_bot, helm_top, helm_r), iron_color,
-             nullptr, 1.0F);
-
-    QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.32F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.96F),
-             iron_color * 1.04F, nullptr, 1.0F);
-
-    auto ring = [&](float y_offset, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.012F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, a, b, helm_r * 1.01F), col, nullptr,
-               1.0F);
-    };
-
-    ring(0.95F, iron_color * 1.06F);
-    ring(-0.02F, iron_color * 1.06F);
-
-    float const visor_forward = helm_r * 0.68F;
-
-    for (int i = 0; i < 3; ++i) {
-      float const y_offset = 0.10F + (0.18F - i * 0.12F);
-      QVector3D const center =
-          headPoint(QVector3D(0.0F, y_offset, visor_forward / head_r));
-      QVector3D const lateral = head.right * (helm_r * 0.30F);
-      QVector3D const visor_l = center - lateral;
-      QVector3D const visor_r = center + lateral;
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, visor_l, visor_r, head_r * 0.010F),
-               DARK_METAL, nullptr, 1.0F);
+    // Use Kingdom heavy helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "kingdom_heavy");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
   }
 

+ 7 - 92
render/entity/nations/kingdom/swordsman_renderer.cpp

@@ -31,6 +31,7 @@
 #include <optional>
 #include <string>
 #include <string_view>
+#include "../../../equipment/equipment_registry.h"
 
 namespace Render::GL::Kingdom {
 
@@ -212,99 +213,13 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
+    // Use Kingdom Heavy helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "kingdom_heavy");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
-
-    QVector3D const steel_color =
-        v.palette.metal * QVector3D(0.95F, 0.96F, 1.0F);
-    QVector3D const brass_color = v.palette.metal * QVector3D(1.3F, 1.1F, 0.7F);
-
-    float const helm_r = head_r * 1.15F;
-    float const helm_ratio = helm_r / head_r;
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    auto ring = [&](float y_offset, float radius_scale, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.015F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, a, b, helm_r * radius_scale), col,
-               nullptr, 1.0F);
-    };
-
-    QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.20F, 0.0F));
-    QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.40F, 0.0F));
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_bot, helm_top, helm_r),
-             steel_color, nullptr, 1.0F);
-
-    QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.98F),
-             steel_color * 1.05F, nullptr, 1.0F);
-
-    ring(1.25F, 1.02F, steel_color * 1.08F);
-    ring(0.50F, 1.02F, steel_color * 1.08F);
-    ring(-0.05F, 1.02F, steel_color * 1.08F);
-
-    float const visor_y = 0.15F;
-    float const visor_forward = helm_r * 0.72F;
-    float const visor_forward_norm = visor_forward / head_r;
-
-    QVector3D const visor_center =
-        headPoint(QVector3D(0.0F, visor_y, visor_forward_norm));
-    QVector3D const lateral = head.right * (helm_r * 0.35F);
-    QVector3D const vertical = head.up * (helm_r * 0.25F);
-
-    QVector3D const visor_hl = visor_center - lateral;
-    QVector3D const visor_hr = visor_center + lateral;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_hl, visor_hr, head_r * 0.012F),
-             QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-
-    QVector3D const visor_vt = visor_center + vertical;
-    QVector3D const visor_vb = visor_center - vertical;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_vb, visor_vt, head_r * 0.012F),
-             QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-
-    auto draw_breathing_hole = [&](float x_norm, float y_norm) {
-      QVector3D const pos = headPoint(
-          QVector3D(x_norm * helm_ratio, y_norm, visor_forward_norm * 0.97F));
-      QMatrix4x4 m = ctx.model;
-      m.translate(pos);
-      m.scale(0.010F);
-      out.mesh(getUnitSphere(), m, QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-    };
-
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(+0.50F, 0.05F - i * 0.10F);
-    }
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(-0.50F, 0.05F - i * 0.10F);
-    }
-
-    QVector3D const cross_center =
-        headPoint(QVector3D(0.0F, 0.60F, helm_ratio * 0.75F));
-    QVector3D const cross_h1 = cross_center - head.right * 0.04F;
-    QVector3D const cross_h2 = cross_center + head.right * 0.04F;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, cross_h1, cross_h2, head_r * 0.008F),
-             brass_color, nullptr, 1.0F);
-
-    QVector3D const cross_v1 = cross_center - head.up * 0.04F;
-    QVector3D const cross_v2 = cross_center + head.up * 0.04F;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, cross_v1, cross_v2, head_r * 0.008F),
-             brass_color, nullptr, 1.0F);
   }
 
   void draw_armorOverlay(const DrawContext &ctx, const HumanoidVariant &v,

+ 12 - 109
render/entity/nations/roman/archer_renderer.cpp

@@ -325,88 +325,25 @@ public:
     using HP = HumanProportions;
 
     auto const &style = resolve_style(ctx);
+    auto &registry = EquipmentRegistry::instance();
+    HumanoidAnimationContext anim_ctx{};
+
     if (!style.show_helmet) {
       if (style.attachment_profile == std::string(k_attachment_headwrap)) {
-        draw_headwrap(ctx, v, pose, out);
+        // Use headwrap helmet from equipment registry
+        auto headwrap = registry.get(EquipmentCategory::Helmet, "headwrap");
+        if (headwrap) {
+          headwrap->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
+        }
       }
       return;
     }
 
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
+    // Use Roman light helmet from equipment registry
+    auto helmet = registry.get(EquipmentCategory::Helmet, "roman_light");
+    if (helmet) {
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    QVector3D const helmet_color =
-        v.palette.metal * QVector3D(1.08F, 0.98F, 0.78F);
-    QVector3D const helmet_accent = helmet_color * 1.12F;
-
-    QVector3D const helmet_top = headPoint(QVector3D(0.0F, 1.28F, 0.0F));
-    QVector3D const helmet_bot = headPoint(QVector3D(0.0F, 0.08F, 0.0F));
-    float const helmet_r = head_r * 1.10F;
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helmet_bot, helmet_top, helmet_r),
-             helmet_color, nullptr, 1.0F);
-
-    QVector3D const apex_pos = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
-    out.mesh(getUnitCone(),
-             coneFromTo(ctx.model, helmet_top, apex_pos, helmet_r * 0.97F),
-             helmet_accent, nullptr, 1.0F);
-
-    auto ring = [&](float y_offset, float r_scale, float h, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      QVector3D const a = center + head.up * (h * 0.5F);
-      QVector3D const b = center - head.up * (h * 0.5F);
-      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, helmet_r * r_scale), col,
-               nullptr, 1.0F);
-    };
-
-    ring(0.35F, 1.07F, 0.020F, helmet_accent);
-    ring(0.65F, 1.03F, 0.015F, helmet_color * 1.05F);
-    ring(0.95F, 1.01F, 0.012F, helmet_color * 1.03F);
-
-    float const cheek_w = head_r * 0.48F;
-    QVector3D const cheek_top = headPoint(QVector3D(0.0F, 0.22F, 0.0F));
-    QVector3D const cheek_bot = headPoint(QVector3D(0.0F, -0.42F, 0.0F));
-
-    QVector3D const cheek_ltop = cheek_top + head.right * (-cheek_w / head_r) + head.forward * 0.38F;
-    QVector3D const cheek_lbot = cheek_bot + head.right * (-cheek_w * 0.82F / head_r) + head.forward * 0.28F;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, cheek_lbot, cheek_ltop, 0.028F),
-             helmet_color * 0.96F, nullptr, 1.0F);
-
-    QVector3D const cheek_rtop = cheek_top + head.right * (cheek_w / head_r) + head.forward * 0.38F;
-    QVector3D const cheek_rbot = cheek_bot + head.right * (cheek_w * 0.82F / head_r) + head.forward * 0.28F;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, cheek_rbot, cheek_rtop, 0.028F),
-             helmet_color * 0.96F, nullptr, 1.0F);
-
-    QVector3D const neck_guard_top = headPoint(QVector3D(0.0F, 0.03F, -0.82F));
-    QVector3D const neck_guard_bot = headPoint(QVector3D(0.0F, -0.32F, -0.88F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, neck_guard_bot, neck_guard_top,
-                             helmet_r * 0.88F),
-             helmet_color * 0.93F, nullptr, 1.0F);
-
-    QVector3D const crest_base = apex_pos;
-    QVector3D const crest_mid = crest_base + head.up * 0.09F;
-    QVector3D const crest_top = crest_mid + head.up * 0.12F;
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, crest_base, crest_mid, 0.018F),
-             helmet_accent, nullptr, 1.0F);
-
-    out.mesh(getUnitCone(), coneFromTo(ctx.model, crest_mid, crest_top, 0.042F),
-             QVector3D(0.88F, 0.18F, 0.18F), nullptr, 1.0F);
-
-    out.mesh(getUnitSphere(), sphereAt(ctx.model, crest_top, 0.020F),
-             helmet_accent, nullptr, 1.0F);
   }
 
   void draw_armorOverlay(const DrawContext &ctx, const HumanoidVariant &v,
@@ -679,40 +616,6 @@ private:
     apply_color(style.metal_color, variant.palette.metal);
     apply_color(style.wood_color, variant.palette.wood);
   }
-
-  void draw_headwrap(const DrawContext &ctx, const HumanoidVariant &v,
-                     const HumanoidPose &pose, ISubmitter &out) const {
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
-    }
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    QVector3D const cloth_color =
-        saturate_color(v.palette.cloth * QVector3D(0.9F, 1.05F, 1.05F));
-
-    QVector3D const band_top = headPoint(QVector3D(0.0F, 0.70F, 0.0F));
-    QVector3D const band_bot = headPoint(QVector3D(0.0F, 0.30F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, band_bot, band_top, head_r * 1.08F),
-             cloth_color, nullptr, 1.0F);
-
-    QVector3D const knot_center = headPoint(QVector3D(0.10F, 0.60F, 0.72F));
-    QMatrix4x4 knot_m = ctx.model;
-    knot_m.translate(knot_center);
-    knot_m.scale(head_r * 0.32F);
-    out.mesh(getUnitSphere(), knot_m, cloth_color * 1.05F, nullptr, 1.0F);
-
-    QVector3D const tail_top = knot_center + head.right * (-0.08F) + head.up * (-0.05F) + head.forward * (-0.06F);
-    QVector3D const tail_bot = tail_top + head.right * 0.02F + head.up * (-0.28F) + head.forward * (-0.08F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, tail_top, tail_bot, head_r * 0.28F),
-             cloth_color * QVector3D(0.92F, 0.98F, 1.05F), nullptr, 1.0F);
-  }
 };
 
 void registerArcherRenderer(Render::GL::EntityRendererRegistry &registry) {

+ 7 - 94
render/entity/nations/roman/horse_swordsman_renderer.cpp

@@ -2,6 +2,7 @@
 #include "../../../../game/core/component.h"
 #include "../../../../game/core/entity.h"
 #include "../../../../game/systems/nation_id.h"
+#include "../../../equipment/equipment_registry.h"
 #include "../../../geom/math_utils.h"
 #include "../../../geom/transforms.h"
 #include "../../../gl/backend.h"
@@ -274,100 +275,12 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    using HP = HumanProportions;
-
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
-    }
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    const QVector3D steel_color = v.palette.metal * STEEL_TINT;
-
-    float helm_r = head_r * 1.15F;
-
-    auto ring = [&](float y_offset, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.015F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, helm_r * 1.02F), col,
-               nullptr, 1.0F);
-    };
-
-    QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.20F, 0.0F));
-    QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.40F, 0.0F));
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_bot, helm_top, helm_r),
-             steel_color, nullptr, 1.0F);
-
-    QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.98F),
-             steel_color * 1.05F, nullptr, 1.0F);
-
-    const QVector3D ring_color = steel_color * 1.08F;
-    ring(1.25F, ring_color);
-    ring(0.50F, ring_color);
-    ring(-0.05F, ring_color);
-
-    const float visor_y_offset = 0.15F;
-    const float visor_z_norm = 0.72F * helm_r / head_r;
-    static const QVector3D visor_color(0.1F, 0.1F, 0.1F);
-
-    QVector3D const visor_center = headPoint(QVector3D(0.0F, visor_y_offset, visor_z_norm));
-    QVector3D const visor_hl = visor_center - head.right * (helm_r * 0.35F);
-    QVector3D const visor_hr = visor_center + head.right * (helm_r * 0.35F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_hl, visor_hr, 0.012F),
-             visor_color, nullptr, 1.0F);
-
-    QVector3D const visor_vt = visor_center + head.up * (helm_r * 0.25F);
-    QVector3D const visor_vb = visor_center - head.up * (helm_r * 0.25F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_vb, visor_vt, 0.012F),
-             visor_color, nullptr, 1.0F);
-
-    auto draw_breathing_hole = [&](float x_sign, float y_offset) {
-      QVector3D const pos = headPoint(QVector3D(x_sign * 0.50F * helm_r / head_r, y_offset, 0.70F * helm_r / head_r));
-      QMatrix4x4 m = ctx.model;
-      m.translate(pos);
-      m.scale(0.010F);
-      out.mesh(getUnitSphere(), m, QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-    };
-
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(1.0F, 0.05F - i * 0.10F);
-    }
-
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(-1.0F, 0.05F - i * 0.10F);
-    }
-
-    const QVector3D plume_base = headPoint(QVector3D(0.0F, 1.50F, 0.0F));
-    const QVector3D brass_color = v.palette.metal * BRASS_TINT;
-
-    QMatrix4x4 plume = ctx.model;
-    plume.translate(plume_base);
-    plume.scale(0.030F, 0.015F, 0.030F);
-    out.mesh(getUnitSphere(), plume, brass_color * 1.2F, nullptr, 1.0F);
-
-    for (int i = 0; i < 5; ++i) {
-      float const offset = i * 0.025F;
-      QVector3D const feather_start =
-          plume_base + QVector3D(0, 0.005F, -0.020F + offset * 0.5F);
-      QVector3D const feather_end =
-          feather_start +
-          QVector3D(0, 0.15F - i * 0.015F, -0.08F + offset * 0.3F);
-
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, feather_start, feather_end, 0.008F),
-               v.palette.cloth * (1.1F - i * 0.05F), nullptr, 1.0F);
+    // Use Roman heavy helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "roman_heavy");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
   }
 

+ 7 - 48
render/entity/nations/roman/spearman_renderer.cpp

@@ -1,6 +1,7 @@
 #include "spearman_renderer.h"
 #include "../../../../game/core/component.h"
 #include "../../../../game/systems/nation_id.h"
+#include "../../../equipment/equipment_registry.h"
 #include "../../../geom/math_utils.h"
 #include "../../../geom/transforms.h"
 #include "../../../gl/backend.h"
@@ -263,54 +264,12 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    using HP = HumanProportions;
-
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
-    }
-
-    const QVector3D iron_color = v.palette.metal * IRON_TINT;
-    const float helm_r = head_r * 1.12F;
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.15F, 0.0F));
-    QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.25F, 0.0F));
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_bot, helm_top, helm_r), iron_color,
-             nullptr, 1.0F);
-
-    QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.32F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.96F),
-             iron_color * 1.04F, nullptr, 1.0F);
-
-    auto ring = [&](float y_offset, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.015F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, helm_r * 1.01F), col,
-               nullptr, 1.0F);
-    };
-
-    ring(0.95F, iron_color * 1.06F);
-    ring(-0.02F, iron_color * 1.06F);
-
-    float const visor_z_norm = 0.68F * helm_r / head_r;
-    for (int i = 0; i < 3; ++i) {
-      float const y_offset = 0.10F + (0.18F - i * 0.12F);
-      QVector3D const visor_center = headPoint(QVector3D(0.0F, y_offset, visor_z_norm));
-      QVector3D const visor_l = visor_center - head.right * (helm_r * 0.30F);
-      QVector3D const visor_r = visor_center + head.right * (helm_r * 0.30F);
-      out.mesh(getUnitCylinder(),
-               cylinderBetween(ctx.model, visor_l, visor_r, 0.010F), DARK_METAL,
-               nullptr, 1.0F);
+    // Use Roman heavy helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "roman_heavy");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
   }
 

+ 7 - 87
render/entity/nations/roman/swordsman_renderer.cpp

@@ -1,6 +1,7 @@
 #include "swordsman_renderer.h"
 #include "../../../../game/core/component.h"
 #include "../../../../game/systems/nation_id.h"
+#include "../../../equipment/equipment_registry.h"
 #include "../../../geom/math_utils.h"
 #include "../../../geom/transforms.h"
 #include "../../../gl/backend.h"
@@ -212,94 +213,13 @@ public:
 
   void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
                   const HumanoidPose &pose, ISubmitter &out) const override {
-    using HP = HumanProportions;
-
-    const AttachmentFrame &head = pose.bodyFrames.head;
-    float const head_r = head.radius;
-    if (head_r <= 0.0F) {
-      return;
+    // Use Roman heavy helmet from equipment registry
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "roman_heavy");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.bodyFrames, v.palette, anim_ctx, out);
     }
-
-    auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
-      return frameLocalPosition(head, normalized);
-    };
-
-    QVector3D const steel_color =
-        v.palette.metal * QVector3D(0.95F, 0.96F, 1.0F);
-
-    float helm_r = head_r * 1.15F;
-
-    auto ring = [&](float y_offset, const QVector3D &col) {
-      QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
-      float const height = head_r * 0.015F;
-      QVector3D const a = center + head.up * (height * 0.5F);
-      QVector3D const b = center - head.up * (height * 0.5F);
-      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, helm_r * 1.02F), col,
-               nullptr, 1.0F);
-    };
-
-    QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.20F, 0.0F));
-    QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.40F, 0.0F));
-
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_bot, helm_top, helm_r),
-             steel_color, nullptr, 1.0F);
-
-    QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.98F),
-             steel_color * 1.05F, nullptr, 1.0F);
-
-    ring(1.25F, steel_color * 1.08F);
-    ring(0.50F, steel_color * 1.08F);
-    ring(-0.05F, steel_color * 1.08F);
-
-    float const visor_y_offset = 0.15F;
-    float const visor_z_norm = 0.72F * helm_r / head_r;
-    QVector3D const visor_center = headPoint(QVector3D(0.0F, visor_y_offset, visor_z_norm));
-
-    QVector3D const visor_hl = visor_center - head.right * (helm_r * 0.35F);
-    QVector3D const visor_hr = visor_center + head.right * (helm_r * 0.35F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_hl, visor_hr, 0.012F),
-             QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-
-    QVector3D const visor_vt = visor_center + head.up * (helm_r * 0.25F);
-    QVector3D const visor_vb = visor_center - head.up * (helm_r * 0.25F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, visor_vb, visor_vt, 0.012F),
-             QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-
-    auto draw_breathing_hole = [&](float x_sign, float y_offset) {
-      QVector3D const pos = headPoint(QVector3D(x_sign * 0.50F * helm_r / head_r, y_offset, 0.70F * helm_r / head_r));
-      QMatrix4x4 m = ctx.model;
-      m.translate(pos);
-      m.scale(0.010F);
-      out.mesh(getUnitSphere(), m, QVector3D(0.1F, 0.1F, 0.1F), nullptr, 1.0F);
-    };
-
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(1.0F, 0.05F - i * 0.10F);
-    }
-
-    for (int i = 0; i < 4; ++i) {
-      draw_breathing_hole(-1.0F, 0.05F - i * 0.10F);
-    }
-
-    QVector3D const cross_center = headPoint(QVector3D(0.0F, 0.60F, (helm_r * 0.75F) / head_r));
-    QVector3D const brass_color = v.palette.metal * QVector3D(1.3F, 1.1F, 0.7F);
-
-    QVector3D const cross_h1 = cross_center - head.right * 0.04F;
-    QVector3D const cross_h2 = cross_center + head.right * 0.04F;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, cross_h1, cross_h2, 0.008F),
-             brass_color, nullptr, 1.0F);
-
-    QVector3D const cross_v1 = cross_center - head.up * 0.04F;
-    QVector3D const cross_v2 = cross_center + head.up * 0.04F;
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, cross_v1, cross_v2, 0.008F),
-             brass_color, nullptr, 1.0F);
   }
 
   void draw_armorOverlay(const DrawContext &ctx, const HumanoidVariant &v,

+ 60 - 0
render/equipment/helmets/headwrap.cpp

@@ -0,0 +1,60 @@
+#include "headwrap.h"
+#include "../../geom/transforms.h"
+#include "../../gl/primitives.h"
+#include "../../humanoid/humanoid_math.h"
+#include "../../humanoid/rig.h"
+#include "../../humanoid/style_palette.h"
+#include "../../submitter.h"
+#include <QMatrix4x4>
+#include <QVector3D>
+
+namespace Render::GL {
+
+using Render::Geom::cylinderBetween;
+using Render::GL::Humanoid::saturate_color;
+
+void HeadwrapRenderer::render(const DrawContext &ctx, const BodyFrames &frames,
+                              const HumanoidPalette &palette,
+                              const HumanoidAnimationContext &anim,
+                              ISubmitter &submitter) {
+  (void)anim; // Unused
+
+  QVector3D const cloth_color =
+      saturate_color(palette.cloth * QVector3D(0.9F, 1.05F, 1.05F));
+  const AttachmentFrame &head = frames.head;
+  float const head_r = head.radius;
+  if (head_r <= 0.0F) {
+    return;
+  }
+
+  auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
+    return HumanoidRendererBase::frameLocalPosition(head, normalized);
+  };
+
+  // Main headwrap band
+  QVector3D const band_top = headPoint(QVector3D(0.0F, 0.70F, 0.0F));
+  QVector3D const band_bot = headPoint(QVector3D(0.0F, 0.30F, 0.0F));
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, band_bot, band_top,
+                                 head_r * 1.08F),
+                 cloth_color, nullptr, 1.0F);
+
+  // Knot on the side
+  QVector3D const knot_center = headPoint(QVector3D(0.10F, 0.60F, 0.72F));
+  QMatrix4x4 knot_m = ctx.model;
+  knot_m.translate(knot_center);
+  knot_m.scale(head_r * 0.32F);
+  submitter.mesh(getUnitSphere(), knot_m, cloth_color * 1.05F, nullptr, 1.0F);
+
+  // Hanging tail
+  QVector3D const tail_top = knot_center + head.right * (-0.08F) +
+                             head.up * (-0.05F) + head.forward * (-0.06F);
+  QVector3D const tail_bot = tail_top + head.right * 0.02F +
+                             head.up * (-0.28F) + head.forward * (-0.08F);
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, tail_top, tail_bot,
+                                 head_r * 0.28F),
+                 cloth_color * QVector3D(0.92F, 0.98F, 1.05F), nullptr, 1.0F);
+}
+
+} // namespace Render::GL

+ 19 - 0
render/equipment/helmets/headwrap.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include "../i_equipment_renderer.h"
+#include "../../humanoid/rig.h"
+#include "../../palette.h"
+
+namespace Render::GL {
+
+class HeadwrapRenderer : public IEquipmentRenderer {
+public:
+  HeadwrapRenderer() = default;
+
+  void render(const DrawContext &ctx, const BodyFrames &frames,
+              const HumanoidPalette &palette,
+              const HumanoidAnimationContext &anim,
+              ISubmitter &submitter) override;
+};
+
+} // namespace Render::GL

+ 139 - 0
render/equipment/helmets/kingdom_heavy_helmet.cpp

@@ -0,0 +1,139 @@
+#include "kingdom_heavy_helmet.h"
+#include "../../geom/transforms.h"
+#include "../../gl/primitives.h"
+#include "../../humanoid/humanoid_math.h"
+#include "../../humanoid/rig.h"
+#include "../../humanoid/style_palette.h"
+#include "../../submitter.h"
+#include <QMatrix4x4>
+#include <QVector3D>
+
+namespace Render::GL {
+
+using Render::Geom::cylinderBetween;
+using Render::Geom::sphereAt;
+using Render::GL::Humanoid::saturate_color;
+
+void KingdomHeavyHelmetRenderer::render(const DrawContext &ctx,
+                                        const BodyFrames &frames,
+                                        const HumanoidPalette &palette,
+                                        const HumanoidAnimationContext &anim,
+                                        ISubmitter &submitter) {
+  (void)anim; // Unused
+
+  const AttachmentFrame &head = frames.head;
+  float const head_r = head.radius;
+  if (head_r <= 0.0F) {
+    return;
+  }
+
+  auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
+    return HumanoidRendererBase::frameLocalPosition(head, normalized);
+  };
+
+  // Great helm steel colors
+  QVector3D const steel_color =
+      saturate_color(palette.metal * QVector3D(0.95F, 0.96F, 1.0F));
+  QVector3D const brass_color =
+      saturate_color(palette.metal * QVector3D(1.3F, 1.1F, 0.7F));
+  QVector3D const visor_color(0.1F, 0.1F, 0.1F);
+
+  float const helm_r = head_r * 1.15F;
+  float const helm_ratio = helm_r / head_r;
+
+  // Main enclosed helm body
+  QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.20F, 0.0F));
+  QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.40F, 0.0F));
+
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, helm_bot, helm_top, helm_r),
+                 steel_color, nullptr, 1.0F);
+
+  // Flat top (characteristic of great helm)
+  QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.98F),
+                 steel_color * 1.05F, nullptr, 1.0F);
+
+  // Decorative reinforcement bands
+  auto ring = [&](float y_offset, float radius_scale, const QVector3D &col) {
+    QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
+    float const height = head_r * 0.015F;
+    QVector3D const a = center + head.up * (height * 0.5F);
+    QVector3D const b = center - head.up * (height * 0.5F);
+    submitter.mesh(getUnitCylinder(),
+                   cylinderBetween(ctx.model, a, b, helm_r * radius_scale),
+                   col, nullptr, 1.0F);
+  };
+
+  ring(1.25F, 1.02F, steel_color * 1.08F);
+  ring(0.50F, 1.02F, steel_color * 1.08F);
+  ring(-0.05F, 1.02F, steel_color * 1.08F);
+
+  // Eye slit (horizontal bar)
+  float const visor_y = 0.15F;
+  float const visor_forward = helm_r * 0.72F;
+  float const visor_forward_norm = visor_forward / head_r;
+  QVector3D const visor_center =
+      headPoint(QVector3D(0.0F, visor_y, visor_forward_norm));
+
+  QVector3D const visor_hl = visor_center - head.right * (helm_r * 0.35F);
+  QVector3D const visor_hr = visor_center + head.right * (helm_r * 0.35F);
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, visor_hl, visor_hr,
+                                 head_r * 0.012F),
+                 visor_color, nullptr, 1.0F);
+
+  // Vertical nose guard
+  QVector3D const visor_vt = visor_center + head.up * (helm_r * 0.25F);
+  QVector3D const visor_vb = visor_center - head.up * (helm_r * 0.25F);
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, visor_vb, visor_vt,
+                                 head_r * 0.012F),
+                 visor_color, nullptr, 1.0F);
+
+  // Breathing holes (arranged in cross pattern)
+  auto draw_breathing_hole = [&](float x_norm, float y_norm) {
+    QVector3D const pos = headPoint(
+        QVector3D(x_norm * helm_ratio, y_norm, visor_forward_norm * 0.97F));
+    QMatrix4x4 m = ctx.model;
+    m.translate(pos);
+    m.scale(0.010F);
+    submitter.mesh(getUnitSphere(), m, visor_color, nullptr, 1.0F);
+  };
+
+  for (int i = 0; i < 4; ++i) {
+    draw_breathing_hole(+0.50F, 0.05F - i * 0.10F);
+    draw_breathing_hole(-0.50F, 0.05F - i * 0.10F);
+  }
+
+  // Heraldic cross on top (brass)
+  QVector3D const top_center = headPoint(QVector3D(0.0F, 1.45F, 0.0F));
+
+  QVector3D const cross_h1 = top_center - head.right * 0.05F;
+  QVector3D const cross_h2 = top_center + head.right * 0.05F;
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, cross_h1, cross_h2,
+                                 head_r * 0.010F),
+                 brass_color, nullptr, 1.0F);
+
+  QVector3D const cross_v1 = top_center - head.forward * 0.05F;
+  QVector3D const cross_v2 = top_center + head.forward * 0.05F;
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, cross_v1, cross_v2,
+                                 head_r * 0.010F),
+                 brass_color, nullptr, 1.0F);
+
+  // Face plate edge (slightly forward from main helm)
+  float const face_forward = helm_r * 0.68F;
+  QVector3D const face_top =
+      headPoint(QVector3D(0.0F, 0.40F, face_forward / head_r));
+  QVector3D const face_bot =
+      headPoint(QVector3D(0.0F, -0.15F, face_forward / head_r));
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, face_bot, face_top,
+                                 head_r * 0.015F),
+                 steel_color * 1.08F, nullptr, 1.0F);
+}
+
+} // namespace Render::GL

+ 21 - 0
render/equipment/helmets/kingdom_heavy_helmet.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#include "../i_equipment_renderer.h"
+#include "../../humanoid/rig.h"
+#include "../../palette.h"
+
+namespace Render::GL {
+
+// Kingdom heavy helmet - great helm style for knights and heavy infantry
+// Features enclosed design with eye slits and breathing holes
+class KingdomHeavyHelmetRenderer : public IEquipmentRenderer {
+public:
+  KingdomHeavyHelmetRenderer() = default;
+
+  void render(const DrawContext &ctx, const BodyFrames &frames,
+              const HumanoidPalette &palette,
+              const HumanoidAnimationContext &anim,
+              ISubmitter &submitter) override;
+};
+
+} // namespace Render::GL

+ 88 - 0
render/equipment/helmets/kingdom_light_helmet.cpp

@@ -0,0 +1,88 @@
+#include "kingdom_light_helmet.h"
+#include "../../geom/transforms.h"
+#include "../../gl/primitives.h"
+#include "../../humanoid/humanoid_math.h"
+#include "../../humanoid/rig.h"
+#include "../../humanoid/style_palette.h"
+#include "../../submitter.h"
+#include <QMatrix4x4>
+#include <QVector3D>
+
+namespace Render::GL {
+
+using Render::Geom::cylinderBetween;
+using Render::GL::Humanoid::saturate_color;
+
+void KingdomLightHelmetRenderer::render(const DrawContext &ctx,
+                                        const BodyFrames &frames,
+                                        const HumanoidPalette &palette,
+                                        const HumanoidAnimationContext &anim,
+                                        ISubmitter &submitter) {
+  (void)anim; // Unused
+
+  const AttachmentFrame &head = frames.head;
+  float const head_r = head.radius;
+  if (head_r <= 0.0F) {
+    return;
+  }
+
+  auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
+    return HumanoidRendererBase::frameLocalPosition(head, normalized);
+  };
+
+  // Kettle hat steel colors
+  QVector3D const steel_color =
+      saturate_color(palette.metal * QVector3D(0.88F, 0.90F, 0.95F));
+  QVector3D const steel_dark = steel_color * 0.82F;
+
+  float const bowl_scale = 1.06F;
+  float const bowl_r = head_r * bowl_scale;
+
+  // Main bowl
+  QVector3D const bowl_top = headPoint(QVector3D(0.0F, 1.10F, 0.0F));
+  QVector3D const bowl_bot = headPoint(QVector3D(0.0F, 0.15F, 0.0F));
+
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, bowl_bot, bowl_top, bowl_r),
+                 steel_color, nullptr, 1.0F);
+
+  // Rounded top cap
+  QMatrix4x4 cap_m = ctx.model;
+  cap_m.translate(bowl_top);
+  cap_m.scale(bowl_r * 0.92F, head_r * 0.28F, bowl_r * 0.92F);
+  submitter.mesh(getUnitSphere(), cap_m, steel_color * 1.05F, nullptr, 1.0F);
+
+  // Wide brim (characteristic kettle hat feature)
+  QVector3D const brim_top = headPoint(QVector3D(0.0F, 0.18F, 0.0F));
+  QVector3D const brim_bot = headPoint(QVector3D(0.0F, 0.08F, 0.0F));
+  float const brim_r = head_r * 1.42F; // Wide brim for protection
+
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, brim_bot, brim_top, brim_r),
+                 steel_dark, nullptr, 1.0F);
+
+  // Reinforcement rings on bowl
+  auto ring = [&](float y_offset, float radius_scale, const QVector3D &col) {
+    QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
+    float const height = head_r * 0.010F;
+    QVector3D const a = center + head.up * (height * 0.5F);
+    QVector3D const b = center - head.up * (height * 0.5F);
+    submitter.mesh(getUnitCylinder(),
+                   cylinderBetween(ctx.model, a, b, bowl_r * radius_scale),
+                   col, nullptr, 1.0F);
+  };
+
+  ring(0.90F, 1.01F, steel_color * 1.05F);
+  ring(0.50F, 1.01F, steel_color * 1.05F);
+  ring(0.20F, 1.01F, steel_color * 1.05F);
+
+  // Brim edge reinforcement
+  QVector3D const brim_edge_top = headPoint(QVector3D(0.0F, 0.09F, 0.0F));
+  QVector3D const brim_edge_bot = headPoint(QVector3D(0.0F, 0.07F, 0.0F));
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, brim_edge_bot, brim_edge_top,
+                                 brim_r * 1.01F),
+                 steel_color * 1.08F, nullptr, 1.0F);
+}
+
+} // namespace Render::GL

+ 21 - 0
render/equipment/helmets/kingdom_light_helmet.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#include "../i_equipment_renderer.h"
+#include "../../humanoid/rig.h"
+#include "../../palette.h"
+
+namespace Render::GL {
+
+// Kingdom light helmet - kettle hat style with wide brim
+// Used by archers and light infantry for protection from arrows
+class KingdomLightHelmetRenderer : public IEquipmentRenderer {
+public:
+  KingdomLightHelmetRenderer() = default;
+
+  void render(const DrawContext &ctx, const BodyFrames &frames,
+              const HumanoidPalette &palette,
+              const HumanoidAnimationContext &anim,
+              ISubmitter &submitter) override;
+};
+
+} // namespace Render::GL

+ 121 - 0
render/equipment/helmets/montefortino_helmet.cpp

@@ -0,0 +1,121 @@
+#include "montefortino_helmet.h"
+#include "../../geom/transforms.h"
+#include "../../gl/primitives.h"
+#include "../../humanoid/humanoid_math.h"
+#include "../../humanoid/rig.h"
+#include "../../humanoid/style_palette.h"
+#include "../../submitter.h"
+#include <QMatrix4x4>
+#include <QVector3D>
+#include <cmath>
+#include <numbers>
+
+namespace Render::GL {
+
+using Render::Geom::cylinderBetween;
+using Render::Geom::sphereAt;
+using Render::GL::Humanoid::saturate_color;
+
+void MontefortinoHelmetRenderer::render(const DrawContext &ctx,
+                                        const BodyFrames &frames,
+                                        const HumanoidPalette &palette,
+                                        const HumanoidAnimationContext &anim,
+                                        ISubmitter &submitter) {
+  (void)anim; // Unused
+
+  const AttachmentFrame &head = frames.head;
+  float const head_r = head.radius;
+  if (head_r <= 0.0F) {
+    return;
+  }
+
+  auto headPoint = [&](const QVector3D &norm) -> QVector3D {
+    return HumanoidRendererBase::frameLocalPosition(head, norm);
+  };
+
+  auto headTransform = [&](const QVector3D &norm, float scale) -> QMatrix4x4 {
+    return HumanoidRendererBase::makeFrameLocalTransform(ctx.model, head, norm,
+                                                         scale);
+  };
+
+  QVector3D const base_metal = palette.metal;
+  QVector3D bronze =
+      saturate_color(base_metal * QVector3D(1.22F, 1.04F, 0.70F));
+  QVector3D patina = saturate_color(bronze * QVector3D(0.88F, 0.96F, 0.92F));
+  QVector3D tinned_highlight =
+      saturate_color(bronze * QVector3D(1.12F, 1.08F, 1.04F));
+  QVector3D leather_band =
+      saturate_color(palette.leatherDark * QVector3D(1.10F, 0.96F, 0.80F));
+
+  // Draw leather cap
+  auto draw_leather_cap = [&]() {
+    QVector3D leather_brown = saturate_color(palette.leatherDark *
+                                             QVector3D(1.15F, 0.95F, 0.78F));
+    QVector3D leather_dark =
+        saturate_color(leather_brown * QVector3D(0.85F, 0.88F, 0.92F));
+    QVector3D bronze_stud =
+        saturate_color(palette.metal * QVector3D(1.20F, 1.02F, 0.70F));
+
+    QMatrix4x4 cap_transform =
+        headTransform(QVector3D(0.0F, 0.70F, 0.0F), 1.0F);
+    cap_transform.scale(0.92F, 0.55F, 0.88F);
+    submitter.mesh(getUnitSphere(), cap_transform, leather_brown, nullptr,
+                   1.0F);
+
+    QVector3D const band_top = headPoint(QVector3D(0.0F, 0.20F, 0.0F));
+    QVector3D const band_bot = headPoint(QVector3D(0.0F, 0.15F, 0.0F));
+
+    submitter.mesh(getUnitCylinder(),
+                   cylinderBetween(ctx.model, band_bot, band_top,
+                                   head_r * 1.02F),
+                   leather_dark, nullptr, 1.0F);
+
+    auto draw_stud = [&](float angle) {
+      QVector3D const stud_pos = headPoint(
+          QVector3D(std::sin(angle) * 1.03F, 0.175F, std::cos(angle) * 1.03F));
+      submitter.mesh(getUnitSphere(),
+                     sphereAt(ctx.model, stud_pos, head_r * 0.012F),
+                     bronze_stud, nullptr, 1.0F);
+    };
+
+    for (int i = 0; i < 4; ++i) {
+      float const angle = (i / 4.0F) * 2.0F * std::numbers::pi_v<float>;
+      draw_stud(angle);
+    }
+  };
+
+  draw_leather_cap();
+
+  // Top knob
+  QMatrix4x4 top_knob = headTransform(QVector3D(0.0F, 0.88F, 0.0F), 0.18F);
+  submitter.mesh(getUnitSphere(), top_knob, tinned_highlight, nullptr, 1.0F);
+
+  // Brow band
+  QVector3D const brow_top = headPoint(QVector3D(0.0F, 0.55F, 0.0F));
+  QVector3D const brow_bot = headPoint(QVector3D(0.0F, 0.42F, 0.0F));
+  QMatrix4x4 brow =
+      cylinderBetween(ctx.model, brow_bot, brow_top, head_r * 1.20F);
+  brow.scale(1.04F, 1.0F, 0.86F);
+  submitter.mesh(getUnitCylinder(), brow, leather_band, nullptr, 1.0F);
+
+  // Rim
+  QVector3D const rim_upper = headPoint(QVector3D(0.0F, 0.40F, 0.0F));
+  QVector3D const rim_lower = headPoint(QVector3D(0.0F, 0.30F, 0.0F));
+  QMatrix4x4 rim =
+      cylinderBetween(ctx.model, rim_lower, rim_upper, head_r * 1.30F);
+  rim.scale(1.06F, 1.0F, 0.90F);
+  submitter.mesh(getUnitCylinder(), rim,
+                 bronze * QVector3D(0.94F, 0.92F, 0.88F), nullptr, 1.0F);
+
+  // Crest
+  QVector3D const crest_front = headPoint(QVector3D(0.0F, 0.92F, 0.82F));
+  QVector3D const crest_back = headPoint(QVector3D(0.0F, 0.92F, -0.90F));
+  QMatrix4x4 crest =
+      cylinderBetween(ctx.model, crest_back, crest_front, head_r * 0.14F);
+  crest.scale(0.54F, 1.0F, 1.0F);
+  submitter.mesh(getUnitCylinder(), crest,
+                 tinned_highlight * QVector3D(0.94F, 0.96F, 1.02F), nullptr,
+                 1.0F);
+}
+
+} // namespace Render::GL

+ 19 - 0
render/equipment/helmets/montefortino_helmet.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include "../i_equipment_renderer.h"
+#include "../../humanoid/rig.h"
+#include "../../palette.h"
+
+namespace Render::GL {
+
+class MontefortinoHelmetRenderer : public IEquipmentRenderer {
+public:
+  MontefortinoHelmetRenderer() = default;
+
+  void render(const DrawContext &ctx, const BodyFrames &frames,
+              const HumanoidPalette &palette,
+              const HumanoidAnimationContext &anim,
+              ISubmitter &submitter) override;
+};
+
+} // namespace Render::GL

+ 193 - 0
render/equipment/helmets/roman_heavy_helmet.cpp

@@ -0,0 +1,193 @@
+#include "roman_heavy_helmet.h"
+#include "../../geom/transforms.h"
+#include "../../gl/primitives.h"
+#include "../../humanoid/humanoid_math.h"
+#include "../../humanoid/rig.h"
+#include "../../humanoid/style_palette.h"
+#include "../../submitter.h"
+#include <QMatrix4x4>
+#include <QVector3D>
+
+namespace Render::GL {
+
+using Render::Geom::coneFromTo;
+using Render::Geom::cylinderBetween;
+using Render::Geom::sphereAt;
+using Render::GL::Humanoid::saturate_color;
+
+void RomanHeavyHelmetRenderer::render(const DrawContext &ctx,
+                                      const BodyFrames &frames,
+                                      const HumanoidPalette &palette,
+                                      const HumanoidAnimationContext &anim,
+                                      ISubmitter &submitter) {
+  (void)anim; // Unused
+
+  const AttachmentFrame &head = frames.head;
+  float const head_r = head.radius;
+  if (head_r <= 0.0F) {
+    return;
+  }
+
+  auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
+    return HumanoidRendererBase::frameLocalPosition(head, normalized);
+  };
+
+  // Imperial Gallic helmet colors (steel with bronze/brass accents)
+  QVector3D const steel_color =
+      saturate_color(palette.metal * QVector3D(0.95F, 0.96F, 1.0F));
+  QVector3D const brass_color =
+      saturate_color(palette.metal * QVector3D(1.3F, 1.1F, 0.7F));
+  QVector3D const visor_color(0.1F, 0.1F, 0.1F);
+
+  float const helm_r = head_r * 1.15F;
+  float const helm_ratio = helm_r / head_r;
+
+  // Main bowl (rounded dome)
+  QVector3D const helm_bot = headPoint(QVector3D(0.0F, -0.20F, 0.0F));
+  QVector3D const helm_top = headPoint(QVector3D(0.0F, 1.40F, 0.0F));
+
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, helm_bot, helm_top, helm_r),
+                 steel_color, nullptr, 1.0F);
+
+  // Top cap
+  QVector3D const cap_top = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, helm_top, cap_top, helm_r * 0.98F),
+                 steel_color * 1.05F, nullptr, 1.0F);
+
+  // Decorative reinforcement rings
+  auto ring = [&](float y_offset, const QVector3D &col) {
+    QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
+    float const height = head_r * 0.015F;
+    QVector3D const a = center + head.up * (height * 0.5F);
+    QVector3D const b = center - head.up * (height * 0.5F);
+    submitter.mesh(getUnitCylinder(),
+                   cylinderBetween(ctx.model, a, b, helm_r * 1.02F), col,
+                   nullptr, 1.0F);
+  };
+
+  ring(1.25F, steel_color * 1.08F);
+  ring(0.50F, steel_color * 1.08F);
+  ring(-0.05F, steel_color * 1.08F);
+
+  // Reinforced brow ridge
+  QVector3D const brow_center = headPoint(QVector3D(0.0F, 0.15F, 0.0F));
+  QVector3D const brow_top = brow_center + head.up * 0.03F;
+  QVector3D const brow_bot = brow_center - head.up * 0.02F;
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, brow_bot, brow_top,
+                                 helm_r * 1.08F),
+                 brass_color * 0.95F, nullptr, 1.0F);
+
+  // Visor cross (face protection)
+  float const visor_y = 0.15F;
+  float const visor_forward = helm_r * 0.72F;
+  float const visor_forward_norm = visor_forward / head_r;
+  QVector3D const visor_center =
+      headPoint(QVector3D(0.0F, visor_y, visor_forward_norm));
+
+  // Horizontal visor bar
+  QVector3D const visor_hl = visor_center - head.right * (helm_r * 0.35F);
+  QVector3D const visor_hr = visor_center + head.right * (helm_r * 0.35F);
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, visor_hl, visor_hr,
+                                 head_r * 0.012F),
+                 visor_color, nullptr, 1.0F);
+
+  // Vertical visor bar
+  QVector3D const visor_vt = visor_center + head.up * (helm_r * 0.25F);
+  QVector3D const visor_vb = visor_center - head.up * (helm_r * 0.25F);
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, visor_vb, visor_vt,
+                                 head_r * 0.012F),
+                 visor_color, nullptr, 1.0F);
+
+  // Breathing holes (4 on each side)
+  auto draw_breathing_hole = [&](float x_sign, float y_offset) {
+    QVector3D const pos = headPoint(
+        QVector3D(x_sign * 0.50F * helm_ratio, y_offset,
+                  visor_forward_norm * 0.97F));
+    QMatrix4x4 m = ctx.model;
+    m.translate(pos);
+    m.scale(0.010F);
+    submitter.mesh(getUnitSphere(), m, visor_color, nullptr, 1.0F);
+  };
+
+  for (int i = 0; i < 4; ++i) {
+    draw_breathing_hole(1.0F, 0.05F - i * 0.10F);
+    draw_breathing_hole(-1.0F, 0.05F - i * 0.10F);
+  }
+
+  // Decorative brass cross on forehead
+  QVector3D const cross_center =
+      headPoint(QVector3D(0.0F, 0.60F, (helm_r * 0.75F) / head_r));
+
+  QVector3D const cross_h1 = cross_center - head.right * 0.04F;
+  QVector3D const cross_h2 = cross_center + head.right * 0.04F;
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, cross_h1, cross_h2,
+                                 head_r * 0.008F),
+                 brass_color, nullptr, 1.0F);
+
+  QVector3D const cross_v1 = cross_center - head.up * 0.04F;
+  QVector3D const cross_v2 = cross_center + head.up * 0.04F;
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, cross_v1, cross_v2,
+                                 head_r * 0.008F),
+                 brass_color, nullptr, 1.0F);
+
+  // Cheek guards (hinged protection for face)
+  float const cheek_w = head_r * 0.52F;
+  QVector3D const cheek_top = headPoint(QVector3D(0.0F, 0.18F, 0.0F));
+  QVector3D const cheek_bot = headPoint(QVector3D(0.0F, -0.45F, 0.0F));
+
+  // Left cheek guard
+  QVector3D const cheek_ltop =
+      cheek_top + head.right * (-cheek_w / head_r) + head.forward * 0.42F;
+  QVector3D const cheek_lbot = cheek_bot +
+                                head.right * (-cheek_w * 0.85F / head_r) +
+                                head.forward * 0.32F;
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, cheek_lbot, cheek_ltop, 0.032F),
+                 steel_color * 0.94F, nullptr, 1.0F);
+
+  // Right cheek guard
+  QVector3D const cheek_rtop =
+      cheek_top + head.right * (cheek_w / head_r) + head.forward * 0.42F;
+  QVector3D const cheek_rbot =
+      cheek_bot + head.right * (cheek_w * 0.85F / head_r) +
+      head.forward * 0.32F;
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, cheek_rbot, cheek_rtop, 0.032F),
+                 steel_color * 0.94F, nullptr, 1.0F);
+
+  // Neck guard (back of helmet)
+  QVector3D const neck_top = headPoint(QVector3D(0.0F, -0.10F, -1.02F));
+  QVector3D const neck_bot = headPoint(QVector3D(0.0F, -0.28F, -0.95F));
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, neck_bot, neck_top,
+                                 helm_r * 0.95F),
+                 steel_color * 0.92F, nullptr, 1.0F);
+
+  // Plume/crest for identification (officer's crest)
+  QVector3D const crest_base = cap_top;
+  QVector3D const crest_mid = crest_base + head.up * 0.08F;
+  QVector3D const crest_top = crest_mid + head.up * 0.15F;
+
+  // Crest holder (brass)
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, crest_base, crest_mid, 0.020F),
+                 brass_color, nullptr, 1.0F);
+
+  // Plume (red - traditional Roman officer color)
+  submitter.mesh(getUnitCone(),
+                 coneFromTo(ctx.model, crest_mid, crest_top, 0.048F),
+                 QVector3D(0.92F, 0.15F, 0.15F), nullptr, 1.0F);
+
+  // Plume ornament (brass ball)
+  submitter.mesh(getUnitSphere(), sphereAt(ctx.model, crest_top, 0.022F),
+                 brass_color, nullptr, 1.0F);
+}
+
+} // namespace Render::GL

+ 23 - 0
render/equipment/helmets/roman_heavy_helmet.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include "../i_equipment_renderer.h"
+#include "../../humanoid/rig.h"
+#include "../../palette.h"
+
+namespace Render::GL {
+
+// Roman heavy helmet - Imperial Gallic style galea for legionaries
+// Features rounded dome, reinforced brow ridge, cheek guards, neck guard,
+// visor cross, breathing holes, brass decorations, and officer's plume/crest
+// Based on historical Roman legionary helmets
+class RomanHeavyHelmetRenderer : public IEquipmentRenderer {
+public:
+  RomanHeavyHelmetRenderer() = default;
+
+  void render(const DrawContext &ctx, const BodyFrames &frames,
+              const HumanoidPalette &palette,
+              const HumanoidAnimationContext &anim,
+              ISubmitter &submitter) override;
+};
+
+} // namespace Render::GL

+ 123 - 0
render/equipment/helmets/roman_light_helmet.cpp

@@ -0,0 +1,123 @@
+#include "roman_light_helmet.h"
+#include "../../geom/transforms.h"
+#include "../../gl/primitives.h"
+#include "../../humanoid/humanoid_math.h"
+#include "../../humanoid/rig.h"
+#include "../../humanoid/style_palette.h"
+#include "../../submitter.h"
+#include <QMatrix4x4>
+#include <QVector3D>
+
+namespace Render::GL {
+
+using Render::Geom::coneFromTo;
+using Render::Geom::cylinderBetween;
+using Render::Geom::sphereAt;
+using Render::GL::Humanoid::saturate_color;
+
+void RomanLightHelmetRenderer::render(const DrawContext &ctx,
+                                      const BodyFrames &frames,
+                                      const HumanoidPalette &palette,
+                                      const HumanoidAnimationContext &anim,
+                                      ISubmitter &submitter) {
+  (void)anim; // Unused
+
+  const AttachmentFrame &head = frames.head;
+  float const head_r = head.radius;
+  if (head_r <= 0.0F) {
+    return;
+  }
+
+  auto headPoint = [&](const QVector3D &normalized) -> QVector3D {
+    return HumanoidRendererBase::frameLocalPosition(head, normalized);
+  };
+
+  // Roman galea (auxiliary helmet) - bronze/brass colored
+  QVector3D const helmet_color =
+      saturate_color(palette.metal * QVector3D(1.08F, 0.98F, 0.78F));
+  QVector3D const helmet_accent = helmet_color * 1.12F;
+
+  // Main helmet bowl
+  QVector3D const helmet_top = headPoint(QVector3D(0.0F, 1.28F, 0.0F));
+  QVector3D const helmet_bot = headPoint(QVector3D(0.0F, 0.08F, 0.0F));
+  float const helmet_r = head_r * 1.10F;
+
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, helmet_bot, helmet_top, helmet_r),
+                 helmet_color, nullptr, 1.0F);
+
+  // Conical top (characteristic Roman galea feature)
+  QVector3D const apex_pos = headPoint(QVector3D(0.0F, 1.48F, 0.0F));
+  submitter.mesh(getUnitCone(),
+                 coneFromTo(ctx.model, helmet_top, apex_pos, helmet_r * 0.97F),
+                 helmet_accent, nullptr, 1.0F);
+
+  // Decorative reinforcement rings
+  auto ring = [&](float y_offset, float r_scale, float h,
+                  const QVector3D &col) {
+    QVector3D const center = headPoint(QVector3D(0.0F, y_offset, 0.0F));
+    QVector3D const a = center + head.up * (h * 0.5F);
+    QVector3D const b = center - head.up * (h * 0.5F);
+    submitter.mesh(getUnitCylinder(),
+                   cylinderBetween(ctx.model, a, b, helmet_r * r_scale), col,
+                   nullptr, 1.0F);
+  };
+
+  ring(0.35F, 1.07F, 0.020F, helmet_accent);
+  ring(0.65F, 1.03F, 0.015F, helmet_color * 1.05F);
+  ring(0.95F, 1.01F, 0.012F, helmet_color * 1.03F);
+
+  // Cheek guards (left and right)
+  float const cheek_w = head_r * 0.48F;
+  QVector3D const cheek_top = headPoint(QVector3D(0.0F, 0.22F, 0.0F));
+  QVector3D const cheek_bot = headPoint(QVector3D(0.0F, -0.42F, 0.0F));
+
+  // Left cheek guard
+  QVector3D const cheek_ltop =
+      cheek_top + head.right * (-cheek_w / head_r) + head.forward * 0.38F;
+  QVector3D const cheek_lbot = cheek_bot +
+                                head.right * (-cheek_w * 0.82F / head_r) +
+                                head.forward * 0.28F;
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, cheek_lbot, cheek_ltop, 0.028F),
+                 helmet_color * 0.96F, nullptr, 1.0F);
+
+  // Right cheek guard
+  QVector3D const cheek_rtop =
+      cheek_top + head.right * (cheek_w / head_r) + head.forward * 0.38F;
+  QVector3D const cheek_rbot =
+      cheek_bot + head.right * (cheek_w * 0.82F / head_r) +
+      head.forward * 0.28F;
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, cheek_rbot, cheek_rtop, 0.028F),
+                 helmet_color * 0.96F, nullptr, 1.0F);
+
+  // Neck guard (back protection)
+  QVector3D const neck_guard_top = headPoint(QVector3D(0.0F, 0.03F, -0.82F));
+  QVector3D const neck_guard_bot = headPoint(QVector3D(0.0F, -0.32F, -0.88F));
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, neck_guard_bot, neck_guard_top,
+                                 helmet_r * 0.88F),
+                 helmet_color * 0.93F, nullptr, 1.0F);
+
+  // Crest/plume for identification
+  QVector3D const crest_base = apex_pos;
+  QVector3D const crest_mid = crest_base + head.up * 0.09F;
+  QVector3D const crest_top = crest_mid + head.up * 0.12F;
+
+  // Crest holder (metal)
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, crest_base, crest_mid, 0.018F),
+                 helmet_accent, nullptr, 1.0F);
+
+  // Plume (red/crimson - traditional Roman color)
+  submitter.mesh(getUnitCone(),
+                 coneFromTo(ctx.model, crest_mid, crest_top, 0.042F),
+                 QVector3D(0.88F, 0.18F, 0.18F), nullptr, 1.0F);
+
+  // Plume ornament (top ball)
+  submitter.mesh(getUnitSphere(), sphereAt(ctx.model, crest_top, 0.020F),
+                 helmet_accent, nullptr, 1.0F);
+}
+
+} // namespace Render::GL

+ 22 - 0
render/equipment/helmets/roman_light_helmet.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include "../i_equipment_renderer.h"
+#include "../../humanoid/rig.h"
+#include "../../palette.h"
+
+namespace Render::GL {
+
+// Roman light helmet - galea (auxiliary/archer helmet)
+// Features conical top, decorative rings, cheek guards, neck guard, and crest/plume
+// Based on historical Roman auxiliary helmets worn by archers and light infantry
+class RomanLightHelmetRenderer : public IEquipmentRenderer {
+public:
+  RomanLightHelmetRenderer() = default;
+
+  void render(const DrawContext &ctx, const BodyFrames &frames,
+              const HumanoidPalette &palette,
+              const HumanoidAnimationContext &anim,
+              ISubmitter &submitter) override;
+};
+
+} // namespace Render::GL

+ 46 - 0
render/equipment/register_equipment.cpp

@@ -1,4 +1,10 @@
 #include "equipment_registry.h"
+#include "helmets/headwrap.h"
+#include "helmets/kingdom_heavy_helmet.h"
+#include "helmets/kingdom_light_helmet.h"
+#include "helmets/montefortino_helmet.h"
+#include "helmets/roman_heavy_helmet.h"
+#include "helmets/roman_light_helmet.h"
 #include "weapons/bow_renderer.h"
 #include "weapons/quiver_renderer.h"
 #include <memory>
@@ -42,6 +48,46 @@ void registerBuiltInEquipment() {
   // Register quiver renderer
   auto quiver = std::make_shared<QuiverRenderer>();
   registry.registerEquipment(EquipmentCategory::Weapon, "quiver", quiver);
+
+  // Register helmet renderers
+  // CARTHAGE HELMETS
+  // Montefortino helmet - Carthaginian/Punic style helmet with bronze bowl
+  // and distinctive top knob, used by heavy infantry (spearmen, swordsmen)
+  auto montefortino_helmet = std::make_shared<MontefortinoHelmetRenderer>();
+  registry.registerEquipment(EquipmentCategory::Helmet, "montefortino",
+                            montefortino_helmet);
+  registry.registerEquipment(EquipmentCategory::Helmet, "carthage_heavy",
+                            montefortino_helmet);
+
+  // Headwrap - cloth head covering used by Carthaginian archers and light infantry
+  auto headwrap = std::make_shared<HeadwrapRenderer>();
+  registry.registerEquipment(EquipmentCategory::Helmet, "headwrap", headwrap);
+  registry.registerEquipment(EquipmentCategory::Helmet, "carthage_light",
+                            headwrap);
+
+  // ROMAN HELMETS
+  // Roman heavy helmet - Imperial Gallic style for legionaries with
+  // visor cross, breathing holes, and brass decorations
+  auto roman_heavy = std::make_shared<RomanHeavyHelmetRenderer>();
+  registry.registerEquipment(EquipmentCategory::Helmet, "roman_heavy",
+                            roman_heavy);
+
+  // Roman light helmet - Simple cap helmet for archers and auxiliaries
+  auto roman_light = std::make_shared<RomanLightHelmetRenderer>();
+  registry.registerEquipment(EquipmentCategory::Helmet, "roman_light",
+                            roman_light);
+
+  // KINGDOM HELMETS
+  // Kingdom heavy helmet - Great helm style for knights and heavy infantry
+  // with enclosed design and heraldic cross
+  auto kingdom_heavy = std::make_shared<KingdomHeavyHelmetRenderer>();
+  registry.registerEquipment(EquipmentCategory::Helmet, "kingdom_heavy",
+                            kingdom_heavy);
+
+  // Kingdom light helmet - Kettle hat with wide brim for archers
+  auto kingdom_light = std::make_shared<KingdomLightHelmetRenderer>();
+  registry.registerEquipment(EquipmentCategory::Helmet, "kingdom_light",
+                            kingdom_light);
 }
 
 } // namespace Render::GL

+ 1 - 0
tests/CMakeLists.txt

@@ -9,6 +9,7 @@ add_executable(standard_of_iron_tests
     render/pose_controller_compatibility_test.cpp
     render/body_frames_test.cpp
     render/equipment_registry_test.cpp
+    render/helmet_renderers_test.cpp
 )
 
 # Link against GTest, project libraries

+ 196 - 0
tests/render/helmet_renderers_test.cpp

@@ -0,0 +1,196 @@
+#include "render/equipment/equipment_registry.h"
+#include "render/equipment/helmets/headwrap.h"
+#include "render/equipment/helmets/montefortino_helmet.h"
+#include "render/humanoid/rig.h"
+#include "render/palette.h"
+#include <gtest/gtest.h>
+#include <memory>
+
+using namespace Render::GL;
+
+namespace {
+
+// Mock submitter for testing
+class MockSubmitter : public ISubmitter {
+public:
+  void mesh(Mesh * /*mesh*/, const QMatrix4x4 & /*transform*/,
+            const QVector3D & /*color*/, Texture * /*texture*/,
+            float /*alpha*/) override {
+    mesh_count++;
+  }
+
+  void cylinder(const QVector3D & /*start*/, const QVector3D & /*end*/,
+                float /*radius*/, const QVector3D & /*color*/,
+                float /*alpha*/) override {
+    cylinder_count++;
+  }
+
+  void selectionRing(const QMatrix4x4 & /*model*/, float /*alphaInner*/,
+                     float /*alphaOuter*/, const QVector3D & /*color*/) override {
+    // Not used in helmet rendering
+  }
+
+  void grid(const QMatrix4x4 & /*model*/, const QVector3D & /*color*/,
+            float /*cellSize*/, float /*thickness*/, float /*extent*/) override {
+    // Not used in helmet rendering
+  }
+
+  void selectionSmoke(const QMatrix4x4 & /*model*/,
+                      const QVector3D & /*color*/,
+                      float /*baseAlpha*/) override {
+    // Not used in helmet rendering
+  }
+
+  int mesh_count = 0;
+  int cylinder_count = 0;
+};
+
+// Helper to create a basic DrawContext
+DrawContext createTestContext() {
+  DrawContext ctx;
+  ctx.model.setToIdentity();
+  ctx.backend = nullptr;
+  ctx.entity = nullptr;
+  return ctx;
+}
+
+// Helper to create basic body frames
+BodyFrames createTestFrames() {
+  BodyFrames frames;
+  frames.head.origin = QVector3D(0.0F, 1.7F, 0.0F);
+  frames.head.right = QVector3D(1.0F, 0.0F, 0.0F);
+  frames.head.up = QVector3D(0.0F, 1.0F, 0.0F);
+  frames.head.forward = QVector3D(0.0F, 0.0F, 1.0F);
+  frames.head.radius = 0.12F;
+  return frames;
+}
+
+// Helper to create basic palette
+HumanoidPalette createTestPalette() {
+  HumanoidPalette palette;
+  palette.skin = QVector3D(0.8F, 0.6F, 0.5F);
+  palette.cloth = QVector3D(0.7F, 0.3F, 0.2F);
+  palette.leather = QVector3D(0.4F, 0.3F, 0.2F);
+  palette.leatherDark = QVector3D(0.3F, 0.2F, 0.1F);
+  palette.metal = QVector3D(0.7F, 0.7F, 0.7F);
+  palette.wood = QVector3D(0.5F, 0.3F, 0.2F);
+  return palette;
+}
+
+} // namespace
+
+class HelmetRenderersTest : public ::testing::Test {
+protected:
+  void SetUp() override {
+    // Ensure built-in equipment is registered
+    registerBuiltInEquipment();
+    
+    ctx = createTestContext();
+    frames = createTestFrames();
+    palette = createTestPalette();
+    anim.inputs.time = 0.0F;
+    anim.inputs.isMoving = false;
+    anim.inputs.is_attacking = false;
+    anim.inputs.isMelee = false;
+  }
+
+  DrawContext ctx;
+  BodyFrames frames;
+  HumanoidPalette palette;
+  HumanoidAnimationContext anim;
+  MockSubmitter submitter;
+};
+
+TEST_F(HelmetRenderersTest, MontefortinoHelmetRendersWithValidFrames) {
+  MontefortinoHelmetRenderer helmet;
+
+  helmet.render(ctx, frames, palette, anim, submitter);
+
+  // Montefortino helmet should render multiple mesh components
+  EXPECT_GT(submitter.mesh_count, 0);
+}
+
+TEST_F(HelmetRenderersTest, MontefortinoHelmetHandlesZeroHeadRadius) {
+  MontefortinoHelmetRenderer helmet;
+  frames.head.radius = 0.0F;
+
+  helmet.render(ctx, frames, palette, anim, submitter);
+
+  // Should not render anything when head radius is zero
+  EXPECT_EQ(submitter.mesh_count, 0);
+}
+
+TEST_F(HelmetRenderersTest, HeadwrapRendersWithValidFrames) {
+  HeadwrapRenderer headwrap;
+
+  headwrap.render(ctx, frames, palette, anim, submitter);
+
+  // Headwrap should render band, knot, and tail
+  EXPECT_GT(submitter.mesh_count, 0);
+}
+
+TEST_F(HelmetRenderersTest, HeadwrapHandlesZeroHeadRadius) {
+  HeadwrapRenderer headwrap;
+  frames.head.radius = 0.0F;
+
+  headwrap.render(ctx, frames, palette, anim, submitter);
+
+  // Should not render anything when head radius is zero
+  EXPECT_EQ(submitter.mesh_count, 0);
+}
+
+TEST_F(HelmetRenderersTest, HelmetsRegisteredInEquipmentRegistry) {
+  auto &registry = EquipmentRegistry::instance();
+
+  // Verify montefortino helmet is registered
+  EXPECT_TRUE(registry.has(EquipmentCategory::Helmet, "montefortino"));
+  auto montefortino = registry.get(EquipmentCategory::Helmet, "montefortino");
+  ASSERT_NE(montefortino, nullptr);
+
+  // Verify headwrap is registered
+  EXPECT_TRUE(registry.has(EquipmentCategory::Helmet, "headwrap"));
+  auto headwrap = registry.get(EquipmentCategory::Helmet, "headwrap");
+  ASSERT_NE(headwrap, nullptr);
+}
+
+TEST_F(HelmetRenderersTest, MontefortinoHelmetFromRegistryRenders) {
+  auto &registry = EquipmentRegistry::instance();
+  auto helmet = registry.get(EquipmentCategory::Helmet, "montefortino");
+  ASSERT_NE(helmet, nullptr);
+
+  helmet->render(ctx, frames, palette, anim, submitter);
+
+  EXPECT_GT(submitter.mesh_count, 0);
+}
+
+TEST_F(HelmetRenderersTest, HeadwrapFromRegistryRenders) {
+  auto &registry = EquipmentRegistry::instance();
+  auto headwrap = registry.get(EquipmentCategory::Helmet, "headwrap");
+  ASSERT_NE(headwrap, nullptr);
+
+  headwrap->render(ctx, frames, palette, anim, submitter);
+
+  EXPECT_GT(submitter.mesh_count, 0);
+}
+
+TEST_F(HelmetRenderersTest, HelmetsUseHeadFrameCoordinates) {
+  // Test that helmets use head frame's coordinate system
+  frames.head.origin = QVector3D(1.0F, 2.0F, 3.0F);
+  frames.head.right = QVector3D(0.0F, 1.0F, 0.0F);  // Rotated frame
+  frames.head.up = QVector3D(-1.0F, 0.0F, 0.0F);
+  frames.head.forward = QVector3D(0.0F, 0.0F, 1.0F);
+  frames.head.radius = 0.12F;
+
+  MontefortinoHelmetRenderer helmet;
+  MockSubmitter submitter1;
+  helmet.render(ctx, frames, palette, anim, submitter1);
+
+  // Helmet should still render even with rotated frame
+  EXPECT_GT(submitter1.mesh_count, 0);
+
+  HeadwrapRenderer headwrap;
+  MockSubmitter submitter2;
+  headwrap.render(ctx, frames, palette, anim, submitter2);
+
+  EXPECT_GT(submitter2.mesh_count, 0);
+}