Browse Source

Merge pull request #232 from djeada/copilot/split-archer-knight-renderers

Split Archer and Knight Renderers — Add Distinct Fabric and Texture Shaders
Adam Djellouli 2 months ago
parent
commit
185faec664

+ 12 - 0
assets/maps/barrack_capture_test.json

@@ -77,6 +77,12 @@
       "z": 58,
       "z": 58,
       "playerId": 1
       "playerId": 1
     },
     },
+    {
+      "type": "knight",
+      "x": 38,
+      "z": 60,
+      "playerId": 1
+    },
     {
     {
       "type": "barracks",
       "type": "barracks",
       "x": 50,
       "x": 50,
@@ -108,6 +114,12 @@
       "z": 58,
       "z": 58,
       "playerId": 2
       "playerId": 2
     },
     },
+    {
+      "type": "knight",
+      "x": 67,
+      "z": 60,
+      "playerId": 2
+    },
     {
     {
       "type": "barracks",
       "type": "barracks",
       "x": 90,
       "x": 90,

+ 178 - 0
assets/shaders/archer.frag

@@ -0,0 +1,178 @@
+#version 330 core
+
+in vec3 v_normal;
+in vec2 v_texCoord;
+in vec3 v_worldPos;
+
+uniform sampler2D u_texture;
+uniform vec3 u_color;
+uniform bool u_useTexture;
+uniform float u_alpha;
+
+out vec4 FragColor;
+
+float hash(vec2 p) {
+  vec3 p3 = fract(vec3(p.xyx) * 0.1031);
+  p3 += dot(p3, p3.yzx + 33.33);
+  return fract((p3.x + p3.y) * p3.z);
+}
+
+float noise(vec2 p) {
+  vec2 i = floor(p);
+  vec2 f = fract(p);
+  f = f * f * (3.0 - 2.0 * f);
+  float a = hash(i);
+  float b = hash(i + vec2(1.0, 0.0));
+  float c = hash(i + vec2(0.0, 1.0));
+  float d = hash(i + vec2(1.0, 1.0));
+  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
+// Roman chainmail (lorica hamata) ring pattern
+float chainmailRings(vec2 p) {
+  vec2 grid = fract(p * 32.0) - 0.5;
+  float ring = length(grid);
+  float ringPattern =
+      smoothstep(0.38, 0.32, ring) - smoothstep(0.28, 0.22, ring);
+
+  // Offset rows for interlocking
+  vec2 offsetGrid = fract(p * 32.0 + vec2(0.5, 0.0)) - 0.5;
+  float offsetRing = length(offsetGrid);
+  float offsetPattern =
+      smoothstep(0.38, 0.32, offsetRing) - smoothstep(0.28, 0.22, offsetRing);
+
+  return (ringPattern + offsetPattern) * 0.14;
+}
+
+// Leather pteruges strips (hanging skirt/shoulder guards)
+float pterugesStrips(vec2 p, float y) {
+  // Vertical leather strips
+  float stripX = fract(p.x * 9.0);
+  float strip = smoothstep(0.15, 0.20, stripX) - smoothstep(0.80, 0.85, stripX);
+
+  // Add leather texture to strips
+  float leatherTex = noise(p * 18.0) * 0.35;
+
+  // Strips hang and curve
+  float hang = smoothstep(0.65, 0.45, y);
+
+  return strip * leatherTex * hang;
+}
+
+void main() {
+  vec3 color = u_color;
+  if (u_useTexture) {
+    color *= texture(u_texture, v_texCoord).rgb;
+  }
+
+  vec3 normal = normalize(v_normal);
+  vec2 uv = v_worldPos.xz * 4.5;
+  float avgColor = (color.r + color.g + color.b) / 3.0;
+
+  // Detect bronze vs steel by color warmth
+  bool isBronze =
+      (color.r > color.g * 1.08 && color.r > color.b * 1.15 && avgColor > 0.50);
+  bool isRedCape = (color.r > color.g * 1.3 && color.r > color.b * 1.4);
+
+  // === ROMAN ARCHER (SAGITTARIUS) MATERIALS ===
+
+  // BRONZE GALEA HELMET & PHALERAE (warm golden metal)
+  if (isBronze) {
+    // Ancient bronze patina and wear
+    float bronzePatina = noise(uv * 8.0) * 0.12;
+    float verdigris = noise(uv * 15.0) * 0.08; // Green oxidation
+
+    // Bronze is less reflective than polished steel
+    float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
+    float bronzeSheen = pow(viewAngle, 7.0) * 0.25;
+    float bronzeFresnel = pow(1.0 - viewAngle, 2.2) * 0.18;
+
+    // Hammer marks from forging
+    float hammerMarks = noise(uv * 25.0) * 0.035;
+
+    color += vec3(bronzeSheen + bronzeFresnel);
+    color -= vec3(bronzePatina * 0.4 + verdigris * 0.3);
+    color += vec3(hammerMarks * 0.5);
+  }
+  // STEEL CHAINMAIL (lorica hamata - grey-blue tint)
+  else if (avgColor > 0.40 && avgColor <= 0.60 && !isRedCape) {
+    // Interlocked iron rings
+    float rings = chainmailRings(v_worldPos.xz);
+
+    // Chainmail has dull metallic sheen
+    float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
+    float chainSheen = pow(viewAngle, 5.0) * 0.16;
+
+    // Iron rust spots
+    float rust = noise(uv * 10.0) * 0.08;
+
+    color += vec3(rings + chainSheen);
+    color -= vec3(rust * 0.4);              // Darken with age
+    color *= 1.0 - noise(uv * 18.0) * 0.06; // Shadow between rings
+  }
+  // RED SAGUM CAPE (bright red woolen cloak)
+  else if (isRedCape) {
+    // Thick woolen weave
+    float weaveX = sin(v_worldPos.x * 55.0);
+    float weaveZ = sin(v_worldPos.z * 55.0);
+    float weave = weaveX * weaveZ * 0.045;
+
+    // Wool texture (fuzzy)
+    float woolFuzz = noise(uv * 20.0) * 0.10;
+
+    // Fabric folds and draping
+    float folds = noise(uv * 6.0) * 0.12 - 0.06;
+
+    // Soft fabric sheen
+    float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
+    float capeSheen = pow(1.0 - viewAngle, 8.0) * 0.08;
+
+    color *= 1.0 + woolFuzz - 0.05 + folds;
+    color += vec3(weave + capeSheen);
+  }
+  // LEATHER PTERUGES & ARMOR STRIPS (tan/brown leather strips)
+  else if (avgColor > 0.35) {
+    // Thick leather with visible grain
+    float leatherGrain = noise(uv * 10.0) * 0.16;
+    float leatherPores = noise(uv * 22.0) * 0.08;
+
+    // Pteruges strip pattern
+    float strips = pterugesStrips(v_worldPos.xz, v_worldPos.y);
+
+    // Worn leather edges
+    float wear = noise(uv * 4.0) * 0.10 - 0.05;
+
+    // Leather has subtle sheen
+    float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
+    float leatherSheen = pow(1.0 - viewAngle, 4.5) * 0.10;
+
+    color *= 1.0 + leatherGrain + leatherPores - 0.08 + wear;
+    color += vec3(strips * 0.15 + leatherSheen);
+  }
+  // DARK ELEMENTS (cingulum belt, straps, manicae)
+  else {
+    float leatherDetail = noise(uv * 8.0) * 0.14;
+    float tooling = noise(uv * 16.0) * 0.06; // Decorative tooling
+    float darkening = noise(uv * 2.5) * 0.08;
+
+    color *= 1.0 + leatherDetail - 0.07 + tooling - darkening;
+  }
+
+  color = clamp(color, 0.0, 1.0);
+
+  // Lighting model - soft wrap for leather/fabric, harder for metal
+  vec3 lightDir = normalize(vec3(1.0, 1.15, 1.0));
+  float nDotL = dot(normal, lightDir);
+
+  // Metal = harder shadows, Fabric/leather = soft wrap
+  float wrapAmount = isBronze ? 0.15 : 0.38;
+  float diff = max(nDotL * (1.0 - wrapAmount) + wrapAmount, 0.22);
+
+  // Enhance contrast for bronze
+  if (isBronze) {
+    diff = pow(diff, 0.90);
+  }
+
+  color *= diff;
+  FragColor = vec4(color, u_alpha);
+}

+ 19 - 0
assets/shaders/archer.vert

@@ -0,0 +1,19 @@
+#version 330 core
+
+layout(location = 0) in vec3 a_position;
+layout(location = 1) in vec3 a_normal;
+layout(location = 2) in vec2 a_texCoord;
+
+uniform mat4 u_mvp;
+uniform mat4 u_model;
+
+out vec3 v_normal;
+out vec2 v_texCoord;
+out vec3 v_worldPos;
+
+void main() {
+  v_normal = mat3(transpose(inverse(u_model))) * a_normal;
+  v_texCoord = a_texCoord;
+  v_worldPos = vec3(u_model * vec4(a_position, 1.0));
+  gl_Position = u_mvp * vec4(a_position, 1.0);
+}

+ 16 - 13
assets/shaders/basic.frag

@@ -35,25 +35,28 @@ vec3 proceduralMaterialVariation(vec3 baseColor, vec3 worldPos, vec3 normal) {
 
 
   vec3 variation = baseColor;
   vec3 variation = baseColor;
 
 
-  if (avgColor < 0.30) {
-    float metalNoise = noise(uv * 8.0) * 0.015;
+  if (avgColor < 0.40) {
+    // Metal/dark materials
+    float metalNoise = noise(uv * 9.0) * 0.018;
     float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
     float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
-    float fresnel = pow(1.0 - viewAngle, 2.0) * 0.08;
+    float fresnel = pow(1.0 - viewAngle, 2.0) * 0.10;
     variation = baseColor + vec3(metalNoise + fresnel);
     variation = baseColor + vec3(metalNoise + fresnel);
   } else if (avgColor > 0.65) {
   } else if (avgColor > 0.65) {
-    float weaveX = sin(worldPos.x * 50.0);
-    float weaveZ = sin(worldPos.z * 50.0);
-    float weavePattern = weaveX * weaveZ * 0.02;
-    float clothNoise = noise(uv * 2.0) * 0.08 - 0.04;
+    // Cloth/bright materials
+    float weaveX = sin(worldPos.x * 55.0);
+    float weaveZ = sin(worldPos.z * 55.0);
+    float weavePattern = weaveX * weaveZ * 0.025;
+    float clothNoise = noise(uv * 2.5) * 0.10 - 0.05;
 
 
     float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
     float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
-    float sheen = pow(1.0 - viewAngle, 3.0) * 0.12;
+    float sheen = pow(1.0 - viewAngle, 3.0) * 0.15;
 
 
     variation = baseColor * (1.0 + clothNoise + weavePattern) + vec3(sheen);
     variation = baseColor * (1.0 + clothNoise + weavePattern) + vec3(sheen);
   } else {
   } else {
-    float leatherNoise = noise(uv * 5.0);
-    float blotches = noise(uv * 1.5) * 0.1 - 0.05;
-    variation = baseColor * (1.0 + leatherNoise * 0.12 - 0.06 + blotches);
+    // Leather/medium materials
+    float leatherNoise = noise(uv * 5.5);
+    float blotches = noise(uv * 1.8) * 0.12 - 0.06;
+    variation = baseColor * (1.0 + leatherNoise * 0.14 - 0.07 + blotches);
   }
   }
 
 
   return clamp(variation, 0.0, 1.0);
   return clamp(variation, 0.0, 1.0);
@@ -71,10 +74,10 @@ void main() {
   vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
   vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
 
 
   float avgColor = (u_color.r + u_color.g + u_color.b) / 3.0;
   float avgColor = (u_color.r + u_color.g + u_color.b) / 3.0;
-  float wrapAmount = avgColor > 0.65 ? 0.5 : 0.0;
+  float wrapAmount = avgColor > 0.65 ? 0.52 : (avgColor > 0.40 ? 0.20 : 0.05);
 
 
   float nDotL = dot(normal, lightDir);
   float nDotL = dot(normal, lightDir);
-  float diff = max(nDotL * (1.0 - wrapAmount) + wrapAmount, 0.2);
+  float diff = max(nDotL * (1.0 - wrapAmount) + wrapAmount, 0.22);
 
 
   color *= diff;
   color *= diff;
   FragColor = vec4(color, u_alpha);
   FragColor = vec4(color, u_alpha);

+ 177 - 0
assets/shaders/knight.frag

@@ -0,0 +1,177 @@
+#version 330 core
+
+in vec3 v_normal;
+in vec2 v_texCoord;
+in vec3 v_worldPos;
+
+uniform sampler2D u_texture;
+uniform vec3 u_color;
+uniform bool u_useTexture;
+uniform float u_alpha;
+
+out vec4 FragColor;
+
+float hash(vec2 p) {
+  vec3 p3 = fract(vec3(p.xyx) * 0.1031);
+  p3 += dot(p3, p3.yzx + 33.33);
+  return fract((p3.x + p3.y) * p3.z);
+}
+
+float noise(vec2 p) {
+  vec2 i = floor(p);
+  vec2 f = fract(p);
+  f = f * f * (3.0 - 2.0 * f);
+  float a = hash(i);
+  float b = hash(i + vec2(1.0, 0.0));
+  float c = hash(i + vec2(0.0, 1.0));
+  float d = hash(i + vec2(1.0, 1.0));
+  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
+// Medieval plate armor articulation lines
+float armorPlates(vec2 p, float y) {
+  // Horizontal articulation lines (overlapping plates)
+  float plateY = fract(y * 6.5);
+  float plateLine = smoothstep(0.90, 0.98, plateY) * 0.12;
+
+  // Brass rivet decorations
+  float rivetX = fract(p.x * 18.0);
+  float rivet = smoothstep(0.48, 0.50, rivetX) * smoothstep(0.52, 0.50, rivetX);
+  float rivetPattern = rivet * step(0.92, plateY) * 0.25; // Brass is brighter
+
+  return plateLine + rivetPattern;
+}
+
+// Chainmail texture pattern
+float chainmailRings(vec2 p) {
+  vec2 grid = fract(p * 35.0) - 0.5;
+  float ring = length(grid);
+  float ringPattern =
+      smoothstep(0.35, 0.30, ring) - smoothstep(0.25, 0.20, ring);
+
+  // Offset every other row for interlinked appearance
+  vec2 offsetGrid = fract(p * 35.0 + vec2(0.5, 0.0)) - 0.5;
+  float offsetRing = length(offsetGrid);
+  float offsetPattern =
+      smoothstep(0.35, 0.30, offsetRing) - smoothstep(0.25, 0.20, offsetRing);
+
+  return (ringPattern + offsetPattern) * 0.15;
+}
+
+void main() {
+  vec3 color = u_color;
+  if (u_useTexture) {
+    color *= texture(u_texture, v_texCoord).rgb;
+  }
+
+  vec3 normal = normalize(v_normal);
+  vec2 uv = v_worldPos.xz * 5.0;
+  float avgColor = (color.r + color.g + color.b) / 3.0;
+
+  // Detect material type by color tone
+  float colorHue =
+      max(max(color.r, color.g), color.b) - min(min(color.r, color.g), color.b);
+  bool isBrass =
+      (color.r > color.g * 1.15 && color.r > color.b * 1.2 && avgColor > 0.55);
+
+  // === MEDIEVAL KNIGHT MATERIALS ===
+
+  // POLISHED STEEL PLATE (Great Helm, cuirass, pauldrons, rerebraces) - bright
+  // silvery
+  if (avgColor > 0.60 && !isBrass) {
+    // Mirror-polished steel finish
+    float brushedMetal = abs(sin(v_worldPos.y * 95.0)) * 0.02;
+
+    // Battle wear: scratches and dents
+    float scratches = noise(uv * 35.0) * 0.018;
+    float dents = noise(uv * 8.0) * 0.025;
+
+    // Plate articulation lines and rivets
+    float plates = armorPlates(v_worldPos.xz, v_worldPos.y);
+
+    // Strong specular reflections (polished metal)
+    float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
+    float fresnel = pow(1.0 - viewAngle, 1.8) * 0.35; // Bright rim lighting
+    float specular = pow(viewAngle, 12.0) * 0.55;     // Sharp mirror highlights
+
+    // Environmental reflections (sky dome)
+    float skyReflection = (normal.y * 0.5 + 0.5) * 0.12;
+
+    color += vec3(fresnel + skyReflection + specular * 1.8);
+    color += vec3(plates);
+    color += vec3(brushedMetal);
+    color -= vec3(scratches + dents * 0.4);
+  }
+  // BRASS ACCENTS (rivets, buckles, crosses, decorations) - golden
+  else if (isBrass) {
+    // Warm metallic brass
+    float brassNoise = noise(uv * 22.0) * 0.025;
+    float patina = noise(uv * 6.0) * 0.08; // Age darkening
+
+    float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
+    float brassSheen = pow(viewAngle, 8.0) * 0.35;
+    float brassFresnel = pow(1.0 - viewAngle, 2.5) * 0.20;
+
+    color += vec3(brassSheen + brassFresnel);
+    color += vec3(brassNoise);
+    color -= vec3(patina * 0.5); // Darker in recesses
+  }
+  // CHAINMAIL AVENTAIL (hanging neck protection) - grey steel rings
+  else if (avgColor > 0.40 && avgColor <= 0.60) {
+    // Interlocked ring texture
+    float rings = chainmailRings(v_worldPos.xz);
+
+    // Chainmail has less shine than plate
+    float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
+    float chainSheen = pow(viewAngle, 6.0) * 0.18;
+
+    // Individual ring highlights
+    float ringHighlights = noise(uv * 30.0) * 0.12;
+
+    color += vec3(rings + chainSheen + ringHighlights);
+    color *= 1.0 - noise(uv * 12.0) * 0.08; // Slight darkening between rings
+  }
+  // HERALDIC SURCOAT (team-colored tabard over armor) - bright cloth
+  else if (avgColor > 0.25) {
+    // Rich fabric weave texture
+    float weaveX = sin(v_worldPos.x * 70.0);
+    float weaveZ = sin(v_worldPos.z * 70.0);
+    float weave = weaveX * weaveZ * 0.04;
+
+    // Embroidered cross emblem texture
+    float embroidery = noise(uv * 12.0) * 0.06;
+
+    // Fabric has soft sheen
+    float viewAngle = abs(dot(normal, normalize(vec3(0.0, 1.0, 0.5))));
+    float fabricSheen = pow(1.0 - viewAngle, 6.0) * 0.08;
+
+    // Heraldic colors are vibrant
+    color *= 1.0 + noise(uv * 5.0) * 0.10 - 0.05;
+    color += vec3(weave + embroidery + fabricSheen);
+  }
+  // LEATHER/DARK ELEMENTS (straps, gloves, scabbard) - dark brown
+  else {
+    float leatherGrain = noise(uv * 10.0) * 0.15;
+    float wearMarks = noise(uv * 3.0) * 0.10;
+
+    color *= 1.0 + leatherGrain - 0.08 + wearMarks - 0.05;
+  }
+
+  color = clamp(color, 0.0, 1.0);
+
+  // Lighting model - hard shadows for metal, soft for fabric
+  vec3 lightDir = normalize(vec3(1.0, 1.2, 1.0));
+  float nDotL = dot(normal, lightDir);
+
+  // Metal = hard shadows, Fabric = soft wrap
+  float wrapAmount = (avgColor > 0.50) ? 0.08 : 0.30;
+  float diff = max(nDotL * (1.0 - wrapAmount) + wrapAmount, 0.18);
+
+  // Extra contrast for polished steel
+  if (avgColor > 0.60 && !isBrass) {
+    diff = pow(diff, 0.85); // Sharper lighting falloff
+  }
+
+  color *= diff;
+  FragColor = vec4(color, u_alpha);
+}

+ 19 - 0
assets/shaders/knight.vert

@@ -0,0 +1,19 @@
+#version 330 core
+
+layout(location = 0) in vec3 a_position;
+layout(location = 1) in vec3 a_normal;
+layout(location = 2) in vec2 a_texCoord;
+
+uniform mat4 u_mvp;
+uniform mat4 u_model;
+
+out vec3 v_normal;
+out vec2 v_texCoord;
+out vec3 v_worldPos;
+
+void main() {
+  v_normal = mat3(transpose(inverse(u_model))) * a_normal;
+  v_texCoord = a_texCoord;
+  v_worldPos = vec3(u_model * vec4(a_position, 1.0));
+  gl_Position = u_mvp * vec4(a_position, 1.0);
+}

+ 2 - 0
render/draw_queue.h

@@ -15,6 +15,7 @@ namespace Render::GL {
 class Mesh;
 class Mesh;
 class Texture;
 class Texture;
 class Buffer;
 class Buffer;
+class Shader;
 } // namespace Render::GL
 } // namespace Render::GL
 
 
 namespace Render::GL {
 namespace Render::GL {
@@ -26,6 +27,7 @@ struct MeshCmd {
   QMatrix4x4 mvp;
   QMatrix4x4 mvp;
   QVector3D color{1, 1, 1};
   QVector3D color{1, 1, 1};
   float alpha = 1.0f;
   float alpha = 1.0f;
+  class Shader *shader = nullptr;
 };
 };
 
 
 struct CylinderCmd {
 struct CylinderCmd {

+ 275 - 16
render/entity/archer_renderer.cpp

@@ -6,19 +6,25 @@
 #include "../../game/visuals/team_colors.h"
 #include "../../game/visuals/team_colors.h"
 #include "../geom/math_utils.h"
 #include "../geom/math_utils.h"
 #include "../geom/transforms.h"
 #include "../geom/transforms.h"
+#include "../gl/backend.h"
 #include "../gl/mesh.h"
 #include "../gl/mesh.h"
 #include "../gl/primitives.h"
 #include "../gl/primitives.h"
+#include "../gl/shader.h"
 #include "../humanoid_base.h"
 #include "../humanoid_base.h"
 #include "../humanoid_math.h"
 #include "../humanoid_math.h"
 #include "../humanoid_specs.h"
 #include "../humanoid_specs.h"
 #include "../palette.h"
 #include "../palette.h"
+#include "../scene_renderer.h"
+#include "../submitter.h"
 #include "registry.h"
 #include "registry.h"
 
 
 #include <QMatrix4x4>
 #include <QMatrix4x4>
+#include <QString>
 #include <QVector3D>
 #include <QVector3D>
 #include <algorithm>
 #include <algorithm>
 #include <cmath>
 #include <cmath>
 #include <cstdint>
 #include <cstdint>
+#include <unordered_map>
 
 
 namespace Render::GL {
 namespace Render::GL {
 
 
@@ -26,6 +32,9 @@ using Render::Geom::clamp01;
 using Render::Geom::clampf;
 using Render::Geom::clampf;
 using Render::Geom::coneFromTo;
 using Render::Geom::coneFromTo;
 using Render::Geom::cylinderBetween;
 using Render::Geom::cylinderBetween;
+using Render::Geom::sphereAt;
+
+static constexpr std::size_t MAX_EXTRAS_CACHE_SIZE = 10000;
 
 
 struct ArcherExtras {
 struct ArcherExtras {
   QVector3D stringCol;
   QVector3D stringCol;
@@ -40,6 +49,14 @@ struct ArcherExtras {
 };
 };
 
 
 class ArcherRenderer : public HumanoidRendererBase {
 class ArcherRenderer : public HumanoidRendererBase {
+public:
+  QVector3D getProportionScaling() const override {
+    return QVector3D(0.92f, 1.00f, 0.95f);
+  }
+
+private:
+  mutable std::unordered_map<uint32_t, ArcherExtras> m_extrasCache;
+
 public:
 public:
   void getVariant(const DrawContext &ctx, uint32_t seed,
   void getVariant(const DrawContext &ctx, uint32_t seed,
                   HumanoidVariant &v) const override {
                   HumanoidVariant &v) const override {
@@ -154,7 +171,7 @@ public:
   }
   }
 
 
   void addAttachments(const DrawContext &ctx, const HumanoidVariant &v,
   void addAttachments(const DrawContext &ctx, const HumanoidVariant &v,
-                      const HumanoidPose &pose,
+                      const HumanoidPose &pose, const AnimationInputs &anim,
                       ISubmitter &out) const override {
                       ISubmitter &out) const override {
     using HP = HumanProportions;
     using HP = HumanProportions;
 
 
@@ -168,19 +185,29 @@ public:
     }
     }
 
 
     ArcherExtras extras;
     ArcherExtras extras;
-    extras.metalHead = Render::Geom::clampVec01(v.palette.metal * 1.15f);
-    extras.stringCol = QVector3D(0.30f, 0.30f, 0.32f);
-    auto tint = [&](float k) {
-      return QVector3D(clamp01(teamTint.x() * k), clamp01(teamTint.y() * k),
-                       clamp01(teamTint.z() * k));
-    };
-    extras.fletch = tint(0.9f);
-    extras.bowTopY = HP::SHOULDER_Y + 0.55f;
-    extras.bowBotY = HP::WAIST_Y - 0.25f;
+    auto it = m_extrasCache.find(seed);
+    if (it != m_extrasCache.end()) {
+      extras = it->second;
+    } else {
+      extras.metalHead = Render::Geom::clampVec01(v.palette.metal * 1.15f);
+      extras.stringCol = QVector3D(0.30f, 0.30f, 0.32f);
+      auto tint = [&](float k) {
+        return QVector3D(clamp01(teamTint.x() * k), clamp01(teamTint.y() * k),
+                         clamp01(teamTint.z() * k));
+      };
+      extras.fletch = tint(0.9f);
+      extras.bowTopY = HP::SHOULDER_Y + 0.55f;
+      extras.bowBotY = HP::WAIST_Y - 0.25f;
+
+      m_extrasCache[seed] = extras;
+
+      if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
+        m_extrasCache.clear();
+      }
+    }
 
 
     drawQuiver(ctx, v, extras, seed, out);
     drawQuiver(ctx, v, extras, seed, out);
 
 
-    AnimationInputs anim = sampleAnimState(ctx);
     float attackPhase = 0.0f;
     float attackPhase = 0.0f;
     if (anim.isAttacking && !anim.isMelee) {
     if (anim.isAttacking && !anim.isMelee) {
       float attackCycleTime = 1.2f;
       float attackCycleTime = 1.2f;
@@ -190,6 +217,227 @@ public:
                     attackPhase, out);
                     attackPhase, out);
   }
   }
 
 
+  void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
+                  const HumanoidPose &pose, ISubmitter &out) const override {
+    using HP = HumanProportions;
+
+    QVector3D helmetColor = v.palette.metal * QVector3D(1.1f, 0.95f, 0.7f);
+    QVector3D helmetTop(0, pose.headPos.y() + pose.headR * 1.25f, 0);
+    QVector3D helmetBot(0, pose.headPos.y() + pose.headR * 0.10f, 0);
+    float helmetR = pose.headR * 1.08f;
+
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, helmetBot, helmetTop, helmetR),
+             helmetColor, nullptr, 1.0f);
+
+    QVector3D apexPos(0, pose.headPos.y() + pose.headR * 1.45f, 0);
+    out.mesh(getUnitCone(),
+             coneFromTo(ctx.model, helmetTop, apexPos, helmetR * 0.95f),
+             helmetColor * 1.05f, nullptr, 1.0f);
+
+    QVector3D browPos(0, pose.headPos.y() + pose.headR * 0.35f, 0);
+    auto ring = [&](const QVector3D &center, float r, float h,
+                    const QVector3D &col) {
+      QVector3D a = center + QVector3D(0, h * 0.5f, 0);
+      QVector3D b = center - QVector3D(0, h * 0.5f, 0);
+      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r), col,
+               nullptr, 1.0f);
+    };
+    ring(browPos, helmetR * 1.06f, 0.018f, helmetColor * 1.1f);
+
+    float cheekW = pose.headR * 0.45f;
+    float cheekH = pose.headR * 0.65f;
+    QVector3D cheekTop(0, pose.headPos.y() + pose.headR * 0.25f, 0);
+    QVector3D cheekBot(0, pose.headPos.y() - pose.headR * 0.40f, 0);
+
+    QVector3D cheekLTop = cheekTop + QVector3D(-cheekW, 0, pose.headR * 0.35f);
+    QVector3D cheekLBot =
+        cheekBot + QVector3D(-cheekW * 0.8f, 0, pose.headR * 0.25f);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, cheekLBot, cheekLTop, 0.025f),
+             helmetColor * 0.95f, nullptr, 1.0f);
+
+    QVector3D cheekRTop = cheekTop + QVector3D(cheekW, 0, pose.headR * 0.35f);
+    QVector3D cheekRBot =
+        cheekBot + QVector3D(cheekW * 0.8f, 0, pose.headR * 0.25f);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, cheekRBot, cheekRTop, 0.025f),
+             helmetColor * 0.95f, nullptr, 1.0f);
+
+    QVector3D neckGuardTop(0, pose.headPos.y() + pose.headR * 0.05f,
+                           -pose.headR * 0.80f);
+    QVector3D neckGuardBot(0, pose.headPos.y() - pose.headR * 0.30f,
+                           -pose.headR * 0.85f);
+    out.mesh(
+        getUnitCylinder(),
+        cylinderBetween(ctx.model, neckGuardBot, neckGuardTop, helmetR * 0.85f),
+        helmetColor * 0.92f, nullptr, 1.0f);
+
+    QVector3D crestBase = apexPos;
+    QVector3D crestTop = crestBase + QVector3D(0, 0.08f, 0);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, crestBase, crestTop, 0.015f),
+             helmetColor * 1.15f, nullptr, 1.0f);
+
+    out.mesh(getUnitCone(),
+             coneFromTo(ctx.model, crestTop, crestTop + QVector3D(0, 0.10f, 0),
+                        0.035f),
+             QVector3D(0.85f, 0.15f, 0.15f), nullptr, 1.0f);
+  }
+
+  void drawArmorOverlay(const DrawContext &ctx, const HumanoidVariant &v,
+                        const HumanoidPose &pose, float yTopCover, float torsoR,
+                        float shoulderHalfSpan, float upperArmR,
+                        const QVector3D &rightAxis,
+                        ISubmitter &out) const override {
+    using HP = HumanProportions;
+
+    auto ring = [&](const QVector3D &center, float r, float h,
+                    const QVector3D &col) {
+      QVector3D a = center + QVector3D(0, h * 0.5f, 0);
+      QVector3D b = center - QVector3D(0, h * 0.5f, 0);
+      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r), col,
+               nullptr, 1.0f);
+    };
+
+    QVector3D mailColor = v.palette.metal * QVector3D(0.85f, 0.87f, 0.92f);
+    QVector3D leatherTrim = v.palette.leatherDark * 0.90f;
+
+    QVector3D mailTop(0, yTopCover + 0.01f, 0);
+    QVector3D mailMid(0, (yTopCover + HP::WAIST_Y) * 0.5f, 0);
+    QVector3D mailBot(0, HP::WAIST_Y + 0.08f, 0);
+    float rTop = torsoR * 1.10f;
+    float rMid = torsoR * 1.08f;
+
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, mailTop, mailMid, rTop), mailColor,
+             nullptr, 1.0f);
+
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, mailMid, mailBot, rMid),
+             mailColor * 0.95f, nullptr, 1.0f);
+
+    for (int i = 0; i < 3; ++i) {
+      float y = mailTop.y() - (i * 0.12f);
+      ring(QVector3D(0, y, 0), rTop * (1.01f + i * 0.005f), 0.012f,
+           leatherTrim);
+    }
+
+    auto drawPauldron = [&](const QVector3D &shoulder,
+                            const QVector3D &outward) {
+      for (int i = 0; i < 3; ++i) {
+        float segY = shoulder.y() + 0.02f - i * 0.035f;
+        float segR = upperArmR * (2.2f - i * 0.15f);
+        QVector3D segTop(shoulder.x(), segY + 0.025f, shoulder.z());
+        QVector3D segBot(shoulder.x(), segY - 0.010f, shoulder.z());
+
+        segTop += outward * 0.02f;
+        segBot += outward * 0.02f;
+
+        out.mesh(getUnitSphere(), sphereAt(ctx.model, segTop, segR),
+                 mailColor * (1.0f - i * 0.05f), nullptr, 1.0f);
+      }
+    };
+
+    drawPauldron(pose.shoulderL, -rightAxis);
+    drawPauldron(pose.shoulderR, rightAxis);
+
+    auto drawManica = [&](const QVector3D &shoulder, const QVector3D &elbow) {
+      QVector3D dir = (elbow - shoulder);
+      float len = dir.length();
+      if (len < 1e-5f)
+        return;
+      dir /= len;
+
+      for (int i = 0; i < 4; ++i) {
+        float t0 = 0.08f + i * 0.18f;
+        float t1 = t0 + 0.16f;
+        QVector3D a = shoulder + dir * (t0 * len);
+        QVector3D b = shoulder + dir * (t1 * len);
+        float r = upperArmR * (1.25f - i * 0.03f);
+        out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r),
+                 mailColor * (0.95f - i * 0.03f), nullptr, 1.0f);
+      }
+    };
+
+    drawManica(pose.shoulderL, pose.elbowL);
+    drawManica(pose.shoulderR, pose.elbowR);
+
+    QVector3D beltTop(0, HP::WAIST_Y + 0.06f, 0);
+    QVector3D beltBot(0, HP::WAIST_Y - 0.02f, 0);
+    float beltR = torsoR * 1.12f;
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, beltTop, beltBot, beltR), leatherTrim,
+             nullptr, 1.0f);
+
+    QVector3D brassColor = v.palette.metal * QVector3D(1.2f, 1.0f, 0.65f);
+    ring(QVector3D(0, HP::WAIST_Y + 0.02f, 0), beltR * 1.02f, 0.010f,
+         brassColor);
+
+    auto drawPteruge = [&](float angle, float yStart, float length) {
+      float rad = torsoR * 1.15f;
+      float x = rad * std::sin(angle);
+      float z = rad * std::cos(angle);
+      QVector3D top(x, yStart, z);
+      QVector3D bot(x * 0.95f, yStart - length, z * 0.95f);
+      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, top, bot, 0.018f),
+               leatherTrim * 0.85f, nullptr, 1.0f);
+    };
+
+    float shoulderPterugeY = yTopCover - 0.02f;
+    for (int i = 0; i < 8; ++i) {
+      float angle = (i / 8.0f) * 2.0f * 3.14159265f;
+      drawPteruge(angle, shoulderPterugeY, 0.14f);
+    }
+
+    float waistPterugeY = HP::WAIST_Y - 0.04f;
+    for (int i = 0; i < 10; ++i) {
+      float angle = (i / 10.0f) * 2.0f * 3.14159265f;
+      drawPteruge(angle, waistPterugeY, 0.18f);
+    }
+
+    QVector3D collarTop(0, yTopCover + 0.018f, 0);
+    QVector3D collarBot(0, yTopCover - 0.008f, 0);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, collarTop, collarBot,
+                             HP::NECK_RADIUS * 1.8f),
+             mailColor * 1.05f, nullptr, 1.0f);
+  }
+
+  void drawShoulderDecorations(const DrawContext &ctx, const HumanoidVariant &v,
+                               const HumanoidPose &pose, float yTopCover,
+                               float yNeck, const QVector3D &rightAxis,
+                               ISubmitter &out) const override {
+    using HP = HumanProportions;
+
+    QVector3D brassColor = v.palette.metal * QVector3D(1.2f, 1.0f, 0.65f);
+
+    auto drawPhalera = [&](const QVector3D &pos) {
+      QMatrix4x4 m = ctx.model;
+      m.translate(pos);
+      m.scale(0.025f);
+      out.mesh(getUnitSphere(), m, brassColor, nullptr, 1.0f);
+    };
+
+    drawPhalera(pose.shoulderL + QVector3D(0, 0.05f, 0.02f));
+
+    drawPhalera(pose.shoulderR + QVector3D(0, 0.05f, 0.02f));
+
+    QVector3D claspPos(0, yNeck + 0.02f, 0.08f);
+    QMatrix4x4 claspM = ctx.model;
+    claspM.translate(claspPos);
+    claspM.scale(0.020f);
+    out.mesh(getUnitSphere(), claspM, brassColor * 1.1f, nullptr, 1.0f);
+
+    QVector3D capeTop = claspPos + QVector3D(0, -0.02f, -0.05f);
+    QVector3D capeBot = claspPos + QVector3D(0, -0.25f, -0.15f);
+    QVector3D redFabric = v.palette.cloth * QVector3D(1.2f, 0.3f, 0.3f);
+
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, capeTop, capeBot, 0.025f),
+             redFabric * 0.85f, nullptr, 1.0f);
+  }
+
 private:
 private:
   static void drawQuiver(const DrawContext &ctx, const HumanoidVariant &v,
   static void drawQuiver(const DrawContext &ctx, const HumanoidVariant &v,
                          const ArcherExtras &extras, uint32_t seed,
                          const ArcherExtras &extras, uint32_t seed,
@@ -292,11 +540,22 @@ private:
 
 
 void registerArcherRenderer(Render::GL::EntityRendererRegistry &registry) {
 void registerArcherRenderer(Render::GL::EntityRendererRegistry &registry) {
   static ArcherRenderer renderer;
   static ArcherRenderer renderer;
-  registry.registerRenderer("archer",
-                            [](const DrawContext &ctx, ISubmitter &out) {
-                              static ArcherRenderer staticRenderer;
-                              staticRenderer.render(ctx, out);
-                            });
+  registry.registerRenderer(
+      "archer", [](const DrawContext &ctx, ISubmitter &out) {
+        static ArcherRenderer staticRenderer;
+        Shader *archerShader = nullptr;
+        if (ctx.backend) {
+          archerShader = ctx.backend->shader(QStringLiteral("archer"));
+        }
+        Renderer *sceneRenderer = dynamic_cast<Renderer *>(&out);
+        if (sceneRenderer && archerShader) {
+          sceneRenderer->setCurrentShader(archerShader);
+        }
+        staticRenderer.render(ctx, out);
+        if (sceneRenderer) {
+          sceneRenderer->setCurrentShader(nullptr);
+        }
+      });
 }
 }
 
 
 } // namespace Render::GL
 } // namespace Render::GL

+ 619 - 169
render/entity/knight_renderer.cpp

@@ -6,15 +6,21 @@
 #include "../../game/visuals/team_colors.h"
 #include "../../game/visuals/team_colors.h"
 #include "../geom/math_utils.h"
 #include "../geom/math_utils.h"
 #include "../geom/transforms.h"
 #include "../geom/transforms.h"
+#include "../gl/backend.h"
 #include "../gl/mesh.h"
 #include "../gl/mesh.h"
 #include "../gl/primitives.h"
 #include "../gl/primitives.h"
+#include "../gl/shader.h"
 #include "../humanoid_base.h"
 #include "../humanoid_base.h"
 #include "../humanoid_math.h"
 #include "../humanoid_math.h"
 #include "../humanoid_specs.h"
 #include "../humanoid_specs.h"
 #include "../palette.h"
 #include "../palette.h"
+#include "../scene_renderer.h"
+#include "../submitter.h"
 #include "registry.h"
 #include "registry.h"
+#include <unordered_map>
 
 
 #include <QMatrix4x4>
 #include <QMatrix4x4>
+#include <QString>
 #include <QVector3D>
 #include <QVector3D>
 #include <algorithm>
 #include <algorithm>
 #include <cmath>
 #include <cmath>
@@ -26,10 +32,14 @@ using Render::Geom::clamp01;
 using Render::Geom::clampf;
 using Render::Geom::clampf;
 using Render::Geom::coneFromTo;
 using Render::Geom::coneFromTo;
 using Render::Geom::cylinderBetween;
 using Render::Geom::cylinderBetween;
+using Render::Geom::sphereAt;
+
+static constexpr std::size_t MAX_EXTRAS_CACHE_SIZE = 10000;
 
 
 static inline float easeInOutCubic(float t) {
 static inline float easeInOutCubic(float t) {
   t = clamp01(t);
   t = clamp01(t);
-  return t < 0.5f ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) / 2.0f;
+  return t < 0.5f ? 4.0f * t * t * t
+                  : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) / 2.0f;
 }
 }
 
 
 static inline float smoothstep(float a, float b, float x) {
 static inline float smoothstep(float a, float b, float x) {
@@ -37,32 +47,42 @@ static inline float smoothstep(float a, float b, float x) {
   return x * x * (3.0f - 2.0f * x);
   return x * x * (3.0f - 2.0f * x);
 }
 }
 
 
-static inline float lerp(float a, float b, float t) { return a * (1.0f - t) + b * t; }
+static inline float lerp(float a, float b, float t) {
+  return a * (1.0f - t) + b * t;
+}
 
 
 static inline QVector3D nlerp(const QVector3D &a, const QVector3D &b, float t) {
 static inline QVector3D nlerp(const QVector3D &a, const QVector3D &b, float t) {
   QVector3D v = a * (1.0f - t) + b * t;
   QVector3D v = a * (1.0f - t) + b * t;
-  if (v.lengthSquared() > 1e-6f) v.normalize();
+  if (v.lengthSquared() > 1e-6f)
+    v.normalize();
   return v;
   return v;
 }
 }
 
 
 struct KnightExtras {
 struct KnightExtras {
   QVector3D metalColor;
   QVector3D metalColor;
   QVector3D shieldColor;
   QVector3D shieldColor;
-  float swordLength = 0.70f;
-  float swordWidth = 0.045f;
+  float swordLength = 0.80f;
+  float swordWidth = 0.065f;
   float shieldRadius = 0.18f;
   float shieldRadius = 0.18f;
 
 
-  // Internal flavor knobs
-  float guardHalfWidth = 0.10f;
-  float handleRadius   = 0.018f;
-  float pommelRadius   = 0.035f;
-  float bladeRicasso   = 0.14f;   // non-tapered segment near guard
-  float bladeTaperBias = 0.65f;   // where taper starts (0..1)
-  bool  shieldCrossDecal = false; // round-shield cross or ring
-  bool  hasScabbard = true;
+  float guardHalfWidth = 0.12f;
+  float handleRadius = 0.016f;
+  float pommelRadius = 0.045f;
+  float bladeRicasso = 0.16f;
+  float bladeTaperBias = 0.65f;
+  bool shieldCrossDecal = false;
+  bool hasScabbard = true;
 };
 };
 
 
 class KnightRenderer : public HumanoidRendererBase {
 class KnightRenderer : public HumanoidRendererBase {
+public:
+  QVector3D getProportionScaling() const override {
+    return QVector3D(1.40f, 1.05f, 1.10f);
+  }
+
+private:
+  mutable std::unordered_map<uint32_t, KnightExtras> m_extrasCache;
+
 public:
 public:
   void getVariant(const DrawContext &ctx, uint32_t seed,
   void getVariant(const DrawContext &ctx, uint32_t seed,
                   HumanoidVariant &v) const override {
                   HumanoidVariant &v) const override {
@@ -81,38 +101,39 @@ public:
       const float attackCycleTime = 0.6f;
       const float attackCycleTime = 0.6f;
       float attackPhase = std::fmod(anim.time * (1.0f / attackCycleTime), 1.0f);
       float attackPhase = std::fmod(anim.time * (1.0f / attackCycleTime), 1.0f);
 
 
-      // Staged positions with anticipation and follow-through
       QVector3D restPos(0.20f, HP::SHOULDER_Y + 0.05f, 0.15f);
       QVector3D restPos(0.20f, HP::SHOULDER_Y + 0.05f, 0.15f);
-      QVector3D preparePos(0.26f, HP::HEAD_TOP_Y + 0.18f, -0.06f); // higher & slightly back
+      QVector3D preparePos(0.26f, HP::HEAD_TOP_Y + 0.18f, -0.06f);
       QVector3D raisedPos(0.25f, HP::HEAD_TOP_Y + 0.22f, 0.02f);
       QVector3D raisedPos(0.25f, HP::HEAD_TOP_Y + 0.22f, 0.02f);
-      QVector3D strikePos(0.30f, HP::WAIST_Y - 0.05f, 0.50f);      // lower: top-to-bottom swing
+      QVector3D strikePos(0.30f, HP::WAIST_Y - 0.05f, 0.50f);
       QVector3D recoverPos(0.22f, HP::SHOULDER_Y + 0.02f, 0.22f);
       QVector3D recoverPos(0.22f, HP::SHOULDER_Y + 0.02f, 0.22f);
 
 
       if (attackPhase < 0.18f) {
       if (attackPhase < 0.18f) {
-        // Anticipation: lift high
+
         float t = easeInOutCubic(attackPhase / 0.18f);
         float t = easeInOutCubic(attackPhase / 0.18f);
         pose.handR = restPos * (1.0f - t) + preparePos * t;
         pose.handR = restPos * (1.0f - t) + preparePos * t;
-        pose.handL = QVector3D(-0.21f, HP::SHOULDER_Y - 0.02f - 0.03f * t, 0.15f);
+        pose.handL =
+            QVector3D(-0.21f, HP::SHOULDER_Y - 0.02f - 0.03f * t, 0.15f);
       } else if (attackPhase < 0.32f) {
       } else if (attackPhase < 0.32f) {
-        // Set up above head
+
         float t = easeInOutCubic((attackPhase - 0.18f) / 0.14f);
         float t = easeInOutCubic((attackPhase - 0.18f) / 0.14f);
         pose.handR = preparePos * (1.0f - t) + raisedPos * t;
         pose.handR = preparePos * (1.0f - t) + raisedPos * t;
         pose.handL = QVector3D(-0.21f, HP::SHOULDER_Y - 0.05f, 0.17f);
         pose.handL = QVector3D(-0.21f, HP::SHOULDER_Y - 0.05f, 0.17f);
       } else if (attackPhase < 0.52f) {
       } else if (attackPhase < 0.52f) {
-        // Top-to-bottom strike
+
         float t = (attackPhase - 0.32f) / 0.20f;
         float t = (attackPhase - 0.32f) / 0.20f;
-        t = t * t * t; // strong acceleration
+        t = t * t * t;
         pose.handR = raisedPos * (1.0f - t) + strikePos * t;
         pose.handR = raisedPos * (1.0f - t) + strikePos * t;
-        pose.handL = QVector3D(-0.21f, HP::SHOULDER_Y - 0.03f * (1.0f - 0.5f * t),
-                               0.17f + 0.20f * t);
+        pose.handL =
+            QVector3D(-0.21f, HP::SHOULDER_Y - 0.03f * (1.0f - 0.5f * t),
+                      0.17f + 0.20f * t);
       } else if (attackPhase < 0.72f) {
       } else if (attackPhase < 0.72f) {
-        // Follow-through to recover
+
         float t = easeInOutCubic((attackPhase - 0.52f) / 0.20f);
         float t = easeInOutCubic((attackPhase - 0.52f) / 0.20f);
         pose.handR = strikePos * (1.0f - t) + recoverPos * t;
         pose.handR = strikePos * (1.0f - t) + recoverPos * t;
         pose.handL = QVector3D(-0.20f, HP::SHOULDER_Y - 0.015f * (1.0f - t),
         pose.handL = QVector3D(-0.20f, HP::SHOULDER_Y - 0.015f * (1.0f - t),
                                lerp(0.37f, 0.20f, t));
                                lerp(0.37f, 0.20f, t));
       } else {
       } else {
-        // Glide back to rest
+
         float t = smoothstep(0.72f, 1.0f, attackPhase);
         float t = smoothstep(0.72f, 1.0f, attackPhase);
         pose.handR = recoverPos * (1.0f - t) + restPos * t;
         pose.handR = recoverPos * (1.0f - t) + restPos * t;
         pose.handL = QVector3D(-0.20f - 0.02f * (1.0f - t),
         pose.handL = QVector3D(-0.20f - 0.02f * (1.0f - t),
@@ -120,18 +141,32 @@ public:
                                lerp(0.20f, 0.15f, t));
                                lerp(0.20f, 0.15f, t));
       }
       }
     } else {
     } else {
-      // Idle stance: sword held more vertically
-      pose.handR = QVector3D(0.22f + armAsymmetry, HP::SHOULDER_Y + 0.06f + armHeightJitter, 0.18f);
-      pose.handL = QVector3D(-0.22f - 0.5f * armAsymmetry, HP::SHOULDER_Y + 0.5f * armHeightJitter, 0.18f);
+
+      pose.handR = QVector3D(0.30f + armAsymmetry,
+                             HP::SHOULDER_Y - 0.02f + armHeightJitter, 0.35f);
+      pose.handL = QVector3D(-0.22f - 0.5f * armAsymmetry,
+                             HP::SHOULDER_Y + 0.5f * armHeightJitter, 0.18f);
     }
     }
   }
   }
 
 
   void addAttachments(const DrawContext &ctx, const HumanoidVariant &v,
   void addAttachments(const DrawContext &ctx, const HumanoidVariant &v,
-                      const HumanoidPose &pose, ISubmitter &out) const override {
+                      const HumanoidPose &pose, const AnimationInputs &anim,
+                      ISubmitter &out) const override {
     uint32_t seed = reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFu;
     uint32_t seed = reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFu;
-    KnightExtras extras = computeKnightExtras(seed, v);
 
 
-    AnimationInputs anim = sampleAnimState(ctx);
+    KnightExtras extras;
+    auto it = m_extrasCache.find(seed);
+    if (it != m_extrasCache.end()) {
+      extras = it->second;
+    } else {
+      extras = computeKnightExtras(seed, v);
+      m_extrasCache[seed] = extras;
+
+      if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
+        m_extrasCache.clear();
+      }
+    }
+
     bool isAttacking = anim.isAttacking && anim.isMelee;
     bool isAttacking = anim.isAttacking && anim.isMelee;
     float attackPhase = 0.0f;
     float attackPhase = 0.0f;
     if (isAttacking) {
     if (isAttacking) {
@@ -142,44 +177,348 @@ public:
     drawSword(ctx, pose, v, extras, isAttacking, attackPhase, out);
     drawSword(ctx, pose, v, extras, isAttacking, attackPhase, out);
     drawShield(ctx, pose, v, extras, out);
     drawShield(ctx, pose, v, extras, out);
 
 
-    // Scabbard on hip when not actively striking
     if (!isAttacking && extras.hasScabbard) {
     if (!isAttacking && extras.hasScabbard) {
       drawScabbard(ctx, pose, v, extras, out);
       drawScabbard(ctx, pose, v, extras, out);
     }
     }
   }
   }
 
 
+  void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
+                  const HumanoidPose &pose, ISubmitter &out) const override {
+    using HP = HumanProportions;
+
+    auto ring = [&](const QVector3D &center, float r, float h,
+                    const QVector3D &col) {
+      QVector3D a = center + QVector3D(0, h * 0.5f, 0);
+      QVector3D b = center - QVector3D(0, h * 0.5f, 0);
+      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r), col,
+               nullptr, 1.0f);
+    };
+
+    QVector3D steelColor = v.palette.metal * QVector3D(0.95f, 0.96f, 1.0f);
+
+    float helmR = pose.headR * 1.15f;
+    QVector3D helmBot(0, pose.headPos.y() - pose.headR * 0.20f, 0);
+    QVector3D helmTop(0, pose.headPos.y() + pose.headR * 1.40f, 0);
+
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, helmBot, helmTop, helmR), steelColor,
+             nullptr, 1.0f);
+
+    QVector3D capTop(0, pose.headPos.y() + pose.headR * 1.48f, 0);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, helmTop, capTop, helmR * 0.98f),
+             steelColor * 1.05f, nullptr, 1.0f);
+
+    ring(QVector3D(0, pose.headPos.y() + pose.headR * 1.25f, 0), helmR * 1.02f,
+         0.015f, steelColor * 1.08f);
+    ring(QVector3D(0, pose.headPos.y() + pose.headR * 0.50f, 0), helmR * 1.02f,
+         0.015f, steelColor * 1.08f);
+    ring(QVector3D(0, pose.headPos.y() - pose.headR * 0.05f, 0), helmR * 1.02f,
+         0.015f, steelColor * 1.08f);
+
+    float visorY = pose.headPos.y() + pose.headR * 0.15f;
+    float visorZ = helmR * 0.72f;
+
+    QVector3D visorHL(-helmR * 0.35f, visorY, visorZ);
+    QVector3D visorHR(helmR * 0.35f, visorY, visorZ);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, visorHL, visorHR, 0.012f),
+             QVector3D(0.1f, 0.1f, 0.1f), nullptr, 1.0f);
+
+    QVector3D visorVT(0, visorY + helmR * 0.25f, visorZ);
+    QVector3D visorVB(0, visorY - helmR * 0.25f, visorZ);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, visorVB, visorVT, 0.012f),
+             QVector3D(0.1f, 0.1f, 0.1f), nullptr, 1.0f);
+
+    auto drawBreathingHole = [&](float x, float y) {
+      QVector3D pos(x, pose.headPos.y() + y, helmR * 0.70f);
+      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) {
+      drawBreathingHole(helmR * 0.50f, pose.headR * (0.05f - i * 0.10f));
+    }
+
+    for (int i = 0; i < 4; ++i) {
+      drawBreathingHole(-helmR * 0.50f, pose.headR * (0.05f - i * 0.10f));
+    }
+
+    QVector3D crossCenter(0, pose.headPos.y() + pose.headR * 0.60f,
+                          helmR * 0.75f);
+    QVector3D brassColor = v.palette.metal * QVector3D(1.3f, 1.1f, 0.7f);
+
+    QVector3D crossH1 = crossCenter + QVector3D(-0.04f, 0, 0);
+    QVector3D crossH2 = crossCenter + QVector3D(0.04f, 0, 0);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, crossH1, crossH2, 0.008f), brassColor,
+             nullptr, 1.0f);
+
+    QVector3D crossV1 = crossCenter + QVector3D(0, -0.04f, 0);
+    QVector3D crossV2 = crossCenter + QVector3D(0, 0.04f, 0);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, crossV1, crossV2, 0.008f), brassColor,
+             nullptr, 1.0f);
+  }
+
+  void drawArmorOverlay(const DrawContext &ctx, const HumanoidVariant &v,
+                        const HumanoidPose &pose, float yTopCover, float torsoR,
+                        float shoulderHalfSpan, float upperArmR,
+                        const QVector3D &rightAxis,
+                        ISubmitter &out) const override {
+    using HP = HumanProportions;
+
+    auto ring = [&](const QVector3D &center, float r, float h,
+                    const QVector3D &col) {
+      QVector3D a = center + QVector3D(0, h * 0.5f, 0);
+      QVector3D b = center - QVector3D(0, h * 0.5f, 0);
+      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r), col,
+               nullptr, 1.0f);
+    };
+
+    QVector3D steelColor = v.palette.metal * QVector3D(0.95f, 0.96f, 1.0f);
+    QVector3D darkSteel = steelColor * 0.85f;
+    QVector3D brassColor = v.palette.metal * QVector3D(1.3f, 1.1f, 0.7f);
+
+    QVector3D bpTop(0, yTopCover + 0.02f, 0);
+    QVector3D bpMid(0, (yTopCover + HP::WAIST_Y) * 0.5f + 0.04f, 0);
+    QVector3D bpBot(0, HP::WAIST_Y + 0.06f, 0);
+    float rChest = torsoR * 1.18f;
+    float rWaist = torsoR * 1.14f;
+
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, bpTop, bpMid, rChest), steelColor,
+             nullptr, 1.0f);
+
+    QVector3D bpMidLow(0, (bpMid.y() + bpBot.y()) * 0.5f, 0);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, bpMid, bpMidLow, rChest * 0.98f),
+             steelColor * 0.99f, nullptr, 1.0f);
+
+    out.mesh(getUnitCone(), coneFromTo(ctx.model, bpBot, bpMidLow, rWaist),
+             steelColor * 0.98f, nullptr, 1.0f);
+
+    auto drawRivet = [&](const QVector3D &pos) {
+      QMatrix4x4 m = ctx.model;
+      m.translate(pos);
+      m.scale(0.012f);
+      out.mesh(getUnitSphere(), m, brassColor, nullptr, 1.0f);
+    };
+
+    for (int i = 0; i < 8; ++i) {
+      float angle = (i / 8.0f) * 2.0f * 3.14159265f;
+      float x = rChest * std::sin(angle) * 0.95f;
+      float z = rChest * std::cos(angle) * 0.95f;
+      drawRivet(QVector3D(x, bpMid.y() + 0.08f, z));
+    }
+
+    auto drawPauldron = [&](const QVector3D &shoulder,
+                            const QVector3D &outward) {
+      for (int i = 0; i < 4; ++i) {
+        float segY = shoulder.y() + 0.04f - i * 0.045f;
+        float segR = upperArmR * (2.5f - i * 0.12f);
+        QVector3D segPos = shoulder + outward * (0.02f + i * 0.008f);
+        segPos.setY(segY);
+
+        out.mesh(getUnitSphere(), sphereAt(ctx.model, segPos, segR),
+                 i == 0 ? steelColor * 1.05f : steelColor * (1.0f - i * 0.03f),
+                 nullptr, 1.0f);
+
+        if (i < 3) {
+          drawRivet(segPos + QVector3D(0, 0.015f, 0.03f));
+        }
+      }
+    };
+
+    drawPauldron(pose.shoulderL, -rightAxis);
+    drawPauldron(pose.shoulderR, rightAxis);
+
+    auto drawArmPlate = [&](const QVector3D &shoulder, const QVector3D &elbow) {
+      QVector3D dir = (elbow - shoulder);
+      float len = dir.length();
+      if (len < 1e-5f)
+        return;
+      dir /= len;
+
+      for (int i = 0; i < 3; ++i) {
+        float t0 = 0.10f + i * 0.25f;
+        float t1 = t0 + 0.22f;
+        QVector3D a = shoulder + dir * (t0 * len);
+        QVector3D b = shoulder + dir * (t1 * len);
+        float r = upperArmR * (1.32f - i * 0.04f);
+
+        out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r),
+                 steelColor * (0.98f - i * 0.02f), nullptr, 1.0f);
+
+        if (i < 2) {
+          drawRivet(b);
+        }
+      }
+    };
+
+    drawArmPlate(pose.shoulderL, pose.elbowL);
+    drawArmPlate(pose.shoulderR, pose.elbowR);
+
+    for (int i = 0; i < 4; ++i) {
+      float y0 = HP::WAIST_Y + 0.04f - i * 0.038f;
+      float y1 = y0 - 0.032f;
+      float r0 = rWaist * (1.06f + i * 0.025f);
+      out.mesh(
+          getUnitCone(),
+          coneFromTo(ctx.model, QVector3D(0, y0, 0), QVector3D(0, y1, 0), r0),
+          steelColor * (0.96f - i * 0.02f), nullptr, 1.0f);
+
+      if (i < 3) {
+        drawRivet(QVector3D(r0 * 0.90f, y0 - 0.016f, 0));
+      }
+    }
+
+    QVector3D gorgetTop(0, yTopCover + 0.025f, 0);
+    QVector3D gorgetBot(0, yTopCover - 0.012f, 0);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, gorgetBot, gorgetTop,
+                             HP::NECK_RADIUS * 2.6f),
+             steelColor * 1.08f, nullptr, 1.0f);
+
+    ring(gorgetTop, HP::NECK_RADIUS * 2.62f, 0.010f, brassColor);
+  }
+
+  void drawShoulderDecorations(const DrawContext &ctx, const HumanoidVariant &v,
+                               const HumanoidPose &pose, float yTopCover,
+                               float yNeck, const QVector3D &rightAxis,
+                               ISubmitter &out) const override {
+    using HP = HumanProportions;
+
+    QVector3D brassColor = v.palette.metal * QVector3D(1.3f, 1.1f, 0.7f);
+    QVector3D chainmailColor = v.palette.metal * QVector3D(0.85f, 0.88f, 0.92f);
+    QVector3D mantlingColor = v.palette.cloth;
+
+    for (int i = 0; i < 5; ++i) {
+      float y = yNeck - i * 0.022f;
+      float r = HP::NECK_RADIUS * (1.85f + i * 0.08f);
+      QVector3D ringPos(0, y, 0);
+      QVector3D a = ringPos + QVector3D(0, 0.010f, 0);
+      QVector3D b = ringPos - QVector3D(0, 0.010f, 0);
+      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, a, b, r),
+               chainmailColor * (1.0f - i * 0.04f), nullptr, 1.0f);
+    }
+
+    QVector3D helmTop(0, HP::HEAD_TOP_Y - HP::HEAD_RADIUS * 0.15f, 0);
+    QMatrix4x4 crestBase = ctx.model;
+    crestBase.translate(helmTop);
+    crestBase.scale(0.025f, 0.015f, 0.025f);
+    out.mesh(getUnitSphere(), crestBase, brassColor * 1.2f, nullptr, 1.0f);
+
+    auto drawStud = [&](const QVector3D &pos) {
+      QMatrix4x4 m = ctx.model;
+      m.translate(pos);
+      m.scale(0.008f);
+      out.mesh(getUnitSphere(), m, brassColor * 1.3f, nullptr, 1.0f);
+    };
+
+    drawStud(helmTop + QVector3D(0.020f, 0, 0.020f));
+    drawStud(helmTop + QVector3D(-0.020f, 0, 0.020f));
+    drawStud(helmTop + QVector3D(0.020f, 0, -0.020f));
+    drawStud(helmTop + QVector3D(-0.020f, 0, -0.020f));
+
+    auto drawMantling = [&](const QVector3D &startPos,
+                            const QVector3D &direction) {
+      QVector3D currentPos = startPos;
+      for (int i = 0; i < 4; ++i) {
+        float segLen = 0.035f - i * 0.005f;
+        float segR = 0.020f - i * 0.003f;
+        QVector3D nextPos = currentPos + direction * segLen;
+        nextPos.setY(nextPos.y() - 0.025f);
+
+        out.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, currentPos, nextPos, segR),
+                 mantlingColor * (1.1f - i * 0.06f), nullptr, 1.0f);
+
+        currentPos = nextPos;
+      }
+    };
+
+    QVector3D mantlingStart(0, HP::CHIN_Y + HP::HEAD_RADIUS * 0.25f, 0);
+    drawMantling(mantlingStart + rightAxis * HP::HEAD_RADIUS * 0.95f,
+                 rightAxis * 0.5f + QVector3D(0, -0.1f, -0.3f));
+    drawMantling(mantlingStart - rightAxis * HP::HEAD_RADIUS * 0.95f,
+                 -rightAxis * 0.5f + QVector3D(0, -0.1f, -0.3f));
+
+    auto drawPauldronRivet = [&](const QVector3D &shoulder,
+                                 const QVector3D &outward) {
+      for (int i = 0; i < 3; ++i) {
+        float segY = shoulder.y() + 0.025f - i * 0.045f;
+        QVector3D rivetPos = shoulder + outward * (0.04f + i * 0.008f);
+        rivetPos.setY(segY);
+
+        drawStud(rivetPos);
+      }
+    };
+
+    drawPauldronRivet(pose.shoulderL, -rightAxis);
+    drawPauldronRivet(pose.shoulderR, rightAxis);
+
+    QVector3D gorgetTop(0, yTopCover + 0.045f, 0);
+    for (int i = 0; i < 6; ++i) {
+      float angle = (i / 6.0f) * 2.0f * 3.14159265f;
+      float x = HP::NECK_RADIUS * 2.58f * std::sin(angle);
+      float z = HP::NECK_RADIUS * 2.58f * std::cos(angle);
+      drawStud(gorgetTop + QVector3D(x, 0, z));
+    }
+
+    QVector3D beltCenter(0, HP::WAIST_Y + 0.03f, HP::TORSO_BOT_R * 1.15f);
+    QMatrix4x4 buckle = ctx.model;
+    buckle.translate(beltCenter);
+    buckle.scale(0.035f, 0.025f, 0.012f);
+    out.mesh(getUnitSphere(), buckle, brassColor * 1.25f, nullptr, 1.0f);
+
+    QVector3D buckleH1 = beltCenter + QVector3D(-0.025f, 0, 0.005f);
+    QVector3D buckleH2 = beltCenter + QVector3D(0.025f, 0, 0.005f);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, buckleH1, buckleH2, 0.006f),
+             brassColor * 1.4f, nullptr, 1.0f);
+
+    QVector3D buckleV1 = beltCenter + QVector3D(0, -0.018f, 0.005f);
+    QVector3D buckleV2 = beltCenter + QVector3D(0, 0.018f, 0.005f);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, buckleV1, buckleV2, 0.006f),
+             brassColor * 1.4f, nullptr, 1.0f);
+  }
+
 private:
 private:
   static KnightExtras computeKnightExtras(uint32_t seed,
   static KnightExtras computeKnightExtras(uint32_t seed,
                                           const HumanoidVariant &v) {
                                           const HumanoidVariant &v) {
     KnightExtras e;
     KnightExtras e;
-    // Subtle cool steel
+
     e.metalColor = QVector3D(0.72f, 0.73f, 0.78f);
     e.metalColor = QVector3D(0.72f, 0.73f, 0.78f);
 
 
-    // Shield base color: cloth/leather bias with seed variation
     float shieldHue = hash01(seed ^ 0x12345u);
     float shieldHue = hash01(seed ^ 0x12345u);
     if (shieldHue < 0.45f) {
     if (shieldHue < 0.45f) {
       e.shieldColor = v.palette.cloth * 1.10f;
       e.shieldColor = v.palette.cloth * 1.10f;
     } else if (shieldHue < 0.90f) {
     } else if (shieldHue < 0.90f) {
       e.shieldColor = v.palette.leather * 1.25f;
       e.shieldColor = v.palette.leather * 1.25f;
     } else {
     } else {
-      // rare: metal-faced shield
+
       e.shieldColor = e.metalColor * 0.95f;
       e.shieldColor = e.metalColor * 0.95f;
     }
     }
 
 
-    // Make swords longer overall with slight variance
-    e.swordLength   = 0.80f + (hash01(seed ^ 0xABCDu) - 0.5f) * 0.16f; // ~0.72..0.88
-    e.swordWidth    = 0.043f + (hash01(seed ^ 0x7777u) - 0.5f) * 0.008f;
-    e.shieldRadius  = 0.16f + (hash01(seed ^ 0xDEF0u) - 0.5f) * 0.04f;
+    e.swordLength = 0.80f + (hash01(seed ^ 0xABCDu) - 0.5f) * 0.16f;
+    e.swordWidth = 0.060f + (hash01(seed ^ 0x7777u) - 0.5f) * 0.010f;
+    e.shieldRadius = 0.16f + (hash01(seed ^ 0xDEF0u) - 0.5f) * 0.04f;
 
 
-    e.guardHalfWidth = 0.09f + (hash01(seed ^ 0x3456u) - 0.5f) * 0.025f;
-    e.handleRadius   = 0.017f + (hash01(seed ^ 0x88AAu) - 0.5f) * 0.004f;
-    e.pommelRadius   = 0.032f + (hash01(seed ^ 0x19C3u) - 0.5f) * 0.006f;
+    e.guardHalfWidth = 0.120f + (hash01(seed ^ 0x3456u) - 0.5f) * 0.020f;
+    e.handleRadius = 0.016f + (hash01(seed ^ 0x88AAu) - 0.5f) * 0.003f;
+    e.pommelRadius = 0.045f + (hash01(seed ^ 0x19C3u) - 0.5f) * 0.006f;
 
 
-    e.bladeRicasso   = clampf(0.12f + (hash01(seed ^ 0xBEEFu) - 0.5f) * 0.06f, 0.08f, 0.18f);
+    e.bladeRicasso =
+        clampf(0.14f + (hash01(seed ^ 0xBEEFu) - 0.5f) * 0.04f, 0.10f, 0.20f);
     e.bladeTaperBias = clamp01(0.6f + (hash01(seed ^ 0xFACEu) - 0.5f) * 0.2f);
     e.bladeTaperBias = clamp01(0.6f + (hash01(seed ^ 0xFACEu) - 0.5f) * 0.2f);
 
 
     e.shieldCrossDecal = (hash01(seed ^ 0xA11Cu) > 0.55f);
     e.shieldCrossDecal = (hash01(seed ^ 0xA11Cu) > 0.55f);
-    e.hasScabbard      = (hash01(seed ^ 0x5CABu) > 0.15f);
+    e.hasScabbard = (hash01(seed ^ 0x5CABu) > 0.15f);
     return e;
     return e;
   }
   }
 
 
@@ -188,223 +527,334 @@ private:
                         bool isAttacking, float attackPhase, ISubmitter &out) {
                         bool isAttacking, float attackPhase, ISubmitter &out) {
     QVector3D gripPos = pose.handR;
     QVector3D gripPos = pose.handR;
 
 
-    // Desired orientation: more vertical overall. During attack, top-to-bottom arc.
-    QVector3D upish(0.05f, 1.0f, 0.15f);   if (upish.lengthSquared()>1e-6f) upish.normalize();
-    QVector3D midish(0.08f, 0.20f, 1.0f);  if (midish.lengthSquared()>1e-6f) midish.normalize();
-    QVector3D downish(0.10f,-1.0f, 0.25f); if (downish.lengthSquared()>1e-6f) downish.normalize();
+    constexpr float kSwordYawDeg = 25.0f;
+    QMatrix4x4 yawM;
+    yawM.rotate(kSwordYawDeg, 0.0f, 1.0f, 0.0f);
 
 
-    QVector3D swordDir = upish; // default idle: vertical with slight forward
+    QVector3D upish = yawM.map(QVector3D(0.05f, 1.0f, 0.15f));
+    QVector3D midish = yawM.map(QVector3D(0.08f, 0.20f, 1.0f));
+    QVector3D downish = yawM.map(QVector3D(0.10f, -1.0f, 0.25f));
+    if (upish.lengthSquared() > 1e-6f)
+      upish.normalize();
+    if (midish.lengthSquared() > 1e-6f)
+      midish.normalize();
+    if (downish.lengthSquared() > 1e-6f)
+      downish.normalize();
+
+    QVector3D swordDir = upish;
 
 
     if (isAttacking) {
     if (isAttacking) {
       if (attackPhase < 0.18f) {
       if (attackPhase < 0.18f) {
-        // Keep blade vertical while lifting
         float t = easeInOutCubic(attackPhase / 0.18f);
         float t = easeInOutCubic(attackPhase / 0.18f);
         swordDir = nlerp(upish, upish, t);
         swordDir = nlerp(upish, upish, t);
       } else if (attackPhase < 0.32f) {
       } else if (attackPhase < 0.32f) {
-        // Slight pre-rotation forward but still mostly up
         float t = easeInOutCubic((attackPhase - 0.18f) / 0.14f);
         float t = easeInOutCubic((attackPhase - 0.18f) / 0.14f);
         swordDir = nlerp(upish, midish, t * 0.35f);
         swordDir = nlerp(upish, midish, t * 0.35f);
       } else if (attackPhase < 0.52f) {
       } else if (attackPhase < 0.52f) {
-        // Main cut: top -> bottom, curved via midish
-        float t = (attackPhase - 0.32f) / 0.20f; // 0..1
-        t = t * t * t; // accelerate
+        float t = (attackPhase - 0.32f) / 0.20f;
+        t = t * t * t;
         if (t < 0.5f) {
         if (t < 0.5f) {
-          float u = t / 0.5f;                 // 0..1
-          swordDir = nlerp(upish, midish, u); // first half of the curve
+          float u = t / 0.5f;
+          swordDir = nlerp(upish, midish, u);
         } else {
         } else {
-          float u = (t - 0.5f) / 0.5f;        // 0..1
-          swordDir = nlerp(midish, downish, u); // second half to downward
+          float u = (t - 0.5f) / 0.5f;
+          swordDir = nlerp(midish, downish, u);
         }
         }
       } else if (attackPhase < 0.72f) {
       } else if (attackPhase < 0.72f) {
-        // Recover: bottom -> mid
         float t = easeInOutCubic((attackPhase - 0.52f) / 0.20f);
         float t = easeInOutCubic((attackPhase - 0.52f) / 0.20f);
         swordDir = nlerp(downish, midish, t);
         swordDir = nlerp(downish, midish, t);
       } else {
       } else {
-        // Settle back to vertical idle
         float t = smoothstep(0.72f, 1.0f, attackPhase);
         float t = smoothstep(0.72f, 1.0f, attackPhase);
         swordDir = nlerp(midish, upish, t);
         swordDir = nlerp(midish, upish, t);
       }
       }
     }
     }
 
 
-    QVector3D handleEnd  = gripPos - swordDir * 0.10f;
-    QVector3D bladeBase  = gripPos;
-    QVector3D bladeTip   = gripPos + swordDir * extras.swordLength;
+    QVector3D handleEnd = gripPos - swordDir * 0.10f;
+    QVector3D bladeBase = gripPos;
+    QVector3D bladeTip = gripPos + swordDir * extras.swordLength;
 
 
-    // Handle (rounded cylinder)
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, handleEnd, bladeBase, extras.handleRadius),
-             v.palette.leather, nullptr, 1.0f);
+    out.mesh(
+        getUnitCylinder(),
+        cylinderBetween(ctx.model, handleEnd, bladeBase, extras.handleRadius),
+        v.palette.leather, nullptr, 1.0f);
 
 
-    // Crossguard
     QVector3D guardCenter = bladeBase;
     QVector3D guardCenter = bladeBase;
     float gw = extras.guardHalfWidth;
     float gw = extras.guardHalfWidth;
-    QVector3D guardL = guardCenter + QVector3D(-gw, 0.0f, 0.0f);
-    QVector3D guardR = guardCenter + QVector3D( gw, 0.0f, 0.0f);
+
+    QVector3D guardRight =
+        QVector3D::crossProduct(QVector3D(0, 1, 0), swordDir);
+    if (guardRight.lengthSquared() < 1e-6f)
+      guardRight = QVector3D::crossProduct(QVector3D(1, 0, 0), swordDir);
+    guardRight.normalize();
+
+    QVector3D guardL = guardCenter - guardRight * gw;
+    QVector3D guardR = guardCenter + guardRight * gw;
+
     out.mesh(getUnitCylinder(),
     out.mesh(getUnitCylinder(),
              cylinderBetween(ctx.model, guardL, guardR, 0.014f),
              cylinderBetween(ctx.model, guardL, guardR, 0.014f),
              extras.metalColor, nullptr, 1.0f);
              extras.metalColor, nullptr, 1.0f);
-    // Guard end caps
-    QMatrix4x4 gl = ctx.model; gl.translate(guardL); gl.scale(0.018f);
+
+    QMatrix4x4 gl = ctx.model;
+    gl.translate(guardL);
+    gl.scale(0.018f);
     out.mesh(getUnitSphere(), gl, extras.metalColor, nullptr, 1.0f);
     out.mesh(getUnitSphere(), gl, extras.metalColor, nullptr, 1.0f);
-    QMatrix4x4 gr = ctx.model; gr.translate(guardR); gr.scale(0.018f);
+    QMatrix4x4 gr = ctx.model;
+    gr.translate(guardR);
+    gr.scale(0.018f);
     out.mesh(getUnitSphere(), gr, extras.metalColor, nullptr, 1.0f);
     out.mesh(getUnitSphere(), gr, extras.metalColor, nullptr, 1.0f);
 
 
-    // Blade: ricasso (cyl) + tapered cone to tip
     float L = extras.swordLength;
     float L = extras.swordLength;
-    float ricassoLen = clampf(extras.bladeRicasso, 0.06f, L * 0.35f);
-    QVector3D ricassoEnd = bladeBase + swordDir * ricassoLen;
     float baseW = extras.swordWidth;
     float baseW = extras.swordWidth;
-    float midW  = baseW * 0.75f;
+    float bladeThickness = baseW * 0.15f;
 
 
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, bladeBase, ricassoEnd, baseW),
-             extras.metalColor, nullptr, 1.0f);
+    float ricassoLen = clampf(extras.bladeRicasso, 0.10f, L * 0.30f);
+    QVector3D ricassoEnd = bladeBase + swordDir * ricassoLen;
 
 
-    out.mesh(getUnitCone(),
-             coneFromTo(ctx.model, ricassoEnd, bladeTip, midW),
-             extras.metalColor, nullptr, 1.0f);
+    float midW = baseW * 0.95f;
+    float tipW = baseW * 0.28f;
+    float tipStartDist = lerp(ricassoLen, L, 0.70f);
+    QVector3D tipStart = bladeBase + swordDir * tipStartDist;
+
+    auto drawFlatSection = [&](const QVector3D &start, const QVector3D &end,
+                               float width, const QVector3D &color) {
+      QVector3D right = QVector3D::crossProduct(swordDir, QVector3D(0, 1, 0));
+      if (right.lengthSquared() < 0.001f) {
+        right = QVector3D::crossProduct(swordDir, QVector3D(1, 0, 0));
+      }
+      right.normalize();
+
+      float offset = width * 0.33f;
+
+      out.mesh(getUnitCylinder(),
+               cylinderBetween(ctx.model, start, end, bladeThickness), color,
+               nullptr, 1.0f);
+
+      out.mesh(getUnitCylinder(),
+               cylinderBetween(ctx.model, start + right * offset,
+                               end + right * offset, bladeThickness * 0.8f),
+               color * 0.92f, nullptr, 1.0f);
+
+      out.mesh(getUnitCylinder(),
+               cylinderBetween(ctx.model, start - right * offset,
+                               end - right * offset, bladeThickness * 0.8f),
+               color * 0.92f, nullptr, 1.0f);
+    };
+
+    drawFlatSection(bladeBase, ricassoEnd, baseW, extras.metalColor);
+
+    drawFlatSection(ricassoEnd, tipStart, midW, extras.metalColor);
+
+    int tipSegments = 3;
+    for (int i = 0; i < tipSegments; ++i) {
+      float t0 = (float)i / tipSegments;
+      float t1 = (float)(i + 1) / tipSegments;
+      QVector3D segStart =
+          tipStart + swordDir * ((bladeTip - tipStart).length() * t0);
+      QVector3D segEnd =
+          tipStart + swordDir * ((bladeTip - tipStart).length() * t1);
+      float w = lerp(midW, tipW, t1);
+      out.mesh(getUnitCylinder(),
+               cylinderBetween(ctx.model, segStart, segEnd, bladeThickness),
+               extras.metalColor * (1.0f - i * 0.03f), nullptr, 1.0f);
+    }
+
+    QVector3D fullerStart = bladeBase + swordDir * (ricassoLen + 0.02f);
+    QVector3D fullerEnd = bladeBase + swordDir * (tipStartDist - 0.06f);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, fullerStart, fullerEnd,
+                             bladeThickness * 0.6f),
+             extras.metalColor * 0.65f, nullptr, 1.0f);
 
 
-    // Pommel
     QVector3D pommel = handleEnd - swordDir * 0.02f;
     QVector3D pommel = handleEnd - swordDir * 0.02f;
     QMatrix4x4 pommelMat = ctx.model;
     QMatrix4x4 pommelMat = ctx.model;
     pommelMat.translate(pommel);
     pommelMat.translate(pommel);
     pommelMat.scale(extras.pommelRadius);
     pommelMat.scale(extras.pommelRadius);
     out.mesh(getUnitSphere(), pommelMat, extras.metalColor, nullptr, 1.0f);
     out.mesh(getUnitSphere(), pommelMat, extras.metalColor, nullptr, 1.0f);
 
 
-    // Motion trail hint during fastest swing (still vertical-ish plane)
     if (isAttacking && attackPhase >= 0.32f && attackPhase < 0.56f) {
     if (isAttacking && attackPhase >= 0.32f && attackPhase < 0.56f) {
       float t = (attackPhase - 0.32f) / 0.24f;
       float t = (attackPhase - 0.32f) / 0.24f;
-      float alpha = 0.35f * (1.0f - t);
-      QVector3D trailStart = bladeBase - swordDir * 0.05f;               // apex (point)
-      QVector3D trailEnd   = bladeBase - swordDir * (0.28f + 0.15f * t); // base
+      float alpha = clamp01(0.35f * (1.0f - t));
+      QVector3D trailStart = bladeBase - swordDir * 0.05f;
+      QVector3D trailEnd = bladeBase - swordDir * (0.28f + 0.15f * t);
       out.mesh(getUnitCone(),
       out.mesh(getUnitCone(),
                coneFromTo(ctx.model, trailEnd, trailStart, baseW * 0.9f),
                coneFromTo(ctx.model, trailEnd, trailStart, baseW * 0.9f),
-               extras.metalColor * 0.9f, nullptr, clamp01(alpha));
+               extras.metalColor * 0.9f, nullptr, alpha);
     }
     }
   }
   }
 
 
-  static void drawShieldDecal(const DrawContext &ctx,
-                              const QVector3D &center,
-                              float radius,
-                              const QVector3D & /*baseColor*/,
-                              const HumanoidVariant &v,
-                              ISubmitter &out) {
-    // Simple heraldic cross decal; color keyed to team cloth
+  static void drawShieldDecal(const DrawContext &ctx, const QVector3D &center,
+                              float radius, const QVector3D &,
+                              const HumanoidVariant &v, ISubmitter &out) {
+
     QVector3D accent = v.palette.cloth * 1.2f;
     QVector3D accent = v.palette.cloth * 1.2f;
     float barR = radius * 0.10f;
     float barR = radius * 0.10f;
 
 
-    // Vertical bar
     QVector3D top = center + QVector3D(0.0f, radius * 0.95f, 0.0f);
     QVector3D top = center + QVector3D(0.0f, radius * 0.95f, 0.0f);
     QVector3D bot = center - QVector3D(0.0f, radius * 0.95f, 0.0f);
     QVector3D bot = center - QVector3D(0.0f, radius * 0.95f, 0.0f);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, top, bot, barR),
+    out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, top, bot, barR),
              accent, nullptr, 1.0f);
              accent, nullptr, 1.0f);
 
 
-    // Horizontal bar
-    QVector3D left  = center + QVector3D(-radius * 0.95f, 0.0f, 0.0f);
-    QVector3D right = center + QVector3D( radius * 0.95f, 0.0f, 0.0f);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, left, right, barR),
+    QVector3D left = center + QVector3D(-radius * 0.95f, 0.0f, 0.0f);
+    QVector3D right = center + QVector3D(radius * 0.95f, 0.0f, 0.0f);
+    out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, left, right, barR),
              accent, nullptr, 1.0f);
              accent, nullptr, 1.0f);
   }
   }
 
 
-  static void drawShieldRing(const DrawContext &ctx,
-                             const QVector3D &center,
-                             float radius,
-                             float thickness,
-                             const QVector3D &color,
-                             ISubmitter &out) {
-    // Approximate ring with segmented cylinders
+  static void drawShieldRing(const DrawContext &ctx, const QVector3D &center,
+                             float radius, float thickness,
+                             const QVector3D &color, ISubmitter &out) {
+
     const int segments = 12;
     const int segments = 12;
     for (int i = 0; i < segments; ++i) {
     for (int i = 0; i < segments; ++i) {
       float a0 = (float)i / segments * 2.0f * 3.14159265f;
       float a0 = (float)i / segments * 2.0f * 3.14159265f;
       float a1 = (float)(i + 1) / segments * 2.0f * 3.14159265f;
       float a1 = (float)(i + 1) / segments * 2.0f * 3.14159265f;
-      QVector3D p0(center.x() + radius * std::cos(a0), center.y() + radius * std::sin(a0), center.z());
-      QVector3D p1(center.x() + radius * std::cos(a1), center.y() + radius * std::sin(a1), center.z());
-      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, p0, p1, thickness), color, nullptr, 1.0f);
+      QVector3D p0(center.x() + radius * std::cos(a0),
+                   center.y() + radius * std::sin(a0), center.z());
+      QVector3D p1(center.x() + radius * std::cos(a1),
+                   center.y() + radius * std::sin(a1), center.z());
+      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, p0, p1, thickness),
+               color, nullptr, 1.0f);
     }
     }
   }
   }
 
 
   static void drawShield(const DrawContext &ctx, const HumanoidPose &pose,
   static void drawShield(const DrawContext &ctx, const HumanoidPose &pose,
                          const HumanoidVariant &v, const KnightExtras &extras,
                          const HumanoidVariant &v, const KnightExtras &extras,
                          ISubmitter &out) {
                          ISubmitter &out) {
-    // Position
-    QVector3D shieldCenter = pose.handL + QVector3D(0.0f, -0.05f, 0.05f);
-
-    // Make the shield body as thin as possible (essentially a disc)
-    const float paperThin = 0.0006f; // near-zero thickness
-    const float halfThin  = paperThin;
-
-    // Front & back "discs" (ultra-thin cylinders), no dome
-    QMatrix4x4 frontMat = ctx.model;
-    frontMat.translate(shieldCenter + QVector3D(0.0f, 0.0f, halfThin));
-    frontMat.scale(extras.shieldRadius, extras.shieldRadius, paperThin);
-    out.mesh(getUnitCylinder(), frontMat, extras.shieldColor, nullptr, 1.0f);
-
-    QMatrix4x4 backMat = ctx.model;
-    backMat.translate(shieldCenter - QVector3D(0.0f, 0.0f, halfThin));
-    backMat.scale(extras.shieldRadius * 0.985f, extras.shieldRadius * 0.985f, paperThin);
-    out.mesh(getUnitCylinder(), backMat, v.palette.leather * 0.8f, nullptr, 1.0f);
-
-    // Thin metal rim (keep, but it reads slimmer)
-    drawShieldRing(ctx, shieldCenter, extras.shieldRadius, 0.010f, (extras.metalColor * 0.95f), out);
-
-    // Decorative inner ring (slim)
-    drawShieldRing(ctx, shieldCenter, extras.shieldRadius * 0.72f, 0.006f, v.palette.leather * 0.9f, out);
-
-    // Boss
-    QMatrix4x4 bossMat = ctx.model;
-    bossMat.translate(shieldCenter + QVector3D(0.0f, 0.0f, 0.02f));
-    bossMat.scale(0.045f);
-    out.mesh(getUnitSphere(), bossMat, extras.metalColor, nullptr, 1.0f);
-
-    // Straps/handle (connect hand to shield back)
-    QVector3D gripA = shieldCenter - QVector3D(0.03f, 0.00f, 0.03f);
-    QVector3D gripB = shieldCenter + QVector3D(0.03f, 0.00f, -0.03f);
-    out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, gripA, gripB, 0.010f), v.palette.leather, nullptr, 1.0f);
-
-    // Optional heraldic cross on shield front
+
+    const float scaleFactor = 2.5f;
+    const float R = extras.shieldRadius * scaleFactor;
+
+    const float yawDeg = -70.0f;
+    QMatrix4x4 rot;
+    rot.rotate(yawDeg, 0.0f, 1.0f, 0.0f);
+
+    const QVector3D n = rot.map(QVector3D(0.0f, 0.0f, 1.0f));
+    const QVector3D axisX = rot.map(QVector3D(1.0f, 0.0f, 0.0f));
+    const QVector3D axisY = rot.map(QVector3D(0.0f, 1.0f, 0.0f));
+
+    QVector3D shieldCenter =
+        pose.handL + axisX * (-R * 0.35f) + axisY * (-0.05f) + n * (0.06f);
+
+    const float plateHalf = 0.0015f;
+    const float plateFull = plateHalf * 2.0f;
+
+    {
+      QMatrix4x4 m = ctx.model;
+      m.translate(shieldCenter + n * plateHalf);
+      m.rotate(yawDeg, 0.0f, 1.0f, 0.0f);
+      m.scale(R, R, plateFull);
+      out.mesh(getUnitCylinder(), m, extras.shieldColor, nullptr, 1.0f);
+    }
+
+    {
+      QMatrix4x4 m = ctx.model;
+      m.translate(shieldCenter - n * plateHalf);
+      m.rotate(yawDeg, 0.0f, 1.0f, 0.0f);
+      m.scale(R * 0.985f, R * 0.985f, plateFull);
+      out.mesh(getUnitCylinder(), m, v.palette.leather * 0.8f, nullptr, 1.0f);
+    }
+
+    auto drawRingRotated = [&](float radius, float thickness,
+                               const QVector3D &color) {
+      const int segments = 16;
+      for (int i = 0; i < segments; ++i) {
+        float a0 = (float)i / segments * 2.0f * 3.14159265f;
+        float a1 = (float)(i + 1) / segments * 2.0f * 3.14159265f;
+
+        QVector3D v0 =
+            QVector3D(radius * std::cos(a0), radius * std::sin(a0), 0.0f);
+        QVector3D v1 =
+            QVector3D(radius * std::cos(a1), radius * std::sin(a1), 0.0f);
+
+        QVector3D p0 = shieldCenter + rot.map(v0);
+        QVector3D p1 = shieldCenter + rot.map(v1);
+
+        out.mesh(getUnitCylinder(),
+                 cylinderBetween(ctx.model, p0, p1, thickness), color, nullptr,
+                 1.0f);
+      }
+    };
+
+    drawRingRotated(R, 0.010f * scaleFactor, extras.metalColor * 0.95f);
+    drawRingRotated(R * 0.72f, 0.006f * scaleFactor, v.palette.leather * 0.90f);
+
+    {
+      QMatrix4x4 m = ctx.model;
+      m.translate(shieldCenter + n * (0.02f * scaleFactor));
+      m.scale(0.045f * scaleFactor);
+      out.mesh(getUnitSphere(), m, extras.metalColor, nullptr, 1.0f);
+    }
+
+    {
+
+      QVector3D gripA = shieldCenter - axisX * 0.035f - n * 0.030f;
+      QVector3D gripB = shieldCenter + axisX * 0.035f - n * 0.030f;
+      out.mesh(getUnitCylinder(),
+               cylinderBetween(ctx.model, gripA, gripB, 0.010f),
+               v.palette.leather, nullptr, 1.0f);
+    }
+
     if (extras.shieldCrossDecal && (extras.shieldColor != extras.metalColor)) {
     if (extras.shieldCrossDecal && (extras.shieldColor != extras.metalColor)) {
-      drawShieldDecal(ctx, shieldCenter + QVector3D(0.0f, 0.0f, paperThin + 0.001f),
-                      extras.shieldRadius * 0.85f, extras.shieldColor, v, out);
+      float decalR = R * 0.85f;
+      float barR = decalR * 0.10f;
+
+      QVector3D centerFront = shieldCenter + n * (plateFull * 0.5f + 0.0015f);
+
+      QVector3D top = centerFront + axisY * (decalR * 0.95f);
+      QVector3D bot = centerFront - axisY * (decalR * 0.95f);
+      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, top, bot, barR),
+               v.palette.cloth * 1.2f, nullptr, 1.0f);
+
+      QVector3D left = centerFront - axisX * (decalR * 0.95f);
+      QVector3D right = centerFront + axisX * (decalR * 0.95f);
+      out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, left, right, barR),
+               v.palette.cloth * 1.2f, nullptr, 1.0f);
     }
     }
   }
   }
 
 
-  static void drawScabbard(const DrawContext &ctx, const HumanoidPose & /*pose*/,
+  static void drawScabbard(const DrawContext &ctx, const HumanoidPose &,
                            const HumanoidVariant &v, const KnightExtras &extras,
                            const HumanoidVariant &v, const KnightExtras &extras,
                            ISubmitter &out) {
                            ISubmitter &out) {
     using HP = HumanProportions;
     using HP = HumanProportions;
 
 
-    // Hang on left hip, angled back
     QVector3D hip(0.10f, HP::WAIST_Y - 0.04f, -0.02f);
     QVector3D hip(0.10f, HP::WAIST_Y - 0.04f, -0.02f);
     QVector3D tip = hip + QVector3D(-0.05f, -0.22f, -0.12f);
     QVector3D tip = hip + QVector3D(-0.05f, -0.22f, -0.12f);
     float sheathR = extras.swordWidth * 0.85f;
     float sheathR = extras.swordWidth * 0.85f;
 
 
-    // Sheath body
-    out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, hip, tip, sheathR), v.palette.leather * 0.9f, nullptr, 1.0f);
+    out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, hip, tip, sheathR),
+             v.palette.leather * 0.9f, nullptr, 1.0f);
 
 
-    // Sheath tip ferrule (single-radius cone)
     out.mesh(getUnitCone(),
     out.mesh(getUnitCone(),
-             coneFromTo(ctx.model,
-                        tip,                                                  // base
-                        tip + QVector3D(-0.02f, -0.02f, -0.02f),             // apex
-                        sheathR),                                             // base radius
+             coneFromTo(ctx.model, tip, tip + QVector3D(-0.02f, -0.02f, -0.02f),
+                        sheathR),
              extras.metalColor, nullptr, 1.0f);
              extras.metalColor, nullptr, 1.0f);
 
 
-    // Straps to belt
     QVector3D strapA = hip + QVector3D(0.00f, 0.03f, 0.00f);
     QVector3D strapA = hip + QVector3D(0.00f, 0.03f, 0.00f);
-    QVector3D belt   = QVector3D(0.12f, HP::WAIST_Y + 0.01f, 0.02f);
-    out.mesh(getUnitCylinder(), cylinderBetween(ctx.model, strapA, belt, 0.006f), v.palette.leather, nullptr, 1.0f);
+    QVector3D belt = QVector3D(0.12f, HP::WAIST_Y + 0.01f, 0.02f);
+    out.mesh(getUnitCylinder(),
+             cylinderBetween(ctx.model, strapA, belt, 0.006f),
+             v.palette.leather, nullptr, 1.0f);
   }
   }
 };
 };
 
 
 void registerKnightRenderer(Render::GL::EntityRendererRegistry &registry) {
 void registerKnightRenderer(Render::GL::EntityRendererRegistry &registry) {
   static KnightRenderer renderer;
   static KnightRenderer renderer;
-  registry.registerRenderer("knight",
-                            [](const DrawContext &ctx, ISubmitter &out) {
-                              static KnightRenderer staticRenderer;
-                              staticRenderer.render(ctx, out);
-                            });
+  registry.registerRenderer(
+      "knight", [](const DrawContext &ctx, ISubmitter &out) {
+        static KnightRenderer staticRenderer;
+        Shader *knightShader = nullptr;
+        if (ctx.backend) {
+          knightShader = ctx.backend->shader(QStringLiteral("knight"));
+        }
+        Renderer *sceneRenderer = dynamic_cast<Renderer *>(&out);
+        if (sceneRenderer && knightShader) {
+          sceneRenderer->setCurrentShader(knightShader);
+        }
+        staticRenderer.render(ctx, out);
+        if (sceneRenderer) {
+          sceneRenderer->setCurrentShader(nullptr);
+        }
+      });
 }
 }
 
 
 } // namespace Render::GL
 } // namespace Render::GL

+ 2 - 0
render/entity/registry.h

@@ -19,6 +19,7 @@ namespace GL {
 class ResourceManager;
 class ResourceManager;
 class Mesh;
 class Mesh;
 class Texture;
 class Texture;
+class Backend;
 } // namespace GL
 } // namespace GL
 } // namespace Render
 } // namespace Render
 
 
@@ -32,6 +33,7 @@ struct DrawContext {
   bool selected = false;
   bool selected = false;
   bool hovered = false;
   bool hovered = false;
   float animationTime = 0.0f;
   float animationTime = 0.0f;
+  class Backend *backend = nullptr;
 };
 };
 
 
 using RenderFunc = std::function<void(const DrawContext &, ISubmitter &out)>;
 using RenderFunc = std::function<void(const DrawContext &, ISubmitter &out)>;

+ 51 - 10
render/gl/backend.cpp

@@ -52,6 +52,8 @@ void Backend::initialize() {
   m_stoneShader = m_shaderCache->get(QStringLiteral("stone_instanced"));
   m_stoneShader = m_shaderCache->get(QStringLiteral("stone_instanced"));
   m_groundShader = m_shaderCache->get(QStringLiteral("ground_plane"));
   m_groundShader = m_shaderCache->get(QStringLiteral("ground_plane"));
   m_terrainShader = m_shaderCache->get(QStringLiteral("terrain_chunk"));
   m_terrainShader = m_shaderCache->get(QStringLiteral("terrain_chunk"));
+  m_archerShader = m_shaderCache->get(QStringLiteral("archer"));
+  m_knightShader = m_shaderCache->get(QStringLiteral("knight"));
   if (!m_basicShader)
   if (!m_basicShader)
     qWarning() << "Backend: basic shader missing";
     qWarning() << "Backend: basic shader missing";
   if (!m_gridShader)
   if (!m_gridShader)
@@ -68,8 +70,14 @@ void Backend::initialize() {
     qWarning() << "Backend: ground_plane shader missing";
     qWarning() << "Backend: ground_plane shader missing";
   if (!m_terrainShader)
   if (!m_terrainShader)
     qWarning() << "Backend: terrain shader missing";
     qWarning() << "Backend: terrain shader missing";
+  if (!m_archerShader)
+    qWarning() << "Backend: archer shader missing";
+  if (!m_knightShader)
+    qWarning() << "Backend: knight shader missing";
 
 
   cacheBasicUniforms();
   cacheBasicUniforms();
+  cacheArcherUniforms();
+  cacheKnightUniforms();
   cacheGridUniforms();
   cacheGridUniforms();
   cacheCylinderUniforms();
   cacheCylinderUniforms();
   cacheFogUniforms();
   cacheFogUniforms();
@@ -458,13 +466,23 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
       if (glIsEnabled(GL_POLYGON_OFFSET_FILL))
       if (glIsEnabled(GL_POLYGON_OFFSET_FILL))
         glDisable(GL_POLYGON_OFFSET_FILL);
         glDisable(GL_POLYGON_OFFSET_FILL);
 
 
-      if (m_lastBoundShader != m_basicShader) {
-        m_basicShader->use();
-        m_lastBoundShader = m_basicShader;
+      Shader *activeShader = it.shader ? it.shader : m_basicShader;
+      if (!activeShader)
+        break;
+
+      BasicUniforms *uniforms = &m_basicUniforms;
+      if (activeShader == m_archerShader)
+        uniforms = &m_archerUniforms;
+      else if (activeShader == m_knightShader)
+        uniforms = &m_knightUniforms;
+
+      if (m_lastBoundShader != activeShader) {
+        activeShader->use();
+        m_lastBoundShader = activeShader;
       }
       }
 
 
-      m_basicShader->setUniform(m_basicUniforms.mvp, it.mvp);
-      m_basicShader->setUniform(m_basicUniforms.model, it.model);
+      activeShader->setUniform(uniforms->mvp, it.mvp);
+      activeShader->setUniform(uniforms->model, it.model);
 
 
       Texture *texToUse = it.texture
       Texture *texToUse = it.texture
                               ? it.texture
                               ? it.texture
@@ -472,13 +490,12 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
       if (texToUse && texToUse != m_lastBoundTexture) {
       if (texToUse && texToUse != m_lastBoundTexture) {
         texToUse->bind(0);
         texToUse->bind(0);
         m_lastBoundTexture = texToUse;
         m_lastBoundTexture = texToUse;
-        m_basicShader->setUniform(m_basicUniforms.texture, 0);
+        activeShader->setUniform(uniforms->texture, 0);
       }
       }
 
 
-      m_basicShader->setUniform(m_basicUniforms.useTexture,
-                                it.texture != nullptr);
-      m_basicShader->setUniform(m_basicUniforms.color, it.color);
-      m_basicShader->setUniform(m_basicUniforms.alpha, it.alpha);
+      activeShader->setUniform(uniforms->useTexture, it.texture != nullptr);
+      activeShader->setUniform(uniforms->color, it.color);
+      activeShader->setUniform(uniforms->alpha, it.alpha);
       it.mesh->draw();
       it.mesh->draw();
       break;
       break;
     }
     }
@@ -598,6 +615,30 @@ void Backend::cacheBasicUniforms() {
   m_basicUniforms.alpha = m_basicShader->uniformHandle("u_alpha");
   m_basicUniforms.alpha = m_basicShader->uniformHandle("u_alpha");
 }
 }
 
 
+void Backend::cacheArcherUniforms() {
+  if (!m_archerShader)
+    return;
+
+  m_archerUniforms.mvp = m_archerShader->uniformHandle("u_mvp");
+  m_archerUniforms.model = m_archerShader->uniformHandle("u_model");
+  m_archerUniforms.texture = m_archerShader->uniformHandle("u_texture");
+  m_archerUniforms.useTexture = m_archerShader->uniformHandle("u_useTexture");
+  m_archerUniforms.color = m_archerShader->uniformHandle("u_color");
+  m_archerUniforms.alpha = m_archerShader->uniformHandle("u_alpha");
+}
+
+void Backend::cacheKnightUniforms() {
+  if (!m_knightShader)
+    return;
+
+  m_knightUniforms.mvp = m_knightShader->uniformHandle("u_mvp");
+  m_knightUniforms.model = m_knightShader->uniformHandle("u_model");
+  m_knightUniforms.texture = m_knightShader->uniformHandle("u_texture");
+  m_knightUniforms.useTexture = m_knightShader->uniformHandle("u_useTexture");
+  m_knightUniforms.color = m_knightShader->uniformHandle("u_color");
+  m_knightUniforms.alpha = m_knightShader->uniformHandle("u_alpha");
+}
+
 void Backend::cacheGridUniforms() {
 void Backend::cacheGridUniforms() {
   if (!m_gridShader)
   if (!m_gridShader)
     return;
     return;

+ 7 - 0
render/gl/backend.h

@@ -82,6 +82,8 @@ private:
   Shader *m_stoneShader = nullptr;
   Shader *m_stoneShader = nullptr;
   Shader *m_groundShader = nullptr;
   Shader *m_groundShader = nullptr;
   Shader *m_terrainShader = nullptr;
   Shader *m_terrainShader = nullptr;
+  Shader *m_archerShader = nullptr;
+  Shader *m_knightShader = nullptr;
 
 
   struct BasicUniforms {
   struct BasicUniforms {
     Shader::UniformHandle mvp{Shader::InvalidUniform};
     Shader::UniformHandle mvp{Shader::InvalidUniform};
@@ -92,6 +94,9 @@ private:
     Shader::UniformHandle alpha{Shader::InvalidUniform};
     Shader::UniformHandle alpha{Shader::InvalidUniform};
   } m_basicUniforms;
   } m_basicUniforms;
 
 
+  BasicUniforms m_archerUniforms;
+  BasicUniforms m_knightUniforms;
+
   struct GridUniforms {
   struct GridUniforms {
     Shader::UniformHandle mvp{Shader::InvalidUniform};
     Shader::UniformHandle mvp{Shader::InvalidUniform};
     Shader::UniformHandle model{Shader::InvalidUniform};
     Shader::UniformHandle model{Shader::InvalidUniform};
@@ -212,6 +217,8 @@ private:
   GLsizei m_stoneVertexCount = 0;
   GLsizei m_stoneVertexCount = 0;
 
 
   void cacheBasicUniforms();
   void cacheBasicUniforms();
+  void cacheArcherUniforms();
+  void cacheKnightUniforms();
   void cacheGridUniforms();
   void cacheGridUniforms();
   void cacheCylinderUniforms();
   void cacheCylinderUniforms();
   void initializeCylinderPipeline();
   void initializeCylinderPipeline();

+ 228 - 0
render/gl/primitives.cpp

@@ -244,6 +244,228 @@ Mesh *createCapsuleMesh(int radialSegments, int heightSegments) {
   return new Mesh(verts, idx);
   return new Mesh(verts, idx);
 }
 }
 
 
+float simpleHash(float seed) {
+  float x = std::sin(seed * 12.9898f) * 43758.5453f;
+  return x - std::floor(x);
+}
+
+Mesh *createUnitTorsoMesh(int radialSegments, int heightSegments) {
+  const float halfH = 0.5f;
+  const float TWO_PI = 6.28318530718f;
+
+  const bool invertProfile = true;
+
+  auto clampf = [](float x, float a, float b) {
+    return x < a ? a : (x > b ? b : x);
+  };
+  auto smoothstep01 = [&](float x) {
+    x = clampf(x, 0.0f, 1.0f);
+    return x * x * (3.0f - 2.0f * x);
+  };
+  auto smoothBand = [&](float t, float a, float b) {
+    float enter = smoothstep01((t - a) / (b - a + 1e-6f));
+    float exit = smoothstep01((t - b) / (a - b - 1e-6f));
+    float v = enter < exit ? enter : exit;
+    return clampf(v, 0.0f, 1.0f);
+  };
+
+  struct Axes {
+    float ax;
+    float az;
+  };
+  struct Key {
+    float t;
+    Axes A;
+  };
+
+  const Key keys[] = {
+      {0.10f, {0.98f, 0.92f}}, {0.20f, {1.02f, 0.96f}}, {0.45f, {0.82f, 0.78f}},
+      {0.65f, {1.20f, 1.04f}}, {0.85f, {1.42f, 1.18f}}, {1.02f, {1.60f, 1.06f}},
+      {1.10f, {1.20f, 0.96f}},
+  };
+  constexpr int KEY_COUNT = sizeof(keys) / sizeof(keys[0]);
+
+  auto catRom = [](float p0, float p1, float p2, float p3, float u) {
+    return 0.5f * ((2.0f * p1) + (-p0 + p2) * u +
+                   (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * u * u +
+                   (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * u * u * u);
+  };
+
+  auto sampleAxes = [&](float t) -> Axes {
+    t = clampf(t, 0.0f, 1.0f);
+    int i = 0;
+    while (i + 1 < KEY_COUNT && t > keys[i + 1].t)
+      ++i;
+    int i0 = i > 0 ? i - 1 : 0;
+    int i1 = i;
+    int i2 = (i + 1 < KEY_COUNT) ? i + 1 : KEY_COUNT - 1;
+    int i3 = (i + 2 < KEY_COUNT) ? i + 2 : KEY_COUNT - 1;
+
+    float denom = (keys[i2].t - keys[i1].t);
+    float u = denom > 1e-6f ? (t - keys[i1].t) / denom : 0.0f;
+    u = clampf(u, 0.0f, 1.0f);
+
+    float ax =
+        catRom(keys[i0].A.ax, keys[i1].A.ax, keys[i2].A.ax, keys[i3].A.ax, u);
+    float az =
+        catRom(keys[i0].A.az, keys[i1].A.az, keys[i2].A.az, keys[i3].A.az, u);
+    return {ax, az};
+  };
+
+  auto ellipseRadius = [](float a, float b, float ang) {
+    float c = std::cos(ang), s = std::sin(ang);
+    float denom = std::sqrt((b * b * c * c) + (a * a * s * s));
+    return (a * b) / (denom + 1e-8f);
+  };
+
+  auto xOffsetAt = [&](float t) {
+    return 0.02f * smoothBand(t, 0.6f, 0.95f) -
+           0.01f * smoothBand(t, 0.0f, 0.2f);
+  };
+  auto zOffsetAt = [&](float t) {
+    float lordosis = -0.03f * smoothBand(t, 0.15f, 0.40f);
+    float chestFwd = 0.035f * smoothBand(t, 0.65f, 0.85f);
+    float neckBack = -0.015f * smoothBand(t, 0.90f, 1.00f);
+    return lordosis + chestFwd + neckBack;
+  };
+  auto twistAt = [&](float t) { return 0.10f * smoothBand(t, 0.55f, 0.95f); };
+
+  auto thetaScale = [&](float t, float ang) {
+    float s = 0.0f;
+    float sinA = std::sin(ang), cosA = std::cos(ang), cos2 = cosA * cosA;
+    s += 0.07f * smoothBand(t, 0.68f, 0.88f) * std::max(0.0f, sinA);
+    s += -0.03f * smoothBand(t, 0.65f, 0.90f) * std::max(0.0f, -sinA);
+    s += 0.06f * smoothBand(t, 0.55f, 0.75f) * cos2;
+    s += -0.02f * smoothBand(t, 0.40f, 0.55f) * cos2;
+    s += 0.015f * smoothBand(t, 0.70f, 0.95f) * cosA;
+    return 1.0f + s;
+  };
+
+  auto micro = [](float s) {
+    float f = std::sin(s * 12.9898f) * 43758.5453f;
+    return f - std::floor(f);
+  };
+
+  auto samplePos = [&](float t, float ang) -> QVector3D {
+    float ts = invertProfile ? (1.0f - t) : t;
+
+    Axes A = sampleAxes(ts);
+    float twist = twistAt(ts);
+    float th = ang + twist;
+
+    float R = ellipseRadius(A.ax, A.az, th);
+    float S = thetaScale(ts, th);
+    float r = R * S;
+
+    float px = r * std::cos(th);
+    float pz = r * std::sin(th);
+
+    px += xOffsetAt(ts);
+    pz += zOffsetAt(ts);
+
+    float py = -halfH + t * (2.0f * halfH);
+
+    float s = (t * 37.0f) + (ang * 3.0f);
+    px += (micro(s) - 0.5f) * 0.004f;
+    pz += (micro(s + 1.23f) - 0.5f) * 0.004f;
+
+    return QVector3D(px, py, pz);
+  };
+
+  std::vector<Vertex> v;
+  std::vector<unsigned int> idx;
+  v.reserve((radialSegments + 1) * (heightSegments + 1) +
+            (radialSegments + 1) * 2 + 2);
+  idx.reserve(radialSegments * heightSegments * 6 + radialSegments * 6);
+
+  for (int y = 0; y <= heightSegments; ++y) {
+    float t = float(y) / float(heightSegments);
+    float dt = 1.0f / float(heightSegments);
+    float vCoord = t;
+
+    for (int i = 0; i <= radialSegments; ++i) {
+      float u = float(i) / float(radialSegments);
+      float ang = u * TWO_PI;
+      float da = TWO_PI / float(radialSegments);
+
+      QVector3D p = samplePos(t, ang);
+      QVector3D pu = samplePos(t, ang + da);
+      QVector3D pv = samplePos(clampf(t + dt, 0.0f, 1.0f), ang);
+
+      QVector3D du = pu - p;
+      QVector3D dv = pv - p;
+
+      QVector3D n = QVector3D::crossProduct(du, dv);
+      if (n.lengthSquared() > 0.0f)
+        n.normalize();
+
+      v.push_back({{p.x(), p.y(), p.z()}, {n.x(), n.y(), n.z()}, {u, vCoord}});
+    }
+  }
+
+  int row = radialSegments + 1;
+  for (int y = 0; y < heightSegments; ++y) {
+    for (int i = 0; i < radialSegments; ++i) {
+      int a = y * row + i;
+      int b = y * row + i + 1;
+      int c = (y + 1) * row + i + 1;
+      int d = (y + 1) * row + i;
+
+      idx.push_back(a);
+      idx.push_back(b);
+      idx.push_back(c);
+      idx.push_back(c);
+      idx.push_back(d);
+      idx.push_back(a);
+    }
+  }
+
+  {
+
+    int baseTop = (int)v.size();
+    float tTop = 1.0f;
+    float tTopS = invertProfile ? (1.0f - tTop) : tTop;
+    QVector3D cTop(xOffsetAt(tTopS), halfH, zOffsetAt(tTopS));
+    v.push_back({{cTop.x(), cTop.y(), cTop.z()}, {0, 1, 0}, {0.5f, 0.5f}});
+    for (int i = 0; i <= radialSegments; ++i) {
+      float u = float(i) / float(radialSegments);
+      float ang = u * TWO_PI;
+      QVector3D p = samplePos(tTop, ang);
+      v.push_back({{p.x(), p.y(), p.z()},
+                   {0, 1, 0},
+                   {0.5f + 0.5f * std::cos(ang), 0.5f + 0.5f * std::sin(ang)}});
+    }
+    for (int i = 1; i <= radialSegments; ++i) {
+      idx.push_back(baseTop);
+      idx.push_back(baseTop + i);
+      idx.push_back(baseTop + i + 1);
+    }
+  }
+  {
+
+    int baseBot = (int)v.size();
+    float tBot = 0.0f;
+    float tBotS = invertProfile ? (1.0f - tBot) : tBot;
+    QVector3D cBot(xOffsetAt(tBotS), -halfH, zOffsetAt(tBotS));
+    v.push_back({{cBot.x(), cBot.y(), cBot.z()}, {0, -1, 0}, {0.5f, 0.5f}});
+    for (int i = 0; i <= radialSegments; ++i) {
+      float u = float(i) / float(radialSegments);
+      float ang = u * TWO_PI;
+      QVector3D p = samplePos(tBot, ang);
+      v.push_back({{p.x(), p.y(), p.z()},
+                   {0, -1, 0},
+                   {0.5f + 0.5f * std::cos(ang), 0.5f + 0.5f * std::sin(ang)}});
+    }
+    for (int i = 1; i <= radialSegments; ++i) {
+      idx.push_back(baseBot);
+      idx.push_back(baseBot + i + 1);
+      idx.push_back(baseBot + i);
+    }
+  }
+
+  return new Mesh(v, idx);
+}
+
 } // namespace
 } // namespace
 
 
 Mesh *getUnitCylinder(int radialSegments) {
 Mesh *getUnitCylinder(int radialSegments) {
@@ -268,4 +490,10 @@ Mesh *getUnitCapsule(int radialSegments, int heightSegments) {
   return s_mesh.get();
   return s_mesh.get();
 }
 }
 
 
+Mesh *getUnitTorso(int radialSegments, int heightSegments) {
+  static std::unique_ptr<Mesh> s_mesh(
+      createUnitTorsoMesh(radialSegments, heightSegments));
+  return s_mesh.get();
+}
+
 } // namespace Render::GL
 } // namespace Render::GL

+ 2 - 0
render/gl/primitives.h

@@ -13,4 +13,6 @@ Mesh *getUnitCone(int radialSegments = 32);
 
 
 Mesh *getUnitCapsule(int radialSegments = 32, int heightSegments = 1);
 Mesh *getUnitCapsule(int radialSegments = 32, int heightSegments = 1);
 
 
+Mesh *getUnitTorso(int radialSegments = 32, int heightSegments = 8);
+
 } // namespace Render::GL
 } // namespace Render::GL

+ 8 - 0
render/gl/shader_cache.h

@@ -78,6 +78,14 @@ public:
     const QString terrainFrag =
     const QString terrainFrag =
         kShaderBase + QStringLiteral("terrain_chunk.frag");
         kShaderBase + QStringLiteral("terrain_chunk.frag");
     load(QStringLiteral("terrain_chunk"), terrainVert, terrainFrag);
     load(QStringLiteral("terrain_chunk"), terrainVert, terrainFrag);
+
+    const QString archerVert = kShaderBase + QStringLiteral("archer.vert");
+    const QString archerFrag = kShaderBase + QStringLiteral("archer.frag");
+    load(QStringLiteral("archer"), archerVert, archerFrag);
+
+    const QString knightVert = kShaderBase + QStringLiteral("knight.vert");
+    const QString knightFrag = kShaderBase + QStringLiteral("knight.frag");
+    load(QStringLiteral("knight"), knightVert, knightFrag);
   }
   }
 
 
   void clear() {
   void clear() {

+ 152 - 70
render/humanoid_base.cpp

@@ -37,6 +37,7 @@ void HumanoidRendererBase::customizePose(const DrawContext &ctx,
 void HumanoidRendererBase::addAttachments(const DrawContext &ctx,
 void HumanoidRendererBase::addAttachments(const DrawContext &ctx,
                                           const HumanoidVariant &v,
                                           const HumanoidVariant &v,
                                           const HumanoidPose &pose,
                                           const HumanoidPose &pose,
+                                          const AnimationInputs &anim,
                                           ISubmitter &out) const {}
                                           ISubmitter &out) const {}
 
 
 QVector3D HumanoidRendererBase::resolveTeamTint(const DrawContext &ctx) {
 QVector3D HumanoidRendererBase::resolveTeamTint(const DrawContext &ctx) {
@@ -139,53 +140,67 @@ AnimationInputs HumanoidRendererBase::sampleAnimState(const DrawContext &ctx) {
   return anim;
   return anim;
 }
 }
 
 
-void HumanoidRendererBase::computeLocomotionPose(uint32_t seed, float time,
-                                                 bool isMoving,
-                                                 HumanoidPose &pose) {
+void HumanoidRendererBase::computeLocomotionPose(
+    uint32_t seed, float time, bool isMoving, const VariationParams &variation,
+    HumanoidPose &pose) {
   using HP = HumanProportions;
   using HP = HumanProportions;
 
 
-  pose.headPos = QVector3D(0.0f, (HP::HEAD_TOP_Y + HP::CHIN_Y) * 0.5f, 0.0f);
-  pose.headR = HP::HEAD_RADIUS;
-  pose.neckBase = QVector3D(0.0f, HP::NECK_BASE_Y, 0.0f);
+  float hScale = variation.heightScale;
 
 
-  pose.shoulderL = QVector3D(-HP::TORSO_TOP_R * 0.98f, HP::SHOULDER_Y, 0.0f);
-  pose.shoulderR = QVector3D(HP::TORSO_TOP_R * 0.98f, HP::SHOULDER_Y, 0.0f);
+  pose.headPos =
+      QVector3D(0.0f, (HP::HEAD_TOP_Y + HP::CHIN_Y) * 0.5f * hScale, 0.0f);
+  pose.headR = HP::HEAD_RADIUS * hScale;
+  pose.neckBase = QVector3D(0.0f, HP::NECK_BASE_Y * hScale, 0.0f);
+
+  float bScale = variation.bulkScale;
+  float sWidth = variation.stanceWidth;
+
+  pose.shoulderL = QVector3D(-HP::TORSO_TOP_R * 0.98f * bScale,
+                             HP::SHOULDER_Y * hScale, 0.0f);
+  pose.shoulderR = QVector3D(HP::TORSO_TOP_R * 0.98f * bScale,
+                             HP::SHOULDER_Y * hScale, 0.0f);
 
 
   pose.footYOffset = 0.02f;
   pose.footYOffset = 0.02f;
-  pose.footL = QVector3D(-HP::SHOULDER_WIDTH * 0.58f,
+  pose.footL = QVector3D(-HP::SHOULDER_WIDTH * 0.58f * sWidth,
                          HP::GROUND_Y + pose.footYOffset, 0.0f);
                          HP::GROUND_Y + pose.footYOffset, 0.0f);
-  pose.footR = QVector3D(HP::SHOULDER_WIDTH * 0.58f,
+  pose.footR = QVector3D(HP::SHOULDER_WIDTH * 0.58f * sWidth,
                          HP::GROUND_Y + pose.footYOffset, 0.0f);
                          HP::GROUND_Y + pose.footYOffset, 0.0f);
 
 
+  pose.shoulderL.setY(pose.shoulderL.y() + variation.shoulderTilt);
+  pose.shoulderR.setY(pose.shoulderR.y() - variation.shoulderTilt);
+
+  float slouchOffset = variation.postureSlump * 0.15f;
+  pose.shoulderL.setZ(pose.shoulderL.z() + slouchOffset);
+  pose.shoulderR.setZ(pose.shoulderR.z() + slouchOffset);
+
   float footAngleJitter = (hash01(seed ^ 0x5678u) - 0.5f) * 0.12f;
   float footAngleJitter = (hash01(seed ^ 0x5678u) - 0.5f) * 0.12f;
   float footDepthJitter = (hash01(seed ^ 0x9ABCu) - 0.5f) * 0.08f;
   float footDepthJitter = (hash01(seed ^ 0x9ABCu) - 0.5f) * 0.08f;
-  float shoulderRotation = (hash01(seed ^ 0x1234u) - 0.5f) * 0.05f;
 
 
   pose.footL.setX(pose.footL.x() + footAngleJitter);
   pose.footL.setX(pose.footL.x() + footAngleJitter);
   pose.footR.setX(pose.footR.x() - footAngleJitter);
   pose.footR.setX(pose.footR.x() - footAngleJitter);
   pose.footL.setZ(pose.footL.z() + footDepthJitter);
   pose.footL.setZ(pose.footL.z() + footDepthJitter);
   pose.footR.setZ(pose.footR.z() - footDepthJitter);
   pose.footR.setZ(pose.footR.z() - footDepthJitter);
 
 
-  pose.shoulderL.setY(pose.shoulderL.y() + shoulderRotation);
-  pose.shoulderR.setY(pose.shoulderR.y() - shoulderRotation);
-
   float armHeightJitter = (hash01(seed ^ 0xABCDu) - 0.5f) * 0.03f;
   float armHeightJitter = (hash01(seed ^ 0xABCDu) - 0.5f) * 0.03f;
   float armAsymmetry = (hash01(seed ^ 0xDEF0u) - 0.5f) * 0.04f;
   float armAsymmetry = (hash01(seed ^ 0xDEF0u) - 0.5f) * 0.04f;
 
 
-  pose.handL = QVector3D(-0.05f + armAsymmetry,
-                         HP::SHOULDER_Y + 0.05f + armHeightJitter, 0.55f);
-  pose.handR =
-      QVector3D(0.15f - armAsymmetry * 0.5f,
-                HP::SHOULDER_Y + 0.15f + armHeightJitter * 0.8f, 0.20f);
+  pose.handL =
+      QVector3D(-0.05f + armAsymmetry,
+                HP::SHOULDER_Y * hScale + 0.05f + armHeightJitter, 0.55f);
+  pose.handR = QVector3D(
+      0.15f - armAsymmetry * 0.5f,
+      HP::SHOULDER_Y * hScale + 0.15f + armHeightJitter * 0.8f, 0.20f);
 
 
   if (isMoving) {
   if (isMoving) {
-    float walkCycleTime = 0.8f;
+
+    float walkCycleTime = 0.8f / variation.walkSpeedMult;
     float walkPhase = fmod(time * (1.0f / walkCycleTime), 1.0f);
     float walkPhase = fmod(time * (1.0f / walkCycleTime), 1.0f);
     float leftPhase = walkPhase;
     float leftPhase = walkPhase;
     float rightPhase = fmod(walkPhase + 0.5f, 1.0f);
     float rightPhase = fmod(walkPhase + 0.5f, 1.0f);
 
 
     const float groundY = HP::GROUND_Y;
     const float groundY = HP::GROUND_Y;
-    const float strideLength = 0.35f;
+
+    const float strideLength = 0.35f * variation.armSwingAmp;
 
 
     auto animateFoot = [groundY, &pose, strideLength](QVector3D &foot,
     auto animateFoot = [groundY, &pose, strideLength](QVector3D &foot,
                                                       float phase) {
                                                       float phase) {
@@ -202,7 +217,8 @@ void HumanoidRendererBase::computeLocomotionPose(uint32_t seed, float time,
     animateFoot(pose.footL, leftPhase);
     animateFoot(pose.footL, leftPhase);
     animateFoot(pose.footR, rightPhase);
     animateFoot(pose.footR, rightPhase);
 
 
-    float hipSway = std::sin(walkPhase * 2.0f * 3.14159f) * 0.02f;
+    float hipSway =
+        std::sin(walkPhase * 2.0f * 3.14159f) * 0.02f * variation.armSwingAmp;
     pose.shoulderL.setX(pose.shoulderL.x() + hipSway);
     pose.shoulderL.setX(pose.shoulderL.x() + hipSway);
     pose.shoulderR.setX(pose.shoulderR.x() + hipSway);
     pose.shoulderR.setX(pose.shoulderR.x() + hipSway);
   }
   }
@@ -224,48 +240,61 @@ void HumanoidRendererBase::computeLocomotionPose(uint32_t seed, float time,
 void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
 void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
                                           const HumanoidVariant &v,
                                           const HumanoidVariant &v,
                                           const HumanoidPose &pose,
                                           const HumanoidPose &pose,
-                                          ISubmitter &out) {
+                                          ISubmitter &out) const {
   using HP = HumanProportions;
   using HP = HumanProportions;
 
 
-  QVector3D torsoTop{0.0f, HP::NECK_BASE_Y - 0.05f, 0.0f};
-  QVector3D torsoBot{0.0f, HP::WAIST_Y, 0.0f};
-  float torsoRadius = HP::TORSO_TOP_R;
-  out.mesh(getUnitCylinder(),
-           cylinderBetween(ctx.model, torsoTop, torsoBot, torsoRadius),
+  QVector3D scaling = getProportionScaling();
+  float widthScale = scaling.x();
+  float heightScale = scaling.y();
+  float headScale = scaling.z();
+
+  QVector3D rightAxis = pose.shoulderR - pose.shoulderL;
+  if (rightAxis.lengthSquared() < 1e-8f)
+    rightAxis = QVector3D(1, 0, 0);
+  rightAxis.normalize();
+
+  const float yShoulder = 0.5f * (pose.shoulderL.y() + pose.shoulderR.y());
+  const float yNeck = pose.neckBase.y();
+  const float shoulderHalfSpan =
+      0.5f * std::abs(pose.shoulderR.x() - pose.shoulderL.x());
+  const float torsoR =
+      std::max(HP::TORSO_TOP_R * widthScale, shoulderHalfSpan * 0.95f);
+
+  const float yTopCover = std::max(yShoulder + 0.04f, yNeck + 0.00f);
+
+  QVector3D tunicTop{0.0f, yTopCover - 0.006f, 0.0f};
+  QVector3D tunicBot{0.0f, HP::WAIST_Y + 0.03f, 0.0f};
+  out.mesh(getUnitTorso(),
+           cylinderBetween(ctx.model, tunicTop, tunicBot, torsoR),
            v.palette.cloth, nullptr, 1.0f);
            v.palette.cloth, nullptr, 1.0f);
 
 
   QVector3D chinPos{0.0f, HP::CHIN_Y, 0.0f};
   QVector3D chinPos{0.0f, HP::CHIN_Y, 0.0f};
   out.mesh(getUnitCylinder(),
   out.mesh(getUnitCylinder(),
-           cylinderBetween(ctx.model, pose.neckBase, chinPos, HP::NECK_RADIUS),
+           cylinderBetween(ctx.model, pose.neckBase, chinPos,
+                           HP::NECK_RADIUS * widthScale),
            v.palette.skin * 0.9f, nullptr, 1.0f);
            v.palette.skin * 0.9f, nullptr, 1.0f);
 
 
-  out.mesh(getUnitSphere(), sphereAt(ctx.model, pose.headPos, pose.headR),
+  out.mesh(getUnitSphere(),
+           sphereAt(ctx.model, pose.headPos, pose.headR * headScale),
            v.palette.skin, nullptr, 1.0f);
            v.palette.skin, nullptr, 1.0f);
 
 
-  float headTopOffset = pose.headR * 0.7f;
-  QVector3D helmBase = pose.headPos + QVector3D(0.0f, headTopOffset, 0.0f);
-  QVector3D helmApex = pose.headPos + QVector3D(0.0f, pose.headR * 2.4f, 0.0f);
-  float helmBaseR = pose.headR * 1.45f;
-  out.mesh(getUnitCone(), coneFromTo(ctx.model, helmBase, helmApex, helmBaseR),
-           v.palette.cloth, nullptr, 1.0f);
-
   QVector3D iris(0.06f, 0.06f, 0.07f);
   QVector3D iris(0.06f, 0.06f, 0.07f);
-  float eyeZ = pose.headR * 0.7f;
-  float eyeY = pose.headPos.y() + pose.headR * 0.1f;
-  float eyeSpacing = pose.headR * 0.35f;
+  float eyeZ = pose.headR * headScale * 0.7f;
+  float eyeY = pose.headPos.y() + pose.headR * headScale * 0.1f;
+  float eyeSpacing = pose.headR * headScale * 0.35f;
   out.mesh(getUnitSphere(),
   out.mesh(getUnitSphere(),
-           ctx.model *
-               sphereAt(QVector3D(-eyeSpacing, eyeY, eyeZ), pose.headR * 0.15f),
+           ctx.model * sphereAt(QVector3D(-eyeSpacing, eyeY, eyeZ),
+                                pose.headR * headScale * 0.15f),
            iris, nullptr, 1.0f);
            iris, nullptr, 1.0f);
   out.mesh(getUnitSphere(),
   out.mesh(getUnitSphere(),
-           ctx.model *
-               sphereAt(QVector3D(eyeSpacing, eyeY, eyeZ), pose.headR * 0.15f),
+           ctx.model * sphereAt(QVector3D(eyeSpacing, eyeY, eyeZ),
+                                pose.headR * headScale * 0.15f),
            iris, nullptr, 1.0f);
            iris, nullptr, 1.0f);
 
 
-  const float upperArmR = HP::UPPER_ARM_R;
-  const float foreArmR = HP::FORE_ARM_R;
-  const float jointR = HP::HAND_RADIUS * 1.05f;
-  const float handR = HP::HAND_RADIUS * 0.95f;
+  const float upperArmR = HP::UPPER_ARM_R * widthScale;
+  const float foreArmR = HP::FORE_ARM_R * widthScale;
+  const float jointR = HP::HAND_RADIUS * widthScale * 1.05f;
+  const float handR = HP::HAND_RADIUS * widthScale * 0.95f;
 
 
   out.mesh(getUnitCylinder(),
   out.mesh(getUnitCylinder(),
            cylinderBetween(ctx.model, pose.shoulderL, pose.elbowL, upperArmR),
            cylinderBetween(ctx.model, pose.shoulderL, pose.elbowL, upperArmR),
@@ -289,11 +318,11 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
   out.mesh(getUnitSphere(), sphereAt(ctx.model, pose.handR, handR),
   out.mesh(getUnitSphere(), sphereAt(ctx.model, pose.handR, handR),
            v.palette.leatherDark * 0.92f, nullptr, 1.0f);
            v.palette.leatherDark * 0.92f, nullptr, 1.0f);
 
 
-  const float hipHalf = HP::UPPER_LEG_R * 1.7f;
+  const float hipHalf = HP::UPPER_LEG_R * widthScale * 1.7f;
   const float maxStance = hipHalf * 2.2f;
   const float maxStance = hipHalf * 2.2f;
 
 
-  const float upperScale = 1.40f * 3.0f;
-  const float lowerScale = 1.35f * 3.0f;
+  const float upperScale = 1.40f * 3.0f * widthScale;
+  const float lowerScale = 1.35f * 3.0f * widthScale;
   const float footLenMul = (5.5f * 0.1f);
   const float footLenMul = (5.5f * 0.1f);
   const float footRadMul = 0.70f;
   const float footRadMul = 0.70f;
 
 
@@ -301,7 +330,6 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
   const float kneeDrop = 0.02f * HP::LOWER_LEG_LEN;
   const float kneeDrop = 0.02f * HP::LOWER_LEG_LEN;
 
 
   const QVector3D FWD(0.f, 0.f, 1.f);
   const QVector3D FWD(0.f, 0.f, 1.f);
-  const QVector3D UP(0.f, 1.f, 0.f);
 
 
   const float upperR = HP::UPPER_LEG_R * upperScale;
   const float upperR = HP::UPPER_LEG_R * upperScale;
   const float lowerR = HP::LOWER_LEG_R * lowerScale;
   const float lowerR = HP::LOWER_LEG_R * lowerScale;
@@ -411,8 +439,38 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
   out.mesh(getUnitCapsule(8, 1),
   out.mesh(getUnitCapsule(8, 1),
            Render::Geom::capsuleBetween(ctx.model, ballR, toeR, toeRad),
            Render::Geom::capsuleBetween(ctx.model, ballR, toeR, toeRad),
            v.palette.leatherDark, nullptr, 1.0f);
            v.palette.leatherDark, nullptr, 1.0f);
+
+  drawHelmet(ctx, v, pose, out);
+
+  drawArmorOverlay(ctx, v, pose, yTopCover, torsoR, shoulderHalfSpan, upperArmR,
+                   rightAxis, out);
+
+  drawShoulderDecorations(ctx, v, pose, yTopCover, yNeck, rightAxis, out);
 }
 }
 
 
+void HumanoidRendererBase::drawHelmet(const DrawContext &ctx,
+                                      const HumanoidVariant &v,
+                                      const HumanoidPose &pose,
+                                      ISubmitter &out) const {
+
+  using HP = HumanProportions;
+  QVector3D capC = pose.headPos + QVector3D(0, pose.headR * 0.8f, 0);
+  out.mesh(getUnitSphere(), sphereAt(ctx.model, capC, pose.headR * 0.85f),
+           v.palette.cloth * 0.9f, nullptr, 1.0f);
+}
+
+void HumanoidRendererBase::drawArmorOverlay(
+    const DrawContext &ctx, const HumanoidVariant &v, const HumanoidPose &pose,
+    float yTopCover, float torsoR, float shoulderHalfSpan, float upperArmR,
+    const QVector3D &rightAxis, ISubmitter &out) const {}
+
+void HumanoidRendererBase::drawShoulderDecorations(const DrawContext &ctx,
+                                                   const HumanoidVariant &v,
+                                                   const HumanoidPose &pose,
+                                                   float yTopCover, float yNeck,
+                                                   const QVector3D &rightAxis,
+                                                   ISubmitter &out) const {}
+
 void HumanoidRendererBase::drawSelectionFX(const DrawContext &ctx,
 void HumanoidRendererBase::drawSelectionFX(const DrawContext &ctx,
                                            ISubmitter &out) {
                                            ISubmitter &out) {
   if (ctx.selected || ctx.hovered) {
   if (ctx.selected || ctx.hovered) {
@@ -441,11 +499,16 @@ void HumanoidRendererBase::render(const DrawContext &ctx,
   FormationParams formation = resolveFormation(ctx);
   FormationParams formation = resolveFormation(ctx);
   AnimationInputs anim = sampleAnimState(ctx);
   AnimationInputs anim = sampleAnimState(ctx);
 
 
+  Engine::Core::UnitComponent *unitComp = nullptr;
+  if (ctx.entity) {
+    unitComp = ctx.entity->getComponent<Engine::Core::UnitComponent>();
+  }
+
   uint32_t seed = 0u;
   uint32_t seed = 0u;
+  if (unitComp) {
+    seed ^= uint32_t(unitComp->ownerId * 2654435761u);
+  }
   if (ctx.entity) {
   if (ctx.entity) {
-    auto *unit = ctx.entity->getComponent<Engine::Core::UnitComponent>();
-    if (unit)
-      seed ^= uint32_t(unit->ownerId * 2654435761u);
     seed ^= uint32_t(reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFu);
     seed ^= uint32_t(reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFu);
   }
   }
 
 
@@ -454,20 +517,30 @@ void HumanoidRendererBase::render(const DrawContext &ctx,
   const int cols = formation.maxPerRow;
   const int cols = formation.maxPerRow;
 
 
   int visibleCount = rows * cols;
   int visibleCount = rows * cols;
-  if (ctx.entity) {
-    auto *unit = ctx.entity->getComponent<Engine::Core::UnitComponent>();
-    if (unit) {
-      int mh = std::max(1, unit->maxHealth);
-      float ratio = std::clamp(unit->health / float(mh), 0.0f, 1.0f);
-      visibleCount = std::max(1, (int)std::ceil(ratio * float(rows * cols)));
-    }
+  if (unitComp) {
+    int mh = std::max(1, unitComp->maxHealth);
+    float ratio = std::clamp(unitComp->health / float(mh), 0.0f, 1.0f);
+    visibleCount = std::max(1, (int)std::ceil(ratio * float(rows * cols)));
   }
   }
 
 
   HumanoidVariant variant;
   HumanoidVariant variant;
   getVariant(ctx, seed, variant);
   getVariant(ctx, seed, variant);
 
 
+  if (!m_proportionScaleCached) {
+    m_cachedProportionScale = getProportionScaling();
+    m_proportionScaleCached = true;
+  }
+  const QVector3D propScale = m_cachedProportionScale;
+  const float heightScale = propScale.y();
+  const bool needsHeightScaling = std::abs(heightScale - 1.0f) > 0.001f;
+
   const QMatrix4x4 kIdentityMatrix;
   const QMatrix4x4 kIdentityMatrix;
 
 
+  auto fastRandom = [](uint32_t &state) -> float {
+    state = state * 1664525u + 1013904223u;
+    return float(state & 0x7FFFFFu) / float(0x7FFFFFu);
+  };
+
   for (int idx = 0; idx < visibleCount; ++idx) {
   for (int idx = 0; idx < visibleCount; ++idx) {
     int r = idx / cols;
     int r = idx / cols;
     int c = idx % cols;
     int c = idx % cols;
@@ -477,16 +550,16 @@ void HumanoidRendererBase::render(const DrawContext &ctx,
 
 
     uint32_t instSeed = seed ^ uint32_t(idx * 9176u);
     uint32_t instSeed = seed ^ uint32_t(idx * 9176u);
 
 
-    float posJitterX = (hash01(instSeed) - 0.5f) * 0.05f;
-    float posJitterZ = (hash01(instSeed ^ 0x12345u) - 0.5f) * 0.05f;
-    float verticalJitter = (hash01(instSeed ^ 0x9E37u) - 0.5f) * 0.03f;
+    uint32_t rngState = instSeed;
+    float posJitterX = (fastRandom(rngState) - 0.5f) * 0.05f;
+    float posJitterZ = (fastRandom(rngState) - 0.5f) * 0.05f;
+    float verticalJitter = (fastRandom(rngState) - 0.5f) * 0.03f;
+    float yawOffset = (fastRandom(rngState) - 0.5f) * 5.0f;
+    float phaseOffset = fastRandom(rngState) * 0.25f;
 
 
     offsetX += posJitterX;
     offsetX += posJitterX;
     offsetZ += posJitterZ;
     offsetZ += posJitterZ;
 
 
-    float yawOffset = (hash01(instSeed ^ 0xABCDu) - 0.5f) * 5.0f;
-    float phaseOffset = hash01(instSeed >> 8) * 0.25f;
-
     QMatrix4x4 instModel;
     QMatrix4x4 instModel;
     if (ctx.entity) {
     if (ctx.entity) {
       if (auto *entT =
       if (auto *entT =
@@ -512,15 +585,24 @@ void HumanoidRendererBase::render(const DrawContext &ctx,
 
 
     DrawContext instCtx{ctx.resources, ctx.entity, ctx.world, instModel};
     DrawContext instCtx{ctx.resources, ctx.entity, ctx.world, instModel};
 
 
+    VariationParams variation = VariationParams::fromSeed(instSeed);
+
+    float combinedHeightScale = heightScale * variation.heightScale;
+    if (needsHeightScaling || std::abs(variation.heightScale - 1.0f) > 0.001f) {
+      QMatrix4x4 scaleMatrix;
+      scaleMatrix.scale(variation.bulkScale, combinedHeightScale, 1.0f);
+      instCtx.model = instCtx.model * scaleMatrix;
+    }
+
     HumanoidPose pose;
     HumanoidPose pose;
     computeLocomotionPose(instSeed, anim.time + phaseOffset, anim.isMoving,
     computeLocomotionPose(instSeed, anim.time + phaseOffset, anim.isMoving,
-                          pose);
+                          variation, pose);
 
 
     customizePose(instCtx, anim, instSeed, pose);
     customizePose(instCtx, anim, instSeed, pose);
 
 
     drawCommonBody(instCtx, variant, pose, out);
     drawCommonBody(instCtx, variant, pose, out);
 
 
-    addAttachments(instCtx, variant, pose, out);
+    addAttachments(instCtx, variant, pose, anim, out);
   }
   }
 
 
   drawSelectionFX(ctx, out);
   drawSelectionFX(ctx, out);

+ 60 - 3
render/humanoid_base.h

@@ -41,6 +41,36 @@ struct HumanoidPose {
   QVector3D footL, footR;
   QVector3D footL, footR;
 };
 };
 
 
+struct VariationParams {
+  float heightScale;
+  float bulkScale;
+  float stanceWidth;
+  float armSwingAmp;
+  float walkSpeedMult;
+  float postureSlump;
+  float shoulderTilt;
+
+  static VariationParams fromSeed(uint32_t seed) {
+    VariationParams v;
+
+    auto nextRand = [](uint32_t &s) -> float {
+      s = s * 1664525u + 1013904223u;
+      return float(s & 0x7FFFFFu) / float(0x7FFFFFu);
+    };
+
+    uint32_t rng = seed;
+    v.heightScale = 0.95f + nextRand(rng) * 0.10f;
+    v.bulkScale = 0.92f + nextRand(rng) * 0.16f;
+    v.stanceWidth = 0.88f + nextRand(rng) * 0.24f;
+    v.armSwingAmp = 0.85f + nextRand(rng) * 0.30f;
+    v.walkSpeedMult = 0.90f + nextRand(rng) * 0.20f;
+    v.postureSlump = nextRand(rng) * 0.08f;
+    v.shoulderTilt = (nextRand(rng) - 0.5f) * 0.06f;
+
+    return v;
+  }
+};
+
 struct HumanoidVariant {
 struct HumanoidVariant {
   HumanoidPalette palette;
   HumanoidPalette palette;
 };
 };
@@ -49,6 +79,10 @@ class HumanoidRendererBase {
 public:
 public:
   virtual ~HumanoidRendererBase() = default;
   virtual ~HumanoidRendererBase() = default;
 
 
+  virtual QVector3D getProportionScaling() const {
+    return QVector3D(1.0f, 1.0f, 1.0f);
+  }
+
   virtual void getVariant(const DrawContext &ctx, uint32_t seed,
   virtual void getVariant(const DrawContext &ctx, uint32_t seed,
                           HumanoidVariant &v) const;
                           HumanoidVariant &v) const;
 
 
@@ -57,22 +91,45 @@ public:
                              HumanoidPose &ioPose) const;
                              HumanoidPose &ioPose) const;
 
 
   virtual void addAttachments(const DrawContext &ctx, const HumanoidVariant &v,
   virtual void addAttachments(const DrawContext &ctx, const HumanoidVariant &v,
-                              const HumanoidPose &pose, ISubmitter &out) const;
+                              const HumanoidPose &pose,
+                              const AnimationInputs &anim,
+                              ISubmitter &out) const;
+
+  virtual void drawHelmet(const DrawContext &ctx, const HumanoidVariant &v,
+                          const HumanoidPose &pose, ISubmitter &out) const;
+
+  virtual void drawArmorOverlay(const DrawContext &ctx,
+                                const HumanoidVariant &v,
+                                const HumanoidPose &pose, float yTopCover,
+                                float torsoR, float shoulderHalfSpan,
+                                float upperArmR, const QVector3D &rightAxis,
+                                ISubmitter &out) const;
+
+  virtual void drawShoulderDecorations(const DrawContext &ctx,
+                                       const HumanoidVariant &v,
+                                       const HumanoidPose &pose,
+                                       float yTopCover, float yNeck,
+                                       const QVector3D &rightAxis,
+                                       ISubmitter &out) const;
 
 
   void render(const DrawContext &ctx, ISubmitter &out) const;
   void render(const DrawContext &ctx, ISubmitter &out) const;
 
 
 protected:
 protected:
+  mutable QVector3D m_cachedProportionScale;
+  mutable bool m_proportionScaleCached = false;
+
   static FormationParams resolveFormation(const DrawContext &ctx);
   static FormationParams resolveFormation(const DrawContext &ctx);
 
 
   static void computeLocomotionPose(uint32_t seed, float time, bool isMoving,
   static void computeLocomotionPose(uint32_t seed, float time, bool isMoving,
+                                    const VariationParams &variation,
                                     HumanoidPose &ioPose);
                                     HumanoidPose &ioPose);
 
 
   static AnimationInputs sampleAnimState(const DrawContext &ctx);
   static AnimationInputs sampleAnimState(const DrawContext &ctx);
 
 
   static QVector3D resolveTeamTint(const DrawContext &ctx);
   static QVector3D resolveTeamTint(const DrawContext &ctx);
 
 
-  static void drawCommonBody(const DrawContext &ctx, const HumanoidVariant &v,
-                             const HumanoidPose &pose, ISubmitter &out);
+  void drawCommonBody(const DrawContext &ctx, const HumanoidVariant &v,
+                      const HumanoidPose &pose, ISubmitter &out) const;
 
 
   static void drawSelectionFX(const DrawContext &ctx, ISubmitter &out);
   static void drawSelectionFX(const DrawContext &ctx, ISubmitter &out);
 };
 };

+ 3 - 3
render/humanoid_specs.h

@@ -13,9 +13,9 @@ struct HumanProportions {
   static constexpr float HEAD_TOP_Y = GROUND_Y + TOTAL_HEIGHT;
   static constexpr float HEAD_TOP_Y = GROUND_Y + TOTAL_HEIGHT;
   static constexpr float CHIN_Y = HEAD_TOP_Y - HEAD_HEIGHT;
   static constexpr float CHIN_Y = HEAD_TOP_Y - HEAD_HEIGHT;
   static constexpr float NECK_BASE_Y = CHIN_Y - 0.08f;
   static constexpr float NECK_BASE_Y = CHIN_Y - 0.08f;
-  static constexpr float SHOULDER_Y = NECK_BASE_Y - 0.12f;
-  static constexpr float CHEST_Y = SHOULDER_Y - 0.42f;
-  static constexpr float WAIST_Y = CHEST_Y - 0.30f;
+  static constexpr float SHOULDER_Y = NECK_BASE_Y - 0.04f;
+  static constexpr float CHEST_Y = SHOULDER_Y - 0.31f;
+  static constexpr float WAIST_Y = CHEST_Y - 0.25f;
 
 
   static constexpr float UPPER_LEG_LEN = 0.35f;
   static constexpr float UPPER_LEG_LEN = 0.35f;
   static constexpr float LOWER_LEG_LEN = 0.35f;
   static constexpr float LOWER_LEG_LEN = 0.35f;

+ 3 - 1
render/scene_renderer.cpp

@@ -80,7 +80,7 @@ void Renderer::mesh(Mesh *mesh, const QMatrix4x4 &model, const QVector3D &color,
   if (!mesh)
   if (!mesh)
     return;
     return;
 
 
-  if (mesh == getUnitCylinder() && (!texture)) {
+  if (mesh == getUnitCylinder() && (!texture) && (!m_currentShader)) {
     QVector3D start, end;
     QVector3D start, end;
     float radius = 0.0f;
     float radius = 0.0f;
     if (detail::decomposeUnitCylinder(model, start, end, radius)) {
     if (detail::decomposeUnitCylinder(model, start, end, radius)) {
@@ -95,6 +95,7 @@ void Renderer::mesh(Mesh *mesh, const QMatrix4x4 &model, const QVector3D &color,
   cmd.mvp = m_viewProj * model;
   cmd.mvp = m_viewProj * model;
   cmd.color = color;
   cmd.color = color;
   cmd.alpha = alpha;
   cmd.alpha = alpha;
+  cmd.shader = m_currentShader;
   if (m_activeQueue)
   if (m_activeQueue)
     m_activeQueue->submit(cmd);
     m_activeQueue->submit(cmd);
 }
 }
@@ -253,6 +254,7 @@ void Renderer::renderWorld(Engine::Core::World *world) {
             (m_selectedIds.find(entity->getId()) != m_selectedIds.end());
             (m_selectedIds.find(entity->getId()) != m_selectedIds.end());
         ctx.hovered = (entity->getId() == m_hoveredEntityId);
         ctx.hovered = (entity->getId() == m_hoveredEntityId);
         ctx.animationTime = m_accumulatedTime;
         ctx.animationTime = m_accumulatedTime;
+        ctx.backend = m_backend.get();
         fn(ctx, *this);
         fn(ctx, *this);
         drawnByRegistry = true;
         drawnByRegistry = true;
       }
       }

+ 4 - 0
render/scene_renderer.h

@@ -91,6 +91,9 @@ public:
                      : nullptr;
                      : nullptr;
   }
   }
 
 
+  void setCurrentShader(Shader *shader) { m_currentShader = shader; }
+  Shader *getCurrentShader() const { return m_currentShader; }
+
   struct GridParams {
   struct GridParams {
     float cellSize = 1.0f;
     float cellSize = 1.0f;
     float thickness = 0.06f;
     float thickness = 0.06f;
@@ -154,6 +157,7 @@ private:
   int m_localOwnerId = 1;
   int m_localOwnerId = 1;
 
 
   QMatrix4x4 m_viewProj;
   QMatrix4x4 m_viewProj;
+  Shader *m_currentShader = nullptr;
 };
 };
 
 
 struct FrameScope {
 struct FrameScope {

+ 6 - 1
render/submitter.h

@@ -43,12 +43,15 @@ inline bool decomposeUnitCylinder(const QMatrix4x4 &model, QVector3D &start,
 class QueueSubmitter : public ISubmitter {
 class QueueSubmitter : public ISubmitter {
 public:
 public:
   explicit QueueSubmitter(DrawQueue *queue) : m_queue(queue) {}
   explicit QueueSubmitter(DrawQueue *queue) : m_queue(queue) {}
+
+  void setShader(Shader *shader) { m_shader = shader; }
+
   void mesh(Mesh *mesh, const QMatrix4x4 &model, const QVector3D &color,
   void mesh(Mesh *mesh, const QMatrix4x4 &model, const QVector3D &color,
             Texture *tex = nullptr, float alpha = 1.0f) override {
             Texture *tex = nullptr, float alpha = 1.0f) override {
     if (!m_queue || !mesh)
     if (!m_queue || !mesh)
       return;
       return;
 
 
-    if (mesh == getUnitCylinder() && (!tex)) {
+    if (mesh == getUnitCylinder() && (!tex) && (!m_shader)) {
       QVector3D start, end;
       QVector3D start, end;
       float radius = 0.0f;
       float radius = 0.0f;
       if (detail::decomposeUnitCylinder(model, start, end, radius)) {
       if (detail::decomposeUnitCylinder(model, start, end, radius)) {
@@ -68,6 +71,7 @@ public:
     cmd.model = model;
     cmd.model = model;
     cmd.color = color;
     cmd.color = color;
     cmd.alpha = alpha;
     cmd.alpha = alpha;
+    cmd.shader = m_shader;
     m_queue->submit(cmd);
     m_queue->submit(cmd);
   }
   }
   void cylinder(const QVector3D &start, const QVector3D &end, float radius,
   void cylinder(const QVector3D &start, const QVector3D &end, float radius,
@@ -118,6 +122,7 @@ public:
 
 
 private:
 private:
   DrawQueue *m_queue = nullptr;
   DrawQueue *m_queue = nullptr;
+  Shader *m_shader = nullptr;
 };
 };
 
 
 } // namespace Render::GL
 } // namespace Render::GL

+ 2 - 3
ui/qml/HUDVictory.qml

@@ -13,16 +13,15 @@ Rectangle {
     visible: (typeof game !== 'undefined' && game.victoryState !== "")
     visible: (typeof game !== 'undefined' && game.victoryState !== "")
     z: 100
     z: 100
 
 
-    // Reset overlay state when a new game starts
-    // This ensures no residual overlay remains after map reload or skirmish restart
     Connections {
     Connections {
-        target: (typeof game !== 'undefined') ? game : null
         function onVictoryStateChanged() {
         function onVictoryStateChanged() {
             if (typeof game !== 'undefined' && game.victoryState === "") {
             if (typeof game !== 'undefined' && game.victoryState === "") {
                 showingSummary = false;
                 showingSummary = false;
                 battleSummary.visible = false;
                 battleSummary.visible = false;
             }
             }
         }
         }
+
+        target: (typeof game !== 'undefined') ? game : null
     }
     }
 
 
     Rectangle {
     Rectangle {