Kaynağa Gözat

improve heavy helmet carthage

djeada 3 hafta önce
ebeveyn
işleme
3372651295

+ 65 - 1
assets/shaders/spearman_carthage.frag

@@ -25,6 +25,7 @@ out vec4 FragColor;
 const vec3 k_leather_base = vec3(0.42, 0.30, 0.20);
 const vec3 k_linen_base = vec3(0.88, 0.83, 0.74);
 const vec3 k_bronze_base = vec3(0.58, 0.44, 0.20);
+const float k_pi = 3.14159265;
 
 float hash21(vec2 p) {
   p = fract(p * vec2(234.34, 435.345));
@@ -160,6 +161,69 @@ MaterialSample sample_bronze(vec3 base_color, vec3 pos, vec3 N, vec3 T,
   return m;
 }
 
+vec3 crest_basis(vec3 n) {
+  float az = atan(n.z, n.x);
+  float el = acos(clamp(n.y, -1.0, 1.0));
+  return vec3(az / (2.0 * k_pi), el / k_pi, n.y);
+}
+
+MaterialSample sample_carthage_helmet(vec3 base_color, vec3 pos, vec3 N, vec3 T,
+                                      vec3 B) {
+  MaterialSample m;
+
+  // Base hammered bronze with wide-area patina streaks.
+  float hammer = fbm(pos * 14.0 + vec3(v_layerNoise));
+  float patina = fbm(pos * vec3(4.2, 6.0, 4.8) + vec3(1.3, 0.0, 2.1));
+  vec3 Np =
+      perturb(N, T, B, vec3((hammer - 0.5) * 0.10, (patina - 0.5) * 0.14, 0.0));
+
+  // Radial meridian ribs anchored to surface direction.
+  vec3 uv = crest_basis(N);
+  float meridian = smoothstep(0.23, 0.0, abs(sin(uv.x * k_pi * 5.5)));
+  float ridge = smoothstep(0.10, 0.0, abs(uv.x - 0.5));
+  float crest = smoothstep(0.34, 0.16, uv.y) * ridge;
+  float rim = smoothstep(0.80, 0.62, uv.y);
+  float tip = smoothstep(0.82, 0.98, uv.y);
+  vec3 rib_normal = vec3(0.0, crest * 0.42 + rim * 0.18, meridian * 0.26);
+
+  // Brushed scratches running around the helmet circumference.
+  float brush = sin(dot(T.xz, vec2(62.0, 54.0)) + uv.x * 18.0 + uv.y * 7.0);
+  float scratch = smoothstep(0.6, 1.0, abs(brush));
+  vec3 scratch_normal = T * (scratch * 0.10);
+
+  Np = normalize(Np + T * rib_normal.z + B * rib_normal.y + scratch_normal);
+
+  // Brow band mask based on height along the helmet (aligns to rim).
+  float brow = smoothstep(0.18, 0.0, abs(uv.y - 0.70));
+  float patina_mix = clamp(patina * 0.65 + brow * 0.3 + rim * 0.25, 0.0, 1.0);
+
+  vec3 tint = mix(vec3(0.0, 1.0, 0.0), base_color, 0.15); // debug extreme green
+  vec3 patina_color = vec3(0.26, 0.52, 0.40);
+  vec3 crest_highlight = vec3(1.0, 0.92, 0.75);
+
+  tint = mix(tint, patina_color, patina_mix * 0.7);
+  tint += crest_highlight * crest * 0.65;
+  tint = mix(tint, tint * vec3(1.06, 1.02, 0.96), rim * 0.35);
+  tint = mix(tint, tint * vec3(1.12, 1.06, 1.00), tip * 0.45);
+  tint += vec3(0.14) * brow;
+  // Edge wear on rim and tip
+  float edgeWear = clamp(rim * 0.6 + tip * 0.4, 0.0, 1.0);
+  tint = mix(tint, tint * vec3(1.25, 1.18, 1.05), edgeWear);
+  // Underside grime just below rim
+  float underside = smoothstep(0.55, 0.28, uv.y);
+  tint = mix(tint, tint * vec3(0.75, 0.70, 0.66), underside * 0.6);
+
+  m.color = tint;
+  m.normal = Np;
+  m.roughness = clamp(0.22 + hammer * 0.26 + patina * 0.18 - crest * 0.12 -
+                          rim * 0.06 - tip * 0.08,
+                      0.10, 0.60);
+  m.F0 = mix(vec3(0.08), vec3(0.96, 0.82, 0.56),
+             clamp(0.48 + crest * 0.4 + brow * 0.18 + rim * 0.12 + tip * 0.16,
+                   0.0, 1.0));
+  return m;
+}
+
 vec3 apply_wet_darkening(vec3 color, float wet_mask) {
   return mix(color, color * 0.6, wet_mask);
 }
@@ -186,7 +250,7 @@ void main() {
 
   MaterialSample mat;
   if (helmet_region) {
-    mat = sample_bronze(base_color, v_worldPos, Nw, Tw, Bw);
+    mat = sample_carthage_helmet(base_color, v_worldPos, Nw, Tw, Bw);
   } else if (upper_region) {
     // Torso mixes linen and leather patches
     MaterialSample linen = sample_linen(base_color, v_worldPos, Nw, Tw, Bw);

+ 15 - 4
assets/shaders/spearman_carthage.vert

@@ -47,10 +47,21 @@ void main() {
   vec3 shearOffset = shearAxis * torsion * 0.004;
   vec3 batteredPos = worldPos + dentOffset + shearOffset;
 
-  vec3 offsetPos = batteredPos + worldNormal * 0.006;
+  // Extra shaping for helmet region (top of character).
+  float height = batteredPos.y;
+  float helmetMask = smoothstep(0.55, 0.90, height);
+  float rimMask =
+      smoothstep(0.60, 0.85, height) * (1.0 - smoothstep(0.88, 1.05, height));
+  float tipMask = smoothstep(1.00, 1.30, height);
+  vec3 radial =
+      normalize(vec3(batteredPos.x, 0.0, batteredPos.z) + vec3(0.0001));
+  vec3 rimFlare = radial * (0.12 * rimMask * helmetMask);
+  vec3 tipTaper = radial * (-0.10 * tipMask * helmetMask);
+
+  vec3 offsetPos = batteredPos + worldNormal * 0.006 + rimFlare + tipTaper;
   mat4 invModel = inverse(u_model);
-  vec4 localBattered = invModel * vec4(batteredPos, 1.0);
-  gl_Position = u_mvp * localBattered;
+  vec4 localOffset = invModel * vec4(offsetPos, 1.0);
+  gl_Position = u_mvp * localOffset;
 
   v_worldPos = offsetPos;
   v_texCoord = a_texCoord;
@@ -59,7 +70,7 @@ void main() {
   v_tangent = t;
   v_bitangent = b;
 
-  float height = offsetPos.y;
+  height = offsetPos.y;
   float layer = 2.0;
   if (height > 1.28)
     layer = 0.0;

+ 58 - 1
assets/shaders/swordsman_carthage.frag

@@ -27,6 +27,7 @@ out vec4 FragColor;
 const vec3 k_bronze_base = vec3(0.60, 0.42, 0.18);
 const vec3 k_linen_base = vec3(0.88, 0.82, 0.72);
 const vec3 k_leather_base = vec3(0.38, 0.25, 0.15);
+const float k_pi = 3.14159265;
 
 float hash21(vec2 p) {
   p = fract(p * vec2(234.34, 435.345));
@@ -167,6 +168,62 @@ MaterialSample sample_chainmail(vec3 base_color, vec3 pos, vec3 N, vec3 T,
   return m;
 }
 
+vec3 crest_basis(vec3 n) {
+  float az = atan(n.z, n.x);
+  float el = acos(clamp(n.y, -1.0, 1.0));
+  return vec3(az / (2.0 * k_pi), el / k_pi, n.y);
+}
+
+MaterialSample sample_carthage_helmet(vec3 base_color, vec3 pos, vec3 N, vec3 T,
+                                      vec3 B) {
+  MaterialSample m;
+
+  float hammer = fbm(pos * 14.0 + vec3(v_layerNoise));
+  float patina = fbm(pos * vec3(4.2, 6.0, 4.8) + vec3(1.3, 0.0, 2.1));
+  vec3 Np =
+      perturb(N, T, B, vec3((hammer - 0.5) * 0.10, (patina - 0.5) * 0.14, 0.0));
+
+  vec3 uv = crest_basis(N);
+  float meridian = smoothstep(0.23, 0.0, abs(sin(uv.x * k_pi * 5.5)));
+  float ridge = smoothstep(0.10, 0.0, abs(uv.x - 0.5));
+  float crest = smoothstep(0.34, 0.16, uv.y) * ridge;
+  float rim = smoothstep(0.80, 0.62, uv.y);
+  float tip = smoothstep(0.82, 0.98, uv.y);
+  vec3 rib_normal = vec3(0.0, crest * 0.42 + rim * 0.18, meridian * 0.26);
+
+  float brush = sin(dot(T.xz, vec2(62.0, 54.0)) + uv.x * 18.0 + uv.y * 7.0);
+  float scratch = smoothstep(0.6, 1.0, abs(brush));
+  vec3 scratch_normal = T * (scratch * 0.10);
+  Np = normalize(Np + T * rib_normal.z + B * rib_normal.y + scratch_normal);
+
+  float brow = smoothstep(0.18, 0.0, abs(uv.y - 0.70));
+  float patina_mix = clamp(patina * 0.65 + brow * 0.3 + rim * 0.25, 0.0, 1.0);
+
+  vec3 tint = mix(vec3(0.0, 1.0, 0.0), base_color, 0.15); // debug extreme green
+  vec3 patina_color = vec3(0.26, 0.52, 0.40);
+  vec3 crest_highlight = vec3(1.0, 0.92, 0.75);
+
+  tint = mix(tint, patina_color, patina_mix * 0.7);
+  tint += crest_highlight * crest * 0.65;
+  tint = mix(tint, tint * vec3(1.06, 1.02, 0.96), rim * 0.35);
+  tint = mix(tint, tint * vec3(1.12, 1.06, 1.00), tip * 0.45);
+  tint += vec3(0.14) * brow;
+  float edgeWear = clamp(rim * 0.6 + tip * 0.4, 0.0, 1.0);
+  tint = mix(tint, tint * vec3(1.25, 1.18, 1.05), edgeWear);
+  float underside = smoothstep(0.55, 0.28, uv.y);
+  tint = mix(tint, tint * vec3(0.75, 0.70, 0.66), underside * 0.6);
+
+  m.color = tint;
+  m.normal = Np;
+  m.roughness = clamp(0.22 + hammer * 0.26 + patina * 0.18 - crest * 0.12 -
+                          rim * 0.06 - tip * 0.08,
+                      0.10, 0.60);
+  m.F0 = mix(vec3(0.08), vec3(0.96, 0.82, 0.56),
+             clamp(0.48 + crest * 0.4 + brow * 0.18 + rim * 0.12 + tip * 0.16,
+                   0.0, 1.0));
+  return m;
+}
+
 MaterialSample sample_lamellar_linen(vec3 base_color, vec3 pos, vec3 N, vec3 T,
                                      vec3 B) {
   MaterialSample m;
@@ -233,7 +290,7 @@ void main() {
 
   MaterialSample mat;
   if (helmet_region) {
-    mat = sample_hammered_bronze(base_color, v_worldPos, Nw, Tw, Bw);
+    mat = sample_carthage_helmet(base_color, v_worldPos, Nw, Tw, Bw);
   } else if (torso_region) {
     MaterialSample bronze = sample_muscle_bronze(
         base_color, v_worldPos, Nw, Tw, Bw, v_cuirassProfile, v_frontMask);

+ 14 - 2
assets/shaders/swordsman_carthage.vert

@@ -51,10 +51,22 @@ void main() {
   vec3 shearOffset = shearAxis * torsion * 0.0035;
 
   vec3 batteredPos = worldPos + dentOffset + shearOffset;
-  vec3 offsetPos = batteredPos + worldNormal * 0.005;
+
+  // Extra helmet shaping: flare rim, tighten tip.
+  float height = batteredPos.y;
+  float helmetMask = smoothstep(0.55, 0.90, height);
+  float rimMask =
+      smoothstep(0.60, 0.85, height) * (1.0 - smoothstep(0.88, 1.05, height));
+  float tipMask = smoothstep(1.00, 1.28, height);
+  vec3 radial =
+      normalize(vec3(batteredPos.x, 0.0, batteredPos.z) + vec3(0.0001));
+  vec3 rimFlare = radial * (0.12 * rimMask * helmetMask);
+  vec3 tipTaper = radial * (-0.10 * tipMask * helmetMask);
+
+  vec3 offsetPos = batteredPos + worldNormal * 0.005 + rimFlare + tipTaper;
 
   mat4 invModel = inverse(u_model);
-  vec4 localPosition = invModel * vec4(batteredPos, 1.0);
+  vec4 localPosition = invModel * vec4(offsetPos, 1.0);
   gl_Position = u_mvp * localPosition;
 
   v_worldPos = offsetPos;

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

@@ -12,6 +12,7 @@
 #include "../../../humanoid/humanoid_specs.h"
 #include "../../../humanoid/pose_controller.h"
 #include "../../../humanoid/rig.h"
+#include "../../../humanoid/spear_pose_utils.h"
 #include "../../../humanoid/style_palette.h"
 #include "../../../palette.h"
 #include "../../../scene_renderer.h"
@@ -160,10 +161,11 @@ public:
                                      (pelvis_y + 0.05F) * t,
                                  0.15F * (1.0F - t) + 0.20F * t);
 
-      QVector3D const hand_l_pos(0.0F,
-                                 lowered_shoulder_y * (1.0F - t) +
-                                     (lowered_shoulder_y - 0.10F) * t,
-                                 0.30F * (1.0F - t) + 0.55F * t);
+      float const offhand_along = lerp(-0.06F, -0.02F, t);
+      float const offhand_drop = 0.10F + 0.02F * t;
+      QVector3D const hand_l_pos =
+          computeOffhandSpearGrip(pose, anim_ctx, hand_r_pos, false,
+                                  offhand_along, offhand_drop, -0.08F);
 
       controller.placeHandAt(false, hand_r_pos);
       controller.placeHandAt(true, hand_l_pos);
@@ -176,9 +178,8 @@ public:
       QVector3D const idle_hand_r(0.28F + arm_asymmetry,
                                   HP::SHOULDER_Y - 0.02F + arm_height_jitter,
                                   0.30F);
-      QVector3D const idle_hand_l(
-          -0.08F - 0.5F * arm_asymmetry,
-          HP::SHOULDER_Y - 0.08F + 0.5F * arm_height_jitter, 0.45F);
+      QVector3D const idle_hand_l = computeOffhandSpearGrip(
+          pose, anim_ctx, idle_hand_r, false, -0.04F, 0.10F, -0.08F);
 
       controller.placeHandAt(false, idle_hand_r);
       controller.placeHandAt(true, idle_hand_l);

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

@@ -10,6 +10,7 @@
 #include "../../../humanoid/humanoid_specs.h"
 #include "../../../humanoid/pose_controller.h"
 #include "../../../humanoid/rig.h"
+#include "../../../humanoid/spear_pose_utils.h"
 #include "../../../humanoid/style_palette.h"
 #include "../../../palette.h"
 #include "../../../scene_renderer.h"
@@ -153,10 +154,11 @@ public:
                                      (pelvis_y + 0.05F) * t,
                                  0.15F * (1.0F - t) + 0.20F * t);
 
-      QVector3D const hand_l_pos(0.0F,
-                                 lowered_shoulder_y * (1.0F - t) +
-                                     (lowered_shoulder_y - 0.10F) * t,
-                                 0.30F * (1.0F - t) + 0.55F * t);
+      float const offhand_along = lerp(-0.06F, -0.02F, t);
+      float const offhand_drop = 0.10F + 0.02F * t;
+      QVector3D const hand_l_pos =
+          computeOffhandSpearGrip(pose, anim_ctx, hand_r_pos, false,
+                                  offhand_along, offhand_drop, -0.08F);
 
       controller.placeHandAt(false, hand_r_pos);
       controller.placeHandAt(true, hand_l_pos);
@@ -169,9 +171,8 @@ public:
       QVector3D const idle_hand_r(0.28F + arm_asymmetry,
                                   HP::SHOULDER_Y - 0.02F + arm_height_jitter,
                                   0.30F);
-      QVector3D const idle_hand_l(
-          -0.08F - 0.5F * arm_asymmetry,
-          HP::SHOULDER_Y - 0.08F + 0.5F * arm_height_jitter, 0.45F);
+      QVector3D const idle_hand_l = computeOffhandSpearGrip(
+          pose, anim_ctx, idle_hand_r, false, -0.04F, 0.10F, -0.08F);
 
       controller.placeHandAt(false, idle_hand_r);
       controller.placeHandAt(true, idle_hand_l);

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

@@ -12,6 +12,7 @@
 #include "../../../humanoid/humanoid_specs.h"
 #include "../../../humanoid/pose_controller.h"
 #include "../../../humanoid/rig.h"
+#include "../../../humanoid/spear_pose_utils.h"
 #include "../../../humanoid/style_palette.h"
 #include "../../../palette.h"
 #include "../../../scene_renderer.h"
@@ -153,10 +154,11 @@ public:
                                      (pelvis_y + 0.05F) * t,
                                  0.15F * (1.0F - t) + 0.20F * t);
 
-      QVector3D const hand_l_pos(0.0F,
-                                 lowered_shoulder_y * (1.0F - t) +
-                                     (lowered_shoulder_y - 0.10F) * t,
-                                 0.30F * (1.0F - t) + 0.55F * t);
+      float const offhand_along = lerp(-0.06F, -0.02F, t);
+      float const offhand_drop = 0.10F + 0.02F * t;
+      QVector3D const hand_l_pos =
+          computeOffhandSpearGrip(pose, anim_ctx, hand_r_pos, false,
+                                  offhand_along, offhand_drop, -0.08F);
 
       controller.placeHandAt(false, hand_r_pos);
       controller.placeHandAt(true, hand_l_pos);
@@ -169,9 +171,8 @@ public:
       QVector3D const idle_hand_r(0.28F + arm_asymmetry,
                                   HP::SHOULDER_Y - 0.02F + arm_height_jitter,
                                   0.30F);
-      QVector3D const idle_hand_l(
-          -0.08F - 0.5F * arm_asymmetry,
-          HP::SHOULDER_Y - 0.08F + 0.5F * arm_height_jitter, 0.45F);
+      QVector3D const idle_hand_l = computeOffhandSpearGrip(
+          pose, anim_ctx, idle_hand_r, false, -0.04F, 0.10F, -0.08F);
 
       controller.placeHandAt(false, idle_hand_r);
       controller.placeHandAt(true, idle_hand_l);

+ 28 - 223
render/equipment/helmets/carthage_heavy_helmet.cpp

@@ -15,6 +15,7 @@ auto mixColor(const QVector3D &a, const QVector3D &b, float t) -> QVector3D {
 
 namespace Render::GL {
 
+using Render::Geom::coneFromTo;
 using Render::Geom::cylinderBetween;
 using Render::Geom::sphereAt;
 
@@ -31,234 +32,38 @@ void CarthageHeavyHelmetRenderer::render(const DrawContext &ctx,
     return;
   }
 
-  render_bowl(ctx, head, submitter);
-
-  if (m_config.has_neck_guard) {
-    render_neck_guard(ctx, head, submitter);
-  }
-
-  if (m_config.has_face_plate) {
-    render_face_plate(ctx, head, submitter);
-  }
-
-  if (m_config.has_hair_crest) {
-    render_crest(ctx, head, submitter);
-  }
-
-  if (m_config.has_cheek_guards) {
-    render_cheek_guards(ctx, head, submitter);
-  }
-}
-
-void CarthageHeavyHelmetRenderer::render_bowl(const DrawContext &ctx,
-                                              const AttachmentFrame &head,
-                                              ISubmitter &submitter) {
   const float R = head.radius;
-  const float helm_scale = 1.2f;
-  const float helmet_y_offset = R * 0.1f;
+  const float lift = R * 0.03f;
+  const float helmet_scale = 1.08f;
   auto head_point = [&](const QVector3D &n) {
-    QVector3D scaled_n = n;
-
-    if (scaled_n.z() > 0.2f) {
-      scaled_n.setZ(std::max(scaled_n.z(), 1.05f / helm_scale));
-    }
-
-    scaled_n = scaled_n * helm_scale;
-
-    QVector3D p = HumanoidRendererBase::frameLocalPosition(head, scaled_n);
-    return p + head.up * helmet_y_offset;
+    QVector3D scaled = n * helmet_scale;
+    return HumanoidRendererBase::frameLocalPosition(head, scaled) +
+           head.up * lift;
   };
 
-  QVector3D bowl_center = head_point(QVector3D(0.0f, 1.47f, 0.0f));
-  QMatrix4x4 bowl = ctx.model;
-  bowl.translate(bowl_center);
-  bowl.scale(R * 1.12f, R * 0.68f, R * 1.08f);
-
-  QVector3D luminous_bronze =
-      mixColor(m_config.bronze_color, m_config.glow_color, 0.35F);
-
-  submitter.mesh(getUnitSphere(), bowl, luminous_bronze, nullptr, 0.3f, 2);
-
-  QVector3D rim_center = head_point(QVector3D(0.0f, 1.17f, 0.0f));
-  QMatrix4x4 rim = ctx.model;
-  rim.translate(rim_center);
-  rim.scale(R * 1.28f, R * 0.16f, R * 1.25f);
-  submitter.mesh(getUnitSphere(), rim, m_config.glow_color, nullptr, 0.16f, 2);
-}
-
-void CarthageHeavyHelmetRenderer::render_cheek_guards(
-    const DrawContext &ctx, const AttachmentFrame &head,
-    ISubmitter &submitter) {
-  const float R = head.radius;
-  const float helm_scale = 1.2f;
-  const float helmet_y_offset = R * 0.1f;
-  auto head_point = [&](const QVector3D &n) {
-    QVector3D scaled_n = n;
-
-    if (scaled_n.z() > 0.2f) {
-      scaled_n.setZ(std::max(scaled_n.z(), 1.05f / helm_scale));
-    }
-
-    scaled_n = scaled_n * helm_scale;
-
-    QVector3D p = HumanoidRendererBase::frameLocalPosition(head, scaled_n);
-    return p + head.up * helmet_y_offset;
-  };
-
-  QVector3D left_cheek = head_point(QVector3D(-0.58f, 0.73f, 0.42f));
-  QVector3D right_cheek = head_point(QVector3D(0.58f, 0.73f, 0.42f));
-
-  QMatrix4x4 left_guard = ctx.model;
-  left_guard.translate(left_cheek);
-  left_guard.scale(R * 0.32f, R * 0.48f, R * 0.18f);
-  left_guard.rotate(-6.0f, QVector3D(0.0f, 0.0f, 1.0f));
-
-  QMatrix4x4 right_guard = ctx.model;
-  right_guard.translate(right_cheek);
-  right_guard.scale(R * 0.32f, R * 0.48f, R * 0.18f);
-  right_guard.rotate(6.0f, QVector3D(0.0f, 0.0f, 1.0f));
-
-  submitter.mesh(getUnitSphere(), left_guard, m_config.bronze_color, nullptr,
-                 0.6f, 2);
-  submitter.mesh(getUnitSphere(), right_guard, m_config.bronze_color, nullptr,
-                 0.6f, 2);
-}
-
-void CarthageHeavyHelmetRenderer::render_face_plate(const DrawContext &ctx,
-                                                    const AttachmentFrame &head,
-                                                    ISubmitter &submitter) {
-  const float R = head.radius;
-  const float helm_scale = 1.2f;
-  const float helmet_y_offset = R * 0.1f;
-  auto head_point = [&](const QVector3D &n) {
-    QVector3D scaled_n = n;
-
-    if (scaled_n.z() > 0.2f) {
-      scaled_n.setZ(std::max(scaled_n.z(), 1.05f / helm_scale));
-    }
-
-    scaled_n = scaled_n * helm_scale;
-
-    QVector3D p = HumanoidRendererBase::frameLocalPosition(head, scaled_n);
-    return p + head.up * helmet_y_offset;
-  };
-
-  QVector3D brow = head_point(QVector3D(0.0f, 1.25f, 0.60f));
-  QVector3D chin = head_point(QVector3D(0.0f, 0.47f, 0.34f));
-  QMatrix4x4 mask =
-      cylinderBetween(ctx.model, chin, brow, std::max(0.10f, R * 0.26f));
-  QVector3D plate_color =
-      mixColor(m_config.bronze_color, m_config.glow_color, 0.25F);
-  submitter.mesh(getUnitCylinder(), mask, plate_color, nullptr, 0.45f, 2);
-
-  QVector3D nose_top = head_point(QVector3D(0.0f, 1.13f, 0.70f));
-  QVector3D nose_bottom = head_point(QVector3D(0.0f, 0.53f, 0.46f));
-  QMatrix4x4 nose = cylinderBetween(ctx.model, nose_bottom, nose_top,
-                                    std::max(0.05f, R * 0.12f));
-  submitter.mesh(getUnitCylinder(), nose, m_config.glow_color, nullptr, 0.65f,
-                 2);
-
-  render_brow_arch(ctx, head, submitter);
-}
-
-void CarthageHeavyHelmetRenderer::render_neck_guard(const DrawContext &ctx,
-                                                    const AttachmentFrame &head,
-                                                    ISubmitter &submitter) {
-  const float R = head.radius;
-  const float helm_scale = 1.2f;
-  const float helmet_y_offset = R * 0.1f;
-  auto head_point = [&](const QVector3D &n) {
-    QVector3D scaled_n = n;
-
-    if (scaled_n.z() > 0.2f) {
-      scaled_n.setZ(std::max(scaled_n.z(), 1.05f / helm_scale));
-    }
-
-    scaled_n = scaled_n * helm_scale;
-
-    QVector3D p = HumanoidRendererBase::frameLocalPosition(head, scaled_n);
-    return p + head.up * helmet_y_offset;
-  };
-
-  QVector3D guard_center = head_point(QVector3D(0.0f, 0.70f, -0.65f));
-  QMatrix4x4 guard = ctx.model;
-  guard.translate(guard_center);
-  guard.scale(R * 1.25f, R * 0.52f, R * 0.58f);
-  QVector3D guard_color =
-      mixColor(m_config.bronze_color, m_config.glow_color, 0.15F);
-  submitter.mesh(getUnitSphere(), guard, guard_color, nullptr, 0.28f, 2);
-}
-
-void CarthageHeavyHelmetRenderer::render_brow_arch(const DrawContext &ctx,
-                                                   const AttachmentFrame &head,
-                                                   ISubmitter &submitter) {
-  const float R = head.radius;
-  const float helm_scale = 1.2f;
-  const float helmet_y_offset = R * 0.1f;
-  auto head_point = [&](const QVector3D &n) {
-    QVector3D scaled_n = n;
-
-    if (scaled_n.z() > 0.2f) {
-      scaled_n.setZ(std::max(scaled_n.z(), 1.05f / helm_scale));
-    }
-
-    scaled_n = scaled_n * helm_scale;
-
-    QVector3D p = HumanoidRendererBase::frameLocalPosition(head, scaled_n);
-    return p + head.up * helmet_y_offset;
-  };
-
-  QVector3D left = head_point(QVector3D(-0.62f, 1.21f, 0.60f));
-  QVector3D right = head_point(QVector3D(0.62f, 1.21f, 0.60f));
-  float arch_radius = std::max(0.04f, R * 0.10f);
-  QMatrix4x4 arch = cylinderBetween(ctx.model, left, right, arch_radius);
-  QVector3D arch_color =
-      mixColor(m_config.glow_color, m_config.bronze_color, 0.5F);
-  submitter.mesh(getUnitCylinder(), arch, arch_color, nullptr, 0.52f, 2);
-
-  QVector3D ridge_top = head_point(QVector3D(0.0f, 1.37f, 0.58f));
-  QMatrix4x4 ridge = ctx.model;
-  ridge.translate(ridge_top);
-  ridge.scale(R * 0.22f, R * 0.10f, R * 0.26f);
-  submitter.mesh(getUnitSphere(), ridge, m_config.glow_color, nullptr, 0.58f,
+  QVector3D const base_color = m_config.bronze_color;
+  QVector3D const accent =
+      mixColor(m_config.bronze_color, m_config.glow_color, 0.32F);
+
+  float base_r = R * 1.04f;
+  QVector3D cone_base = head_point(QVector3D(0.0f, 0.58f, 0.0f));
+  QVector3D cone_tip = head_point(QVector3D(0.0f, 1.46f, 0.0f));
+  submitter.mesh(getUnitCone(),
+                 coneFromTo(ctx.model, cone_base, cone_tip, base_r), base_color,
+                 nullptr, 1.0f, 2);
+
+  QVector3D tip_base = head_point(QVector3D(0.0f, 1.12f, 0.0f));
+  QVector3D tip_apex = head_point(QVector3D(0.0f, 1.70f, 0.0f));
+  submitter.mesh(getUnitCone(),
+                 coneFromTo(ctx.model, tip_base, tip_apex,
+                            std::max(0.05f, base_r * 0.28f)),
+                 accent, nullptr, 1.0f, 2);
+
+  QMatrix4x4 tip_cap =
+      sphereAt(ctx.model, tip_apex + head.up * (R * 0.015f), R * 0.06f);
+  submitter.mesh(getUnitSphere(), tip_cap,
+                 mixColor(accent, m_config.glow_color, 0.48F), nullptr, 1.0f,
                  2);
 }
 
-void CarthageHeavyHelmetRenderer::render_crest(const DrawContext &ctx,
-                                               const AttachmentFrame &head,
-                                               ISubmitter &submitter) {
-  const float R = head.radius;
-  const float helm_scale = 1.2f;
-  const float helmet_y_offset = R * 0.1f;
-  auto head_point = [&](const QVector3D &n) {
-    QVector3D scaled_n = n;
-
-    if (scaled_n.z() > 0.2f) {
-      scaled_n.setZ(std::max(scaled_n.z(), 1.05f / helm_scale));
-    }
-
-    scaled_n = scaled_n * helm_scale;
-
-    QVector3D p = HumanoidRendererBase::frameLocalPosition(head, scaled_n);
-    return p + head.up * helmet_y_offset;
-  };
-
-  QVector3D crest_back = head_point(QVector3D(0.0f, 1.73f, -0.28f));
-  QVector3D crest_front = head_point(QVector3D(0.0f, 1.73f, 0.28f));
-  float crest_radius = std::max(0.06f, R * 0.26f);
-  QMatrix4x4 crest_bridge =
-      cylinderBetween(ctx.model, crest_back, crest_front, crest_radius);
-  submitter.mesh(getUnitCylinder(), crest_bridge, m_config.crest_color, nullptr,
-                 0.52f, 2);
-
-  QVector3D plume_top = head_point(QVector3D(0.0f, 2.25f, 0.0f));
-  QVector3D plume_base = head_point(QVector3D(0.0f, 1.63f, 0.0f));
-  float plume_radius = std::max(0.05f, R * 0.18f);
-  QMatrix4x4 plume =
-      cylinderBetween(ctx.model, plume_base, plume_top, plume_radius);
-  QVector3D plume_color =
-      mixColor(m_config.crest_color, m_config.glow_color, 0.40F);
-  submitter.mesh(getUnitCylinder(), plume, plume_color, nullptr, 0.70f, 2);
-}
-
 } // namespace Render::GL

+ 0 - 13
render/equipment/helmets/carthage_heavy_helmet.h

@@ -29,19 +29,6 @@ public:
 
 private:
   CarthageHeavyHelmetConfig m_config;
-
-  void render_bowl(const DrawContext &ctx, const AttachmentFrame &head,
-                   ISubmitter &submitter);
-  void render_cheek_guards(const DrawContext &ctx, const AttachmentFrame &head,
-                           ISubmitter &submitter);
-  void render_face_plate(const DrawContext &ctx, const AttachmentFrame &head,
-                         ISubmitter &submitter);
-  void render_neck_guard(const DrawContext &ctx, const AttachmentFrame &head,
-                         ISubmitter &submitter);
-  void render_brow_arch(const DrawContext &ctx, const AttachmentFrame &head,
-                        ISubmitter &submitter);
-  void render_crest(const DrawContext &ctx, const AttachmentFrame &head,
-                    ISubmitter &submitter);
 };
 
 } // namespace Render::GL

+ 2 - 41
render/equipment/weapons/spear_renderer.cpp

@@ -3,6 +3,7 @@
 #include "../../geom/transforms.h"
 #include "../../gl/primitives.h"
 #include "../../humanoid/rig.h"
+#include "../../humanoid/spear_pose_utils.h"
 #include "../../submitter.h"
 
 #include <QMatrix4x4>
@@ -23,47 +24,7 @@ void SpearRenderer::render(const DrawContext &ctx, const BodyFrames &frames,
                            ISubmitter &submitter) {
   QVector3D const grip_pos = frames.hand_r.origin;
 
-  bool const is_attacking = anim.inputs.is_attacking && anim.inputs.is_melee;
-  float attack_phase = 0.0F;
-  if (is_attacking) {
-    attack_phase =
-        std::fmod(anim.inputs.time * SPEARMAN_INV_ATTACK_CYCLE_TIME, 1.0F);
-  }
-
-  QVector3D spear_dir = QVector3D(0.05F, 0.55F, 0.85F);
-  if (spear_dir.lengthSquared() > 1e-6F) {
-    spear_dir.normalize();
-  }
-
-  if (anim.inputs.is_in_hold_mode || anim.inputs.is_exiting_hold) {
-    float const t = anim.inputs.is_in_hold_mode
-                        ? 1.0F
-                        : (1.0F - anim.inputs.hold_exit_progress);
-
-    QVector3D braced_dir = QVector3D(0.05F, 0.40F, 0.91F);
-    if (braced_dir.lengthSquared() > 1e-6F) {
-      braced_dir.normalize();
-    }
-
-    spear_dir = spear_dir * (1.0F - t) + braced_dir * t;
-    if (spear_dir.lengthSquared() > 1e-6F) {
-      spear_dir.normalize();
-    }
-  } else if (is_attacking) {
-    if (attack_phase >= 0.30F && attack_phase < 0.50F) {
-      float const t = (attack_phase - 0.30F) / 0.20F;
-
-      QVector3D attack_dir = QVector3D(0.03F, -0.15F, 1.0F);
-      if (attack_dir.lengthSquared() > 1e-6F) {
-        attack_dir.normalize();
-      }
-
-      spear_dir = spear_dir * (1.0F - t) + attack_dir * t;
-      if (spear_dir.lengthSquared() > 1e-6F) {
-        spear_dir.normalize();
-      }
-    }
-  }
+  QVector3D const spear_dir = computeSpearDirection(anim.inputs);
 
   QVector3D const shaft_base = grip_pos - spear_dir * 0.28F;
   QVector3D shaft_mid = grip_pos + spear_dir * (m_config.spear_length * 0.5F);

+ 9 - 0
render/humanoid/pose_controller.cpp

@@ -1,5 +1,6 @@
 #include "pose_controller.h"
 #include "humanoid_math.h"
+#include "spear_pose_utils.h"
 #include <QVector3D>
 #include <algorithm>
 #include <cmath>
@@ -360,6 +361,14 @@ void HumanoidPoseController::spearThrust(float attack_phase) {
                   HP::SHOULDER_Y - 0.06F + 0.01F * t, lerp(0.35F, 0.25F, t));
   }
 
+  float const thrust_extent =
+      std::clamp((attack_phase - 0.20F) / 0.60F, 0.0F, 1.0F);
+  float const along_offset = -0.06F + 0.02F * thrust_extent;
+  float const y_drop = 0.10F + 0.02F * thrust_extent;
+
+  hand_l_target = computeOffhandSpearGrip(m_pose, m_anim_ctx, hand_r_target,
+                                          false, along_offset, y_drop, -0.08F);
+
   placeHandAt(false, hand_r_target);
   placeHandAt(true, hand_l_target);
 }

+ 74 - 0
render/humanoid/spear_pose_utils.h

@@ -0,0 +1,74 @@
+#pragma once
+
+#include "../entity/renderer_constants.h"
+#include "../gl/humanoid/animation/animation_inputs.h"
+#include "../gl/humanoid/humanoid_types.h"
+
+#include <QVector3D>
+#include <algorithm>
+#include <cmath>
+
+namespace Render::GL {
+
+inline auto
+computeSpearDirection(const AnimationInputs &anim_inputs) -> QVector3D {
+
+  auto normalize = [](QVector3D dir) {
+    if (dir.lengthSquared() > 1e-6F) {
+      dir.normalize();
+    }
+    return dir;
+  };
+
+  QVector3D spear_dir = normalize(QVector3D(0.05F, 0.55F, 0.85F));
+
+  if (anim_inputs.is_in_hold_mode || anim_inputs.is_exiting_hold) {
+    float const t = anim_inputs.is_in_hold_mode
+                        ? 1.0F
+                        : (1.0F - anim_inputs.hold_exit_progress);
+
+    QVector3D const braced_dir = normalize(QVector3D(0.05F, 0.40F, 0.91F));
+    spear_dir = normalize(spear_dir * (1.0F - t) + braced_dir * t);
+  } else if (anim_inputs.is_attacking && anim_inputs.is_melee) {
+    float const attack_phase =
+        std::fmod(anim_inputs.time * SPEARMAN_INV_ATTACK_CYCLE_TIME, 1.0F);
+    if (attack_phase >= 0.30F && attack_phase < 0.50F) {
+      float const t = (attack_phase - 0.30F) / 0.20F;
+
+      QVector3D const attack_dir = normalize(QVector3D(0.03F, -0.15F, 1.0F));
+      spear_dir = normalize(spear_dir * (1.0F - t) + attack_dir * t);
+    }
+  }
+
+  return spear_dir;
+}
+
+inline auto computeOffhandSpearGrip(const HumanoidPose &pose,
+                                    const HumanoidAnimationContext &anim_ctx,
+                                    const QVector3D &main_hand_pos,
+                                    bool main_is_left, float along_offset,
+                                    float y_drop = 0.05F,
+                                    float lateral_offset = 0.05F) -> QVector3D {
+  QVector3D const spear_dir = computeSpearDirection(anim_ctx.inputs);
+
+  QVector3D offhand = main_hand_pos + spear_dir * along_offset;
+
+  QVector3D right_axis = pose.shoulder_r - pose.shoulder_l;
+  right_axis.setY(0.0F);
+  if (right_axis.lengthSquared() < 1e-6F) {
+    right_axis = QVector3D(1.0F, 0.0F, 0.0F);
+  } else {
+    right_axis.normalize();
+  }
+
+  offhand += (main_is_left ? right_axis : -right_axis) * lateral_offset;
+  offhand.setY(offhand.y() - y_drop);
+
+  QVector3D torso_center = (pose.shoulder_l + pose.shoulder_r) * 0.5F;
+  torso_center.setY(offhand.y());
+  offhand = offhand * 0.65F + torso_center * 0.35F;
+
+  return offhand;
+}
+
+} // namespace Render::GL