Browse Source

Implement Roman lorica segmentata armor and scutum shield

Roman Heavy Armor (Lorica Segmentata):
- 8 horizontal overlapping metal bands wrapping around torso
- Bands follow natural ribcage curve, wider at chest, narrow at waist
- Asymmetric depth: forward-projecting chest, compressed back
- Visible rivets and clasps on alternating bands for authentic detail
- Layered shoulder guards with brass rivets
- Limited front-to-back depth for compact, powerful silhouette

Roman Shield (Scutum):
- Large curved rectangular shield (1.2m height × 0.65m width)
- Gentle wraparound curvature for body protection
- Central vertical ridge for structural strength
- Prominent metal boss (umbo) at center
- Bronze/brass reinforced rim around all edges
- Red facing with decorative rivets
- Attaches to left hand frame

Both implementations focus on geometric precision, metal surfaces, and functional construction details as specified.

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 3 months ago
parent
commit
7dfbca4e0a

+ 1 - 0
render/CMakeLists.txt

@@ -77,6 +77,7 @@ add_library(render_gl STATIC
     equipment/armor/carthage_armor.cpp
     equipment/weapons/bow_renderer.cpp
     equipment/weapons/quiver_renderer.cpp
+    equipment/weapons/roman_scutum.cpp
     equipment/helmets/montefortino_helmet.cpp
     equipment/helmets/headwrap.cpp
     equipment/helmets/roman_heavy_helmet.cpp

+ 242 - 25
render/equipment/armor/roman_armor.cpp

@@ -1,5 +1,4 @@
 #include "roman_armor.h"
-#include "tunic_renderer.h"
 #include "../../geom/transforms.h"
 #include "../../gl/primitives.h"
 #include "../../humanoid/humanoid_math.h"
@@ -14,23 +13,175 @@
 
 namespace Render::GL {
 
+using Render::Geom::coneFromTo;
+using Render::Geom::cylinderBetween;
+using Render::Geom::sphereAt;
+using Render::GL::Humanoid::saturate_color;
+
 void RomanHeavyArmorRenderer::render(const DrawContext &ctx,
                                       const BodyFrames &frames,
                                       const HumanoidPalette &palette,
                                       const HumanoidAnimationContext &anim,
                                       ISubmitter &submitter) {
-  // Roman heavy armor - lorica segmentata style
-  TunicConfig config;
-  config.torso_scale = 1.08F;
-  config.shoulder_width_scale = 1.18F; // More compact shoulders
-  config.chest_depth_scale = 0.82F; // Deeper chest for segmented plates
-  config.waist_taper = 0.90F;
-  config.include_pauldrons = true;
-  config.include_gorget = true;
-  config.include_belt = true;
-
-  TunicRenderer renderer(config);
-  renderer.render(ctx, frames, palette, anim, submitter);
+  (void)anim; // Armor is rigid
+
+  const AttachmentFrame &torso = frames.torso;
+  if (torso.radius <= 0.0F) {
+    return;
+  }
+
+  using HP = HumanProportions;
+
+  QVector3D const steel_color =
+      saturate_color(palette.metal * QVector3D(0.92F, 0.94F, 0.98F));
+  QVector3D const brass_color =
+      saturate_color(palette.metal * QVector3D(1.2F, 1.0F, 0.6F));
+  QVector3D const leather_color =
+      saturate_color(palette.leather * QVector3D(0.6F, 0.4F, 0.3F));
+
+  const QVector3D &origin = torso.origin;
+  const QVector3D &right = torso.right;
+  const QVector3D &up = torso.up;
+  const QVector3D &forward = torso.forward;
+
+  // Lorica segmentata - horizontal overlapping bands
+  float const torso_r = torso.radius * 1.08F;
+  float const shoulder_width = torso_r * 1.18F;
+  float const chest_depth_front = torso_r * 1.15F;
+  float const chest_depth_back = torso_r * 0.82F;
+
+  // Define horizontal bands from shoulder to waist
+  constexpr int num_bands = 8;
+  float const y_top = HP::SHOULDER_Y;
+  float const y_bottom = HP::WAIST_Y + 0.05F;
+  float const band_height = (y_top - y_bottom) / static_cast<float>(num_bands);
+
+  constexpr int segments = 20; // More segments for smoother bands
+  constexpr float pi = std::numbers::pi_v<float>;
+
+  // Render each horizontal band
+  for (int band = 0; band < num_bands; ++band) {
+    float const y_band_top = y_top - static_cast<float>(band) * band_height;
+    float const y_band_bottom = y_band_top - band_height * 0.92F; // Slight gap
+
+    // Calculate band radius based on position (wider at chest, narrower at waist)
+    float const t = static_cast<float>(band) / static_cast<float>(num_bands - 1);
+    float const width_scale = shoulder_width * (1.0F - t * 0.18F);
+
+    // Shade alternating bands slightly for visual distinction
+    QVector3D band_color = steel_color * (1.0F - static_cast<float>(band % 2) * 0.05F);
+
+    // Asymmetric depth function - forward chest, compressed back
+    auto getRadius = [&](float angle) -> float {
+      float const cos_a = std::cos(angle);
+      float depth = (cos_a > 0.0F) ? chest_depth_front : chest_depth_back;
+      return width_scale * depth * (std::abs(cos_a) * 0.25F + 0.75F);
+    };
+
+    // Create curved band segments
+    for (int i = 0; i < segments; ++i) {
+      float const angle1 = (static_cast<float>(i) / segments) * 2.0F * pi;
+      float const angle2 = (static_cast<float>(i + 1) / segments) * 2.0F * pi;
+
+      float const cos1 = std::cos(angle1);
+      float const sin1 = std::sin(angle1);
+      float const cos2 = std::cos(angle2);
+      float const sin2 = std::sin(angle2);
+
+      float const r1 = getRadius(angle1);
+      float const r2 = getRadius(angle2);
+
+      // Top edge of band
+      QVector3D const p1_top =
+          origin + right * (r1 * sin1) + forward * (r1 * cos1) + up * (y_band_top - origin.y());
+      QVector3D const p2_top =
+          origin + right * (r2 * sin2) + forward * (r2 * cos2) + up * (y_band_top - origin.y());
+
+      // Bottom edge of band
+      QVector3D const p1_bot =
+          origin + right * (r1 * sin1) + forward * (r1 * cos1) +
+          up * (y_band_bottom - origin.y());
+      QVector3D const p2_bot =
+          origin + right * (r2 * sin2) + forward * (r2 * cos2) +
+          up * (y_band_bottom - origin.y());
+
+      // Render band segment as thin cylinder
+      float const seg_r = (r1 + r2) * 0.5F * 0.04F;
+      submitter.mesh(getUnitCylinder(),
+                     cylinderBetween(ctx.model, p1_top, p1_bot, seg_r),
+                     band_color, nullptr, 1.0F);
+      submitter.mesh(getUnitCylinder(),
+                     cylinderBetween(ctx.model, p2_top, p2_bot, seg_r),
+                     band_color, nullptr, 1.0F);
+      submitter.mesh(getUnitCylinder(),
+                     cylinderBetween(ctx.model, p1_top, p2_top, seg_r),
+                     band_color, nullptr, 1.0F);
+      submitter.mesh(getUnitCylinder(),
+                     cylinderBetween(ctx.model, p1_bot, p2_bot, seg_r),
+                     band_color, nullptr, 1.0F);
+    }
+
+    // Add rivets/clasps to every other band
+    if (band % 2 == 0) {
+      for (int i = 0; i < 6; ++i) {
+        float const angle = (static_cast<float>(i) / 6.0F) * 2.0F * pi;
+        float const r = getRadius(angle) * 0.95F;
+        float const y_rivet = (y_band_top + y_band_bottom) * 0.5F;
+        QVector3D rivet_pos =
+            origin + right * (r * std::sin(angle)) +
+            forward * (r * std::cos(angle)) + up * (y_rivet - origin.y());
+
+        QMatrix4x4 m = ctx.model;
+        m.translate(rivet_pos);
+        m.scale(0.01F);
+        submitter.mesh(getUnitSphere(), m, brass_color, nullptr, 1.0F);
+      }
+    }
+  }
+
+  // Shoulder guards - layered segments
+  auto renderShoulderGuard = [&](const QVector3D &shoulder_pos,
+                                  const QVector3D &outward) {
+    constexpr int shoulder_segments = 4;
+    float const upper_arm_r = HP::UPPER_ARM_R;
+
+    for (int i = 0; i < shoulder_segments; ++i) {
+      float const seg_y = shoulder_pos.y() - static_cast<float>(i) * 0.04F;
+      float const seg_r = upper_arm_r * (2.3F - static_cast<float>(i) * 0.15F);
+      QVector3D seg_pos =
+          shoulder_pos + outward * (0.03F + static_cast<float>(i) * 0.01F);
+      seg_pos.setY(seg_y);
+
+      submitter.mesh(getUnitSphere(), sphereAt(ctx.model, seg_pos, seg_r),
+                     steel_color * (1.0F - static_cast<float>(i) * 0.04F),
+                     nullptr, 1.0F);
+
+      // Rivets on shoulder guards
+      if (i < 3) {
+        QMatrix4x4 m = ctx.model;
+        m.translate(seg_pos + QVector3D(0, 0.02F, 0.04F));
+        m.scale(0.008F);
+        submitter.mesh(getUnitSphere(), m, brass_color, nullptr, 1.0F);
+      }
+    }
+  };
+
+  renderShoulderGuard(frames.shoulderL.origin, -right);
+  renderShoulderGuard(frames.shoulderR.origin, right);
+
+  // Belt/waist protection
+  QVector3D const waist_center(origin.x(), HP::WAIST_Y, origin.z());
+  for (int i = 0; i < 3; ++i) {
+    float const y0 = HP::WAIST_Y - static_cast<float>(i) * 0.035F;
+    float const y1 = y0 - 0.03F;
+    float const r = torso.radius * (1.05F + static_cast<float>(i) * 0.02F);
+
+    submitter.mesh(getUnitCone(),
+                   coneFromTo(ctx.model, QVector3D(waist_center.x(), y0, waist_center.z()),
+                              QVector3D(waist_center.x(), y1, waist_center.z()), r),
+                   leather_color * (0.95F - static_cast<float>(i) * 0.05F),
+                   nullptr, 1.0F);
+  }
 }
 
 void RomanLightArmorRenderer::render(const DrawContext &ctx,
@@ -38,18 +189,84 @@ void RomanLightArmorRenderer::render(const DrawContext &ctx,
                                       const HumanoidPalette &palette,
                                       const HumanoidAnimationContext &anim,
                                       ISubmitter &submitter) {
-  // Roman light armor - lighter version
-  TunicConfig config;
-  config.torso_scale = 1.04F;
-  config.shoulder_width_scale = 1.12F;
-  config.chest_depth_scale = 0.86F;
-  config.waist_taper = 0.93F;
-  config.include_pauldrons = false;
-  config.include_gorget = false;
-  config.include_belt = true;
-
-  TunicRenderer renderer(config);
-  renderer.render(ctx, frames, palette, anim, submitter);
+  (void)anim; // Armor is rigid
+
+  const AttachmentFrame &torso = frames.torso;
+  if (torso.radius <= 0.0F) {
+    return;
+  }
+
+  using HP = HumanProportions;
+
+  QVector3D const steel_color =
+      saturate_color(palette.metal * QVector3D(0.9F, 0.93F, 0.97F));
+  QVector3D const leather_color =
+      saturate_color(palette.leather * QVector3D(0.55F, 0.38F, 0.28F));
+
+  const QVector3D &origin = torso.origin;
+  const QVector3D &right = torso.right;
+  const QVector3D &up = torso.up;
+  const QVector3D &forward = torso.forward;
+
+  // Light armor - fewer bands, simpler construction
+  float const torso_r = torso.radius * 1.04F;
+  float const shoulder_width = torso_r * 1.12F;
+  float const chest_depth_front = torso_r * 1.10F;
+  float const chest_depth_back = torso_r * 0.86F;
+
+  constexpr int num_bands = 4; // Fewer bands for light armor
+  float const y_top = HP::SHOULDER_Y - 0.02F;
+  float const y_bottom = HP::CHEST_Y;
+  float const band_height = (y_top - y_bottom) / static_cast<float>(num_bands);
+
+  constexpr int segments = 16;
+  constexpr float pi = std::numbers::pi_v<float>;
+
+  for (int band = 0; band < num_bands; ++band) {
+    float const y_band_top = y_top - static_cast<float>(band) * band_height;
+    float const y_band_bottom = y_band_top - band_height * 0.90F;
+
+    float const t = static_cast<float>(band) / static_cast<float>(num_bands - 1);
+    float const width_scale = shoulder_width * (1.0F - t * 0.12F);
+
+    QVector3D band_color = steel_color * (1.0F - static_cast<float>(band % 2) * 0.04F);
+
+    for (int i = 0; i < segments; ++i) {
+      float const angle1 = (static_cast<float>(i) / segments) * 2.0F * pi;
+      float const angle2 = (static_cast<float>(i + 1) / segments) * 2.0F * pi;
+
+      auto getRadius = [&](float angle) -> float {
+        float const cos_a = std::cos(angle);
+        float depth = (cos_a > 0.0F) ? chest_depth_front : chest_depth_back;
+        return width_scale * depth * (std::abs(cos_a) * 0.3F + 0.7F);
+      };
+
+      float const r1 = getRadius(angle1);
+      float const r2 = getRadius(angle2);
+
+      QVector3D const p1_top =
+          origin + right * (r1 * std::sin(angle1)) +
+          forward * (r1 * std::cos(angle1)) + up * (y_band_top - origin.y());
+      QVector3D const p1_bot =
+          origin + right * (r1 * std::sin(angle1)) +
+          forward * (r1 * std::cos(angle1)) + up * (y_band_bottom - origin.y());
+
+      float const seg_r = r1 * 0.035F;
+      submitter.mesh(getUnitCylinder(),
+                     cylinderBetween(ctx.model, p1_top, p1_bot, seg_r),
+                     band_color, nullptr, 1.0F);
+    }
+  }
+
+  // Simple belt
+  QVector3D const waist_center(origin.x(), HP::WAIST_Y + 0.02F, origin.z());
+  float const belt_r = torso.radius * 1.02F;
+  QVector3D const belt_top(waist_center.x(), waist_center.y() + 0.02F, waist_center.z());
+  QVector3D const belt_bot(waist_center.x(), waist_center.y() - 0.02F, waist_center.z());
+
+  submitter.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, belt_top, belt_bot, belt_r),
+                 leather_color, nullptr, 1.0F);
 }
 
 } // namespace Render::GL

+ 5 - 0
render/equipment/register_equipment.cpp

@@ -10,6 +10,7 @@
 #include "helmets/roman_light_helmet.h"
 #include "weapons/bow_renderer.h"
 #include "weapons/quiver_renderer.h"
+#include "weapons/roman_scutum.h"
 #include <memory>
 
 namespace Render::GL {
@@ -46,6 +47,10 @@ void registerBuiltInEquipment() {
   auto quiver = std::make_shared<QuiverRenderer>();
   registry.registerEquipment(EquipmentCategory::Weapon, "quiver", quiver);
 
+  auto roman_scutum = std::make_shared<RomanScutumRenderer>();
+  registry.registerEquipment(EquipmentCategory::Weapon, "roman_scutum",
+                             roman_scutum);
+
   auto montefortino_helmet = std::make_shared<MontefortinoHelmetRenderer>();
   registry.registerEquipment(EquipmentCategory::Helmet, "montefortino",
                              montefortino_helmet);

+ 185 - 0
render/equipment/weapons/roman_scutum.cpp

@@ -0,0 +1,185 @@
+#include "roman_scutum.h"
+#include "../../geom/transforms.h"
+#include "../../gl/primitives.h"
+#include "../../humanoid/humanoid_math.h"
+#include "../../humanoid/humanoid_specs.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 RomanScutumRenderer::render(const DrawContext &ctx,
+                                  const BodyFrames &frames,
+                                  const HumanoidPalette &palette,
+                                  const HumanoidAnimationContext &anim,
+                                  ISubmitter &submitter) {
+  (void)anim; // Shield is rigid relative to hand
+
+  // Attach to left hand
+  const AttachmentFrame &handL = frames.handL;
+  if (handL.radius <= 0.0F) {
+    return;
+  }
+
+  using HP = HumanProportions;
+
+  // Shield colors - red facing with bronze/brass rim and boss
+  QVector3D const shield_red =
+      saturate_color(palette.cloth * QVector3D(1.5F, 0.3F, 0.3F));
+  QVector3D const bronze_color =
+      saturate_color(palette.metal * QVector3D(1.3F, 1.0F, 0.5F));
+  QVector3D const wood_color =
+      saturate_color(QVector3D(0.5F, 0.35F, 0.25F));
+
+  // Shield dimensions - large rectangular curved shield
+  constexpr float shield_height = 1.2F; // From shoulder to knee
+  constexpr float shield_width = 0.65F;
+  constexpr float shield_curve = 0.25F; // Curvature depth
+  constexpr float rim_thickness = 0.015F;
+  constexpr float boss_radius = 0.12F;
+
+  // Position shield at left hand, oriented to face forward
+  QVector3D const shield_center = handL.origin + handL.forward * 0.15F;
+  QVector3D const shield_up = handL.up;
+  QVector3D const shield_right = handL.right;
+  QVector3D const shield_forward = handL.forward;
+
+  // Create curved shield surface with vertical segments
+  constexpr int vertical_segments = 12;
+  constexpr int horizontal_segments = 16;
+
+  for (int v = 0; v < vertical_segments; ++v) {
+    for (int h = 0; h < horizontal_segments; ++h) {
+      // Parametric position on shield
+      float const v_t = static_cast<float>(v) / static_cast<float>(vertical_segments);
+      float const h_t = static_cast<float>(h) / static_cast<float>(horizontal_segments);
+
+      // Position on flat shield
+      float const y_local = (v_t - 0.5F) * shield_height;
+      float const x_local = (h_t - 0.5F) * shield_width;
+
+      // Curvature - shield wraps around body
+      float const curve_offset = shield_curve * (1.0F - std::abs(x_local / (shield_width * 0.5F)));
+
+      // World position
+      QVector3D segment_pos = shield_center + shield_up * y_local +
+                              shield_right * x_local +
+                              shield_forward * curve_offset;
+
+      // Render small shield segment
+      QMatrix4x4 m = ctx.model;
+      m.translate(segment_pos);
+      m.scale(0.03F, 0.05F, 0.01F);
+
+      // Color variation for wood grain effect
+      QVector3D segment_color = shield_red * (1.0F + (v % 2) * 0.05F - 0.025F);
+      submitter.mesh(getUnitSphere(), m, segment_color, nullptr, 1.0F);
+    }
+  }
+
+  // Central vertical ridge
+  constexpr int ridge_segments = 10;
+  for (int i = 0; i < ridge_segments; ++i) {
+    float const t = static_cast<float>(i) / static_cast<float>(ridge_segments - 1);
+    float const y_local = (t - 0.5F) * shield_height * 0.9F;
+
+    QVector3D ridge_pos =
+        shield_center + shield_up * y_local + shield_forward * (shield_curve + 0.02F);
+
+    QMatrix4x4 m = ctx.model;
+    m.translate(ridge_pos);
+    m.scale(0.025F, 0.06F, 0.015F);
+    submitter.mesh(getUnitSphere(), m, bronze_color * 0.9F, nullptr, 1.0F);
+  }
+
+  // Central boss (umbo) - prominent dome
+  QVector3D const boss_center = shield_center + shield_forward * (shield_curve + 0.08F);
+  
+  // Boss base ring
+  for (int i = 0; i < 12; ++i) {
+    float const angle = (static_cast<float>(i) / 12.0F) * 2.0F * std::numbers::pi_v<float>;
+    QVector3D ring_pos = boss_center + shield_right * (boss_radius * std::cos(angle)) +
+                         shield_up * (boss_radius * std::sin(angle));
+
+    QMatrix4x4 m = ctx.model;
+    m.translate(ring_pos);
+    m.scale(0.018F);
+    submitter.mesh(getUnitSphere(), m, bronze_color, nullptr, 1.0F);
+  }
+
+  // Boss dome
+  submitter.mesh(getUnitSphere(), sphereAt(ctx.model, boss_center, boss_radius * 0.8F),
+                 bronze_color * 1.1F, nullptr, 1.0F);
+
+  // Reinforced rim - top edge
+  float const y_pos = shield_height * 0.48F;
+  for (int i = 0; i < 10; ++i) {
+    float const t = static_cast<float>(i) / 9.0F;
+    float const x_local = (t - 0.5F) * shield_width * 0.95F;
+    float const curve_off = shield_curve * (1.0F - std::abs(x_local / (shield_width * 0.5F)));
+    
+    QVector3D rim_pos = shield_center + shield_up * y_pos +
+                        shield_right * x_local + shield_forward * curve_off;
+    QMatrix4x4 m = ctx.model;
+    m.translate(rim_pos);
+    m.scale(rim_thickness);
+    submitter.mesh(getUnitSphere(), m, bronze_color * 0.95F, nullptr, 1.0F);
+  }
+
+  // Bottom rim
+  float const y_pos_bot = -shield_height * 0.48F;
+  for (int i = 0; i < 10; ++i) {
+    float const t = static_cast<float>(i) / 9.0F;
+    float const x_local = (t - 0.5F) * shield_width * 0.95F;
+    float const curve_off = shield_curve * (1.0F - std::abs(x_local / (shield_width * 0.5F)));
+    
+    QVector3D rim_pos = shield_center + shield_up * y_pos_bot +
+                        shield_right * x_local + shield_forward * curve_off;
+    QMatrix4x4 m = ctx.model;
+    m.translate(rim_pos);
+    m.scale(rim_thickness);
+    submitter.mesh(getUnitSphere(), m, bronze_color * 0.95F, nullptr, 1.0F);
+  }
+
+  // Left and right rims
+  for (int side = 0; side < 2; ++side) {
+    float const x_pos_side = (side == 0 ? -1.0F : 1.0F) * shield_width * 0.48F;
+    float const curve_off = shield_curve * (1.0F - std::abs(x_pos_side / (shield_width * 0.5F)));
+    
+    for (int i = 0; i < 12; ++i) {
+      float const t = static_cast<float>(i) / 11.0F;
+      float const y_local = (t - 0.5F) * shield_height * 0.95F;
+      
+      QVector3D rim_pos = shield_center + shield_up * y_local +
+                          shield_right * x_pos_side + shield_forward * curve_off;
+      QMatrix4x4 m = ctx.model;
+      m.translate(rim_pos);
+      m.scale(rim_thickness);
+      submitter.mesh(getUnitSphere(), m, bronze_color * 0.95F, nullptr, 1.0F);
+    }
+  }
+
+  // Decorative rivets around boss
+  for (int i = 0; i < 8; ++i) {
+    float const angle = (static_cast<float>(i) / 8.0F) * 2.0F * std::numbers::pi_v<float>;
+    float const rivet_dist = boss_radius * 1.3F;
+    QVector3D rivet_pos = boss_center + shield_right * (rivet_dist * std::cos(angle)) +
+                          shield_up * (rivet_dist * std::sin(angle));
+
+    QMatrix4x4 m = ctx.model;
+    m.translate(rivet_pos);
+    m.scale(0.012F);
+    submitter.mesh(getUnitSphere(), m, bronze_color * 1.15F, nullptr, 1.0F);
+  }
+}
+
+} // namespace Render::GL

+ 20 - 0
render/equipment/weapons/roman_scutum.h

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