Browse Source

Merge pull request #499 from djeada/bow

Fix bow position and carthage shaders
Adam Djellouli 1 week ago
parent
commit
a03fbce203

+ 394 - 82
assets/shaders/archer_carthage.frag

@@ -1,5 +1,10 @@
 #version 330 core
 #version 330 core
 
 
+// ============================================================================
+// CARTHAGINIAN ARCHER - Rich Brown Leather & Team-Colored Cloak
+// Unique earth-tone palette with vibrant team colors
+// ============================================================================
+
 in vec3 v_worldNormal;
 in vec3 v_worldNormal;
 in vec3 v_worldPos;
 in vec3 v_worldPos;
 in vec2 v_texCoord;
 in vec2 v_texCoord;
@@ -12,128 +17,435 @@ uniform int u_materialId;
 
 
 out vec4 FragColor;
 out vec4 FragColor;
 
 
-float saturate(float v) { return clamp(v, 0.0, 1.0); }
-vec3 saturate(vec3 v) { return clamp(v, 0.0, 1.0); }
+// ============================================================================
+// CONSTANTS & HELPERS
+// ============================================================================
+
+const float PI = 3.14159265359;
+const vec3 ARCHER_SKIN_BASE = vec3(0.08, 0.07, 0.065);
+// Reuse the spearman helmet brown so the hat matches the infantry tone
+const vec3 SPEARMAN_HELMET_BROWN = vec3(0.36, 0.22, 0.10);
 
 
-float hash12(vec2 p) {
+float saturate(float x) { return clamp(x, 0.0, 1.0); }
+vec3 saturate3(vec3 x) { return clamp(x, 0.0, 1.0); }
+
+vec3 boostSaturation(vec3 color, float amount) {
+  float grey = dot(color, vec3(0.299, 0.587, 0.114));
+  return mix(vec3(grey), color, 1.0 + amount);
+}
+
+float hash2(vec2 p) {
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   p3 += dot(p3, p3.yzx + 33.33);
   p3 += dot(p3, p3.yzx + 33.33);
   return fract((p3.x + p3.y) * p3.z);
   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 = hash2(i);
+  float b = hash2(i + vec2(1.0, 0.0));
+  float c = hash2(i + vec2(0.0, 1.0));
+  float d = hash2(i + vec2(1.0, 1.0));
+  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
 float fbm(vec2 p) {
 float fbm(vec2 p) {
   float v = 0.0;
   float v = 0.0;
   float a = 0.5;
   float a = 0.5;
-  for (int i = 0; i < 4; ++i) {
-    v += a * hash12(p);
-    p *= 2.0;
-    a *= 0.55;
+  mat2 rot = mat2(0.87, 0.50, -0.50, 0.87);
+  for (int i = 0; i < 5; ++i) {
+    v += a * noise(p);
+    p = rot * p * 2.0 + vec2(100.0);
+    a *= 0.5;
   }
   }
   return v;
   return v;
 }
 }
 
 
-float D_GGX(float NdotH, float a) {
-  float a2 = a * a;
-  float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
-  return a2 / max(3.14159 * d * d, 1e-5);
-}
+// ============================================================================
+// PBR FUNCTIONS
+// ============================================================================
 
 
-float geometry_schlick(float NdotX, float k) {
-  return NdotX / max(NdotX * (1.0 - k) + k, 1e-5);
+float D_GGX(float NdotH, float roughness) {
+  float a = roughness * roughness;
+  float a2 = a * a;
+  float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
+  return a2 / (PI * denom * denom + 1e-6);
 }
 }
 
 
-float geometry_smith(float NdotV, float NdotL, float roughness) {
+float G_SchlickGGX(float NdotX, float roughness) {
   float r = roughness + 1.0;
   float r = roughness + 1.0;
   float k = (r * r) / 8.0;
   float k = (r * r) / 8.0;
-  return geometry_schlick(NdotV, k) * geometry_schlick(NdotL, k);
+  return NdotX / (NdotX * (1.0 - k) + k + 1e-6);
+}
+
+float G_Smith(float NdotV, float NdotL, float roughness) {
+  return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness);
+}
+
+vec3 F_Schlick(float cosTheta, vec3 F0) {
+  float t = 1.0 - cosTheta;
+  float t5 = t * t * t * t * t;
+  return F0 + (1.0 - F0) * t5;
+}
+
+// ============================================================================
+// LEATHER TEXTURE - RICH BROWN PALETTE
+// ============================================================================
+
+// Multiple brown leather tones
+vec3 brownLeatherPalette(float variation) {
+  // Range from warm chocolate to reddish-brown to tan
+  vec3 darkBrown = vec3(0.28, 0.18, 0.10);    // Deep chocolate
+  vec3 redBrown = vec3(0.45, 0.25, 0.15);     // Ox-blood/cordovan
+  vec3 warmBrown = vec3(0.55, 0.38, 0.22);    // Saddle brown
+  vec3 lightTan = vec3(0.68, 0.52, 0.35);     // Natural tan
+  
+  if (variation < 0.33) {
+    return mix(darkBrown, redBrown, variation * 3.0);
+  } else if (variation < 0.66) {
+    return mix(redBrown, warmBrown, (variation - 0.33) * 3.0);
+  } else {
+    return mix(warmBrown, lightTan, (variation - 0.66) * 3.0);
+  }
+}
+
+float leatherGrain(vec2 uv, float scale) {
+  float coarse = fbm(uv * scale);
+  float medium = fbm(uv * scale * 2.5 + 7.3);
+  float fine = noise(uv * scale * 6.0);
+  float pores = smoothstep(0.55, 0.65, noise(uv * scale * 4.0));
+  return coarse * 0.4 + medium * 0.35 + fine * 0.15 + pores * 0.1;
+}
+
+float stitchPattern(vec2 uv, float spacing) {
+  float stitch = fract(uv.y * spacing);
+  stitch = smoothstep(0.4, 0.5, stitch) * smoothstep(0.6, 0.5, stitch);
+  float seamLine = smoothstep(0.48, 0.50, fract(uv.x * 4.0)) * 
+                   smoothstep(0.52, 0.50, fract(uv.x * 4.0));
+  return stitch * seamLine;
+}
+
+float battleWear(vec3 pos) {
+  float scratch1 = smoothstep(0.72, 0.77, noise(pos.xy * 22.0 + pos.z * 4.0));
+  float scratch2 = smoothstep(0.74, 0.79, noise(pos.zy * 18.0 - 2.5));
+  float scuff = fbm(pos.xz * 7.0) * fbm(pos.xy * 10.0);
+  scuff = smoothstep(0.35, 0.55, scuff);
+  return (scratch1 + scratch2) * 0.25 + scuff * 0.35;
+}
+
+float oilSheen(vec3 pos, vec3 N, vec3 V) {
+  float facing = 1.0 - abs(dot(N, V));
+  float variation = fbm(pos.xz * 12.0) * 0.5 + 0.5;
+  return facing * facing * variation;
 }
 }
 
 
-vec3 fresnel_schlick(float cos_theta, vec3 F0) {
-  return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);
+// ============================================================================
+// CLOAK FABRIC
+// ============================================================================
+
+float cloakFolds(vec3 pos) {
+  // Large flowing folds
+  float folds = sin(pos.x * 8.0 + pos.y * 3.0) * 0.5 + 0.5;
+  folds += sin(pos.z * 6.0 - pos.y * 2.0) * 0.3;
+  // Add wind-swept variation
+  float wind = fbm(pos.xz * 4.0 + pos.y * 2.0);
+  return folds * 0.6 + wind * 0.4;
+}
+
+float fabricWeave(vec2 uv) {
+  float warpX = sin(uv.x * 100.0) * 0.5 + 0.5;
+  float weftY = sin(uv.y * 100.0) * 0.5 + 0.5;
+  return warpX * weftY;
 }
 }
 
 
+// ============================================================================
+// BOW WOOD PATTERNS
+// ============================================================================
+
+float woodGrainBow(vec3 pos) {
+  // Long flowing grain along the length of the bow
+  float grain = sin(pos.y * 40.0 + fbm(pos.xy * 8.0) * 3.0);
+  grain = grain * 0.5 + 0.5;
+  float rings = fbm(vec2(pos.x * 20.0, pos.z * 20.0));
+  return grain * 0.7 + rings * 0.3;
+}
+
+// ============================================================================
+// MAIN
+// ============================================================================
+
 void main() {
 void main() {
-  vec3 base = u_color;
+  vec3 baseColor = clamp(u_color, 0.0, 1.0);
   if (u_useTexture) {
   if (u_useTexture) {
-    base *= texture(u_texture, v_texCoord).rgb;
+    baseColor *= texture(u_texture, v_texCoord).rgb;
   }
   }
-
+  
+  // Material IDs: 0=skin, 1=armor, 2=helmet, 3=weapon, 4=shield, 5=cloak
   bool is_skin = (u_materialId == 0);
   bool is_skin = (u_materialId == 0);
   bool is_armor = (u_materialId == 1);
   bool is_armor = (u_materialId == 1);
   bool is_helmet = (u_materialId == 2);
   bool is_helmet = (u_materialId == 2);
   bool is_weapon = (u_materialId == 3);
   bool is_weapon = (u_materialId == 3);
-
+  bool is_shield = (u_materialId == 4);
+  bool is_cloak = (u_materialId == 5 || u_materialId >= 12);
+  
   vec3 N = normalize(v_worldNormal);
   vec3 N = normalize(v_worldNormal);
   vec3 V = normalize(vec3(0.0, 0.0, 1.0));
   vec3 V = normalize(vec3(0.0, 0.0, 1.0));
   vec3 L = normalize(vec3(0.5, 1.0, 0.4));
   vec3 L = normalize(vec3(0.5, 1.0, 0.4));
   vec3 H = normalize(L + V);
   vec3 H = normalize(L + V);
-
-  // Light leather armor for archers.
+  
+  vec3 albedo = baseColor;
   float metallic = 0.0;
   float metallic = 0.0;
-  float roughness = 0.55;
-  vec3 albedo = base;
-
+  float roughness = 0.5;
+  float sheen = 0.0;
+  
+  // ========== MATERIAL SETUP ==========
+  
   if (is_skin) {
   if (is_skin) {
-    albedo = mix(base, vec3(0.90, 0.78, 0.68), 0.30);
-    roughness = 0.6;
-    // Jagged leather pants for lower body
-    float pants_mask = 1.0 - smoothstep(0.38, 0.60, v_worldPos.y);
-    float jag = fbm(v_worldPos.xz * 15.0 + v_worldPos.y * 3.0);
-    vec3 leather = vec3(0.42, 0.30, 0.20) - vec3(0.04) * jag;
-    albedo = mix(albedo, leather, pants_mask);
-    roughness = mix(roughness, 0.55, pants_mask);
-  } else if (is_armor || is_helmet) {
-    vec3 leather = vec3(0.50, 0.34, 0.22);
-    float grain = fbm(v_worldPos.xy * 13.0);
-    float crack = fbm(v_worldPos.zy * 9.5 + vec2(2.1, 4.2));
-    float wear = grain * 0.38 + crack * 0.28;
-    albedo = mix(leather, base, 0.45);
-    albedo -= vec3(0.05) * wear;
-    metallic = 0.05;
-    roughness = mix(0.48, 0.62, wear);
-  } else if (is_weapon) {
-    // Bow: wood body with darker handle wrap and subtle string shine.
-    float y = clamp(v_worldPos.y, 0.0, 1.0);
-    float wrap = smoothstep(0.35, 0.45, y) * smoothstep(0.55, 0.45, y);
-
-    vec3 wood = vec3(0.52, 0.36, 0.22);
-    vec3 wrap_col = vec3(0.28, 0.18, 0.12);
-    vec3 string = vec3(0.82, 0.78, 0.70);
-
-    float wood_grain = fbm(v_worldPos.xz * 16.0 + v_worldPos.y * 5.0);
-    vec3 wood_color = mix(wood, base, 0.35) + vec3(0.05) * wood_grain;
-    vec3 wrap_color = mix(wrap_col, base, 0.20);
+    vec3 skinBase = ARCHER_SKIN_BASE;
+    vec3 teamTint = mix(skinBase, baseColor, 0.2);
+    float toneNoise = fbm(v_worldPos.xz * 3.1) - 0.5;
+    albedo = clamp(teamTint + vec3(toneNoise) * 0.04, 0.0, 1.0);
+    metallic = 0.0;
+    roughness = 0.58;
+    
+    // Leather arm guards and leg wrappings
+    float armGuard = smoothstep(0.55, 0.65, v_worldPos.y) * 
+                     smoothstep(0.75, 0.65, v_worldPos.y);
+    float legWrap = 1.0 - smoothstep(0.25, 0.45, v_worldPos.y);
+    float leatherMask = max(armGuard, legWrap);
+    
+    // Brown leather with grain variation
+    float leatherVar = fbm(v_worldPos.xy * 5.0);
+    vec3 leatherColor = brownLeatherPalette(leatherVar);
+    float grain = leatherGrain(v_worldPos.xy, 16.0);
+    leatherColor *= 0.9 + grain * 0.2;
+    
+    // Braided pattern on wrappings
+    float braid = sin(v_worldPos.y * 30.0) * 0.5 + 0.5;
+    braid *= smoothstep(0.3, 0.5, fract(v_worldPos.x * 8.0));
+    leatherColor *= 0.95 + braid * 0.1;
+    
+    albedo = mix(albedo, leatherColor, leatherMask);
+    roughness = mix(roughness, 0.45, leatherMask);
+    sheen = leatherMask * 0.3;
+    
+  } else if (is_armor) {
+    // ====== RICH BROWN LEATHER ARMOR ======
+    float leatherVar = fbm(v_worldPos.xy * 4.0 + v_worldPos.z * 2.0);
+    vec3 leatherBase = brownLeatherPalette(leatherVar);
+    
+    float grain = leatherGrain(v_worldPos.xy, 14.0);
+    float wear = battleWear(v_worldPos);
+    float oil = oilSheen(v_worldPos, N, V);
+    float stitches = stitchPattern(v_worldPos.xy, 15.0);
+    
+    // Rich leather from palette, enhanced with base color tint
+    albedo = mix(leatherBase, baseColor * 0.8, 0.25);
+    albedo = boostSaturation(albedo, 0.25);
+    albedo *= 0.88 + grain * 0.24;
+    
+    // Layered leather effect - darker in creases
+    float depth = fbm(v_worldPos.xy * 6.0);
+    albedo = mix(albedo * 0.72, albedo * 1.12, depth);
+    
+    // Worn edges lighter
+    vec3 wornColor = albedo * 1.3 + vec3(0.1, 0.08, 0.05);
+    albedo = mix(albedo, wornColor, wear * 0.45);
+    
+    // Visible stitching
+    albedo = mix(albedo, albedo * 0.55, stitches * 0.8);
+    
+    // Leather straps with buckles
+    float straps = smoothstep(0.45, 0.48, fract(v_worldPos.x * 6.0)) *
+                   smoothstep(0.55, 0.52, fract(v_worldPos.x * 6.0));
+    vec3 strapColor = brownLeatherPalette(0.15); // Darker straps
+    albedo = mix(albedo, strapColor, straps * 0.6);
+    
+    metallic = 0.0;
+    roughness = 0.42 - oil * 0.14 + grain * 0.1;
+    roughness = clamp(roughness, 0.28, 0.55);
+    sheen = oil * 0.55;
+    
+  } else if (is_helmet) {
+    // Leather cap with bronze trim (dark brown base)
+    float grain = leatherGrain(v_worldPos.xz, 12.0);
+    vec3 leatherColor = SPEARMAN_HELMET_BROWN;
+    leatherColor = mix(leatherColor, baseColor * 0.4, 0.1);
+    leatherColor *= 0.9 + grain * 0.25;
 
 
-    albedo = mix(wood_color, wrap_color, wrap);
+    // Bronze cheek guards and trim
+    float bronzeTrim = smoothstep(0.7, 0.75, abs(v_worldPos.x) * 2.0);
+    bronzeTrim += smoothstep(0.85, 0.9, v_worldPos.y);
+    vec3 bronzeColor = vec3(0.64, 0.42, 0.24);
+    bronzeColor = boostSaturation(bronzeColor, 0.3);
+    
+    albedo = mix(leatherColor, bronzeColor, bronzeTrim);
+    metallic = mix(0.0, 0.88, bronzeTrim);
+    roughness = mix(0.45, 0.25, bronzeTrim);
+    sheen = (1.0 - bronzeTrim) * 0.4;
+    
+  } else if (is_weapon) {
+    // ====== BOW - Beautiful wood with leather grip ======
+    float h = v_worldPos.y;
+    
+    // Grip area in the middle
+    float grip = smoothstep(0.38, 0.45, h) * smoothstep(0.62, 0.55, h);
+    // Limbs are the curved parts
+    float limbs = 1.0 - grip;
+    // String at extremes
+    float stringArea = smoothstep(0.92, 1.0, h) + smoothstep(0.08, 0.0, h);
+    
+    // Beautiful yew/ash wood for bow limbs
+    float woodGrain = woodGrainBow(v_worldPos);
+    vec3 woodLight = vec3(0.72, 0.55, 0.35);  // Heartwood
+    vec3 woodDark = vec3(0.48, 0.32, 0.18);   // Sapwood contrast
+    vec3 woodColor = mix(woodDark, woodLight, woodGrain);
+    woodColor = boostSaturation(woodColor, 0.2);
+    
+    // Polish sheen on wood
+    float woodPolish = pow(max(dot(reflect(-V, N), L), 0.0), 24.0);
+    
+    // Leather grip wrap
+    float gripGrain = leatherGrain(v_worldPos.xy, 25.0);
+    vec3 gripLeather = brownLeatherPalette(0.25); // Dark leather
+    gripLeather *= 0.9 + gripGrain * 0.2;
+    
+    // Criss-cross wrap pattern
+    float wrapPattern = sin(v_worldPos.y * 50.0 + v_worldPos.x * 20.0);
+    wrapPattern = smoothstep(-0.2, 0.2, wrapPattern);
+    gripLeather *= 0.9 + wrapPattern * 0.15;
+    
+    // String - natural gut/linen color
+    vec3 stringColor = vec3(0.85, 0.80, 0.72);
+    float stringShine = pow(max(dot(reflect(-V, N), L), 0.0), 48.0);
+    
+    albedo = woodColor;
+    albedo = mix(albedo, gripLeather, grip);
+    albedo = mix(albedo, stringColor, stringArea * 0.8);
+    
     metallic = 0.0;
     metallic = 0.0;
-    roughness = mix(0.50, 0.42, wrap);
-
-    // Add a tiny clearcoat for the string region near top/bottom.
-    float string_mask = smoothstep(0.90, 1.02, y) + smoothstep(0.10, -0.05, y);
-    if (string_mask > 0.0) {
-      albedo = mix(albedo, string, clamp(string_mask, 0.0, 1.0) * 0.5);
-      roughness = mix(roughness, 0.30, clamp(string_mask, 0.0, 1.0));
-    }
+    roughness = mix(0.38, 0.48, grip);
+    roughness = mix(roughness, 0.32, stringArea);
+    sheen = woodPolish * limbs * 0.4 + grip * 0.3 + stringShine * stringArea * 0.3;
+    
+  } else if (is_shield) {
+    // Leather-covered wooden shield
+    float leatherVar = fbm(v_worldPos.xz * 5.0);
+    vec3 leatherColor = brownLeatherPalette(leatherVar * 0.7 + 0.15);
+    
+    float grain = leatherGrain(v_worldPos.xz, 10.0);
+    leatherColor *= 0.88 + grain * 0.24;
+    
+    // Central boss and edge trim in bronze
+    float dist = length(v_worldPos.xz);
+    float boss = smoothstep(0.15, 0.0, dist);
+    float edgeRim = smoothstep(0.85, 0.95, dist);
+    
+    vec3 bronzeColor = vec3(0.85, 0.65, 0.35);
+    bronzeColor = boostSaturation(bronzeColor, 0.25);
+    
+    albedo = leatherColor;
+    albedo = mix(albedo, bronzeColor, boss + edgeRim * 0.7);
+    
+    metallic = mix(0.0, 0.9, boss + edgeRim * 0.6);
+    roughness = mix(0.48, 0.22, boss);
+    sheen = (1.0 - boss) * (1.0 - edgeRim) * 0.35;
+    
+  } else if (is_cloak) {
+    // ====== TEAM-COLORED CLOAK ======
+    // Use u_color for team color - make it vibrant!
+    vec3 teamColor = boostSaturation(baseColor, 0.5);
+    teamColor *= 1.2; // Brighten
+    
+    float folds = cloakFolds(v_worldPos);
+    float weave = fabricWeave(v_worldPos.xy);
+    
+    // Rich fabric with fold shadows and highlights
+    albedo = teamColor;
+    albedo *= 0.75 + folds * 0.5; // Folds create depth
+    albedo *= 0.95 + weave * 0.08;
+    
+    // Slight color shift in shadows (cooler)
+    vec3 shadowTint = mix(teamColor, teamColor * vec3(0.85, 0.9, 1.0), 0.3);
+    albedo = mix(shadowTint, albedo, folds);
+    
+    // Fabric edge fray
+    float edgeFray = noise(v_worldPos.xy * 40.0);
+    albedo *= 0.97 + edgeFray * 0.06;
+    
+    metallic = 0.0;
+    roughness = 0.72;
+    // Velvet-like sheen on fabric
+    sheen = pow(1.0 - max(dot(N, V), 0.0), 4.0) * 0.25;
+    
+  } else {
+    // Default - leather-tinted
+    float leatherVar = fbm(v_worldPos.xy * 6.0);
+    albedo = mix(brownLeatherPalette(leatherVar), baseColor, 0.4);
+    albedo = boostSaturation(albedo, 0.15);
+    metallic = 0.0;
+    roughness = 0.55;
   }
   }
-
+  
+  // ========== PBR LIGHTING ==========
+  
   float NdotL = max(dot(N, L), 0.0);
   float NdotL = max(dot(N, L), 0.0);
-  float NdotV = max(dot(N, V), 0.0);
+  float NdotV = max(dot(N, V), 0.001);
   float NdotH = max(dot(N, H), 0.0);
   float NdotH = max(dot(N, H), 0.0);
   float VdotH = max(dot(V, H), 0.0);
   float VdotH = max(dot(V, H), 0.0);
-
-  float a = max(0.02, roughness * roughness);
-  float D = D_GGX(NdotH, a);
-  float G = geometry_smith(NdotV, NdotL, roughness);
+  
   vec3 F0 = mix(vec3(0.04), albedo, metallic);
   vec3 F0 = mix(vec3(0.04), albedo, metallic);
-  vec3 F = fresnel_schlick(VdotH, F0);
-  vec3 spec = (D * G * F) / max(4.0 * NdotL * NdotV + 1e-5, 1e-5);
-
-  vec3 kd = (vec3(1.0) - F) * (1.0 - metallic);
-  vec3 diffuse = kd * albedo / 3.14159;
-
-  float rim = pow(1.0 - max(dot(N, V), 0.0), 3.0);
-  vec3 ambient = albedo * 0.34 + vec3(0.04) * rim;
-  vec3 color = ambient + (diffuse + spec) * NdotL;
-
-  FragColor = vec4(saturate(color), u_alpha);
+  
+  float D = D_GGX(NdotH, max(roughness, 0.01));
+  float G = G_Smith(NdotV, NdotL, roughness);
+  vec3 F = F_Schlick(VdotH, F0);
+  vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001);
+  
+  vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
+  vec3 diffuse = kD * albedo / PI;
+  
+  vec3 color = (diffuse + specular * 1.6) * NdotL * 2.0;
+  
+  // ====== SHEEN EFFECTS ======
+  if (sheen > 0.01) {
+    vec3 R = reflect(-V, N);
+    float sheenSpec = pow(max(dot(R, L), 0.0), 14.0);
+    color += albedo * sheenSpec * sheen * 1.4;
+    
+    float edgeSheen = pow(1.0 - NdotV, 3.0);
+    color += vec3(0.92, 0.88, 0.78) * edgeSheen * sheen * 0.35;
+  }
+  
+  // ====== METALLIC SHINE (bronze) ======
+  if (metallic > 0.5) {
+    vec3 R = reflect(-V, N);
+    float specPower = 128.0 / (roughness * roughness + 0.001);
+    specPower = min(specPower, 512.0);
+    float mirrorSpec = pow(max(dot(R, L), 0.0), specPower);
+    color += albedo * mirrorSpec * 1.1;
+    
+    float hotspot = pow(NdotH, 256.0);
+    color += vec3(1.0) * hotspot * (1.0 - roughness);
+  }
+  
+  // ====== WARM AMBIENT ======
+  vec3 ambient = albedo * 0.36;
+  
+  // Subsurface hint for leather
+  float sss = pow(saturate(dot(-N, L)), 2.5) * 0.12;
+  ambient += albedo * vec3(1.1, 0.92, 0.75) * sss;
+  
+  // Rim light
+  float rim = pow(1.0 - NdotV, 3.5);
+  ambient += vec3(0.32, 0.30, 0.26) * rim * 0.22;
+  
+  color += ambient;
+  
+  // Tone mapping
+  color = color / (color + vec3(0.55));
+  color = pow(color, vec3(0.94));
+  
+  FragColor = vec4(saturate3(color), u_alpha);
 }
 }

+ 507 - 369
assets/shaders/horse_archer_carthage.frag

@@ -1,9 +1,14 @@
 #version 330 core
 #version 330 core
 
 
+// ============================================================================
+// CARTHAGINIAN HORSE ARCHER - Rich Brown Leather & Team-Colored Cloak
+// Unique earth-tone palette with vibrant team colors
+// ============================================================================
+
 in vec3 v_normal;
 in vec3 v_normal;
 in vec2 v_texCoord;
 in vec2 v_texCoord;
 in vec3 v_worldPos;
 in vec3 v_worldPos;
-in float v_armorLayer; // Armor layer from vertex shader
+in float v_armorLayer;
 
 
 uniform sampler2D u_texture;
 uniform sampler2D u_texture;
 uniform vec3 u_color;
 uniform vec3 u_color;
@@ -13,15 +18,26 @@ uniform int u_materialId;
 
 
 out vec4 FragColor;
 out vec4 FragColor;
 
 
-// ---------------------
-// utilities & noise
-// ---------------------
+// ============================================================================
+// CONSTANTS & HELPERS
+// ============================================================================
+
 const float PI = 3.14159265359;
 const float PI = 3.14159265359;
 
 
+// *** Core bases
+const vec3 LEATHER_BASE_BROWN = vec3(0.36, 0.22, 0.10);
+const vec3 BRONZE_BASE_COLOR  = vec3(0.86, 0.66, 0.36);
+const vec3 ARCHER_SKIN_BASE    = vec3(0.08, 0.07, 0.065);
+
 float saturate(float x) { return clamp(x, 0.0, 1.0); }
 float saturate(float x) { return clamp(x, 0.0, 1.0); }
-vec3 saturate(vec3 x) { return clamp(x, 0.0, 1.0); }
+vec3 saturate3(vec3 x) { return clamp(x, 0.0, 1.0); }
 
 
-float hash(vec2 p) {
+vec3 boostSaturation(vec3 color, float amount) {
+  float grey = dot(color, vec3(0.299, 0.587, 0.114));
+  return mix(vec3(grey), color, 1.0 + amount);
+}
+
+float hash2(vec2 p) {
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   p3 += dot(p3, p3.yzx + 33.33);
   p3 += dot(p3, p3.yzx + 33.33);
   return fract((p3.x + p3.y) * p3.z);
   return fract((p3.x + p3.y) * p3.z);
@@ -31,408 +47,530 @@ float noise(vec2 p) {
   vec2 i = floor(p);
   vec2 i = floor(p);
   vec2 f = fract(p);
   vec2 f = fract(p);
   f = f * f * (3.0 - 2.0 * f);
   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));
+  float a = hash2(i);
+  float b = hash2(i + vec2(1.0, 0.0));
+  float c = hash2(i + vec2(0.0, 1.0));
+  float d = hash2(i + vec2(1.0, 1.0));
   return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
   return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
 }
 }
 
 
 float fbm(vec2 p) {
 float fbm(vec2 p) {
+  float v = 0.0;
   float a = 0.5;
   float a = 0.5;
-  float f = 0.0;
+  mat2 rot = mat2(0.87, 0.50, -0.50, 0.87);
   for (int i = 0; i < 5; ++i) {
   for (int i = 0; i < 5; ++i) {
-    f += a * noise(p);
-    p *= 2.03;
+    v += a * noise(p);
+    p = rot * p * 2.0 + vec2(100.0);
     a *= 0.5;
     a *= 0.5;
   }
   }
-  return f;
+  return v;
 }
 }
 
 
-// anti-aliased step
-float aa_step(float edge, float x) {
-  float w = fwidth(x);
-  return smoothstep(edge - w, edge + w, x);
+// ============================================================================
+// PBR FUNCTIONS
+// ============================================================================
+
+float D_GGX(float NdotH, float roughness) {
+  float a = roughness * roughness;
+  float a2 = a * a;
+  float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
+  return a2 / (PI * denom * denom + 1e-6);
 }
 }
 
 
-// ---------------------
-// patterns
-// ---------------------
-
-// plate seams + rivets (AA)
-float armor_plates(vec2 p, float y) {
-  float plate_y = fract(y * 6.5);
-  float line = smoothstep(0.92, 0.98, plate_y) - smoothstep(0.98, 1.0, plate_y);
-  // anti-aliased line thickness
-  line = smoothstep(0.0, fwidth(plate_y) * 2.0, line) * 0.12;
-
-  // rivets on top seams
-  float rivet_x = fract(p.x * 18.0);
-  float rivet =
-      smoothstep(0.48, 0.50, rivet_x) * smoothstep(0.52, 0.50, rivet_x);
-  rivet *= step(0.92, plate_y);
-  return line + rivet * 0.25;
+float G_SchlickGGX(float NdotX, float roughness) {
+  float r = roughness + 1.0;
+  float k = (r * r) / 8.0;
+  return NdotX / (NdotX * (1.0 - k) + k + 1e-6);
 }
 }
 
 
-// linked ring suggestion (AA)
-float chainmail_rings(vec2 p) {
-  vec2 uv = p * 35.0;
+float G_Smith(float NdotV, float NdotL, float roughness) {
+  return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness);
+}
 
 
-  vec2 g0 = fract(uv) - 0.5;
-  float r0 = length(g0);
-  float fw0 = fwidth(r0) * 1.2;
-  float ring0 = smoothstep(0.30 + fw0, 0.30 - fw0, r0) -
-                smoothstep(0.20 + fw0, 0.20 - fw0, r0);
+vec3 F_Schlick(float cosTheta, vec3 F0) {
+  float t = 1.0 - cosTheta;
+  float t5 = t * t * t * t * t;
+  return F0 + (1.0 - F0) * t5;
+}
 
 
-  vec2 g1 = fract(uv + vec2(0.5, 0.0)) - 0.5;
-  float r1 = length(g1);
-  float fw1 = fwidth(r1) * 1.2;
-  float ring1 = smoothstep(0.30 + fw1, 0.30 - fw1, r1) -
-                smoothstep(0.20 + fw1, 0.20 - fw1, r1);
+// ============================================================================
+// LEATHER TEXTURE - RICH BROWN PALETTE
+// ============================================================================
+
+vec3 brownLeatherPalette(float variation) {
+  vec3 darkBrown = vec3(0.28, 0.18, 0.10);
+  vec3 redBrown  = vec3(0.45, 0.25, 0.15);
+  vec3 warmBrown = vec3(0.55, 0.38, 0.22);
+  vec3 lightTan  = vec3(0.68, 0.52, 0.35);
+  
+  if (variation < 0.33) {
+    return mix(darkBrown, redBrown, variation * 3.0);
+  } else if (variation < 0.66) {
+    return mix(redBrown, warmBrown, (variation - 0.33) * 3.0);
+  } else {
+    return mix(warmBrown, lightTan, (variation - 0.66) * 3.0);
+  }
+}
 
 
-  return (ring0 + ring1) * 0.15;
+float leatherGrain(vec2 uv, float scale) {
+  float coarse = fbm(uv * scale);
+  float medium = fbm(uv * scale * 2.5 + 7.3);
+  float fine = noise(uv * scale * 6.0);
+  float pores = smoothstep(0.55, 0.65, noise(uv * scale * 4.0));
+  return coarse * 0.4 + medium * 0.35 + fine * 0.15 + pores * 0.1;
 }
 }
 
 
-float horse_hide_pattern(vec2 p) {
-  float grain = fbm(p * 80.0) * 0.10;
-  float ripple = sin(p.x * 22.0) * cos(p.y * 28.0) * 0.035;
-  float mottling = smoothstep(0.55, 0.65, fbm(p * 6.0)) * 0.07;
-  return grain + ripple + mottling;
+float stitchPattern(vec2 uv, float spacing) {
+  float stitch = fract(uv.y * spacing);
+  stitch = smoothstep(0.4, 0.5, stitch) * smoothstep(0.6, 0.5, stitch);
+  float seamLine = smoothstep(0.48, 0.50, fract(uv.x * 4.0)) * 
+                   smoothstep(0.52, 0.50, fract(uv.x * 4.0));
+  return stitch * seamLine;
 }
 }
 
 
-// ---------------------
-// microfacet shading
-// ---------------------
-vec3 fresnel_schlick(float cos_theta, vec3 F0) {
-  return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);
+float battleWear(vec3 pos) {
+  float scratch1 = smoothstep(0.72, 0.77, noise(pos.xy * 22.0 + pos.z * 4.0));
+  float scratch2 = smoothstep(0.74, 0.79, noise(pos.zy * 18.0 - 2.5));
+  float scuff = fbm(pos.xz * 7.0) * fbm(pos.xy * 10.0);
+  scuff = smoothstep(0.35, 0.55, scuff);
+  return (scratch1 + scratch2) * 0.25 + scuff * 0.35;
 }
 }
 
 
-float D_GGX(float NdotH, float rough) {
-  float a = max(0.001, rough);
-  float a2 = a * a;
-  float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
-  return a2 / max(1e-6, (PI * d * d));
+float oilSheen(vec3 pos, vec3 N, vec3 V) {
+  float facing = 1.0 - abs(dot(N, V));
+  float variation = fbm(pos.xz * 12.0) * 0.5 + 0.5;
+  return facing * facing * variation;
 }
 }
 
 
-float G_Smith(float NdotV, float NdotL, float rough) {
-  float r = rough + 1.0;
-  float k = (r * r) / 8.0;
-  float g_v = NdotV / (NdotV * (1.0 - k) + k);
-  float g_l = NdotL / (NdotL * (1.0 - k) + k);
-  return g_v * g_l;
+// ============================================================================
+// CLOAK & FABRIC
+// ============================================================================
+
+float cloakFolds(vec3 pos) {
+  float folds = sin(pos.x * 8.0 + pos.y * 3.0) * 0.5 + 0.5;
+  folds += sin(pos.z * 6.0 - pos.y * 2.0) * 0.3;
+  float wind = fbm(pos.xz * 4.0 + pos.y * 2.0);
+  return folds * 0.6 + wind * 0.4;
 }
 }
 
 
-// screen-space bump from a height field h(uv) in world XZ
-vec3 perturb_normal_ws(vec3 N, vec3 world_pos, float h, float scale) {
-  vec3 dpdx = dFdx(world_pos);
-  vec3 dpdy = dFdy(world_pos);
-  vec3 T = normalize(dpdx);
-  vec3 B = normalize(cross(N, T));
-  float hx = dFdx(h);
-  float hy = dFdy(h);
-  vec3 Np = normalize(N - scale * (hx * B + hy * T));
-  return Np;
+float fabricWeave(vec2 uv) {
+  float warpX = sin(uv.x * 100.0) * 0.5 + 0.5;
+  float weftY = sin(uv.y * 100.0) * 0.5 + 0.5;
+  return warpX * weftY;
 }
 }
 
 
-// hemisphere ambient (sky/ground)
-vec3 hemilight(vec3 N) {
-  vec3 sky = vec3(0.46, 0.70, 0.82);
-  vec3 ground = vec3(0.22, 0.18, 0.14);
-  float t = saturate(N.y * 0.5 + 0.5);
-  return mix(ground, sky, t) * 0.29;
+// ============================================================================
+// HORSE PATTERNS
+// ============================================================================
+
+float horseCoatPattern(vec2 uv) {
+  float coarse = fbm(uv * 3.0) * 0.15;
+  float fine = noise(uv * 25.0) * 0.08;
+  float dapple = smoothstep(0.4, 0.6, fbm(uv * 5.0)) * 0.1;
+  return coarse + fine + dapple;
 }
 }
 
 
-// ---------------------
-// main
-// ---------------------
-void main() {
-  vec3 base_color = u_color;
-  if (u_useTexture)
-    base_color *= texture(u_texture, v_texCoord).rgb;
+float furStrand(vec2 uv, float density) {
+  float strand = sin(uv.x * density) * cos(uv.y * density * 0.7);
+  strand = strand * 0.5 + 0.5;
+  float variation = noise(uv * density * 0.5);
+  return strand * 0.3 + variation * 0.2;
+}
 
 
-  vec3 N = normalize(v_normal);
-  vec2 uv = v_worldPos.xz * 5.0;
-
-  float avg = (base_color.r + base_color.g + base_color.b) * (1.0 / 3.0);
-  float hue_span = max(max(base_color.r, base_color.g), base_color.b) -
-                   min(min(base_color.r, base_color.g), base_color.b);
-
-  // Material ID: 0=rider skin, 1=armor, 2=helmet, 3=weapon, 4=shield,
-  // 5=rider clothing, 6=horse hide, 7=horse mane, 8=horse hoof,
-  // 9=saddle leather, 10=bridle, 11=saddle blanket,
-  // 12=cloak drape, 13=cloak shoulder
-  bool is_rider_skin = (u_materialId == 0);
-  bool is_armor = (u_materialId == 1);
-  bool is_helmet = (u_materialId == 2);
-  bool is_weapon = (u_materialId == 3);
-  bool is_shield = (u_materialId == 4);
-  bool is_rider_clothing = (u_materialId == 5);
-  bool is_cloak = (u_materialId == 12 || u_materialId == 13);
-  bool is_horse_hide = (u_materialId == 6);
-  bool is_horse_mane = (u_materialId == 7);
-  bool is_horse_hoof = (u_materialId == 8);
-  bool is_saddle_leather = (u_materialId == 9);
-  bool is_bridle = (u_materialId == 10);
-  bool is_saddle_blanket = (u_materialId == 11);
+// ============================================================================
+// BOW WOOD
+// ============================================================================
 
 
-  if (is_rider_skin) {
-    // Carthage horse archer: dark complexion.
-    vec3 target = vec3(0.32, 0.24, 0.18);
-    float tone_noise = fbm(v_worldPos.xz * 3.1) - 0.5;
-    base_color = clamp(target + vec3(tone_noise) * 0.05, 0.0, 1.0);
-    if (u_useTexture) {
-      vec3 tex = texture(u_texture, v_texCoord).rgb;
-      float eye_mask = step(0.25, 1.0 - dot(tex, vec3(0.299, 0.587, 0.114)));
-      base_color = mix(base_color, vec3(0.02), eye_mask); // black eyes
-    }
-  }
+float woodGrainBow(vec3 pos) {
+  float grain = sin(pos.y * 40.0 + fbm(pos.xy * 8.0) * 3.0);
+  grain = grain * 0.5 + 0.5;
+  float rings = fbm(vec2(pos.x * 20.0, pos.z * 20.0));
+  return grain * 0.7 + rings * 0.3;
+}
 
 
-  // Material-based detection only (no fallbacks)
-  bool is_brass = is_helmet;
-  bool is_steel = false;
-  bool is_chain = false;
-  bool is_fabric = is_rider_clothing || is_saddle_blanket || is_cloak;
-  bool is_leather = is_saddle_leather || is_bridle;
+// ============================================================================
+// MAIN
+// ============================================================================
 
 
-  // Team-tint cloaks while preserving base styling.
-  if (is_cloak) {
-    base_color = mix(base_color, saturate(u_color), 0.75);
+void main() {
+  vec3 baseColor = clamp(u_color, 0.0, 1.0);
+  if (u_useTexture) {
+    baseColor *= texture(u_texture, v_texCoord).rgb;
   }
   }
-
-  // lighting frame
-  vec3 L = normalize(vec3(1.0, 1.2, 1.0));
-  vec3 V = normalize(
-      vec3(0.0, 1.0, 0.5)); // stable view proxy (keeps interface unchanged)
+  
+  // Material IDs
+  bool is_rider_skin      = (u_materialId == 0);
+  bool is_armor           = (u_materialId == 1);
+  bool is_helmet          = (u_materialId == 2);
+  bool is_weapon          = (u_materialId == 3);
+  bool is_shield          = (u_materialId == 4);
+  bool is_rider_clothing  = (u_materialId == 5);
+  bool is_horse_hide      = (u_materialId == 6);
+  bool is_horse_mane      = (u_materialId == 7);
+  bool is_horse_hoof      = (u_materialId == 8);
+  bool is_saddle_leather  = (u_materialId == 9);
+  bool is_bridle          = (u_materialId == 10);
+  bool is_saddle_blanket  = (u_materialId == 11);
+  bool is_cloak           = (u_materialId == 12 || u_materialId == 13);
+  
+  vec3 N = normalize(v_normal);
+  vec3 V = normalize(vec3(0.0, 0.5, 1.0));
+  vec3 L = normalize(vec3(0.5, 1.0, 0.4));
   vec3 H = normalize(L + V);
   vec3 H = normalize(L + V);
-
-  float NdotL = saturate(dot(N, L));
-  float NdotV = saturate(dot(N, V));
-  float NdotH = saturate(dot(N, H));
-  float VdotH = saturate(dot(V, H));
-
-  // wrap diffuse like original (softens lambert)
-  float wrap_amount = (avg > 0.50) ? 0.08 : 0.30;
-  float NdotL_wrap = max(NdotL * (1.0 - wrap_amount) + wrap_amount, 0.12);
-
-  // base material params
+  
+  vec3 albedo = baseColor;
+  float metallic = 0.0;
   float roughness = 0.5;
   float roughness = 0.5;
-  vec3 F0 = vec3(0.04); // dielectric default
-  float metalness = 0.0;
-  vec3 albedo = base_color;
-
-  // micro details / masks (re-used)
-  float n_small = fbm(uv * 6.0);
-  float n_large = fbm(uv * 2.0);
-  float cavity = 1.0 - (n_large * 0.25 + n_small * 0.15);
-
-  // ---------------------
-  // MATERIAL BRANCHES
-  // ---------------------
-  vec3 col = vec3(0.0);
-  vec3 ambient = hemilight(N) * (0.85 + 0.15 * cavity);
-
-  if (is_armor) {
-    // Leather-first mix, consistent with infantry light armor
-    float leather_grain = fbm(uv * 12.0) * 0.12;
-    float linen_weave = fbm(uv * 6.0) * 0.08;
-    float scale_hint = armor_plates(v_worldPos.xz, v_worldPos.y) * 0.35;
-
-    float h = fbm(vec2(v_worldPos.y * 22.0, v_worldPos.z * 7.0));
-    N = perturb_normal_ws(N, v_worldPos, h, 0.30);
-
-    vec3 leather_tint = vec3(0.44, 0.30, 0.19);
-    vec3 linen_tint = vec3(0.86, 0.80, 0.72);
-    vec3 bronze_tint = vec3(0.62, 0.46, 0.20);
-    vec3 chain_tint = vec3(0.78, 0.80, 0.82);
-
-    // Treat the entire armor mesh as torso to avoid clipping by height bands.
-    float torsoBand = 1.0;
-    float skirtBand = 0.0;
-    float linenBlend = skirtBand * 0.40;
-    float bronzeBlend = torsoBand * 0.45;
-    float chainBlend = torsoBand * 0.20;
-    float leatherOverlay = skirtBand * 0.90 + torsoBand * 0.30;
-    float edge = 1.0 - clamp(dot(N, vec3(0.0, 1.0, 0.0)), 0.0, 1.0);
-    vec3 highlight = vec3(0.10, 0.08, 0.05) * smoothstep(0.3, 0.9, edge);
-
-    albedo = leather_tint;
-    albedo = mix(albedo, linen_tint, linenBlend);
-    albedo = mix(albedo, bronze_tint, bronzeBlend);
-    albedo = mix(albedo, chain_tint, chainBlend);
-    albedo = mix(albedo, leather_tint + highlight, leatherOverlay);
-
-    roughness = 0.42 - leather_grain * 0.12 + linen_weave * 0.08;
-    roughness = clamp(roughness, 0.26, 0.62);
-    F0 = mix(vec3(0.06), bronze_tint, 0.22);
-
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * albedo * 0.70;
-    col += NdotL_wrap * (albedo * (1.0 - F) + spec * 0.65);
-    col += vec3(scale_hint * 0.4 + leather_grain * 0.2 + linen_weave * 0.15);
-
+  float sheen = 0.0;
+  
+  // ========== MATERIAL SETUP ==========
+  
+  if (is_rider_skin) {
+    vec3 skinBase = ARCHER_SKIN_BASE;
+    vec3 teamTint = mix(skinBase, baseColor, 0.2);
+    float toneNoise = fbm(v_worldPos.xz * 3.1) - 0.5;
+    albedo = clamp(teamTint + vec3(toneNoise) * 0.04, 0.0, 1.0);
+    metallic = 0.0;
+    roughness = 0.58;
+    
+  } else if (is_armor) {
+    // ====== RICH BROWN LEATHER ARMOR (UV-based, clearly rough leather) ======
+    vec2 leatherUV = v_texCoord * 6.0;                         // *** UV-driven
+    float leatherVar = fbm(leatherUV * 2.0);
+    vec3 leatherPalette = brownLeatherPalette(leatherVar);
+    
+    // *** Core brown leather base, lightly tinted by team color
+    vec3 tint = mix(vec3(1.0), baseColor, 0.25);
+    vec3 leatherBase = mix(LEATHER_BASE_BROWN, leatherPalette, 0.5);
+    albedo = leatherBase * tint;
+    albedo = boostSaturation(albedo, 0.25);
+    
+    float grain    = leatherGrain(leatherUV, 8.0);              // *** stronger grain
+    float wear     = battleWear(v_worldPos);
+    float oil      = oilSheen(v_worldPos, N, V);
+    float stitches = stitchPattern(leatherUV, 18.0);            // *** visible seams
+    
+    // Strong grain contrast
+    float grainStrength = 0.55;
+    albedo = mix(albedo * 0.7, albedo * 1.3, grain * grainStrength);
+    
+    // Darker creases / lighter raised areas
+    float depth = fbm(leatherUV * 3.0);
+    albedo = mix(albedo * 0.7, albedo * 1.15, depth);
+    
+    // Worn edges lighter & slightly desaturated
+    vec3 wornColor = albedo * 1.3 + vec3(0.1, 0.08, 0.05);
+    wornColor = mix(wornColor, vec3(dot(wornColor, vec3(0.3, 0.59, 0.11))), 0.15);
+    albedo = mix(albedo, wornColor, wear * 0.6);
+    
+    // Stitch darkening
+    albedo = mix(albedo, albedo * 0.55, stitches * 0.9);
+    
+    // Leather straps (keep existing pattern)
+    float straps = smoothstep(0.45, 0.48, fract(v_worldPos.x * 6.0)) *
+                   smoothstep(0.55, 0.52, fract(v_worldPos.x * 6.0));
+    vec3 strapColor = brownLeatherPalette(0.15);
+    albedo = mix(albedo, strapColor, straps * 0.6);
+    
+    // *** Fully leather-like roughness
+    metallic  = 0.0;
+    float baseRough = 0.85;
+    roughness = baseRough - oil * 0.25 + grain * 0.05;
+    roughness = clamp(roughness, 0.65, 0.9);
+    sheen     = oil * 0.6;
+    
+  } else if (is_helmet) {
+    // ====== BRONZE-HEAVY HELMET WITH SMALL LEATHER UNDER-CAP ======
+    
+    // Leather under-cap near bottom/inside
+    float leatherZone = smoothstep(0.2, 0.0, v_worldPos.y);
+    leatherZone *= smoothstep(0.25, 0.15, abs(v_worldPos.x));
+    
+    float helmLeatherVar = fbm(v_worldPos.xz * 6.0);
+    vec3 helmLeather = brownLeatherPalette(helmLeatherVar * 0.6 + 0.2);
+    float helmGrain = leatherGrain(v_worldPos.xz, 12.0);
+    helmLeather *= 0.9 + helmGrain * 0.2;
+    
+    // *** Hammered bronze shell
+    vec2 bronzeUV   = v_texCoord * 10.0;
+    float bronzeNoise = fbm(bronzeUV * 2.5);
+    float hammer      = noise(bronzeUV * 20.0);
+    
+    vec3 bronzeColor = BRONZE_BASE_COLOR;
+    vec3 patina      = vec3(0.78, 0.82, 0.70);
+    bronzeColor = mix(bronzeColor, patina, bronzeNoise * 0.25);
+    bronzeColor *= 0.9 + hammer * 0.25;
+    bronzeColor = boostSaturation(bronzeColor, 0.2);
+    
+    // Extra bright bronze trim on cheek guards and ridge
+    float trimMask = 0.0;
+    trimMask += smoothstep(0.65, 0.8, abs(v_worldPos.x) * 1.8); // sides
+    trimMask += smoothstep(0.8, 0.9, v_worldPos.y);             // top
+    trimMask = saturate(trimMask);
+    
+    vec3 trimBronze = bronzeColor * 1.25 + vec3(0.06, 0.04, 0.02);
+    
+    // Combine bronze + trim, with small leather zone
+    vec3 helmBase = mix(bronzeColor, trimBronze, trimMask * 0.7);
+    albedo = mix(helmLeather, helmBase, 1.0 - leatherZone * 0.7);
+    
+    // Strongly metallic bronze, non-metallic leather
+    float bronzeMetal  = 0.95;
+    float leatherMetal = 0.0;
+    metallic = mix(bronzeMetal, leatherMetal, leatherZone);
+    
+    // Bronze relatively smooth, leather rough
+    float bronzeRough = 0.32 + hammer * 0.05;
+    float leatherRough = 0.7;
+    roughness = mix(bronzeRough, leatherRough, leatherZone);
+    
+    sheen = (1.0 - leatherZone) * 0.35;
+    
+  } else if (is_weapon) {
+    // ====== BOW ======
+    float h = v_worldPos.y;
+    float grip = smoothstep(0.38, 0.45, h) * smoothstep(0.62, 0.55, h);
+    float limbs = 1.0 - grip;
+    float stringArea = smoothstep(0.92, 1.0, h) + smoothstep(0.08, 0.0, h);
+    
+    float woodGrain = woodGrainBow(v_worldPos);
+    vec3 woodLight = vec3(0.72, 0.55, 0.35);
+    vec3 woodDark = vec3(0.48, 0.32, 0.18);
+    vec3 woodColor = mix(woodDark, woodLight, woodGrain);
+    woodColor = boostSaturation(woodColor, 0.2);
+    
+    float woodPolish = pow(max(dot(reflect(-V, N), L), 0.0), 24.0);
+    
+    float gripGrain = leatherGrain(v_worldPos.xy, 25.0);
+    vec3 gripLeather = brownLeatherPalette(0.25);
+    gripLeather *= 0.9 + gripGrain * 0.2;
+    
+    float wrapPattern = sin(v_worldPos.y * 50.0 + v_worldPos.x * 20.0);
+    wrapPattern = smoothstep(-0.2, 0.2, wrapPattern);
+    gripLeather *= 0.9 + wrapPattern * 0.15;
+    
+    vec3 stringColor = vec3(0.85, 0.80, 0.72);
+    float stringShine = pow(max(dot(reflect(-V, N), L), 0.0), 48.0);
+    
+    albedo = woodColor;
+    albedo = mix(albedo, gripLeather, grip);
+    albedo = mix(albedo, stringColor, stringArea * 0.8);
+    
+    metallic = 0.0;
+    roughness = mix(0.38, 0.48, grip);
+    roughness = mix(roughness, 0.32, stringArea);
+    sheen = woodPolish * limbs * 0.4 + grip * 0.3 + stringShine * stringArea * 0.3;
+    
+  } else if (is_shield) {
+    float leatherVar = fbm(v_worldPos.xz * 5.0);
+    vec3 leatherColor = brownLeatherPalette(leatherVar * 0.7 + 0.15);
+    
+    float grain = leatherGrain(v_worldPos.xz, 10.0);
+    leatherColor *= 0.88 + grain * 0.24;
+    
+    float dist = length(v_worldPos.xz);
+    float boss = smoothstep(0.15, 0.0, dist);
+    float edgeRim = smoothstep(0.85, 0.95, dist);
+    
+    vec3 bronzeColor = vec3(0.85, 0.65, 0.35);
+    bronzeColor = boostSaturation(bronzeColor, 0.25);
+    
+    albedo = leatherColor;
+    albedo = mix(albedo, bronzeColor, boss + edgeRim * 0.7);
+    
+    metallic = mix(0.0, 0.9, boss + edgeRim * 0.6);
+    roughness = mix(0.48, 0.22, boss);
+    sheen = (1.0 - boss) * (1.0 - edgeRim) * 0.35;
+    
+  } else if (is_rider_clothing) {
+    // Linen tunic
+    float weave = fabricWeave(v_worldPos.xy);
+    float threadVar = noise(v_worldPos.xz * 40.0);
+    
+    albedo = boostSaturation(baseColor, 0.2);
+    albedo *= 0.92 + weave * 0.08 + threadVar * 0.06;
+    
+    float folds = fbm(v_worldPos.xy * 6.0);
+    albedo *= 0.88 + folds * 0.18;
+    
+    metallic = 0.0;
+    roughness = 0.72;
+    sheen = pow(1.0 - max(dot(N, V), 0.0), 4.0) * 0.12;
+    
+  } else if (is_cloak) {
+    // ====== TEAM-COLORED CLOAK ======
+    vec3 teamColor = boostSaturation(baseColor, 0.5);
+    teamColor *= 1.2;
+    
+    float folds = cloakFolds(v_worldPos);
+    float weave = fabricWeave(v_worldPos.xy);
+    
+    albedo = teamColor;
+    albedo *= 0.75 + folds * 0.5;
+    albedo *= 0.95 + weave * 0.08;
+    
+    vec3 shadowTint = mix(teamColor, teamColor * vec3(0.85, 0.9, 1.0), 0.3);
+    albedo = mix(shadowTint, albedo, folds);
+    
+    float edgeFray = noise(v_worldPos.xy * 40.0);
+    albedo *= 0.97 + edgeFray * 0.06;
+    
+    metallic = 0.0;
+    roughness = 0.72;
+    sheen = pow(1.0 - max(dot(N, V), 0.0), 4.0) * 0.25;
+    
   } else if (is_horse_hide) {
   } else if (is_horse_hide) {
-    vec3 coat = vec3(0.36, 0.32, 0.28);
-    float hide_tex = horse_hide_pattern(v_worldPos.xz);
-    float grain = fbm(v_worldPos.xz * 18.0 + v_worldPos.y * 2.5);
-    albedo = coat * (1.0 + hide_tex * 0.06 - grain * 0.04);
-
-    roughness = 0.75;
-    F0 = vec3(0.02);
-    metalness = 0.0;
-
-    float h = fbm(v_worldPos.xz * 22.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.18);
-
-    col += ambient * albedo;
-    col += NdotL_wrap * albedo * 0.9;
-
+    // ====== HORSE COAT ======
+    float coat = horseCoatPattern(v_worldPos.xz);
+    float fur = furStrand(v_worldPos.xz, 60.0);
+    
+    albedo = boostSaturation(baseColor, 0.18);
+    albedo *= 0.9 + coat * 0.15 + fur * 0.1;
+    
+    float muscle = fbm(v_worldPos.xy * 4.0);
+    albedo *= 0.95 + muscle * 0.1;
+    
+    metallic = 0.0;
+    roughness = 0.65;
+    sheen = 0.22;
+    
   } else if (is_horse_mane) {
   } else if (is_horse_mane) {
-    float strand = sin(uv.x * 140.0) * 0.2 + noise(uv * 120.0) * 0.15;
-    float clump = noise(uv * 15.0) * 0.4;
-    float frizz = fbm(uv * 40.0) * 0.15;
-
-    float h = fbm(v_worldPos.xz * 25.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.18);
-
-    albedo = vec3(0.08, 0.07, 0.07) *
-             (1.0 + strand * 0.02 + clump * 0.02 + frizz * 0.02);
-
-    roughness = 0.70;
-    F0 = vec3(0.02);
-    metalness = 0.0;
-
-    col += ambient * albedo;
-    col += NdotL_wrap * albedo * 0.85;
-
-  } else if (is_steel) {
-    float brushed =
-        abs(sin(v_worldPos.y * 95.0)) * 0.02 + noise(uv * 35.0) * 0.015;
-    float dents = noise(uv * 8.0) * 0.03;
-    float plates = armor_plates(v_worldPos.xz, v_worldPos.y);
-
-    // bump from brushing
-    float h = fbm(vec2(v_worldPos.y * 25.0, v_worldPos.z * 6.0));
-    N = perturb_normal_ws(N, v_worldPos, h, 0.35);
-
-    // steel-like params
-    metalness = 1.0;
-    F0 = vec3(0.92);
-    roughness = 0.28 + brushed * 2.0 + dents * 0.6;
-    roughness = clamp(roughness, 0.15, 0.55);
-
-    // base tint & sky reflection lift
-    albedo = mix(vec3(0.60), base_color, 0.25);
-    float sky_refl = (N.y * 0.5 + 0.5) * 0.10;
-
-    // microfacet spec only for metals
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0 * albedo); // slight tint
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * 0.3; // metals rely more on spec
-    col += NdotL_wrap * spec * 1.5;
-    col += vec3(plates) + vec3(sky_refl) - vec3(dents * 0.25) + vec3(brushed);
-
-  } else if (is_brass) {
-    float brass_noise = noise(uv * 22.0) * 0.02;
-    float patina = fbm(uv * 4.0) * 0.12; // larger-scale patina
-
-    // bump from subtle hammering
-    float h = fbm(uv * 18.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.30);
-
-    metalness = 1.0;
-    vec3 brass_tint = vec3(0.94, 0.78, 0.45);
-    F0 = mix(brass_tint, base_color, 0.5);
-    roughness = clamp(0.32 + patina * 0.45, 0.18, 0.75);
-
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * 0.25;
-    col += NdotL_wrap * spec * 1.35;
-    col += vec3(brass_noise) - vec3(patina * 0.35);
-
-  } else if (is_chain) {
-    float rings = chainmail_rings(v_worldPos.xz);
-    float ring_hi = noise(uv * 30.0) * 0.10;
-
-    // small pitted bump
-    float h = fbm(uv * 35.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.25);
-
-    metalness = 1.0;
-    F0 = vec3(0.86);
-    roughness = 0.35;
-
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * 0.25;
-    col += NdotL_wrap * (spec * (1.2 + rings)) + vec3(ring_hi);
-    // slight diffuse damping to keep chainmail darker in cavities
-    col *= (0.95 - 0.10 * (1.0 - cavity));
-
-  } else if (is_fabric) {
-    float weave_x = sin(v_worldPos.x * 70.0);
-    float weave_z = sin(v_worldPos.z * 70.0);
-    float weave = weave_x * weave_z * 0.04;
-    float embroidery = fbm(uv * 6.0) * 0.08;
-
-    float h = fbm(uv * 22.0) * 0.7 + weave * 0.6;
-    N = perturb_normal_ws(N, v_worldPos, h, 0.35);
-
-    roughness = 0.78;
-    F0 = vec3(0.035);
-    metalness = 0.0;
-
-    vec3 F = fresnel_schlick(VdotH, F0);
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    float sheen = pow(1.0 - NdotV, 6.0) * 0.10;
-    albedo *= 1.0 + fbm(uv * 5.0) * 0.10 - 0.05;
-
-    col += ambient * albedo;
-    col += NdotL_wrap * (albedo * (1.0 - F) + spec * 0.3) +
-           vec3(weave + embroidery + sheen);
-
-  } else { // leather
-    float grain = fbm(uv * 10.0) * 0.15;
-    float wear = fbm(uv * 3.0) * 0.12;
-
-    float h = fbm(uv * 18.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.28);
-
-    roughness = 0.58 - wear * 0.15;
-    F0 = vec3(0.038);
-    metalness = 0.0;
-
-    vec3 F = fresnel_schlick(VdotH, F0);
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    float sheen = pow(1.0 - NdotV, 4.0) * 0.06;
-
-    albedo *= (1.0 + grain - 0.06 + wear * 0.05);
-    col += ambient * albedo;
-    col += NdotL_wrap * (albedo * (1.0 - F)) + spec * 0.4 + vec3(sheen);
+    float strand = furStrand(v_worldPos.xy, 120.0);
+    float clump = fbm(v_worldPos.xy * 8.0);
+    
+    albedo = baseColor * 0.35;
+    albedo = boostSaturation(albedo, 0.15);
+    albedo *= 0.85 + strand * 0.2 + clump * 0.1;
+    
+    metallic = 0.0;
+    roughness = 0.75;
+    sheen = 0.18;
+    
+  } else if (is_horse_hoof) {
+    float grain = fbm(v_worldPos.xy * 20.0);
+    
+    albedo = vec3(0.18, 0.15, 0.12);
+    albedo = boostSaturation(albedo, 0.1);
+    albedo *= 0.9 + grain * 0.15;
+    
+    metallic = 0.0;
+    roughness = 0.45;
+    sheen = 0.28;
+    
+  } else if (is_saddle_leather || is_bridle) {
+    // ====== TACK - Rich brown leather ======
+    float leatherVar = fbm(v_worldPos.xy * 5.0);
+    vec3 tackLeather = brownLeatherPalette(leatherVar * 0.5 + 0.3);
+    
+    float grain = leatherGrain(v_worldPos.xy, 18.0);
+    float wear = battleWear(v_worldPos) * 0.5;
+    float oil = oilSheen(v_worldPos, N, V);
+    
+    albedo = tackLeather;
+    albedo = boostSaturation(albedo, 0.28);
+    albedo *= 0.88 + grain * 0.22;
+    
+    vec3 wornColor = albedo * 1.2 + vec3(0.06, 0.04, 0.02);
+    albedo = mix(albedo, wornColor, wear * 0.4);
+    
+    if (is_bridle) {
+      float buckle = smoothstep(0.78, 0.82, noise(v_worldPos.xz * 15.0));
+      vec3 brassColor = vec3(0.82, 0.62, 0.32);
+      albedo = mix(albedo, brassColor, buckle * 0.85);
+      metallic = mix(0.0, 0.85, buckle);
+      roughness = mix(0.42, 0.28, buckle);
+    } else {
+      metallic = 0.0;
+      roughness = 0.42 - oil * 0.12 + grain * 0.08;
+    }
+    
+    sheen = oil * 0.45;
+    
+  } else if (is_saddle_blanket) {
+    // Woven wool - can have team color accent
+    float weaveX = sin(v_worldPos.x * 50.0);
+    float weaveZ = sin(v_worldPos.z * 50.0);
+    float weave = weaveX * weaveZ * 0.5 + 0.5;
+    float fuzz = noise(v_worldPos.xz * 80.0);
+    
+    vec3 woolBase = vec3(0.45, 0.35, 0.25);
+    vec3 teamStripe = boostSaturation(baseColor, 0.4);
+    
+    float stripe = smoothstep(0.35, 0.45, fract(v_worldPos.x * 3.0));
+    stripe *= smoothstep(0.65, 0.55, fract(v_worldPos.x * 3.0));
+    
+    albedo = mix(woolBase, teamStripe, stripe * 0.7);
+    albedo = boostSaturation(albedo, 0.2);
+    albedo *= 0.88 + weave * 0.12 + fuzz * 0.06;
+    
+    metallic = 0.0;
+    roughness = 0.82;
+    sheen = 0.08;
+    
+  } else {
+    // Default - leather-tinted
+    float leatherVar = fbm(v_worldPos.xy * 6.0);
+    albedo = mix(brownLeatherPalette(leatherVar), baseColor, 0.4);
+    albedo = boostSaturation(albedo, 0.15);
+    metallic = 0.0;
+    roughness = 0.55;
   }
   }
-
-  // Apply Carthage-specific tint - more teal/turquoise for visibility
-  col = mix(col, vec3(0.20, 0.55, 0.60),
-            saturate((base_color.g + base_color.b) * 0.5) * 0.20);
-  col = saturate(col);
-  FragColor = vec4(col, u_alpha);
+  
+  // ========== PBR LIGHTING ==========
+  
+  float NdotL = max(dot(N, L), 0.0);
+  float NdotV = max(dot(N, V), 0.001);
+  float NdotH = max(dot(N, H), 0.0);
+  float VdotH = max(dot(V, H), 0.0);
+  
+  vec3 F0 = mix(vec3(0.04), albedo, metallic);
+  
+  float D = D_GGX(NdotH, max(roughness, 0.01));
+  float G = G_Smith(NdotV, NdotL, roughness);
+  vec3 F = F_Schlick(VdotH, F0);
+  vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001);
+  
+  vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
+  vec3 diffuse = kD * albedo / PI;
+  
+  vec3 color = (diffuse + specular * 1.6) * NdotL * 2.0;
+  
+  // ====== SHEEN EFFECTS ======
+  if (sheen > 0.01) {
+    vec3 R = reflect(-V, N);
+    float sheenSpec = pow(max(dot(R, L), 0.0), 14.0);
+    color += albedo * sheenSpec * sheen * 1.4;
+    
+    float edgeSheen = pow(1.0 - NdotV, 3.0);
+    color += vec3(0.92, 0.88, 0.78) * edgeSheen * sheen * 0.35;
+  }
+  
+  // ====== METALLIC SHINE (bronze) ======
+  if (metallic > 0.5) {
+    vec3 R = reflect(-V, N);
+    float specPower = 128.0 / (roughness * roughness + 0.001);
+    specPower = min(specPower, 512.0);
+    float mirrorSpec = pow(max(dot(R, L), 0.0), specPower);
+    color += albedo * mirrorSpec * 1.1;
+    
+    float hotspot = pow(NdotH, 256.0);
+    color += vec3(1.0) * hotspot * (1.0 - roughness);
+  }
+  
+  // ====== WARM AMBIENT ======
+  vec3 ambient = albedo * 0.36;
+  
+  float sss = pow(saturate(dot(-N, L)), 2.5) * 0.12;
+  ambient += albedo * vec3(1.1, 0.92, 0.75) * sss;
+  
+  float rim = pow(1.0 - NdotV, 3.5);
+  ambient += vec3(0.32, 0.30, 0.26) * rim * 0.22;
+  
+  color += ambient;
+  
+  // Tone mapping
+  color = color / (color + vec3(0.55));
+  color = pow(color, vec3(0.94));
+  
+  FragColor = vec4(saturate3(color), u_alpha);
 }
 }

+ 444 - 370
assets/shaders/horse_spearman_carthage.frag

@@ -1,9 +1,13 @@
 #version 330 core
 #version 330 core
 
 
+// ============================================================================
+// CARTHAGINIAN HORSE SPEARMAN - Rich Leather Armor with Battle-Worn Character
+// ============================================================================
+
 in vec3 v_normal;
 in vec3 v_normal;
 in vec2 v_texCoord;
 in vec2 v_texCoord;
 in vec3 v_worldPos;
 in vec3 v_worldPos;
-in float v_armorLayer; // Armor layer from vertex shader
+in float v_armorLayer;
 
 
 uniform sampler2D u_texture;
 uniform sampler2D u_texture;
 uniform vec3 u_color;
 uniform vec3 u_color;
@@ -13,15 +17,25 @@ uniform int u_materialId;
 
 
 out vec4 FragColor;
 out vec4 FragColor;
 
 
-// ---------------------
-// utilities & noise
-// ---------------------
+// ============================================================================
+// CONSTANTS & HELPERS
+// ============================================================================
+
 const float PI = 3.14159265359;
 const float PI = 3.14159265359;
 
 
+// *** Core base colors
+const vec3 LEATHER_BASE_BROWN = vec3(0.36, 0.22, 0.10);
+const vec3 BRONZE_BASE_COLOR  = vec3(0.86, 0.66, 0.36);
+
 float saturate(float x) { return clamp(x, 0.0, 1.0); }
 float saturate(float x) { return clamp(x, 0.0, 1.0); }
-vec3 saturate(vec3 x) { return clamp(x, 0.0, 1.0); }
+vec3 saturate3(vec3 x) { return clamp(x, 0.0, 1.0); }
 
 
-float hash(vec2 p) {
+vec3 boostSaturation(vec3 color, float amount) {
+  float grey = dot(color, vec3(0.299, 0.587, 0.114));
+  return mix(vec3(grey), color, 1.0 + amount);
+}
+
+float hash2(vec2 p) {
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   p3 += dot(p3, p3.yzx + 33.33);
   p3 += dot(p3, p3.yzx + 33.33);
   return fract((p3.x + p3.y) * p3.z);
   return fract((p3.x + p3.y) * p3.z);
@@ -31,406 +45,466 @@ float noise(vec2 p) {
   vec2 i = floor(p);
   vec2 i = floor(p);
   vec2 f = fract(p);
   vec2 f = fract(p);
   f = f * f * (3.0 - 2.0 * f);
   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));
+  float a = hash2(i);
+  float b = hash2(i + vec2(1.0, 0.0));
+  float c = hash2(i + vec2(0.0, 1.0));
+  float d = hash2(i + vec2(1.0, 1.0));
   return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
   return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
 }
 }
 
 
 float fbm(vec2 p) {
 float fbm(vec2 p) {
+  float v = 0.0;
   float a = 0.5;
   float a = 0.5;
-  float f = 0.0;
+  mat2 rot = mat2(0.87, 0.50, -0.50, 0.87);
   for (int i = 0; i < 5; ++i) {
   for (int i = 0; i < 5; ++i) {
-    f += a * noise(p);
-    p *= 2.03;
+    v += a * noise(p);
+    p = rot * p * 2.0 + vec2(100.0);
     a *= 0.5;
     a *= 0.5;
   }
   }
-  return f;
+  return v;
 }
 }
 
 
-// anti-aliased step
-float aa_step(float edge, float x) {
-  float w = fwidth(x);
-  return smoothstep(edge - w, edge + w, x);
-}
+// ============================================================================
+// PBR FUNCTIONS
+// ============================================================================
 
 
-// ---------------------
-// patterns
-// ---------------------
-
-// plate seams + rivets (AA)
-float armor_plates(vec2 p, float y) {
-  float plate_y = fract(y * 6.5);
-  float line = smoothstep(0.92, 0.98, plate_y) - smoothstep(0.98, 1.0, plate_y);
-  // anti-aliased line thickness
-  line = smoothstep(0.0, fwidth(plate_y) * 2.0, line) * 0.12;
-
-  // rivets on top seams
-  float rivet_x = fract(p.x * 18.0);
-  float rivet =
-      smoothstep(0.48, 0.50, rivet_x) * smoothstep(0.52, 0.50, rivet_x);
-  rivet *= step(0.92, plate_y);
-  return line + rivet * 0.25;
+float D_GGX(float NdotH, float roughness) {
+  float a = roughness * roughness;
+  float a2 = a * a;
+  float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
+  return a2 / (PI * denom * denom + 1e-6);
 }
 }
 
 
-// linked ring suggestion (AA)
-float chainmail_rings(vec2 p) {
-  vec2 uv = p * 35.0;
-
-  vec2 g0 = fract(uv) - 0.5;
-  float r0 = length(g0);
-  float fw0 = fwidth(r0) * 1.2;
-  float ring0 = smoothstep(0.30 + fw0, 0.30 - fw0, r0) -
-                smoothstep(0.20 + fw0, 0.20 - fw0, r0);
-
-  vec2 g1 = fract(uv + vec2(0.5, 0.0)) - 0.5;
-  float r1 = length(g1);
-  float fw1 = fwidth(r1) * 1.2;
-  float ring1 = smoothstep(0.30 + fw1, 0.30 - fw1, r1) -
-                smoothstep(0.20 + fw1, 0.20 - fw1, r1);
-
-  return (ring0 + ring1) * 0.15;
+float G_SchlickGGX(float NdotX, float roughness) {
+  float r = roughness + 1.0;
+  float k = (r * r) / 8.0;
+  return NdotX / (NdotX * (1.0 - k) + k + 1e-6);
 }
 }
 
 
-float horse_hide_pattern(vec2 p) {
-  float grain = fbm(p * 80.0) * 0.10;
-  float ripple = sin(p.x * 22.0) * cos(p.y * 28.0) * 0.035;
-  float mottling = smoothstep(0.55, 0.65, fbm(p * 6.0)) * 0.07;
-  return grain + ripple + mottling;
+float G_Smith(float NdotV, float NdotL, float roughness) {
+  return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness);
 }
 }
 
 
-// ---------------------
-// microfacet shading
-// ---------------------
-vec3 fresnel_schlick(float cos_theta, vec3 F0) {
-  return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);
+vec3 F_Schlick(float cosTheta, vec3 F0) {
+  float t = 1.0 - cosTheta;
+  float t5 = t * t * t * t * t;
+  return F0 + (1.0 - F0) * t5;
 }
 }
 
 
-float D_GGX(float NdotH, float rough) {
-  float a = max(0.001, rough);
-  float a2 = a * a;
-  float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
-  return a2 / max(1e-6, (PI * d * d));
+// ============================================================================
+// LEATHER TEXTURE PATTERNS
+// ============================================================================
+
+// *** Multi-tone brown leather palette
+vec3 brownLeatherPalette(float variation) {
+  vec3 darkBrown = vec3(0.28, 0.18, 0.10);
+  vec3 redBrown  = vec3(0.45, 0.25, 0.15);
+  vec3 warmBrown = vec3(0.55, 0.38, 0.22);
+  vec3 lightTan  = vec3(0.68, 0.52, 0.35);
+  
+  if (variation < 0.33) {
+    return mix(darkBrown, redBrown, variation * 3.0);
+  } else if (variation < 0.66) {
+    return mix(redBrown, warmBrown, (variation - 0.33) * 3.0);
+  } else {
+    return mix(warmBrown, lightTan, (variation - 0.66) * 3.0);
+  }
 }
 }
 
 
-float G_Smith(float NdotV, float NdotL, float rough) {
-  float r = rough + 1.0;
-  float k = (r * r) / 8.0;
-  float g_v = NdotV / (NdotV * (1.0 - k) + k);
-  float g_l = NdotL / (NdotL * (1.0 - k) + k);
-  return g_v * g_l;
+float leatherGrain(vec2 uv, float scale) {
+  float coarse = fbm(uv * scale);
+  float medium = fbm(uv * scale * 2.5 + 7.3);
+  float fine = noise(uv * scale * 6.0);
+  float pores = smoothstep(0.55, 0.65, noise(uv * scale * 4.0));
+  return coarse * 0.4 + medium * 0.35 + fine * 0.15 + pores * 0.1;
 }
 }
 
 
-// screen-space bump from a height field h(uv) in world XZ
-vec3 perturb_normal_ws(vec3 N, vec3 world_pos, float h, float scale) {
-  vec3 dpdx = dFdx(world_pos);
-  vec3 dpdy = dFdy(world_pos);
-  vec3 T = normalize(dpdx);
-  vec3 B = normalize(cross(N, T));
-  float hx = dFdx(h);
-  float hy = dFdy(h);
-  vec3 Np = normalize(N - scale * (hx * B + hy * T));
-  return Np;
+float stitchPattern(vec2 uv, float spacing) {
+  float stitch = fract(uv.y * spacing);
+  stitch = smoothstep(0.4, 0.5, stitch) * smoothstep(0.6, 0.5, stitch);
+  float seamLine = smoothstep(0.48, 0.50, fract(uv.x * 3.0)) * 
+                   smoothstep(0.52, 0.50, fract(uv.x * 3.0));
+  return stitch * seamLine;
 }
 }
 
 
-// hemisphere ambient (sky/ground)
-vec3 hemilight(vec3 N) {
-  vec3 sky = vec3(0.46, 0.70, 0.82);
-  vec3 ground = vec3(0.22, 0.18, 0.14);
-  float t = saturate(N.y * 0.5 + 0.5);
-  return mix(ground, sky, t) * 0.29;
+float battleWear(vec3 pos) {
+  float scratch1 = smoothstep(0.7, 0.75, noise(pos.xy * 25.0 + pos.z * 5.0));
+  float scratch2 = smoothstep(0.72, 0.77, noise(pos.zy * 20.0 - 3.7));
+  float scuff = fbm(pos.xz * 8.0) * fbm(pos.xy * 12.0);
+  scuff = smoothstep(0.3, 0.5, scuff);
+  float edgeWear = smoothstep(0.4, 0.8, pos.y) * fbm(pos.xz * 6.0);
+  return (scratch1 + scratch2) * 0.3 + scuff * 0.4 + edgeWear * 0.3;
 }
 }
 
 
-// ---------------------
-// main
-// ---------------------
-void main() {
-  vec3 base_color = u_color;
-  if (u_useTexture)
-    base_color *= texture(u_texture, v_texCoord).rgb;
+float oilSheen(vec3 pos, vec3 N, vec3 V) {
+  float facing = 1.0 - abs(dot(N, V));
+  float variation = fbm(pos.xz * 15.0) * 0.5 + 0.5;
+  return facing * facing * variation;
+}
 
 
-  vec3 N = normalize(v_normal);
-  vec2 uv = v_worldPos.xz * 5.0;
+// ============================================================================
+// HORSE PATTERNS
+// ============================================================================
 
 
-  float avg = (base_color.r + base_color.g + base_color.b) * (1.0 / 3.0);
-  float hue_span = max(max(base_color.r, base_color.g), base_color.b) -
-                   min(min(base_color.r, base_color.g), base_color.b);
+float horseCoatPattern(vec2 uv) {
+  float coarse = fbm(uv * 3.0) * 0.15;
+  float fine = noise(uv * 25.0) * 0.08;
+  float dapple = smoothstep(0.4, 0.6, fbm(uv * 5.0)) * 0.1;
+  return coarse + fine + dapple;
+}
 
 
-  // Material ID: 0=rider skin, 1=armor, 2=helmet, 3=weapon, 4=shield,
-  // 5=rider clothing, 6=horse hide, 7=horse mane, 8=horse hoof,
-  // 9=saddle leather, 10=bridle, 11=saddle blanket
-  bool is_rider_skin = (u_materialId == 0);
-  bool is_armor = (u_materialId == 1);
-  bool is_helmet = (u_materialId == 2);
-  bool is_weapon = (u_materialId == 3);
-  bool is_shield = (u_materialId == 4);
-  bool is_rider_clothing = (u_materialId == 5);
-  bool is_horse_hide = (u_materialId == 6);
-  bool is_horse_mane = (u_materialId == 7);
-  bool is_horse_hoof = (u_materialId == 8);
-  bool is_saddle_leather = (u_materialId == 9);
-  bool is_bridle = (u_materialId == 10);
-  bool is_saddle_blanket = (u_materialId == 11);
+float furStrand(vec2 uv, float density) {
+  float strand = sin(uv.x * density) * cos(uv.y * density * 0.7);
+  strand = strand * 0.5 + 0.5;
+  float variation = noise(uv * density * 0.5);
+  return strand * 0.3 + variation * 0.2;
+}
 
 
-  // Material-based detection only (no fallbacks)
-  bool is_brass = is_helmet;
-  bool is_chain = false;
-  bool is_steel = false;
-  bool is_fabric = is_rider_clothing || is_saddle_blanket;
-  bool is_leather = is_saddle_leather || is_bridle;
+// ============================================================================
+// MAIN
+// ============================================================================
 
 
-  if (is_rider_skin) {
-    // Carthage horse spearman: dark complexion.
-    vec3 target = vec3(0.32, 0.24, 0.18);
-    float tone_noise = fbm(v_worldPos.xz * 3.1) - 0.5;
-    base_color = clamp(target + vec3(tone_noise) * 0.05, 0.0, 1.0);
-    if (u_useTexture) {
-      vec3 tex = texture(u_texture, v_texCoord).rgb;
-      float eye_mask = step(0.25, 1.0 - dot(tex, vec3(0.299, 0.587, 0.114)));
-      base_color = mix(base_color, vec3(0.02), eye_mask); // black eyes
-    }
+void main() {
+  vec3 baseColor = clamp(u_color, 0.0, 1.0);
+  if (u_useTexture) {
+    baseColor *= texture(u_texture, v_texCoord).rgb;
   }
   }
-
-  // lighting frame
-  vec3 L = normalize(vec3(1.0, 1.2, 1.0));
-  vec3 V = normalize(
-      vec3(0.0, 1.0, 0.5)); // stable view proxy (keeps interface unchanged)
+  baseColor = boostSaturation(baseColor, 0.25);
+  
+  // Material IDs
+  bool is_rider_skin      = (u_materialId == 0);
+  bool is_armor           = (u_materialId == 1);
+  bool is_helmet          = (u_materialId == 2);
+  bool is_weapon          = (u_materialId == 3);
+  bool is_shield          = (u_materialId == 4);
+  bool is_rider_clothing  = (u_materialId == 5);
+  bool is_horse_hide      = (u_materialId == 6);
+  bool is_horse_mane      = (u_materialId == 7);
+  bool is_horse_hoof      = (u_materialId == 8);
+  bool is_saddle_leather  = (u_materialId == 9);
+  bool is_bridle          = (u_materialId == 10);
+  bool is_saddle_blanket  = (u_materialId == 11);
+  
+  vec3 N = normalize(v_normal);
+  vec3 V = normalize(vec3(0.0, 0.5, 1.0));
+  vec3 L = normalize(vec3(0.5, 1.0, 0.4));
   vec3 H = normalize(L + V);
   vec3 H = normalize(L + V);
-
-  float NdotL = saturate(dot(N, L));
-  float NdotV = saturate(dot(N, V));
-  float NdotH = saturate(dot(N, H));
-  float VdotH = saturate(dot(V, H));
-
-  // wrap diffuse like original (softens lambert)
-  float wrap_amount = (avg > 0.50) ? 0.08 : 0.30;
-  float NdotL_wrap = max(NdotL * (1.0 - wrap_amount) + wrap_amount, 0.12);
-
-  // base material params
+  
+  vec3 albedo = baseColor;
+  float metallic = 0.0;
   float roughness = 0.5;
   float roughness = 0.5;
-  vec3 F0 = vec3(0.04); // dielectric default
-  float metalness = 0.0;
-  vec3 albedo = base_color;
-
-  // micro details / masks (re-used)
-  float n_small = fbm(uv * 6.0);
-  float n_large = fbm(uv * 2.0);
-  float cavity = 1.0 - (n_large * 0.25 + n_small * 0.15);
-
-  // ---------------------
-  // MATERIAL BRANCHES
-  // ---------------------
-  vec3 col = vec3(0.0);
-  vec3 ambient = hemilight(N) * (0.85 + 0.15 * cavity);
-
-  if (is_armor) {
-    // Leather-first mix with subtle bronze and linen to match infantry
-    float leather_grain = fbm(uv * 12.0) * 0.12;
-    float linen_weave = fbm(uv * 6.0) * 0.08;
-    float scale_hint = armor_plates(v_worldPos.xz, v_worldPos.y) * 0.35;
-
-    float h = fbm(vec2(v_worldPos.y * 22.0, v_worldPos.z * 7.0));
-    N = perturb_normal_ws(N, v_worldPos, h, 0.30);
-
-    vec3 leather_tint = vec3(0.44, 0.30, 0.19);
-    vec3 linen_tint = vec3(0.86, 0.80, 0.72);
-    vec3 bronze_tint = vec3(0.62, 0.46, 0.20);
-    vec3 chain_tint = vec3(0.78, 0.80, 0.82);
-
-    // Treat the entire armor mesh as torso to avoid clipping by height bands.
-    float torsoBand = 1.0;
-    float skirtBand = 0.0;
-    float linenBlend = skirtBand * 0.40;
-    float bronzeBlend = torsoBand * 0.45;
-    float chainBlend = torsoBand * 0.20;
-    float leatherOverlay = skirtBand * 0.90 + torsoBand * 0.30;
-    float edge = 1.0 - clamp(dot(N, vec3(0.0, 1.0, 0.0)), 0.0, 1.0);
-    vec3 highlight = vec3(0.10, 0.08, 0.05) * smoothstep(0.3, 0.9, edge);
-
-    albedo = leather_tint;
-    albedo = mix(albedo, linen_tint, linenBlend);
-    albedo = mix(albedo, bronze_tint, bronzeBlend);
-    albedo = mix(albedo, chain_tint, chainBlend);
-    albedo = mix(albedo, leather_tint + highlight, leatherOverlay);
-
-    float leather_depth = clamp(
-        leatherOverlay * 0.8 + linenBlend * 0.2 + bronzeBlend * 0.15, 0.0, 1.0);
-    albedo = mix(albedo, albedo * 0.88 + vec3(0.04, 0.03, 0.02),
-                 leather_depth * 0.35);
-
-    roughness = 0.42 - leather_grain * 0.12 + linen_weave * 0.08;
-    roughness = clamp(roughness, 0.26, 0.62);
-    F0 = mix(vec3(0.06), bronze_tint, 0.22);
-
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * albedo * 0.70;
-    col += NdotL_wrap * (albedo * (1.0 - F) + spec * 0.65);
-    col += vec3(scale_hint * 0.4 + leather_grain * 0.2 + linen_weave * 0.15);
-
+  float sheen = 0.0;
+  
+  // ========== MATERIAL SETUP ==========
+  
+  if (is_rider_skin) {
+    // Warm Mediterranean skin
+    albedo = mix(baseColor, vec3(0.75, 0.58, 0.45), 0.3);
+    albedo = boostSaturation(albedo, 0.15);
+    metallic = 0.0;
+    roughness = 0.55;
+    
+  } else if (is_armor) {
+    // ====== RICH BROWN LEATHER ARMOR (UV-based, clearly rough leather) ======
+    vec2 leatherUV = v_texCoord * 6.0;                        // *** UV-driven pattern
+    float leatherVar = fbm(leatherUV * 2.0);
+    vec3 leatherPalette = brownLeatherPalette(leatherVar);
+    
+    // *** Core leather base, lightly tinted by team color
+    vec3 tint = mix(vec3(1.0), baseColor, 0.25);
+    vec3 leatherBase = mix(LEATHER_BASE_BROWN, leatherPalette, 0.5);
+    albedo = leatherBase * tint;
+    albedo = boostSaturation(albedo, 0.25);
+    
+    float grain    = leatherGrain(leatherUV, 8.0);             // *** sharper grain
+    float wear     = battleWear(v_worldPos);
+    float oil      = oilSheen(v_worldPos, N, V);
+    float stitches = stitchPattern(leatherUV, 18.0);           // *** clear seams
+    
+    // Strong grain contrast
+    float grainStrength = 0.55;
+    albedo = mix(albedo * 0.7, albedo * 1.3, grain * grainStrength);
+    
+    // Darker in creases, lighter on raised areas
+    float depth = fbm(leatherUV * 3.0);
+    albedo = mix(albedo * 0.7, albedo * 1.15, depth);
+    
+    // Worn edges lighter & slightly desaturated
+    vec3 wornColor = albedo * 1.3 + vec3(0.1, 0.08, 0.05);
+    wornColor = mix(wornColor, vec3(dot(wornColor, vec3(0.3, 0.59, 0.11))), 0.15);
+    albedo = mix(albedo, wornColor, wear * 0.6);
+    
+    // Stitch darkening
+    albedo = mix(albedo, albedo * 0.55, stitches * 0.9);
+    
+    // Leather straps using world-space bands (keep existing idea)
+    float straps = smoothstep(0.45, 0.48, fract(v_worldPos.x * 6.0)) *
+                   smoothstep(0.55, 0.52, fract(v_worldPos.x * 6.0));
+    vec3 strapColor = brownLeatherPalette(0.15);
+    albedo = mix(albedo, strapColor, straps * 0.6);
+    
+    // *** Strongly rough leather, only mildly smoothed by oil
+    metallic  = 0.0;
+    float baseRough = 0.85;
+    roughness = baseRough - oil * 0.25 + grain * 0.05;
+    roughness = clamp(roughness, 0.65, 0.9);
+    sheen     = oil * 0.6;
+    
+  } else if (is_helmet) {
+    // ====== BRONZE-HEAVY HELMET WITH SMALL LEATHER UNDER-CAP ======
+    
+    // Leather under-cap near bottom / inside
+    float leatherZone = smoothstep(0.2, 0.0, v_worldPos.y);
+    leatherZone *= smoothstep(0.25, 0.15, abs(v_worldPos.x));
+    
+    float helmLeatherVar = fbm(v_worldPos.xz * 6.0);
+    vec3 helmLeather = brownLeatherPalette(helmLeatherVar * 0.6 + 0.2);
+    float helmGrain = leatherGrain(v_worldPos.xz, 12.0);
+    helmLeather *= 0.9 + helmGrain * 0.2;
+    
+    // Hammered bronze shell
+    vec2 bronzeUV = v_texCoord * 10.0;
+    float bronzeNoise = fbm(bronzeUV * 2.5);
+    float hammer = noise(bronzeUV * 20.0);
+    
+    vec3 bronzeColor = BRONZE_BASE_COLOR;
+    vec3 patina = vec3(0.78, 0.82, 0.70);
+    bronzeColor = mix(bronzeColor, patina, bronzeNoise * 0.25);
+    bronzeColor *= 0.9 + hammer * 0.25;
+    bronzeColor = boostSaturation(bronzeColor, 0.2);
+    
+    // Extra bright bronze trim on cheek guards and crest
+    float trimMask = 0.0;
+    trimMask += smoothstep(0.65, 0.8, abs(v_worldPos.x) * 1.8); // side plates
+    trimMask += smoothstep(0.8, 0.9, v_worldPos.y);             // top ridge
+    trimMask = saturate(trimMask);
+    
+    vec3 trimBronze = bronzeColor * 1.25 + vec3(0.06, 0.04, 0.02);
+    
+    // Combine bronze + trim, with small leather zone
+    vec3 helmBase = mix(bronzeColor, trimBronze, trimMask * 0.7);
+    albedo = mix(helmLeather, helmBase, 1.0 - leatherZone * 0.7);
+    
+    // Strongly metallic bronze, non-metallic leather
+    float bronzeMetal  = 0.95;
+    float leatherMetal = 0.0;
+    metallic = mix(bronzeMetal, leatherMetal, leatherZone);
+    
+    // Bronze relatively smooth, leather rough
+    float bronzeRough  = 0.32 + hammer * 0.05;
+    float leatherRough = 0.7;
+    roughness = mix(bronzeRough, leatherRough, leatherZone);
+    
+    sheen = (1.0 - leatherZone) * 0.35;
+    
+  } else if (is_weapon) {
+    // Spear
+    float h = v_worldPos.y;
+    float tip = smoothstep(0.40, 0.55, h);
+    float binding = smoothstep(0.35, 0.42, h) * (1.0 - tip);
+    
+    vec3 woodColor = boostSaturation(baseColor * 0.85, 0.3);
+    float woodGrain = fbm(vec2(v_worldPos.x * 8.0, v_worldPos.y * 35.0));
+    woodColor *= 0.85 + woodGrain * 0.3;
+    float woodSheen = pow(max(dot(reflect(-V, N), L), 0.0), 16.0);
+    
+    vec3 bindColor = baseColor * 0.6;
+    bindColor = boostSaturation(bindColor, 0.2);
+    float bindGrain = leatherGrain(v_worldPos.xy, 25.0);
+    bindColor *= 0.9 + bindGrain * 0.2;
+    
+    vec3 ironColor = vec3(0.55, 0.55, 0.58);
+    float ironBrush = fbm(v_worldPos.xy * 40.0);
+    ironColor += vec3(0.08) * ironBrush;
+    ironColor = mix(ironColor, baseColor * 0.3 + vec3(0.4), 0.15);
+    
+    albedo = woodColor;
+    albedo = mix(albedo, bindColor, binding);
+    albedo = mix(albedo, ironColor, tip);
+    
+    metallic = mix(0.0, 0.85, tip);
+    roughness = mix(0.38, 0.28, tip);
+    roughness = mix(roughness, 0.5, binding);
+    sheen = woodSheen * (1.0 - tip) * (1.0 - binding) * 0.3;
+    
+  } else if (is_shield) {
+    float dist = length(v_worldPos.xz);
+    float boss = smoothstep(0.18, 0.0, dist);
+    float bossRim = smoothstep(0.22, 0.18, dist) * (1.0 - boss);
+    
+    float shieldGrain = leatherGrain(v_worldPos.xz, 10.0);
+    float shieldWear = battleWear(v_worldPos);
+    
+    albedo = boostSaturation(baseColor, 0.3);
+    albedo *= 0.9 + shieldGrain * 0.2;
+    albedo = mix(albedo, albedo * 1.2, shieldWear * 0.3);
+    
+    vec3 bronzeColor = vec3(0.88, 0.68, 0.38);
+    bronzeColor = boostSaturation(bronzeColor, 0.25);
+    albedo = mix(albedo, bronzeColor, boss + bossRim * 0.8);
+    
+    metallic = mix(0.0, 0.9, boss + bossRim * 0.7);
+    roughness = mix(0.45, 0.22, boss);
+    
+  } else if (is_rider_clothing) {
+    // Linen tunic with texture
+    float weave = sin(v_worldPos.x * 80.0) * sin(v_worldPos.z * 80.0);
+    weave = weave * 0.5 + 0.5;
+    float threadVar = noise(v_worldPos.xz * 40.0);
+    
+    albedo = boostSaturation(baseColor, 0.25);
+    albedo *= 0.92 + weave * 0.08 + threadVar * 0.06;
+    
+    float folds = fbm(v_worldPos.xy * 6.0);
+    albedo *= 0.9 + folds * 0.15;
+    
+    metallic = 0.0;
+    roughness = 0.72;
+    sheen = pow(1.0 - max(dot(N, V), 0.0), 4.0) * 0.15;
+    
   } else if (is_horse_hide) {
   } else if (is_horse_hide) {
-    vec3 coat = vec3(0.36, 0.32, 0.28);
-    float hide_tex = horse_hide_pattern(v_worldPos.xz);
-    float grain = fbm(v_worldPos.xz * 18.0 + v_worldPos.y * 2.5);
-    albedo = coat * (1.0 + hide_tex * 0.06 - grain * 0.04);
-
-    roughness = 0.75;
-    F0 = vec3(0.02);
-    metalness = 0.0;
-
-    float h = fbm(v_worldPos.xz * 22.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.18);
-
-    col += ambient * albedo;
-    col += NdotL_wrap * albedo * 0.9;
-
+    // ====== HORSE COAT ======
+    float coat = horseCoatPattern(v_worldPos.xz);
+    float fur = furStrand(v_worldPos.xz, 60.0);
+    
+    albedo = boostSaturation(baseColor, 0.2);
+    albedo *= 0.9 + coat * 0.15 + fur * 0.1;
+    
+    float muscle = fbm(v_worldPos.xy * 4.0);
+    albedo *= 0.95 + muscle * 0.1;
+    
+    metallic = 0.0;
+    roughness = 0.65;
+    sheen = 0.25;
+    
   } else if (is_horse_mane) {
   } else if (is_horse_mane) {
-    float strand = sin(uv.x * 140.0) * 0.2 + noise(uv * 120.0) * 0.15;
-    float clump = noise(uv * 15.0) * 0.4;
-    float frizz = fbm(uv * 40.0) * 0.15;
-
-    float h = fbm(v_worldPos.xz * 25.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.18);
-
-    albedo = vec3(0.08, 0.07, 0.07) *
-             (1.0 + strand * 0.02 + clump * 0.02 + frizz * 0.02);
-
-    roughness = 0.70;
-    F0 = vec3(0.02);
-    metalness = 0.0;
-
-    col += ambient * albedo;
-    col += NdotL_wrap * albedo * 0.85;
-
-  } else if (is_steel) {
-    float brushed =
-        abs(sin(v_worldPos.y * 95.0)) * 0.02 + noise(uv * 35.0) * 0.015;
-    float dents = noise(uv * 8.0) * 0.03;
-    float plates = armor_plates(v_worldPos.xz, v_worldPos.y);
-
-    // bump from brushing
-    float h = fbm(vec2(v_worldPos.y * 25.0, v_worldPos.z * 6.0));
-    N = perturb_normal_ws(N, v_worldPos, h, 0.35);
-
-    // steel-like params
-    metalness = 1.0;
-    F0 = vec3(0.92);
-    roughness = 0.28 + brushed * 2.0 + dents * 0.6;
-    roughness = clamp(roughness, 0.15, 0.55);
-
-    // base tint & sky reflection lift
-    albedo = mix(vec3(0.60), base_color, 0.25);
-    float sky_refl = (N.y * 0.5 + 0.5) * 0.10;
-
-    // microfacet spec only for metals
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0 * albedo); // slight tint
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * 0.3; // metals rely more on spec
-    col += NdotL_wrap * spec * 1.5;
-    col += vec3(plates) + vec3(sky_refl) - vec3(dents * 0.25) + vec3(brushed);
-
-  } else if (is_brass) {
-    float brass_noise = noise(uv * 22.0) * 0.02;
-    float patina = fbm(uv * 4.0) * 0.12; // larger-scale patina
-
-    // bump from subtle hammering
-    float h = fbm(uv * 18.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.30);
-
-    metalness = 1.0;
-    vec3 brass_tint = vec3(0.94, 0.78, 0.45);
-    F0 = mix(brass_tint, base_color, 0.5);
-    roughness = clamp(0.32 + patina * 0.45, 0.18, 0.75);
-
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * 0.25;
-    col += NdotL_wrap * spec * 1.35;
-    col += vec3(brass_noise) - vec3(patina * 0.35);
-
-  } else if (is_chain) {
-    float rings = chainmail_rings(v_worldPos.xz);
-    float ring_hi = noise(uv * 30.0) * 0.10;
-
-    // small pitted bump
-    float h = fbm(uv * 35.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.25);
-
-    metalness = 1.0;
-    F0 = vec3(0.86);
-    roughness = 0.35;
-
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * 0.25;
-    col += NdotL_wrap * (spec * (1.2 + rings)) + vec3(ring_hi);
-    // slight diffuse damping to keep chainmail darker in cavities
-    col *= (0.95 - 0.10 * (1.0 - cavity));
-
-  } else if (is_fabric) {
-    float weave_x = sin(v_worldPos.x * 70.0);
-    float weave_z = sin(v_worldPos.z * 70.0);
-    float weave = weave_x * weave_z * 0.04;
-    float embroidery = fbm(uv * 6.0) * 0.08;
-
-    float h = fbm(uv * 22.0) * 0.7 + weave * 0.6;
-    N = perturb_normal_ws(N, v_worldPos, h, 0.35);
-
-    roughness = 0.78;
-    F0 = vec3(0.035);
-    metalness = 0.0;
-
-    vec3 F = fresnel_schlick(VdotH, F0);
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    float sheen = pow(1.0 - NdotV, 6.0) * 0.10;
-    albedo *= 1.0 + fbm(uv * 5.0) * 0.10 - 0.05;
-
-    col += ambient * albedo;
-    col += NdotL_wrap * (albedo * (1.0 - F) + spec * 0.3) +
-           vec3(weave + embroidery + sheen);
-
-  } else { // leather
-    float grain = fbm(uv * 10.0) * 0.15;
-    float wear = fbm(uv * 3.0) * 0.12;
-
-    float h = fbm(uv * 18.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.28);
-
-    roughness = 0.58 - wear * 0.15;
-    F0 = vec3(0.038);
-    metalness = 0.0;
-
-    vec3 F = fresnel_schlick(VdotH, F0);
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    float sheen = pow(1.0 - NdotV, 4.0) * 0.06;
-
-    albedo *= (1.0 + grain - 0.06 + wear * 0.05);
-    col += ambient * albedo;
-    col += NdotL_wrap * (albedo * (1.0 - F)) + spec * 0.4 + vec3(sheen);
+    float strand = furStrand(v_worldPos.xy, 120.0);
+    float clump = fbm(v_worldPos.xy * 8.0);
+    
+    albedo = baseColor * 0.35;
+    albedo = boostSaturation(albedo, 0.15);
+    albedo *= 0.85 + strand * 0.2 + clump * 0.1;
+    
+    metallic = 0.0;
+    roughness = 0.75;
+    sheen = 0.2;
+    
+  } else if (is_horse_hoof) {
+    float grain = fbm(v_worldPos.xy * 20.0);
+    
+    albedo = vec3(0.18, 0.15, 0.12);
+    albedo = boostSaturation(albedo, 0.1);
+    albedo *= 0.9 + grain * 0.15;
+    
+    metallic = 0.0;
+    roughness = 0.45;
+    sheen = 0.3;
+    
+  } else if (is_saddle_leather || is_bridle) {
+    // ====== TACK LEATHER ======
+    float grain = leatherGrain(v_worldPos.xy, 18.0);
+    float wear = battleWear(v_worldPos) * 0.6;
+    float oil = oilSheen(v_worldPos, N, V);
+    
+    albedo = baseColor * 1.1;
+    albedo = boostSaturation(albedo, 0.3);
+    albedo *= 0.88 + grain * 0.22;
+    
+    vec3 wornColor = albedo * 1.2 + vec3(0.06, 0.04, 0.02);
+    albedo = mix(albedo, wornColor, wear * 0.4);
+    
+    if (is_bridle) {
+      float buckle = smoothstep(0.78, 0.82, noise(v_worldPos.xz * 15.0));
+      vec3 brassColor = vec3(0.82, 0.62, 0.32);
+      albedo = mix(albedo, brassColor, buckle * 0.85);
+      metallic = mix(0.0, 0.85, buckle);
+      roughness = mix(0.42, 0.28, buckle);
+    } else {
+      metallic = 0.0;
+      roughness = 0.42 - oil * 0.12 + grain * 0.08;
+    }
+    
+    sheen = oil * 0.5;
+    
+  } else if (is_saddle_blanket) {
+    // Woven wool blanket
+    float weaveX = sin(v_worldPos.x * 50.0);
+    float weaveZ = sin(v_worldPos.z * 50.0);
+    float weave = weaveX * weaveZ * 0.5 + 0.5;
+    float fuzz = noise(v_worldPos.xz * 80.0);
+    
+    albedo = boostSaturation(baseColor, 0.35);
+    albedo *= 0.88 + weave * 0.12 + fuzz * 0.06;
+    
+    float stripe = smoothstep(0.4, 0.5, fract(v_worldPos.x * 4.0));
+    stripe *= smoothstep(0.6, 0.5, fract(v_worldPos.x * 4.0));
+    albedo = mix(albedo, albedo * 0.7, stripe * 0.3);
+    
+    metallic = 0.0;
+    roughness = 0.82;
+    sheen = 0.1;
+    
+  } else {
+    albedo = boostSaturation(baseColor, 0.2);
+    metallic = 0.0;
+    roughness = 0.55;
   }
   }
-
-  // Apply Carthage-specific tint - more teal/turquoise for visibility
-  col = mix(col, vec3(0.20, 0.55, 0.60),
-            saturate((base_color.g + base_color.b) * 0.5) * 0.20);
-  col = saturate(col);
-  FragColor = vec4(col, u_alpha);
+  
+  // ========== PBR LIGHTING ==========
+  
+  float NdotL = max(dot(N, L), 0.0);
+  float NdotV = max(dot(N, V), 0.001);
+  float NdotH = max(dot(N, H), 0.0);
+  float VdotH = max(dot(V, H), 0.0);
+  
+  vec3 F0 = mix(vec3(0.04), albedo, metallic);
+  
+  float D = D_GGX(NdotH, max(roughness, 0.01));
+  float G = G_Smith(NdotV, NdotL, roughness);
+  vec3 F = F_Schlick(VdotH, F0);
+  vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001);
+  
+  vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
+  vec3 diffuse = kD * albedo / PI;
+  
+  vec3 color = (diffuse + specular * 1.8) * NdotL * 2.0;
+  
+  // ====== LEATHER/COAT SHEEN EFFECT ======
+  if (sheen > 0.01) {
+    vec3 R = reflect(-V, N);
+    float sheenSpec = pow(max(dot(R, L), 0.0), 12.0);
+    color += albedo * sheenSpec * sheen * 1.5;
+    
+    float edgeSheen = pow(1.0 - NdotV, 3.0);
+    color += vec3(0.95, 0.90, 0.80) * edgeSheen * sheen * 0.4;
+  }
+  
+  // ====== METALLIC SHINE (bronze parts) ======
+  if (metallic > 0.5) {
+    vec3 R = reflect(-V, N);
+    float specPower = 128.0 / (roughness * roughness + 0.001);
+    specPower = min(specPower, 512.0);
+    float mirrorSpec = pow(max(dot(R, L), 0.0), specPower);
+    color += albedo * mirrorSpec * 1.2;
+    
+    float hotspot = pow(NdotH, 256.0);
+    color += vec3(1.0) * hotspot * (1.0 - roughness);
+  }
+  
+  // ====== WARM AMBIENT ======
+  vec3 ambient = albedo * 0.38;
+  
+  float sss = pow(saturate(dot(-N, L)), 2.0) * 0.12;
+  ambient += albedo * vec3(1.1, 0.9, 0.7) * sss;
+  
+  float rim = pow(1.0 - NdotV, 3.5);
+  ambient += vec3(0.35, 0.32, 0.28) * rim * 0.25;
+  
+  color += ambient;
+  
+  // Tone mapping
+  color = color / (color + vec3(0.55));
+  color = pow(color, vec3(0.94));
+  
+  FragColor = vec4(saturate3(color), u_alpha);
 }
 }

+ 326 - 369
assets/shaders/horse_swordsman_carthage.frag

@@ -1,9 +1,13 @@
 #version 330 core
 #version 330 core
 
 
+// ============================================================================
+// CARTHAGINIAN HORSE SWORDSMAN - Armor & Helmet changed to dark metallic
+// ============================================================================
+
 in vec3 v_normal;
 in vec3 v_normal;
 in vec2 v_texCoord;
 in vec2 v_texCoord;
 in vec3 v_worldPos;
 in vec3 v_worldPos;
-in float v_armorLayer; // Armor layer from vertex shader
+in float v_armorLayer;
 
 
 uniform sampler2D u_texture;
 uniform sampler2D u_texture;
 uniform vec3 u_color;
 uniform vec3 u_color;
@@ -13,13 +17,27 @@ uniform int u_materialId;
 
 
 out vec4 FragColor;
 out vec4 FragColor;
 
 
-// ---------------------
-// utilities & noise
-// ---------------------
+// ============================================================================
+// CONSTANTS & HELPERS
+// ============================================================================
+
 const float PI = 3.14159265359;
 const float PI = 3.14159265359;
 
 
-float saturate(float x) { return clamp(x, 0.0, 1.0); }
-vec3 saturate(vec3 x) { return clamp(x, 0.0, 1.0); }
+// ORIGINAL bronze kept for weapon/shield/etc
+const vec3 BRONZE_BASE_COLOR = vec3(0.86, 0.66, 0.36);
+
+// NEW dark metal for armor + helmet only
+const vec3 DARK_METAL_COLOR = vec3(0.14, 0.14, 0.16);
+// Dark brown base for the Carthaginian rider shield
+const vec3 SHIELD_BROWN_COLOR = vec3(0.18, 0.09, 0.035);
+
+float saturatef(float x) { return clamp(x, 0.0, 1.0); }
+vec3 saturate3(vec3 x) { return clamp(x, 0.0, 1.0); }
+
+vec3 boostSaturation(vec3 color, float amount) {
+  float grey = dot(color, vec3(0.299, 0.587, 0.114));
+  return mix(vec3(grey), color, 1.0 + amount);
+}
 
 
 float hash(vec2 p) {
 float hash(vec2 p) {
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
@@ -39,406 +57,345 @@ float noise(vec2 p) {
 }
 }
 
 
 float fbm(vec2 p) {
 float fbm(vec2 p) {
+  float v = 0.0;
   float a = 0.5;
   float a = 0.5;
-  float f = 0.0;
-  for (int i = 0; i < 5; ++i) {
-    f += a * noise(p);
-    p *= 2.03;
+  mat2 rot = mat2(0.87, 0.50, -0.50, 0.87);
+  for (int i = 0; i < 4; ++i) {
+    v += a * noise(p);
+    p = rot * p * 2.0 + vec2(100.0);
     a *= 0.5;
     a *= 0.5;
   }
   }
-  return f;
+  return v;
 }
 }
 
 
-// anti-aliased step
-float aa_step(float edge, float x) {
-  float w = fwidth(x);
-  return smoothstep(edge - w, edge + w, x);
-}
+// ============================================================================
+// PBR
+// ============================================================================
 
 
-// ---------------------
-// patterns
-// ---------------------
-
-// plate seams + rivets (AA)
-float armor_plates(vec2 p, float y) {
-  float plate_y = fract(y * 6.5);
-  float line = smoothstep(0.92, 0.98, plate_y) - smoothstep(0.98, 1.0, plate_y);
-  // anti-aliased line thickness
-  line = smoothstep(0.0, fwidth(plate_y) * 2.0, line) * 0.12;
-
-  // rivets on top seams
-  float rivet_x = fract(p.x * 18.0);
-  float rivet =
-      smoothstep(0.48, 0.50, rivet_x) * smoothstep(0.52, 0.50, rivet_x);
-  rivet *= step(0.92, plate_y);
-  return line + rivet * 0.25;
+float D_GGX(float NdotH, float roughness) {
+  float a = roughness * roughness;
+  float a2 = a * a;
+  float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
+  return a2 / (PI * denom * denom + 1e-6);
 }
 }
 
 
-// linked ring suggestion (AA)
-float chainmail_rings(vec2 p) {
-  vec2 uv = p * 35.0;
-
-  vec2 g0 = fract(uv) - 0.5;
-  float r0 = length(g0);
-  float fw0 = fwidth(r0) * 1.2;
-  float ring0 = smoothstep(0.30 + fw0, 0.30 - fw0, r0) -
-                smoothstep(0.20 + fw0, 0.20 - fw0, r0);
-
-  vec2 g1 = fract(uv + vec2(0.5, 0.0)) - 0.5;
-  float r1 = length(g1);
-  float fw1 = fwidth(r1) * 1.2;
-  float ring1 = smoothstep(0.30 + fw1, 0.30 - fw1, r1) -
-                smoothstep(0.20 + fw1, 0.20 - fw1, r1);
+float G_SchlickGGX(float NdotX, float roughness) {
+  float r = roughness + 1.0;
+  float k = (r * r) / 8.0;
+  return NdotX / (NdotX * (1.0 - k) + k + 1e-6);
+}
 
 
-  return (ring0 + ring1) * 0.15;
+float G_Smith(float NdotV, float NdotL, float roughness) {
+  return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness);
 }
 }
 
 
-float horse_hide_pattern(vec2 p) {
-  float grain = fbm(p * 80.0) * 0.10;
-  float ripple = sin(p.x * 22.0) * cos(p.y * 28.0) * 0.035;
-  float mottling = smoothstep(0.55, 0.65, fbm(p * 6.0)) * 0.07;
-  return grain + ripple + mottling;
+vec3 F_Schlick(float cosTheta, vec3 F0) {
+  float t = 1.0 - cosTheta;
+  return F0 + (1.0 - F0) * (t * t * t * t * t);
 }
 }
 
 
-// ---------------------
-// microfacet shading
-// ---------------------
-vec3 fresnel_schlick(float cos_theta, vec3 F0) {
-  return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);
+// ============================================================================
+// PATTERNS
+// ============================================================================
+
+float hammerPattern(vec3 pos) {
+  float coarse = fbm(pos.xz * 14.0);
+  float fine = fbm(pos.xy * 30.0 + 5.3);
+  float micro = noise(pos.yz * 50.0);
+  return coarse * 0.45 + fine * 0.35 + micro * 0.2;
 }
 }
 
 
-float D_GGX(float NdotH, float rough) {
-  float a = max(0.001, rough);
-  float a2 = a * a;
-  float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
-  return a2 / max(1e-6, (PI * d * d));
+float scaleArmor(vec2 uv) {
+  vec2 id = floor(uv * 9.0);
+  vec2 f = fract(uv * 9.0);
+  float offset = mod(id.y, 2.0) * 0.5;
+  f.x = fract(f.x + offset);
+  float d = length((f - 0.5) * vec2(1.0, 1.4));
+  float edge = smoothstep(0.52, 0.42, d);
+  float highlight = smoothstep(0.32, 0.22, d);
+  return edge + highlight * 0.5;
 }
 }
 
 
-float G_Smith(float NdotV, float NdotL, float rough) {
-  float r = rough + 1.0;
-  float k = (r * r) / 8.0;
-  float g_v = NdotV / (NdotV * (1.0 - k) + k);
-  float g_l = NdotL / (NdotL * (1.0 - k) + k);
-  return g_v * g_l;
+float chainmailRings(vec2 p) {
+  vec2 uv = p * 28.0;
+  vec2 g0 = fract(uv) - 0.5;
+  float r0 = length(g0);
+  float ring0 = smoothstep(0.32, 0.28, r0) - smoothstep(0.22, 0.18, r0);
+  vec2 g1 = fract(uv + vec2(0.5, 0.0)) - 0.5;
+  float r1 = length(g1);
+  float ring1 = smoothstep(0.32, 0.28, r1) - smoothstep(0.22, 0.18, r1);
+  return (ring0 + ring1) * 0.6;
 }
 }
 
 
-// screen-space bump from a height field h(uv) in world XZ
-vec3 perturb_normal_ws(vec3 N, vec3 world_pos, float h, float scale) {
-  vec3 dpdx = dFdx(world_pos);
-  vec3 dpdy = dFdy(world_pos);
-  vec3 T = normalize(dpdx);
-  vec3 B = normalize(cross(N, T));
-  float hx = dFdx(h);
-  float hy = dFdy(h);
-  vec3 Np = normalize(N - scale * (hx * B + hy * T));
-  return Np;
+float horseHide(vec2 p) {
+  float grain = fbm(p * 60.0) * 0.08;
+  float ripple = sin(p.x * 18.0) * cos(p.y * 22.0) * 0.03;
+  float mottling = smoothstep(0.5, 0.7, fbm(p * 5.0)) * 0.06;
+  return grain + ripple + mottling;
 }
 }
 
 
-// hemisphere ambient (sky/ground)
-vec3 hemilight(vec3 N) {
-  vec3 sky = vec3(0.46, 0.70, 0.82);
-  vec3 ground = vec3(0.22, 0.18, 0.14);
-  float t = saturate(N.y * 0.5 + 0.5);
-  return mix(ground, sky, t) * 0.29;
+float leatherGrain(vec2 p) {
+  float coarse = fbm(p * 12.0) * 0.15;
+  float fine = noise(p * 35.0) * 0.08;
+  return coarse + fine;
 }
 }
 
 
-// ---------------------
-// main
-// ---------------------
+// ============================================================================
+// MAIN
+// ============================================================================
+
 void main() {
 void main() {
-  vec3 base_color = u_color;
-  if (u_useTexture)
-    base_color *= texture(u_texture, v_texCoord).rgb;
+  vec3 baseColor = clamp(u_color, 0.0, 1.0);
+  if (u_useTexture) baseColor *= texture(u_texture, v_texCoord).rgb;
+  baseColor = boostSaturation(baseColor, 0.25);
 
 
   vec3 N = normalize(v_normal);
   vec3 N = normalize(v_normal);
-  vec2 uv = v_worldPos.xz * 5.0;
-
-  float avg = (base_color.r + base_color.g + base_color.b) * (1.0 / 3.0);
-  float hue_span = max(max(base_color.r, base_color.g), base_color.b) -
-                   min(min(base_color.r, base_color.g), base_color.b);
-
-  // Material ID: 0=rider skin, 1=armor, 2=helmet, 3=weapon, 4=shield,
-  // 5=rider clothing, 6=horse hide, 7=horse mane, 8=horse hoof,
-  // 9=saddle leather, 10=bridle, 11=saddle blanket
-  bool is_rider_skin = (u_materialId == 0);
-  bool is_body_armor = (u_materialId == 1);
-  bool is_helmet = (u_materialId == 2);
-  bool is_weapon = (u_materialId == 3);
-  bool is_shield = (u_materialId == 4);
+  vec3 V = normalize(vec3(0.0, 0.5, 1.0));
+  vec3 L = normalize(vec3(0.5, 1.0, 0.4));
+  vec3 H = normalize(L + V);
+
+  bool is_rider_skin     = (u_materialId == 0);
+  bool is_body_armor     = (u_materialId == 1);
+  bool is_helmet         = (u_materialId == 2);
+  bool is_weapon         = (u_materialId == 3);
+  bool is_shield         = (u_materialId == 4);
   bool is_rider_clothing = (u_materialId == 5);
   bool is_rider_clothing = (u_materialId == 5);
-  bool is_horse_hide = (u_materialId == 6);
-  bool is_horse_mane = (u_materialId == 7);
-  bool is_horse_hoof = (u_materialId == 8);
+  bool is_horse_hide     = (u_materialId == 6);
+  bool is_horse_mane     = (u_materialId == 7);
+  bool is_horse_hoof     = (u_materialId == 8);
   bool is_saddle_leather = (u_materialId == 9);
   bool is_saddle_leather = (u_materialId == 9);
-  bool is_bridle = (u_materialId == 10);
+  bool is_bridle         = (u_materialId == 10);
   bool is_saddle_blanket = (u_materialId == 11);
   bool is_saddle_blanket = (u_materialId == 11);
 
 
-  // Material-based detection only (no fallbacks)
-  bool is_brass = is_helmet;
-  bool is_steel = false;
-  bool is_chain = false;
-  bool is_fabric = is_rider_clothing || is_saddle_blanket;
-  bool is_leather = is_saddle_leather || is_bridle;
+  vec3 albedo = baseColor;
+  float metallic = 0.0;
+  float roughness = 0.5;
+
+  // =======================================================================
+  // MATERIALS
+  // =======================================================================
 
 
   if (is_rider_skin) {
   if (is_rider_skin) {
-    // Carthage horse swordsman: light complexion.
-    vec3 target = vec3(0.96, 0.86, 0.76);
-    float tone_noise = fbm(v_worldPos.xz * 3.1) - 0.5;
-    base_color = clamp(target + vec3(tone_noise) * 0.05, 0.0, 1.0);
-    if (u_useTexture) {
-      vec3 tex = texture(u_texture, v_texCoord).rgb;
-      float eye_mask = step(0.25, 1.0 - dot(tex, vec3(0.299, 0.587, 0.114)));
-      base_color = mix(base_color, vec3(0.02), eye_mask); // black eyes
-    }
+    albedo = mix(baseColor, vec3(0.92, 0.76, 0.62), 0.25);
+    albedo = boostSaturation(albedo, 0.15);
+    metallic = 0.0;
+    roughness = 0.55;
   }
   }
 
 
-  // lighting frame
-  vec3 L = normalize(vec3(1.0, 1.2, 1.0));
-  vec3 V = normalize(
-      vec3(0.0, 1.0, 0.5)); // stable view proxy (keeps interface unchanged)
-  vec3 H = normalize(L + V);
+  else if (is_body_armor) {
+    // ========== DARK METAL BODY ARMOR ==========
+    vec2 metalUV = v_texCoord * 6.0;
+    float hammer = hammerPattern(vec3(metalUV, v_worldPos.y));
+    float scales = scaleArmor(metalUV);
+    float chain  = chainmailRings(v_worldPos.xz);
 
 
-  float NdotL = saturate(dot(N, L));
-  float NdotV = saturate(dot(N, V));
-  float NdotH = saturate(dot(N, H));
-  float VdotH = saturate(dot(V, H));
+    vec3 metal = DARK_METAL_COLOR;
+    float patinaNoise = fbm(metalUV * 2.5);
+    vec3 patinaTint = DARK_METAL_COLOR * 1.12;
+    metal = mix(metal, patinaTint, patinaNoise * 0.20);
 
 
-  // wrap diffuse like original (softens lambert)
-  float wrap_amount = (avg > 0.50) ? 0.08 : 0.30;
-  float NdotL_wrap = max(NdotL * (1.0 - wrap_amount) + wrap_amount, 0.12);
+    vec3 teamTint = mix(vec3(1.0), baseColor, 0.25);
+    albedo = metal * teamTint;
 
 
-  // base material params
-  float roughness = 0.5;
-  vec3 F0 = vec3(0.04); // dielectric default
-  float metalness = 0.0;
-  vec3 albedo = base_color;
-
-  // micro details / masks (re-used)
-  float n_small = fbm(uv * 6.0);
-  float n_large = fbm(uv * 2.0);
-  float cavity = 1.0 - (n_large * 0.25 + n_small * 0.15);
-
-  // ---------------------
-  // MATERIAL BRANCHES
-  // ---------------------
-  vec3 col = vec3(0.0);
-  vec3 ambient = hemilight(N) * (0.85 + 0.15 * cavity);
-
-  if (is_body_armor) {
-    // Bronze + chain + linen mix to match infantry look.
-    float brushed =
-        abs(sin(v_worldPos.y * 55.0)) * 0.02 + noise(uv * 28.0) * 0.015;
-    float plates = armor_plates(v_worldPos.xz, v_worldPos.y);
-    float rings = chainmail_rings(v_worldPos.xz);
-    float linen = fbm(uv * 5.0);
-
-    // bump from light hammering
-    float h = fbm(vec2(v_worldPos.y * 18.0, v_worldPos.z * 6.0));
-    N = perturb_normal_ws(N, v_worldPos, h, 0.32);
-
-    vec3 bronze_tint = vec3(0.62, 0.46, 0.20);
-    vec3 steel_tint = vec3(0.68, 0.70, 0.74);
-    vec3 linen_tint = vec3(0.86, 0.80, 0.72);
-    vec3 leather_tint = vec3(0.38, 0.25, 0.15);
-
-    // Treat entire armor mesh as torso to avoid height-based clipping.
-    float torsoBand = 1.0;
-    float skirtBand = 0.0;
-    float mailBlend =
-        clamp(smoothstep(0.15, 0.85, rings + cavity * 0.25), 0.15, 1.0) *
-        torsoBand;
-    float cuirassBlend = torsoBand;
-    float leatherBlend = skirtBand * 0.65;
-    float linenBlend = skirtBand * 0.45;
-
-    vec3 bronze = mix(bronze_tint, base_color, 0.40);
-    vec3 chain_col = mix(steel_tint, base_color, 0.25);
-    vec3 linen_col = mix(linen_tint, base_color, 0.20);
-    vec3 leather_col = mix(leather_tint, base_color, 0.30);
-
-    albedo = bronze;
-    albedo = mix(albedo, chain_col, mailBlend);
-    albedo = mix(albedo, linen_col, linenBlend);
-    albedo = mix(albedo, leather_col, leatherBlend);
-
-    // bias toward brighter metal luma
-    float armor_luma = dot(albedo, vec3(0.299, 0.587, 0.114));
-    albedo = mix(albedo, albedo * 1.20, smoothstep(0.30, 0.65, armor_luma));
-
-    roughness = 0.32 + brushed * 1.2;
-    roughness = clamp(roughness, 0.18, 0.55);
-    F0 = mix(vec3(0.74), albedo, 0.25);
-
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * mix(vec3(1.0), albedo, 0.25);
-    col += NdotL_wrap * (spec * 1.35);
-    col += vec3(plates) * 0.35 + vec3(rings * 0.25) + vec3(linen * linenBlend);
-
-  } else if (is_horse_hide) {
-    // Horses: unified natural coat, fully matte, no sparkle.
-    vec3 coat = vec3(0.36, 0.32, 0.28);
-    float hide_tex = horse_hide_pattern(v_worldPos.xz);
-    float grain = fbm(v_worldPos.xz * 18.0 + v_worldPos.y * 2.5);
-    albedo = coat * (1.0 + hide_tex * 0.06 - grain * 0.04);
-
-    roughness = 0.75;
-    F0 = vec3(0.02);
-    metalness = 0.0;
-
-    // Slight normal bump from hair grain without spec pop.
-    float h = fbm(v_worldPos.xz * 22.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.18);
-
-    col += ambient * albedo;
-    col += NdotL_wrap * albedo * 0.9;
-
-  } else if (is_horse_mane) {
-    // Mane: dark, matte.
-    float strand = sin(uv.x * 140.0) * 0.2 + noise(uv * 120.0) * 0.15;
-    float clump = noise(uv * 15.0) * 0.4;
-    float frizz = fbm(uv * 40.0) * 0.15;
-
-    float h = fbm(v_worldPos.xz * 25.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.18);
-
-    albedo = vec3(0.08, 0.07, 0.07) *
-             (1.0 + strand * 0.02 + clump * 0.02 + frizz * 0.02);
-
-    roughness = 0.70;
-    F0 = vec3(0.02);
-    metalness = 0.0;
-
-    col += ambient * albedo;
-    col += NdotL_wrap * albedo * 0.85;
-
-  } else if (is_steel) {
-    float brushed =
-        abs(sin(v_worldPos.y * 95.0)) * 0.02 + noise(uv * 35.0) * 0.015;
-    float dents = noise(uv * 8.0) * 0.03;
-    float plates = armor_plates(v_worldPos.xz, v_worldPos.y);
-
-    // bump from brushing
-    float h = fbm(vec2(v_worldPos.y * 25.0, v_worldPos.z * 6.0));
-    N = perturb_normal_ws(N, v_worldPos, h, 0.35);
-
-    // steel-like params
-    metalness = 1.0;
-    F0 = vec3(0.92);
-    roughness = 0.28 + brushed * 2.0 + dents * 0.6;
-    roughness = clamp(roughness, 0.15, 0.55);
-
-    // base tint & sky reflection lift
-    albedo = mix(vec3(0.60), base_color, 0.25);
-    float sky_refl = (N.y * 0.5 + 0.5) * 0.10;
-
-    // microfacet spec only for metals
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0 * albedo); // slight tint
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * 0.3; // metals rely more on spec
-    col += NdotL_wrap * spec * 1.5;
-    col += vec3(plates) + vec3(sky_refl) - vec3(dents * 0.25) + vec3(brushed);
-
-  } else if (is_brass) {
-    float brass_noise = noise(uv * 22.0) * 0.02;
-    float patina = fbm(uv * 4.0) * 0.12; // larger-scale patina
-
-    // bump from subtle hammering
-    float h = fbm(uv * 18.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.30);
-
-    metalness = 1.0;
-    vec3 brass_tint = vec3(0.94, 0.78, 0.45);
-    F0 = mix(brass_tint, base_color, 0.5);
-    roughness = clamp(0.32 + patina * 0.45, 0.18, 0.75);
-
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * 0.25;
-    col += NdotL_wrap * spec * 1.35;
-    col += vec3(brass_noise) - vec3(patina * 0.35);
-
-  } else if (is_chain) {
-    float rings = chainmail_rings(v_worldPos.xz);
-    float ring_hi = noise(uv * 30.0) * 0.10;
-
-    // small pitted bump
-    float h = fbm(uv * 35.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.25);
-
-    metalness = 1.0;
-    F0 = vec3(0.86);
-    roughness = 0.35;
-
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 F = fresnel_schlick(VdotH, F0);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    col += ambient * 0.25;
-    col += NdotL_wrap * (spec * (1.2 + rings)) + vec3(ring_hi);
-    // slight diffuse damping to keep chainmail darker in cavities
-    col *= (0.95 - 0.10 * (1.0 - cavity));
-
-  } else if (is_fabric) {
-    float weave_x = sin(v_worldPos.x * 70.0);
-    float weave_z = sin(v_worldPos.z * 70.0);
-    float weave = weave_x * weave_z * 0.04;
-    float embroidery = fbm(uv * 6.0) * 0.08;
-
-    float h = fbm(uv * 22.0) * 0.7 + weave * 0.6;
-    N = perturb_normal_ws(N, v_worldPos, h, 0.35);
-
-    roughness = 0.78;
-    F0 = vec3(0.035);
-    metalness = 0.0;
-
-    vec3 F = fresnel_schlick(VdotH, F0);
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    float sheen = pow(1.0 - NdotV, 6.0) * 0.10;
-    albedo *= 1.0 + fbm(uv * 5.0) * 0.10 - 0.05;
-
-    col += ambient * albedo;
-    col += NdotL_wrap * (albedo * (1.0 - F) + spec * 0.3) +
-           vec3(weave + embroidery + sheen);
-
-  } else { // leather / hooves fallback
-    float grain = fbm(uv * 10.0) * 0.15;
-    float wear = fbm(uv * 3.0) * 0.12;
-
-    float h = fbm(uv * 18.0);
-    N = perturb_normal_ws(N, v_worldPos, h, 0.28);
-
-    roughness = 0.58 - wear * 0.15;
-    F0 = vec3(0.038);
-    metalness = 0.0;
-
-    vec3 F = fresnel_schlick(VdotH, F0);
-    float D = D_GGX(saturate(dot(N, H)), roughness);
-    float G = G_Smith(saturate(dot(N, V)), saturate(dot(N, L)), roughness);
-    vec3 spec = (D * G) * F / max(1e-5, 4.0 * NdotV * NdotL);
-
-    float sheen = pow(1.0 - NdotV, 4.0) * 0.06;
-
-    albedo *= (1.0 + grain - 0.06 + wear * 0.05);
-    col += ambient * albedo;
-    col += NdotL_wrap * (albedo * (1.0 - F)) + spec * 0.4 + vec3(sheen);
+    albedo *= 0.9 + hammer * 0.25;
+    albedo = mix(albedo, albedo * 1.20, scales * 0.45);
+    albedo += vec3(0.05) * chain;
+
+    metallic = 1.0;
+    roughness = clamp(0.22 + hammer * 0.08 - scales * 0.08, 0.10, 0.33);
+  }
+
+  else if (is_helmet) {
+    // ========== DARK METAL HELMET ==========
+    vec2 metalUV = v_texCoord * 8.0;
+    float hammer = hammerPattern(vec3(metalUV, v_worldPos.y));
+    float scales = scaleArmor(metalUV * 1.1);
+
+    vec3 metal = DARK_METAL_COLOR;
+    float patinaNoise = fbm(metalUV * 3.0);
+    vec3 patinaTint = DARK_METAL_COLOR * 1.10;
+    metal = mix(metal, patinaTint, patinaNoise * 0.18);
+
+    float crest = smoothstep(0.8, 0.9, v_worldPos.y);
+    crest *= smoothstep(0.45, 0.30, abs(v_worldPos.x));
+    vec3 crestColor = boostSaturation(baseColor, 0.6) * 1.3;
+
+    albedo = mix(metal, crestColor, crest * 0.5);
+
+    albedo *= 0.9 + hammer * 0.25;
+    albedo = mix(albedo, albedo * 1.18, scales * 0.50);
+
+    metallic = 1.0;
+    roughness = clamp(0.18 + hammer * 0.08 - scales * 0.08, 0.08, 0.28);
+  }
+
+  else if (is_weapon) {
+    float h = v_worldPos.y;
+    float blade = smoothstep(0.25, 0.48, h);
+    float guard = smoothstep(0.15, 0.25, h) * (1.0 - blade);
+    float polish = fbm(v_worldPos.xy * 40.0);
+
+    vec3 handle = boostSaturation(baseColor * 0.85, 0.3);
+    handle += vec3(0.05) * polish;
+
+    vec3 guardCol = mix(BRONZE_BASE_COLOR, baseColor * 1.1, 0.3);
+    guardCol = boostSaturation(guardCol, 0.35);
+
+    vec3 steel = vec3(0.88, 0.90, 0.95);
+    steel += vec3(0.06) * polish;
+    steel = mix(steel, baseColor * 0.35 + vec3(0.55), 0.12);
+
+    albedo = mix(handle, guardCol, guard);
+    albedo = mix(albedo, steel, blade);
+
+    metallic = mix(0.1, 1.0, blade + guard * 0.9);
+    roughness = mix(0.5, 0.04, blade);
   }
   }
 
 
-  col = mix(col, vec3(0.32, 0.60, 0.66),
-            saturate((base_color.g + base_color.b) * 0.4) * 0.12);
-  col = saturate(col);
-  FragColor = vec4(col, u_alpha);
+  else if (is_shield) {
+    float dist = length(v_worldPos.xz);
+    float boss = smoothstep(0.28, 0.0, dist);
+    float rings = sin(dist * 18.0) * 0.5 + 0.5;
+    rings = smoothstep(0.35, 0.65, rings) * (1.0 - boss);
+
+    vec3 faceColor =
+        boostSaturation(mix(SHIELD_BROWN_COLOR, baseColor, 0.12), 0.35);
+    vec3 bronze = BRONZE_BASE_COLOR;
+    vec3 metalMix = mix(faceColor, bronze, boss + rings * 0.6);
+
+    albedo = mix(faceColor, metalMix, boss + rings * 0.5);
+
+    metallic = mix(0.25, 1.0, boss);
+    roughness = mix(0.5, 0.10, boss);
+  }
+
+  else if (is_rider_clothing || is_saddle_blanket) {
+    float weave = sin(v_worldPos.x * 55.0) * sin(v_worldPos.z * 55.0) * 0.04;
+    float texture_var = fbm(v_worldPos.xz * 8.0);
+
+    albedo = boostSaturation(baseColor, 0.4);
+    albedo *= 1.0 + weave + texture_var * 0.12;
+
+    metallic = 0.0;
+    roughness = 0.7;
+  }
+
+  else if (is_horse_hide) {
+    float hide = horseHide(v_worldPos.xz);
+    float grain = fbm(v_worldPos.xz * 22.0);
+
+    albedo = boostSaturation(baseColor, 0.2);
+    albedo *= 1.0 + hide * 0.08 - grain * 0.05;
+
+    metallic = 0.0;
+    roughness = 0.65;
+
+    float sheen = pow(1.0 - max(dot(N, V), 0.0), 4.0) * 0.15;
+    albedo += vec3(sheen) * 0.5;
+  }
+
+  else if (is_horse_mane) {
+    float strand =
+        sin(v_worldPos.x * 120.0) * 0.15 + noise(v_worldPos.xy * 100.0) * 0.1;
+
+    albedo = baseColor * 0.4;
+    albedo = boostSaturation(albedo, 0.15);
+    albedo *= 1.0 + strand * 0.08;
+
+    metallic = 0.0;
+    roughness = 0.5;
+  }
+
+  else if (is_horse_hoof) {
+    float grain = fbm(v_worldPos.xy * 15.0);
+    albedo = baseColor * 0.35;
+    albedo *= 1.0 + grain * 0.1;
+    metallic = 0.0;
+    roughness = 0.45;
+  }
+
+  else if (is_saddle_leather || is_bridle) {
+    float grain = leatherGrain(v_worldPos.xz);
+    float wear = fbm(v_worldPos.xz * 4.0) * 0.08;
+
+    albedo = boostSaturation(baseColor, 0.3);
+    albedo *= 1.0 + grain * 0.15 - wear;
+
+    metallic = 0.0;
+    roughness = 0.5 - wear * 0.1;
+
+    float sheen = pow(1.0 - max(dot(N, V), 0.0), 5.0) * 0.12;
+    albedo += vec3(sheen) * baseColor;
+  }
+
+  else {
+    albedo = boostSaturation(baseColor, 0.2);
+    metallic = 0.0;
+    roughness = 0.6;
+  }
+
+  // =======================================================================
+  // PBR LIGHTING
+  // =======================================================================
+
+  float NdotL = max(dot(N, L), 0.0);
+  float NdotV = max(dot(N, V), 0.001);
+  float NdotH = max(dot(N, H), 0.0);
+  float VdotH = max(dot(V, H), 0.0);
+
+  vec3 F0 = mix(vec3(0.04), albedo, metallic);
+
+  float D = D_GGX(NdotH, max(roughness, 0.01));
+  float G = G_Smith(NdotV, NdotL, roughness);
+  vec3 F = F_Schlick(VdotH, F0);
+
+  vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001);
+  vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
+  vec3 diffuse = kD * albedo / PI;
+
+  vec3 color = (diffuse + specular * 2.2) * NdotL * 2.1;
+
+  // Enhanced metallic shine
+  if (metallic > 0.5) {
+    vec3 R = reflect(-V, N);
+
+    float specPower = 240.0 / (roughness * roughness + 0.001);
+    specPower = min(specPower, 2000.0);
+    float mirrorSpec = pow(max(dot(R, L), 0.0), specPower);
+    color += albedo * mirrorSpec * 1.8;
+
+    float hotspot = pow(NdotH, 500.0);
+    color += vec3(1.2) * hotspot * (1.0 - roughness * 1.8);
+
+    float softSpec = pow(max(dot(R, L), 0.0), 28.0);
+    color += albedo * softSpec * 0.6;
+
+    vec3 skyCol    = vec3(0.55, 0.65, 0.85);
+    vec3 groundCol = vec3(0.38, 0.32, 0.25);
+    float upFace   = R.y * 0.5 + 0.5;
+    vec3 envReflect = mix(groundCol, skyCol, upFace);
+    color += envReflect * (1.0 - roughness) * 0.5;
+  }
+
+  vec3 ambient = albedo * 0.42;
+
+  vec3 skyAmbient    = vec3(0.45, 0.55, 0.70);
+  vec3 groundAmbient = vec3(0.30, 0.25, 0.20);
+  float hemi = N.y * 0.5 + 0.5;
+
+  ambient *= mix(groundAmbient, skyAmbient, hemi) * 1.4;
+
+  float rim = pow(1.0 - NdotV, 3.5);
+  ambient += vec3(0.35, 0.40, 0.50) * rim * 0.32;
+
+  if (metallic > 0.5) {
+    ambient += albedo * 0.16 * (1.0 - roughness);
+  }
+
+  color += ambient;
+
+  color = color / (color + vec3(0.55));
+  color = pow(color, vec3(0.9));
+
+  FragColor = vec4(saturate3(color), u_alpha);
 }
 }

+ 291 - 83
assets/shaders/spearman_carthage.frag

@@ -1,5 +1,9 @@
 #version 330 core
 #version 330 core
 
 
+// ============================================================================
+// CARTHAGINIAN SPEARMAN - Rich Leather Armor with Battle-Worn Character
+// ============================================================================
+
 in vec3 v_worldNormal;
 in vec3 v_worldNormal;
 in vec3 v_worldPos;
 in vec3 v_worldPos;
 in vec2 v_texCoord;
 in vec2 v_texCoord;
@@ -12,128 +16,332 @@ uniform int u_materialId;
 
 
 out vec4 FragColor;
 out vec4 FragColor;
 
 
-// Simple PBR-ish helpers
-float saturate(float v) { return clamp(v, 0.0, 1.0); }
-vec3 saturate(vec3 v) { return clamp(v, 0.0, 1.0); }
+// ============================================================================
+// CONSTANTS & HELPERS
+// ============================================================================
+
+const float PI = 3.14159265359;
+const vec3 LEATHER_BROWN = vec3(0.36, 0.22, 0.10); // *** fixed realistic brown
+
+float saturate(float x) { return clamp(x, 0.0, 1.0); }
+vec3 saturate3(vec3 x) { return clamp(x, 0.0, 1.0); }
+
+// Boost saturation
+vec3 boostSaturation(vec3 color, float amount) {
+  float grey = dot(color, vec3(0.299, 0.587, 0.114));
+  return mix(vec3(grey), color, 1.0 + amount);
+}
 
 
-float hash12(vec2 p) {
+float hash2(vec2 p) {
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   p3 += dot(p3, p3.yzx + 33.33);
   p3 += dot(p3, p3.yzx + 33.33);
   return fract((p3.x + p3.y) * p3.z);
   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 = hash2(i);
+  float b = hash2(i + vec2(1.0, 0.0));
+  float c = hash2(i + vec2(0.0, 1.0));
+  float d = hash2(i + vec2(1.0, 1.0));
+  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
 float fbm(vec2 p) {
 float fbm(vec2 p) {
   float v = 0.0;
   float v = 0.0;
   float a = 0.5;
   float a = 0.5;
-  for (int i = 0; i < 4; ++i) {
-    v += a * hash12(p);
-    p *= 2.0;
-    a *= 0.55;
+  mat2 rot = mat2(0.87, 0.50, -0.50, 0.87);
+  for (int i = 0; i < 5; ++i) {
+    v += a * noise(p);
+    p = rot * p * 2.0 + vec2(100.0);
+    a *= 0.5;
   }
   }
   return v;
   return v;
 }
 }
 
 
-float D_GGX(float NdotH, float a) {
-  float a2 = a * a;
-  float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
-  return a2 / max(3.14159 * d * d, 1e-5);
-}
+// ============================================================================
+// PBR FUNCTIONS
+// ============================================================================
 
 
-float geometry_schlick(float NdotX, float k) {
-  return NdotX / max(NdotX * (1.0 - k) + k, 1e-5);
+float D_GGX(float NdotH, float roughness) {
+  float a = roughness * roughness;
+  float a2 = a * a;
+  float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
+  return a2 / (PI * denom * denom + 1e-6);
 }
 }
 
 
-float geometry_smith(float NdotV, float NdotL, float roughness) {
+float G_SchlickGGX(float NdotX, float roughness) {
   float r = roughness + 1.0;
   float r = roughness + 1.0;
   float k = (r * r) / 8.0;
   float k = (r * r) / 8.0;
-  return geometry_schlick(NdotV, k) * geometry_schlick(NdotL, k);
+  return NdotX / (NdotX * (1.0 - k) + k + 1e-6);
 }
 }
 
 
-vec3 fresnel_schlick(float cos_theta, vec3 F0) {
-  return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);
+float G_Smith(float NdotV, float NdotL, float roughness) {
+  return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness);
 }
 }
 
 
+vec3 F_Schlick(float cosTheta, vec3 F0) {
+  float t = 1.0 - cosTheta;
+  float t5 = t * t * t * t * t;
+  return F0 + (1.0 - F0) * t5;
+}
+
+// ============================================================================
+// LEATHER TEXTURE PATTERNS
+// ============================================================================
+
+// Natural leather grain - irregular pores and creases
+float leatherGrain(vec2 uv, float scale) {
+  float coarse = fbm(uv * scale);
+  float medium = fbm(uv * scale * 2.5 + 7.3);
+  float fine = noise(uv * scale * 6.0);
+  
+  // Create pore-like depressions
+  float pores = smoothstep(0.55, 0.65, noise(uv * scale * 4.0));
+  
+  return coarse * 0.4 + medium * 0.35 + fine * 0.15 + pores * 0.1;
+}
+
+// Stitching pattern for leather seams
+float stitchPattern(vec2 uv, float spacing) {
+  float stitch = fract(uv.y * spacing);
+  stitch = smoothstep(0.4, 0.5, stitch) * smoothstep(0.6, 0.5, stitch);
+  
+  // Only show stitches along certain lines
+  float seamLine = smoothstep(0.48, 0.50, fract(uv.x * 3.0)) * 
+                   smoothstep(0.52, 0.50, fract(uv.x * 3.0));
+  return stitch * seamLine;
+}
+
+// Battle wear - scratches, scuffs, worn edges
+float battleWear(vec3 pos) {
+  float scratch1 = smoothstep(0.7, 0.75, noise(pos.xy * 25.0 + pos.z * 5.0));
+  float scratch2 = smoothstep(0.72, 0.77, noise(pos.zy * 20.0 - 3.7));
+  float scuff = fbm(pos.xz * 8.0) * fbm(pos.xy * 12.0);
+  scuff = smoothstep(0.3, 0.5, scuff);
+  float edgeWear = smoothstep(0.4, 0.8, pos.y) * fbm(pos.xz * 6.0);
+  return (scratch1 + scratch2) * 0.3 + scuff * 0.4 + edgeWear * 0.3;
+}
+
+// Oiled leather sheen pattern
+float oilSheen(vec3 pos, vec3 N, vec3 V) {
+  float facing = 1.0 - abs(dot(N, V));
+  float variation = fbm(pos.xz * 15.0) * 0.5 + 0.5;
+  return facing * facing * variation;
+}
+
+// ============================================================================
+// MAIN
+// ============================================================================
+
 void main() {
 void main() {
-  vec3 base = u_color;
+  // Get and enhance base color
+  vec3 baseColor = clamp(u_color, 0.0, 1.0);
   if (u_useTexture) {
   if (u_useTexture) {
-    base *= texture(u_texture, v_texCoord).rgb;
+    baseColor *= texture(u_texture, v_texCoord).rgb;
   }
   }
-
-  // Material layout: 0=skin, 1=armor, 2=helmet, 3=weapon, 4=shield
-  bool is_skin = (u_materialId == 0);
-  bool is_armor = (u_materialId == 1);
+  baseColor = boostSaturation(baseColor, 0.25);
+  
+  bool is_skin   = (u_materialId == 0);
+  bool is_armor  = (u_materialId == 1);
   bool is_helmet = (u_materialId == 2);
   bool is_helmet = (u_materialId == 2);
   bool is_weapon = (u_materialId == 3);
   bool is_weapon = (u_materialId == 3);
-
+  bool is_shield = (u_materialId == 4);
+  
   vec3 N = normalize(v_worldNormal);
   vec3 N = normalize(v_worldNormal);
   vec3 V = normalize(vec3(0.0, 0.0, 1.0));
   vec3 V = normalize(vec3(0.0, 0.0, 1.0));
   vec3 L = normalize(vec3(0.5, 1.0, 0.4));
   vec3 L = normalize(vec3(0.5, 1.0, 0.4));
   vec3 H = normalize(L + V);
   vec3 H = normalize(L + V);
-
-  // Lightweight leather armor for spearmen.
+  
+  vec3 albedo = baseColor;
   float metallic = 0.0;
   float metallic = 0.0;
-  float roughness = 0.55;
-  vec3 albedo = base;
-
+  float roughness = 0.5;
+  float sheen = 0.0;
+  
+  // ========== MATERIAL SETUP ==========
+  
   if (is_skin) {
   if (is_skin) {
-    albedo = mix(base, vec3(0.84, 0.72, 0.62), 0.35);
+    // Warm Mediterranean skin
+    albedo = mix(baseColor, vec3(0.88, 0.72, 0.58), 0.25);
+    albedo = boostSaturation(albedo, 0.15);
     metallic = 0.0;
     metallic = 0.0;
-    roughness = 0.6;
-    // Jagged leather pants for lower body
-    float pants_mask = 1.0 - smoothstep(0.38, 0.60, v_worldPos.y);
-    float jag = fbm(v_worldPos.xz * 15.0 + v_worldPos.y * 3.0);
-    vec3 leather = vec3(0.42, 0.30, 0.20) - vec3(0.04) * jag;
-    albedo = mix(albedo, leather, pants_mask);
-    roughness = mix(roughness, 0.55, pants_mask);
+    roughness = 0.55;
+    
+    // Leather leg wrappings
+    float legWrap = 1.0 - smoothstep(0.30, 0.55, v_worldPos.y);
+    float wrapGrain = leatherGrain(v_worldPos.xz, 18.0);
+    vec3 wrapColor = baseColor * 0.7;
+    wrapColor = boostSaturation(wrapColor, 0.2);
+    wrapColor -= vec3(0.06) * wrapGrain;
+    
+    float bands = sin(v_worldPos.y * 25.0) * 0.5 + 0.5;
+    bands = smoothstep(0.3, 0.7, bands);
+    wrapColor *= 0.9 + bands * 0.15;
+    
+    albedo = mix(albedo, wrapColor, legWrap);
+    roughness = mix(roughness, 0.48, legWrap);
+    
   } else if (is_armor || is_helmet) {
   } else if (is_armor || is_helmet) {
-    vec3 leather_tint = vec3(0.46, 0.32, 0.20);
-    // Add grain and scars to break up flatness.
-    float grain = fbm(v_worldPos.xy * 14.0);
-    float scar = fbm(v_worldPos.zy * 8.0 + vec2(1.7, 2.9));
-    float wear = grain * 0.45 + scar * 0.30;
-    albedo = mix(leather_tint, base, 0.4);
-    albedo -= vec3(0.06) * wear;
-    metallic = 0.05;
-    roughness = mix(0.46, 0.65, wear);
+    // ====== RICH LEATHER ARMOR ======
+    
+    // *** Use UVs so grain is visible and tiles nicely
+    vec2 leatherUV = v_texCoord * 6.0; 
+    
+    float grain     = leatherGrain(leatherUV, 8.0);          // *** UV-based grain
+    float stitches  = stitchPattern(leatherUV, 18.0);        // *** UV-based seams
+    float wear      = battleWear(v_worldPos);
+    float oil       = oilSheen(v_worldPos, N, V);
+    
+    // *** Force a realistic brown leather base, only slightly tinted by u_color
+    vec3 tint = mix(vec3(1.0), baseColor, 0.25);
+    albedo = LEATHER_BROWN * tint;                           // *** core leather color
+    albedo = boostSaturation(albedo, 0.25);
+    
+    // *** Stronger grain contrast
+    float grainStrength = 0.55;
+    albedo = mix(albedo * 0.7, albedo * 1.3, grain * grainStrength);
+    
+    // Darker in creases, lighter on raised areas
+    float depth = fbm(leatherUV * 3.0);                      // *** use UV for consistency
+    albedo = mix(albedo * 0.7, albedo * 1.15, depth);
+    
+    // Worn areas are lighter/more faded + slightly desaturated
+    vec3 wornColor = albedo * 1.25 + vec3(0.07, 0.05, 0.03);
+    wornColor = mix(wornColor, vec3(dot(wornColor, vec3(0.3,0.59,0.11))), 0.15);
+    albedo = mix(albedo, wornColor, wear * 0.65);
+    
+    // Stitching detail (darker seams)
+    albedo = mix(albedo, albedo * 0.55, stitches * 0.9);
+    
+    // Leather is not metallic but quite rough
+    metallic = 0.0;
+    
+    // *** Full leather roughness, only slightly reduced where oiled
+    float baseRoughness = 0.85;
+    roughness = baseRoughness - oil * 0.25 + grain * 0.05;
+    roughness = clamp(roughness, 0.65, 0.9);                 // *** always quite rough
+    
+    // Sheen mainly from oil
+    sheen = oil * 0.6;
+    
+    // Bronze studs/reinforcements on helmet
+    if (is_helmet) {
+      float studs = smoothstep(0.75, 0.8, noise(v_worldPos.xz * 12.0));
+      vec3 bronzeColor = vec3(0.85, 0.65, 0.35);
+      albedo = mix(albedo, bronzeColor, studs * 0.8);
+      metallic = mix(metallic, 0.9, studs);
+      roughness = mix(roughness, 0.25, studs);
+    }
+    
   } else if (is_weapon) {
   } else if (is_weapon) {
-    // Spear: metal head + wooden shaft based on height.
-    float tip = smoothstep(0.45, 0.65, v_worldPos.y);
-    float shaft = 1.0 - tip;
-    vec3 wood = vec3(0.52, 0.36, 0.22);
-    vec3 steel = vec3(0.74, 0.76, 0.80);
-
-    float wood_grain = fbm(v_worldPos.xz * 18.0 + v_worldPos.y * 4.0);
-    float steel_brush = fbm(v_worldPos.xy * 32.0);
-
-    vec3 wood_color = mix(wood, base, 0.35);
-    wood_color += vec3(0.05) * wood_grain;
-
-    vec3 steel_color = mix(steel, base, 0.25);
-    steel_color += vec3(0.06) * steel_brush;
-
-    albedo = mix(wood_color, steel_color, tip);
-    metallic = mix(0.05, 0.85, tip);
-    roughness = mix(0.50, 0.24, tip);
+    // Spear with wooden shaft and iron tip
+    float h = v_worldPos.y;
+    float tip = smoothstep(0.40, 0.55, h);
+    float binding = smoothstep(0.35, 0.42, h) * (1.0 - tip);
+    
+    vec3 woodColor = boostSaturation(baseColor * 0.85, 0.3);
+    float woodGrain = fbm(vec2(v_worldPos.x * 8.0, v_worldPos.y * 35.0));
+    woodColor *= 0.85 + woodGrain * 0.3;
+    float woodSheen = pow(max(dot(reflect(-V, N), L), 0.0), 16.0);
+    
+    vec3 bindColor = baseColor * 0.6;
+    bindColor = boostSaturation(bindColor, 0.2);
+    float bindGrain = leatherGrain(v_worldPos.xy, 25.0);
+    bindColor *= 0.9 + bindGrain * 0.2;
+    
+    vec3 ironColor = vec3(0.55, 0.55, 0.58);
+    float ironBrush = fbm(v_worldPos.xy * 40.0);
+    ironColor += vec3(0.08) * ironBrush;
+    ironColor = mix(ironColor, baseColor * 0.3 + vec3(0.4), 0.15);
+    
+    albedo = woodColor;
+    albedo = mix(albedo, bindColor, binding);
+    albedo = mix(albedo, ironColor, tip);
+    
+    metallic = mix(0.0, 0.85, tip);
+    roughness = mix(0.38, 0.28, tip);
+    roughness = mix(roughness, 0.5, binding);
+    sheen = woodSheen * (1.0 - tip) * (1.0 - binding) * 0.3;
+    
+  } else if (is_shield) {
+    // Leather-covered wooden shield with bronze boss
+    float dist = length(v_worldPos.xz);
+    float boss = smoothstep(0.18, 0.0, dist);
+    float bossRim = smoothstep(0.22, 0.18, dist) * (1.0 - boss);
+    
+    float shieldGrain = leatherGrain(v_worldPos.xz, 10.0);
+    float shieldWear = battleWear(v_worldPos);
+    
+    albedo = boostSaturation(baseColor, 0.3);
+    albedo *= 0.9 + shieldGrain * 0.2;
+    albedo = mix(albedo, albedo * 1.2, shieldWear * 0.3);
+    
+    vec3 bronzeColor = vec3(0.88, 0.68, 0.38);
+    bronzeColor = boostSaturation(bronzeColor, 0.25);
+    albedo = mix(albedo, bronzeColor, boss + bossRim * 0.8);
+    
+    metallic = mix(0.0, 0.9, boss + bossRim * 0.7);
+    roughness = mix(0.45, 0.22, boss);
+    
+  } else {
+    albedo = boostSaturation(baseColor, 0.2);
+    metallic = 0.0;
+    roughness = 0.55;
   }
   }
-
+  
+  // ========== PBR LIGHTING ==========
+  
   float NdotL = max(dot(N, L), 0.0);
   float NdotL = max(dot(N, L), 0.0);
-  float NdotV = max(dot(N, V), 0.0);
+  float NdotV = max(dot(N, V), 0.001);
   float NdotH = max(dot(N, H), 0.0);
   float NdotH = max(dot(N, H), 0.0);
   float VdotH = max(dot(V, H), 0.0);
   float VdotH = max(dot(V, H), 0.0);
-
-  float a = max(0.02, roughness * roughness);
-  float D = D_GGX(NdotH, a);
-  float G = geometry_smith(NdotV, NdotL, roughness);
+  
   vec3 F0 = mix(vec3(0.04), albedo, metallic);
   vec3 F0 = mix(vec3(0.04), albedo, metallic);
-  vec3 F = fresnel_schlick(VdotH, F0);
-  vec3 spec = (D * G * F) / max(4.0 * NdotL * NdotV + 1e-5, 1e-5);
-
-  vec3 kd = (vec3(1.0) - F) * (1.0 - metallic);
-  vec3 diffuse = kd * albedo / 3.14159;
-
-  float rim = pow(1.0 - max(dot(N, V), 0.0), 3.0);
-  vec3 ambient = albedo * 0.34 + vec3(0.04) * rim;
-  vec3 color = ambient + (diffuse + spec) * NdotL;
-
-  FragColor = vec4(saturate(color), u_alpha);
+  
+  float D = D_GGX(NdotH, max(roughness, 0.01));
+  float G = G_Smith(NdotV, NdotL, roughness);
+  vec3 F = F_Schlick(VdotH, F0);
+  vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001);
+  
+  vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
+  vec3 diffuse = kD * albedo / PI;
+  
+  vec3 color = (diffuse + specular * 1.8) * NdotL * 2.0;
+  
+  // ====== LEATHER SHEEN EFFECT ======
+  if (sheen > 0.01) {
+    vec3 R = reflect(-V, N);
+    float sheenSpec = pow(max(dot(R, L), 0.0), 12.0);
+    color += albedo * sheenSpec * sheen * 1.5;
+    float edgeSheen = pow(1.0 - NdotV, 3.0);
+    color += vec3(0.95, 0.90, 0.80) * edgeSheen * sheen * 0.4;
+  }
+  
+  // ====== METALLIC SHINE (for bronze parts) ======
+  if (metallic > 0.5) {
+    vec3 R = reflect(-V, N);
+    float specPower = 128.0 / (roughness * roughness + 0.001);
+    specPower = min(specPower, 512.0);
+    float mirrorSpec = pow(max(dot(R, L), 0.0), specPower);
+    color += albedo * mirrorSpec * 1.2;
+    float hotspot = pow(NdotH, 256.0);
+    color += vec3(1.0) * hotspot * (1.0 - roughness);
+  }
+  
+  // ====== WARM AMBIENT ======
+  vec3 ambient = albedo * 0.38;
+  float sss = pow(saturate(dot(-N, L)), 2.0) * 0.15;
+  ambient += albedo * vec3(1.1, 0.9, 0.7) * sss;
+  float rim = pow(1.0 - NdotV, 3.5);
+  ambient += vec3(0.35, 0.32, 0.28) * rim * 0.25;
+  
+  color += ambient;
+  
+  // Tone mapping
+  color = color / (color + vec3(0.55));
+  color = pow(color, vec3(0.94));
+  
+  FragColor = vec4(saturate3(color), u_alpha);
 }
 }

+ 258 - 93
assets/shaders/swordsman_carthage.frag

@@ -1,5 +1,9 @@
 #version 330 core
 #version 330 core
 
 
+// ============================================================================
+// CARTHAGINIAN SWORDSMAN - Modified: Armor & Helmet now dark metallic
+// ============================================================================
+
 in vec3 v_worldNormal;
 in vec3 v_worldNormal;
 in vec3 v_worldPos;
 in vec3 v_worldPos;
 in vec2 v_texCoord;
 in vec2 v_texCoord;
@@ -12,139 +16,300 @@ uniform int u_materialId;
 
 
 out vec4 FragColor;
 out vec4 FragColor;
 
 
-// Simple PBR-ish helpers
-float saturate(float v) { return clamp(v, 0.0, 1.0); }
-vec3 saturate(vec3 v) { return clamp(v, 0.0, 1.0); }
+// ============================================================================
+// CONSTANTS & HELPERS
+// ============================================================================
+
+const float PI = 3.14159265359;
+
+// ORIGINAL bronze kept for shield/weapon/etc
+const vec3 BRONZE_BASE_COLOR = vec3(0.86, 0.66, 0.36);
+
+// NEW — dark metal for armor + helmet
+const vec3 DARK_METAL_COLOR = vec3(0.14, 0.14, 0.16);
+// Dark brown for the Carthaginian shield
+const vec3 SHIELD_BROWN_COLOR = vec3(0.18, 0.09, 0.035);
+
+float saturate(float x) { return clamp(x, 0.0, 1.0); }
+vec3 saturate3(vec3 x) { return clamp(x, 0.0, 1.0); }
+
+vec3 boostSaturation(vec3 color, float amount) {
+  float grey = dot(color, vec3(0.299, 0.587, 0.114));
+  return mix(vec3(grey), color, 1.0 + amount);
+}
 
 
-float hash12(vec2 p) {
+float hash2(vec2 p) {
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   vec3 p3 = fract(vec3(p.xyx) * 0.1031);
   p3 += dot(p3, p3.yzx + 33.33);
   p3 += dot(p3, p3.yzx + 33.33);
   return fract((p3.x + p3.y) * p3.z);
   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 = hash2(i);
+  float b = hash2(i + vec2(1.0, 0.0));
+  float c = hash2(i + vec2(0.0, 1.0));
+  float d = hash2(i + vec2(1.0, 1.0));
+  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
 float fbm(vec2 p) {
 float fbm(vec2 p) {
   float v = 0.0;
   float v = 0.0;
   float a = 0.5;
   float a = 0.5;
+  mat2 rot = mat2(0.87, 0.50, -0.50, 0.87);
   for (int i = 0; i < 4; ++i) {
   for (int i = 0; i < 4; ++i) {
-    v += a * hash12(p);
-    p *= 2.0;
-    a *= 0.55;
+    v += a * noise(p);
+    p = rot * p * 2.0 + vec2(100.0);
+    a *= 0.5;
   }
   }
   return v;
   return v;
 }
 }
 
 
-float D_GGX(float NdotH, float a) {
-  float a2 = a * a;
-  float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
-  return a2 / max(3.14159 * d * d, 1e-5);
-}
+// ============================================================================
+// PBR FUNCTIONS
+// ============================================================================
 
 
-float geometry_schlick(float NdotX, float k) {
-  return NdotX / max(NdotX * (1.0 - k) + k, 1e-5);
+float D_GGX(float NdotH, float roughness) {
+  float a = roughness * roughness;
+  float a2 = a * a;
+  float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
+  return a2 / (PI * denom * denom + 1e-6);
 }
 }
 
 
-float geometry_smith(float NdotV, float NdotL, float roughness) {
+float G_SchlickGGX(float NdotX, float roughness) {
   float r = roughness + 1.0;
   float r = roughness + 1.0;
   float k = (r * r) / 8.0;
   float k = (r * r) / 8.0;
-  return geometry_schlick(NdotV, k) * geometry_schlick(NdotL, k);
+  return NdotX / (NdotX * (1.0 - k) + k + 1e-6);
 }
 }
 
 
-vec3 fresnel_schlick(float cos_theta, vec3 F0) {
-  return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);
+float G_Smith(float NdotV, float NdotL, float roughness) {
+  return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness);
 }
 }
 
 
+vec3 F_Schlick(float cosTheta, vec3 F0) {
+  float t = 1.0 - cosTheta;
+  float t5 = t * t * t * t * t;
+  return F0 + (1.0 - F0) * t5;
+}
+
+// ============================================================================
+// METAL / ARMOR PATTERNS
+// ============================================================================
+
+float hammerPattern(vec3 pos) {
+  float coarse = fbm(pos.xz * 12.0);
+  float fine   = fbm(pos.xy * 28.0 + 7.3);
+  float micro  = noise(pos.yz * 45.0);
+  return coarse * 0.5 + fine * 0.35 + micro * 0.15;
+}
+
+float scaleArmor(vec2 uv) {
+  vec2 id = floor(uv * 8.0);
+  vec2 f = fract(uv * 8.0);
+  float offset = mod(id.y, 2.0) * 0.5;
+  f.x = fract(f.x + offset);
+  float d = length((f - 0.5) * vec2(1.0, 1.5));
+  float edge = smoothstep(0.55, 0.45, d);
+  float highlight = smoothstep(0.35, 0.25, d);
+  return edge + highlight * 0.4;
+}
+
+// ============================================================================
+// MAIN
+// ============================================================================
+
 void main() {
 void main() {
-  vec3 base = clamp(u_color, 0.0, 1.0);
+  vec3 baseColor = clamp(u_color, 0.0, 1.0);
   if (u_useTexture) {
   if (u_useTexture) {
-    base *= texture(u_texture, v_texCoord).rgb;
+    baseColor *= texture(u_texture, v_texCoord).rgb;
   }
   }
-
-  // Material layout: 0=skin, 1=armor, 2=helmet, 3=weapon, 4=shield
-  bool is_skin = (u_materialId == 0);
-  bool is_armor = (u_materialId == 1);
+  baseColor = boostSaturation(baseColor, 0.3);
+  
+  bool is_skin   = (u_materialId == 0);
+  bool is_armor  = (u_materialId == 1);
   bool is_helmet = (u_materialId == 2);
   bool is_helmet = (u_materialId == 2);
   bool is_weapon = (u_materialId == 3);
   bool is_weapon = (u_materialId == 3);
-
+  bool is_shield = (u_materialId == 4);
+  
   vec3 N = normalize(v_worldNormal);
   vec3 N = normalize(v_worldNormal);
   vec3 V = normalize(vec3(0.0, 0.0, 1.0));
   vec3 V = normalize(vec3(0.0, 0.0, 1.0));
   vec3 L = normalize(vec3(0.5, 1.0, 0.4));
   vec3 L = normalize(vec3(0.5, 1.0, 0.4));
   vec3 H = normalize(L + V);
   vec3 H = normalize(L + V);
-
-  // Heavy golden armor for swordsmen.
+  
+  vec3 albedo = baseColor;
   float metallic = 0.0;
   float metallic = 0.0;
-  float roughness = 0.55;
-  vec3 albedo = base;
-
+  float roughness = 0.5;
+  
+  // ========================================================================
+  // MATERIALS
+  // ========================================================================
+  
   if (is_skin) {
   if (is_skin) {
-    albedo = mix(base, vec3(0.93, 0.83, 0.72), 0.35);
+    albedo = mix(baseColor, vec3(0.95, 0.78, 0.65), 0.2);
+    albedo = boostSaturation(albedo, 0.15);
     metallic = 0.0;
     metallic = 0.0;
-    roughness = 0.6;
-    // Jagged leather pants for lower body
-    float pants_mask = 1.0 - smoothstep(0.38, 0.60, v_worldPos.y);
-    float jag = fbm(v_worldPos.xz * 15.0 + v_worldPos.y * 3.0);
-    vec3 leather = vec3(0.44, 0.30, 0.20) - vec3(0.04) * jag;
-    albedo = mix(albedo, leather, pants_mask);
-    roughness = mix(roughness, 0.54, pants_mask);
-  } else if (is_armor || is_helmet) {
-    // Bright gold with hammered/patina variation.
-    vec3 gold = vec3(0.95, 0.82, 0.45); // keep energy under 1 to avoid pinking
-    float hammer = fbm(v_worldPos.xz * 18.0);
-    float patina = fbm(v_worldPos.xy * 8.0 + vec2(1.7, 3.1));
-    float hammered = clamp(hammer * 0.8 + patina * 0.2, 0.0, 1.0);
-    albedo = mix(gold, base, 0.05);
-    albedo += vec3(0.16) * hammered;
+    roughness = 0.55;
+    
+    float pants = 1.0 - smoothstep(0.35, 0.60, v_worldPos.y);
+    float grain = fbm(v_worldPos.xz * 18.0 + v_worldPos.y * 4.0);
+    vec3 leather = baseColor * 0.75;
+    leather = boostSaturation(leather, 0.2);
+    leather -= vec3(0.08) * grain;
+    albedo = mix(albedo, leather, pants);
+    roughness = mix(roughness, 0.5, pants);
+    
+  }
+  else if (is_armor) {
+    // ==================================================================
+    // DARK METAL ARMOR (replacing bronze)
+    // ==================================================================
+    vec2 metalUV = v_texCoord * 6.0;
+    float hammer = hammerPattern(vec3(metalUV, v_worldPos.y));
+    float scales = scaleArmor(metalUV);
+    
+    vec3 metal = DARK_METAL_COLOR;
+    float patinaNoise = fbm(metalUV * 2.5);
+    vec3 patinaTint = DARK_METAL_COLOR * 1.15;
+    metal = mix(metal, patinaTint, patinaNoise * 0.2);
+    
+    vec3 teamTint = mix(vec3(1.0), baseColor, 0.25);
+    albedo = metal * teamTint;
+    
+    albedo *= 0.9 + hammer * 0.25;
+    albedo = mix(albedo, albedo * 1.2, scales * 0.5);
+    
     metallic = 1.0;
     metallic = 1.0;
-    roughness = mix(0.04, 0.10, hammered);
-  } else if (is_weapon) {
-    // Sword: wooden grip, brass guard/pommel, steel blade.
-    float h = clamp(v_worldPos.y, 0.0, 1.0);
-    float blade_mask = smoothstep(0.30, 0.55, h);
-    float guard_mask = smoothstep(0.20, 0.35, h) * (1.0 - blade_mask);
-
-    vec3 wood = vec3(0.46, 0.32, 0.20);
-    vec3 steel = vec3(0.75, 0.77, 0.82);
-    vec3 brass = vec3(0.86, 0.70, 0.36);
-
-    float wood_grain = fbm(v_worldPos.xz * 14.0 + v_worldPos.y * 6.0);
-    float steel_brush = fbm(v_worldPos.xy * 30.0);
-
-    vec3 handle = mix(wood, base, 0.35) + vec3(0.05) * wood_grain;
-    vec3 blade = mix(steel, base, 0.20) + vec3(0.05) * steel_brush;
-    vec3 guard = mix(brass, base, 0.15);
-
-    albedo = mix(handle, guard, guard_mask);
-    albedo = mix(albedo, blade, blade_mask);
-
-    metallic = mix(0.05, 1.0, blade_mask + guard_mask * 0.8);
-    roughness = mix(0.50, 0.18, blade_mask);
+    roughness = 0.22 + hammer * 0.08 - scales * 0.08;
+    roughness = clamp(roughness, 0.10, 0.35);
+  }
+  else if (is_helmet) {
+    // ==================================================================
+    // DARK METAL HELMET (replacing bronze)
+    // ==================================================================
+    vec2 metalUV = v_texCoord * 8.0;
+    float hammer = hammerPattern(vec3(metalUV, v_worldPos.y));
+    float scales = scaleArmor(metalUV * 1.2);
+    
+    vec3 metal = DARK_METAL_COLOR;
+    float patinaNoise = fbm(metalUV * 3.0);
+    vec3 patinaTint = DARK_METAL_COLOR * 1.12;
+    metal = mix(metal, patinaTint, patinaNoise * 0.18);
+    
+    float crest = smoothstep(0.8, 0.9, v_worldPos.y);
+    crest *= smoothstep(0.4, 0.3, abs(v_worldPos.x));
+    
+    vec3 crestColor = boostSaturation(baseColor, 0.5) * 1.3;
+    
+    albedo = mix(metal, crestColor, crest * 0.5);
+    
+    albedo *= 0.9 + hammer * 0.25;
+    albedo = mix(albedo, albedo * 1.18, scales * 0.5);
+    
+    metallic = 1.0;
+    roughness = 0.18 + hammer * 0.08 - scales * 0.08;
+    roughness = clamp(roughness, 0.08, 0.30);
+  }
+  else if (is_weapon) {
+    float h = v_worldPos.y;
+    float blade = smoothstep(0.28, 0.50, h);
+    float guard = smoothstep(0.18, 0.28, h) * (1.0 - blade);
+    
+    float polish = fbm(v_worldPos.xy * 35.0);
+    
+    vec3 handle = boostSaturation(baseColor * 0.9, 0.25);
+    handle += vec3(0.06) * polish;
+    
+    vec3 guardCol = mix(BRONZE_BASE_COLOR, baseColor * 1.1, 0.3);
+    guardCol = boostSaturation(guardCol, 0.3);
+    
+    vec3 steel = vec3(0.85, 0.87, 0.92);
+    steel += vec3(0.08) * polish;
+    steel = mix(steel, baseColor * 0.4 + vec3(0.55), 0.15);
+    
+    albedo = mix(handle, guardCol, guard);
+    albedo = mix(albedo, steel, blade);
+    
+    metallic = mix(0.15, 1.0, blade + guard * 0.9);
+    roughness = mix(0.45, 0.05, blade);
   }
   }
+  else if (is_shield) {
+    float dist = length(v_worldPos.xz);
+    float boss = smoothstep(0.25, 0.0, dist);
+    float rings = sin(dist * 20.0) * 0.5 + 0.5;
+    rings = smoothstep(0.3, 0.7, rings) * (1.0 - boss);
+    
+    vec3 shieldFace = boostSaturation(mix(SHIELD_BROWN_COLOR, baseColor, 0.12), 0.35);
+    vec3 bronze = BRONZE_BASE_COLOR;
+    vec3 shieldMetal = mix(shieldFace, bronze, boss + rings * 0.6);
 
 
+    albedo = mix(shieldFace, shieldMetal, boss + rings * 0.5);
+    
+    metallic  = mix(0.2, 1.0, boss + rings * 0.7);
+    roughness = mix(0.45, 0.12, boss);
+  }
+  else {
+    albedo = boostSaturation(baseColor, 0.2);
+    metallic = 0.0;
+    roughness = 0.6;
+  }
+  
+  // ========================================================================
+  // PBR LIGHTING
+  // ========================================================================
+  
   float NdotL = max(dot(N, L), 0.0);
   float NdotL = max(dot(N, L), 0.0);
-  float NdotV = max(dot(N, V), 0.0);
+  float NdotV = max(dot(N, V), 0.001);
   float NdotH = max(dot(N, H), 0.0);
   float NdotH = max(dot(N, H), 0.0);
   float VdotH = max(dot(V, H), 0.0);
   float VdotH = max(dot(V, H), 0.0);
-
-  float a = max(0.02, roughness * roughness);
-  float D = D_GGX(NdotH, a);
-  float G = geometry_smith(NdotV, NdotL, roughness);
+  
   vec3 F0 = mix(vec3(0.04), albedo, metallic);
   vec3 F0 = mix(vec3(0.04), albedo, metallic);
-  vec3 F = fresnel_schlick(VdotH, F0);
-  vec3 spec = (D * G * F) / max(4.0 * NdotL * NdotV + 1e-5, 1e-5);
-
-  vec3 kd = (vec3(1.0) - F) * (1.0 - metallic);
-  vec3 diffuse = kd * albedo / 3.14159;
-
-  // Extra clearcoat/spec boost to force shiny gold read.
-  vec3 clearF = fresnel_schlick(NdotV, vec3(0.10));
-  float clearD = D_GGX(NdotH, 0.035);
-  float clearG = geometry_smith(NdotV, NdotL, 0.12);
-  vec3 clearcoat =
-      (clearD * clearG * clearF) / max(4.0 * NdotL * NdotV + 1e-5, 1e-5);
-
-  float rim = pow(1.0 - max(dot(N, V), 0.0), 3.0);
-  vec3 ambient = albedo * 0.40 + vec3(0.08) * rim;
-  vec3 highlight = vec3(0.30) * pow(NdotL, 14.0) * metallic;
-  vec3 color = ambient + (diffuse + spec * 1.8 + clearcoat) * NdotL + highlight;
-
-  FragColor = vec4(saturate(color), u_alpha);
+  
+  float D = D_GGX(NdotH, max(roughness, 0.01));
+  float G = G_Smith(NdotV, NdotL, roughness);
+  vec3 F = F_Schlick(VdotH, F0);
+  vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001);
+  
+  vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
+  vec3 diffuse = kD * albedo / PI;
+  
+  vec3 color = (diffuse + specular * 2.0) * NdotL * 2.0;
+  
+  if (metallic > 0.5) {
+    vec3 R = reflect(-V, N);
+    
+    float specPower = 196.0 / (roughness * roughness + 0.001);
+    specPower = min(specPower, 1536.0);
+    float mirrorSpec = pow(max(dot(R, L), 0.0), specPower);
+    color += albedo * mirrorSpec * 1.6;
+    
+    float hotspot = pow(NdotH, 400.0);
+    color += vec3(1.1) * hotspot * (1.0 - roughness * 1.6);
+    
+    float softSpec = pow(max(dot(R, L), 0.0), 32.0);
+    color += albedo * softSpec * 0.5;
+    
+    vec3 skyCol    = vec3(0.6, 0.7, 0.9);
+    vec3 groundCol = vec3(0.4, 0.35, 0.28);
+    float upFace   = R.y * 0.5 + 0.5;
+    vec3 envReflect = mix(groundCol, skyCol, upFace);
+    color += envReflect * (1.0 - roughness) * 0.4;
+  }
+  
+  vec3 ambient = albedo * 0.42;
+  
+  float rim = pow(1.0 - NdotV, 3.5);
+  ambient += vec3(0.4, 0.45, 0.55) * rim * 0.28;
+  
+  if (metallic > 0.5) {
+    ambient += albedo * 0.12 * (1.0 - roughness);
+  }
+  
+  color += ambient;
+  
+  color = color / (color + vec3(0.6));
+  color = pow(color, vec3(0.92));
+  
+  FragColor = vec4(saturate3(color), u_alpha);
 }
 }

+ 6 - 6
render/entity/arrow_vfx_renderer.cpp

@@ -96,9 +96,9 @@ static inline ArcherPose makePose(uint32_t seed) {
 
 
   using HP = HumanProportions;
   using HP = HumanProportions;
 
 
-  P.hand_l = QVector3D(P.bowX - 0.05F, HP::SHOULDER_Y + 0.05F, 0.55F);
+  P.hand_r = QVector3D(P.bowX + 0.03F, HP::SHOULDER_Y + 0.08F, 0.55F);
 
 
-  P.hand_r = QVector3D(0.15F, HP::SHOULDER_Y + 0.15F, 0.20F);
+  P.hand_l = QVector3D(-0.02F, HP::SHOULDER_Y + 0.12F, 0.50F);
 
 
   QVector3D shoulder_to_hand_l = P.hand_l - P.shoulder_l;
   QVector3D shoulder_to_hand_l = P.hand_l - P.shoulder_l;
   float distL = shoulder_to_hand_l.length();
   float distL = shoulder_to_hand_l.length();
@@ -324,13 +324,13 @@ static inline void drawBowAndArrow(const DrawContext &p, ISubmitter &out,
   const QVector3D up(0.0F, 1.0F, 0.0F);
   const QVector3D up(0.0F, 1.0F, 0.0F);
   const QVector3D forward(0.0F, 0.0F, 1.0F);
   const QVector3D forward(0.0F, 0.0F, 1.0F);
 
 
-  QVector3D grip = P.hand_l;
+  QVector3D grip = P.hand_r;
   QVector3D top_end(P.bowX, P.bowTopY, grip.z());
   QVector3D top_end(P.bowX, P.bowTopY, grip.z());
   QVector3D bot_end(P.bowX, P.bowBotY, grip.z());
   QVector3D bot_end(P.bowX, P.bowBotY, grip.z());
 
 
   QVector3D nock(P.bowX,
   QVector3D nock(P.bowX,
-                 clampf(P.hand_r.y(), P.bowBotY + 0.05F, P.bowTopY - 0.05F),
-                 clampf(P.hand_r.z(), grip.z() - 0.30F, grip.z() + 0.30F));
+                 clampf(P.hand_l.y(), P.bowBotY + 0.05F, P.bowTopY - 0.05F),
+                 clampf(P.hand_l.z(), grip.z() - 0.30F, grip.z() + 0.30F));
 
 
   constexpr int k_bow_curve_segments = 22;
   constexpr int k_bow_curve_segments = 22;
   auto q_bezier = [](const QVector3D &a, const QVector3D &c, const QVector3D &b,
   auto q_bezier = [](const QVector3D &a, const QVector3D &c, const QVector3D &b,
@@ -358,7 +358,7 @@ static inline void drawBowAndArrow(const DrawContext &p, ISubmitter &out,
   out.mesh(getUnitCylinder(),
   out.mesh(getUnitCylinder(),
            cylinderBetween(p.model, nock, bot_end, P.stringR), C.stringCol,
            cylinderBetween(p.model, nock, bot_end, P.stringR), C.stringCol,
            nullptr, 1.0F);
            nullptr, 1.0F);
-  out.mesh(getUnitCylinder(), cylinderBetween(p.model, P.hand_r, nock, 0.0045F),
+  out.mesh(getUnitCylinder(), cylinderBetween(p.model, P.hand_l, nock, 0.0045F),
            C.stringCol * 0.9F, nullptr, 1.0F);
            C.stringCol * 0.9F, nullptr, 1.0F);
 
 
   QVector3D tail = nock - forward * 0.06F;
   QVector3D tail = nock - forward * 0.06F;

+ 15 - 15
render/entity/nations/carthage/archer_renderer.cpp

@@ -137,34 +137,34 @@ public:
       controller.lean(QVector3D(0.0F, 0.0F, 1.0F),
       controller.lean(QVector3D(0.0F, 0.0F, 1.0F),
                       t * k_lean_amount_multiplier);
                       t * k_lean_amount_multiplier);
 
 
-      QVector3D const hold_hand_l(
-          bow_x - 0.15F, controller.get_shoulder_y(true) + 0.30F, 0.55F);
       QVector3D const hold_hand_r(
       QVector3D const hold_hand_r(
-          bow_x + 0.12F, controller.get_shoulder_y(false) + 0.15F, 0.10F);
-      QVector3D const normal_hand_l(bow_x - 0.05F + arm_asymmetry,
+          bow_x + 0.03F, controller.get_shoulder_y(false) + 0.30F, 0.55F);
+      QVector3D const hold_hand_l(
+          bow_x - 0.02F, controller.get_shoulder_y(true) + 0.12F, 0.55F);
+      QVector3D const normal_hand_r(bow_x + 0.03F - arm_asymmetry,
                                     HP::SHOULDER_Y + 0.05F + arm_height_jitter,
                                     HP::SHOULDER_Y + 0.05F + arm_height_jitter,
                                     0.55F);
                                     0.55F);
-      QVector3D const normal_hand_r(
-          0.15F - arm_asymmetry * 0.5F,
-          HP::SHOULDER_Y + 0.15F + arm_height_jitter * 0.8F, 0.20F);
+      QVector3D const normal_hand_l(
+          bow_x - 0.02F + arm_asymmetry * 0.5F,
+          HP::SHOULDER_Y + 0.12F + arm_height_jitter * 0.8F, 0.50F);
 
 
-      QVector3D const blended_hand_l =
-          normal_hand_l * (1.0F - t) + hold_hand_l * t;
       QVector3D const blended_hand_r =
       QVector3D const blended_hand_r =
           normal_hand_r * (1.0F - t) + hold_hand_r * t;
           normal_hand_r * (1.0F - t) + hold_hand_r * t;
+      QVector3D const blended_hand_l =
+          normal_hand_l * (1.0F - t) + hold_hand_l * t;
 
 
-      controller.placeHandAt(true, blended_hand_l);
       controller.placeHandAt(false, blended_hand_r);
       controller.placeHandAt(false, blended_hand_r);
+      controller.placeHandAt(true, blended_hand_l);
     } else {
     } else {
-      QVector3D const idle_hand_l(bow_x - 0.05F + arm_asymmetry,
+      QVector3D const idle_hand_r(bow_x + 0.03F - arm_asymmetry,
                                   HP::SHOULDER_Y + 0.05F + arm_height_jitter,
                                   HP::SHOULDER_Y + 0.05F + arm_height_jitter,
                                   0.55F);
                                   0.55F);
-      QVector3D const idle_hand_r(
-          0.15F - arm_asymmetry * 0.5F,
-          HP::SHOULDER_Y + 0.15F + arm_height_jitter * 0.8F, 0.20F);
+      QVector3D const idle_hand_l(
+          bow_x - 0.05F + arm_asymmetry * 0.5F,
+          HP::SHOULDER_Y + 0.14F + arm_height_jitter * 0.8F, 0.48F);
 
 
-      controller.placeHandAt(true, idle_hand_l);
       controller.placeHandAt(false, idle_hand_r);
       controller.placeHandAt(false, idle_hand_r);
+      controller.placeHandAt(true, idle_hand_l);
     }
     }
 
 
     if (anim.is_attacking && !anim.is_in_hold_mode) {
     if (anim.is_attacking && !anim.is_in_hold_mode) {

+ 15 - 15
render/entity/nations/roman/archer_renderer.cpp

@@ -115,34 +115,34 @@ public:
       controller.lean(QVector3D(0.0F, 0.0F, 1.0F),
       controller.lean(QVector3D(0.0F, 0.0F, 1.0F),
                       t * k_lean_amount_multiplier);
                       t * k_lean_amount_multiplier);
 
 
-      QVector3D const hold_hand_l(
-          bow_x - 0.15F, controller.get_shoulder_y(true) + 0.30F, 0.55F);
       QVector3D const hold_hand_r(
       QVector3D const hold_hand_r(
-          bow_x + 0.12F, controller.get_shoulder_y(false) + 0.15F, 0.10F);
-      QVector3D const normal_hand_l(bow_x - 0.05F + arm_asymmetry,
+          bow_x + 0.03F, controller.get_shoulder_y(false) + 0.30F, 0.55F);
+      QVector3D const hold_hand_l(
+          bow_x - 0.02F, controller.get_shoulder_y(true) + 0.12F, 0.55F);
+      QVector3D const normal_hand_r(bow_x + 0.03F - arm_asymmetry,
                                     HP::SHOULDER_Y + 0.05F + arm_height_jitter,
                                     HP::SHOULDER_Y + 0.05F + arm_height_jitter,
                                     0.55F);
                                     0.55F);
-      QVector3D const normal_hand_r(
-          0.15F - arm_asymmetry * 0.5F,
-          HP::SHOULDER_Y + 0.15F + arm_height_jitter * 0.8F, 0.20F);
+      QVector3D const normal_hand_l(
+          bow_x - 0.02F + arm_asymmetry * 0.5F,
+          HP::SHOULDER_Y + 0.12F + arm_height_jitter * 0.8F, 0.50F);
 
 
-      QVector3D const blended_hand_l =
-          normal_hand_l * (1.0F - t) + hold_hand_l * t;
       QVector3D const blended_hand_r =
       QVector3D const blended_hand_r =
           normal_hand_r * (1.0F - t) + hold_hand_r * t;
           normal_hand_r * (1.0F - t) + hold_hand_r * t;
+      QVector3D const blended_hand_l =
+          normal_hand_l * (1.0F - t) + hold_hand_l * t;
 
 
-      controller.placeHandAt(true, blended_hand_l);
       controller.placeHandAt(false, blended_hand_r);
       controller.placeHandAt(false, blended_hand_r);
+      controller.placeHandAt(true, blended_hand_l);
     } else {
     } else {
-      QVector3D const idle_hand_l(bow_x - 0.05F + arm_asymmetry,
+      QVector3D const idle_hand_r(bow_x + 0.03F - arm_asymmetry,
                                   HP::SHOULDER_Y + 0.05F + arm_height_jitter,
                                   HP::SHOULDER_Y + 0.05F + arm_height_jitter,
                                   0.55F);
                                   0.55F);
-      QVector3D const idle_hand_r(
-          0.15F - arm_asymmetry * 0.5F,
-          HP::SHOULDER_Y + 0.15F + arm_height_jitter * 0.8F, 0.20F);
+      QVector3D const idle_hand_l(
+          bow_x - 0.05F + arm_asymmetry * 0.5F,
+          HP::SHOULDER_Y + 0.14F + arm_height_jitter * 0.8F, 0.48F);
 
 
-      controller.placeHandAt(true, idle_hand_l);
       controller.placeHandAt(false, idle_hand_r);
       controller.placeHandAt(false, idle_hand_r);
+      controller.placeHandAt(true, idle_hand_l);
     }
     }
 
 
     if (anim.is_attacking && !anim.is_in_hold_mode) {
     if (anim.is_attacking && !anim.is_in_hold_mode) {

+ 10 - 7
render/equipment/weapons/bow_renderer.cpp

@@ -34,7 +34,8 @@ void BowRenderer::render(const DrawContext &ctx, const BodyFrames &frames,
   const QVector3D up(0.0F, 1.0F, 0.0F);
   const QVector3D up(0.0F, 1.0F, 0.0F);
   const QVector3D forward(0.0F, 0.0F, 1.0F);
   const QVector3D forward(0.0F, 0.0F, 1.0F);
 
 
-  QVector3D const grip = frames.hand_l.origin;
+  // Right hand now holds the bow grip; use it as anchor for the bow plane.
+  QVector3D const grip = frames.hand_r.origin;
 
 
   float const bow_half_height = (m_config.bow_top_y - m_config.bow_bot_y) *
   float const bow_half_height = (m_config.bow_top_y - m_config.bow_bot_y) *
                                 0.5F * m_config.bow_height_scale;
                                 0.5F * m_config.bow_height_scale;
@@ -42,7 +43,7 @@ void BowRenderer::render(const DrawContext &ctx, const BodyFrames &frames,
   float const bow_top_y = bow_mid_y + bow_half_height;
   float const bow_top_y = bow_mid_y + bow_half_height;
   float const bow_bot_y = bow_mid_y - bow_half_height;
   float const bow_bot_y = bow_mid_y - bow_half_height;
 
 
-  QVector3D outward = frames.hand_l.right;
+  QVector3D outward = frames.hand_r.right;
   if (outward.lengthSquared() < 1e-6F) {
   if (outward.lengthSquared() < 1e-6F) {
     outward = QVector3D(-1.0F, 0.0F, 0.0F);
     outward = QVector3D(-1.0F, 0.0F, 0.0F);
   }
   }
@@ -52,7 +53,8 @@ void BowRenderer::render(const DrawContext &ctx, const BodyFrames &frames,
   } else {
   } else {
     outward.normalize();
     outward.normalize();
   }
   }
-  QVector3D const side = outward * 0.10F;
+  // Keep the bow plane close to the grip so the hand actually touches it.
+  QVector3D const side = outward * 0.02F;
 
 
   float const bow_plane_x = grip.x() + m_config.bow_x + side.x();
   float const bow_plane_x = grip.x() + m_config.bow_x + side.x();
   float const bow_plane_z = grip.z() + side.z();
   float const bow_plane_z = grip.z() + side.z();
@@ -60,10 +62,11 @@ void BowRenderer::render(const DrawContext &ctx, const BodyFrames &frames,
   QVector3D const top_end(bow_plane_x, bow_top_y, bow_plane_z);
   QVector3D const top_end(bow_plane_x, bow_top_y, bow_plane_z);
   QVector3D const bot_end(bow_plane_x, bow_bot_y, bow_plane_z);
   QVector3D const bot_end(bow_plane_x, bow_bot_y, bow_plane_z);
 
 
-  QVector3D const right_hand = frames.hand_r.origin;
+  QVector3D const string_hand = frames.hand_l.origin;
   QVector3D const nock(
   QVector3D const nock(
-      bow_plane_x, clampf(right_hand.y(), bow_bot_y + 0.05F, bow_top_y - 0.05F),
-      clampf(right_hand.z(), bow_plane_z - 0.30F, bow_plane_z + 0.30F));
+      bow_plane_x,
+      clampf(string_hand.y(), bow_bot_y + 0.05F, bow_top_y - 0.05F),
+      clampf(string_hand.z(), bow_plane_z - 0.30F, bow_plane_z + 0.30F));
 
 
   constexpr int k_bowstring_segments = 22;
   constexpr int k_bowstring_segments = 22;
   auto q_bezier = [](const QVector3D &a, const QVector3D &c, const QVector3D &b,
   auto q_bezier = [](const QVector3D &a, const QVector3D &c, const QVector3D &b,
@@ -108,7 +111,7 @@ void BowRenderer::render(const DrawContext &ctx, const BodyFrames &frames,
   if (is_bow_attacking) {
   if (is_bow_attacking) {
     submitter.mesh(
     submitter.mesh(
         getUnitCylinder(),
         getUnitCylinder(),
-        cylinderBetween(ctx.model, frames.hand_r.origin, nock, 0.0045F),
+        cylinderBetween(ctx.model, frames.hand_l.origin, nock, 0.0045F),
         m_config.string_color * 0.9F, nullptr, 1.0F, m_config.material_id);
         m_config.string_color * 0.9F, nullptr, 1.0F, m_config.material_id);
   }
   }
 
 

+ 13 - 12
render/humanoid/pose_controller.cpp

@@ -197,11 +197,12 @@ void HumanoidPoseController::aimBow(float draw_phase) {
 
 
   draw_phase = std::clamp(draw_phase, 0.0F, 1.0F);
   draw_phase = std::clamp(draw_phase, 0.0F, 1.0F);
 
 
-  QVector3D const aim_pos(0.18F, HP::SHOULDER_Y + 0.18F, 0.35F);
-  QVector3D const draw_pos(0.22F, HP::SHOULDER_Y + 0.10F, -0.30F);
-  QVector3D const release_pos(0.18F, HP::SHOULDER_Y + 0.20F, 0.10F);
+  // Keep string hand closer to bow plane so it actually reaches the chord.
+  QVector3D const aim_pos(-0.02F, HP::SHOULDER_Y + 0.18F, 0.42F);
+  QVector3D const draw_pos(-0.05F, HP::SHOULDER_Y + 0.12F, 0.22F);
+  QVector3D const release_pos(-0.02F, HP::SHOULDER_Y + 0.20F, 0.34F);
 
 
-  QVector3D hand_r_target;
+  QVector3D hand_l_target;
   float shoulder_twist = 0.0F;
   float shoulder_twist = 0.0F;
   float head_recoil = 0.0F;
   float head_recoil = 0.0F;
 
 
@@ -209,35 +210,35 @@ void HumanoidPoseController::aimBow(float draw_phase) {
 
 
     float t = draw_phase / 0.20F;
     float t = draw_phase / 0.20F;
     t = t * t;
     t = t * t;
-    hand_r_target = aim_pos * (1.0F - t) + draw_pos * t;
+    hand_l_target = aim_pos * (1.0F - t) + draw_pos * t;
     shoulder_twist = t * 0.08F;
     shoulder_twist = t * 0.08F;
   } else if (draw_phase < 0.50F) {
   } else if (draw_phase < 0.50F) {
 
 
-    hand_r_target = draw_pos;
+    hand_l_target = draw_pos;
     shoulder_twist = 0.08F;
     shoulder_twist = 0.08F;
   } else if (draw_phase < 0.58F) {
   } else if (draw_phase < 0.58F) {
 
 
     float t = (draw_phase - 0.50F) / 0.08F;
     float t = (draw_phase - 0.50F) / 0.08F;
     t = t * t * t;
     t = t * t * t;
-    hand_r_target = draw_pos * (1.0F - t) + release_pos * t;
+    hand_l_target = draw_pos * (1.0F - t) + release_pos * t;
     shoulder_twist = 0.08F * (1.0F - t * 0.6F);
     shoulder_twist = 0.08F * (1.0F - t * 0.6F);
     head_recoil = t * 0.04F;
     head_recoil = t * 0.04F;
   } else {
   } else {
 
 
     float t = (draw_phase - 0.58F) / 0.42F;
     float t = (draw_phase - 0.58F) / 0.42F;
     t = 1.0F - (1.0F - t) * (1.0F - t);
     t = 1.0F - (1.0F - t) * (1.0F - t);
-    hand_r_target = release_pos * (1.0F - t) + aim_pos * t;
+    hand_l_target = release_pos * (1.0F - t) + aim_pos * t;
     shoulder_twist = 0.08F * 0.4F * (1.0F - t);
     shoulder_twist = 0.08F * 0.4F * (1.0F - t);
     head_recoil = 0.04F * (1.0F - t);
     head_recoil = 0.04F * (1.0F - t);
   }
   }
 
 
-  QVector3D const hand_l_target(0.0F - 0.05F, HP::SHOULDER_Y + 0.05F, 0.55F);
-  placeHandAt(true, hand_l_target);
+  QVector3D const hand_r_target(0.03F, HP::SHOULDER_Y + 0.08F, 0.55F);
   placeHandAt(false, hand_r_target);
   placeHandAt(false, hand_r_target);
+  placeHandAt(true, hand_l_target);
 
 
   if (shoulder_twist > 0.01F) {
   if (shoulder_twist > 0.01F) {
-    m_pose.shoulder_r.setY(m_pose.shoulder_r.y() + shoulder_twist);
-    m_pose.shoulder_l.setY(m_pose.shoulder_l.y() - shoulder_twist * 0.5F);
+    m_pose.shoulder_l.setY(m_pose.shoulder_l.y() + shoulder_twist);
+    m_pose.shoulder_r.setY(m_pose.shoulder_r.y() - shoulder_twist * 0.5F);
   }
   }
 
 
   if (head_recoil > 0.01F) {
   if (head_recoil > 0.01F) {