feat(assets): wardrobe engine + proof t-shirt — batch-fit, offset-shell, 4-region tint, thrds logo (T-1089)
Engine: tooling/garment-fit/blender_batch_fit_skinned.py (G1 — the skinned Surface-Deform batch the old script couldn't produce; self-check green), blender_author_offset_shell.py (route c: garment shells from OUR body segments, weights inherited by construction, bone-plane cuts, procedural RGBA region mask, UV2 chest channel), make_logo.py. Shader: toon_garment.gdshader — channel-blended 4-region tint + UV2 logo composited after tint / before toon shading. Proof: tshirt_modern fitted to the six healthy bodies, manifest entry with style:modern + logo_capable, thrds wordmark, 18-assertion test suite, 216-capture chromakey QA. Key finding (Q-060 evidence): single-reference SD-fit of an offset-shell degrades on girth-divergent bodies (muscular_m worst) — 24mm standoff tripled headroom but the mechanism limits. Route guidance recorded on T-1089: per-body shell authoring for offset-shell garments; SD-fit for derived/hand-authored ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 355 KiB |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"hides": ["torso", "arm_upper_l", "arm_upper_r"],
|
||||
"torso_variant": "full",
|
||||
"multi_region": true
|
||||
}
|
||||
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -54,6 +54,11 @@
|
||||
},
|
||||
"peasant_tunic": {
|
||||
"slot": "torso"
|
||||
},
|
||||
"tshirt_modern": {
|
||||
"slot": "torso",
|
||||
"style": "modern",
|
||||
"logo_capable": true
|
||||
}
|
||||
},
|
||||
"accessories": []
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
shader_type spatial;
|
||||
render_mode unshaded;
|
||||
|
||||
// Multi-region recolorable garment shader (T-1089 gap G3 + G4 logo decal).
|
||||
//
|
||||
// Supersedes toon_masked.gdshader for CLOTHING: that shader routes a single
|
||||
// tint through recolor_mask.r only. This one channel-routes FOUR tints through
|
||||
// an RGBA region mask (R/G/B/A → tint_0..3), channel-BLENDED rather than
|
||||
// indexed-greyscale, so bilinear filtering cross-fades neighbouring regions
|
||||
// instead of smearing through wrong index levels at a boundary (feasibility G3).
|
||||
// A logo decal is then sampled on a dedicated UV2 (TEXCOORD_1) chest channel and
|
||||
// composited AFTER region tinting (brand colours stay faithful) but BEFORE the
|
||||
// toon shadow (the logo darkens with the fabric it sits on).
|
||||
//
|
||||
// Region-mask convention (authored by blender_author_offset_shell.py):
|
||||
// R = region 0 G = region 1 B = region 2 A = region 3
|
||||
// Per-texel channels partition to ~1.0; unmasked (all-zero) texels keep the
|
||||
// original fabric albedo untinted.
|
||||
|
||||
// Flat fabric base albedo (embedded in the garment GLB material).
|
||||
uniform sampler2D albedo_tex : source_color;
|
||||
// RGBA region mask, UV0-aligned. hint_default_black → a missing mask disables
|
||||
// tinting (falls back to the raw albedo) rather than tinting everything.
|
||||
uniform sampler2D region_mask : hint_default_black;
|
||||
// Logo decal, sampled on UV2. hint_default_transparent → an unbound logo
|
||||
// contributes nothing.
|
||||
uniform sampler2D logo_tex : source_color, hint_default_transparent;
|
||||
|
||||
// Per-region tints — fed from CharacterVisualDescriptor.clothing_tints[item][0..3].
|
||||
uniform vec4 tint_0 : source_color = vec4(0.80, 0.80, 0.80, 1.0);
|
||||
uniform vec4 tint_1 : source_color = vec4(0.80, 0.80, 0.80, 1.0);
|
||||
uniform vec4 tint_2 : source_color = vec4(0.80, 0.80, 0.80, 1.0);
|
||||
uniform vec4 tint_3 : source_color = vec4(0.80, 0.80, 0.80, 1.0);
|
||||
|
||||
// 0.0 = no logo (default; keeps every non-logo garment cheap), 1.0 = draw logo.
|
||||
uniform float logo_enabled : hint_range(0.0, 1.0) = 0.0;
|
||||
|
||||
// Toon shadow (same curve as toon_masked.gdshader for a consistent look).
|
||||
uniform float shadow_strength : hint_range(0.0, 1.0) = 0.2;
|
||||
uniform float shadow_threshold : hint_range(0.0, 1.0) = 0.3;
|
||||
|
||||
void fragment() {
|
||||
vec4 original = texture(albedo_tex, UV);
|
||||
vec4 m = texture(region_mask, UV);
|
||||
|
||||
// Luminance-preserving recolor: keep fabric shading detail, replace hue.
|
||||
float luma = dot(original.rgb, vec3(0.299, 0.587, 0.114));
|
||||
vec3 region_tint = m.r * tint_0.rgb
|
||||
+ m.g * tint_1.rgb
|
||||
+ m.b * tint_2.rgb
|
||||
+ m.a * tint_3.rgb;
|
||||
vec3 tinted = region_tint * (luma * 1.5 + 0.2); // scale luma to avoid too-dark
|
||||
// Blend toward the tint by total region weight; unmasked areas stay original.
|
||||
float region_weight = clamp(m.r + m.g + m.b + m.a, 0.0, 1.0);
|
||||
vec3 base = mix(original.rgb, tinted, region_weight);
|
||||
|
||||
// Logo decal on UV2, guarded to the authored [0,1] chest box.
|
||||
vec2 luv = UV2;
|
||||
float in_box = step(0.0, luv.x) * step(luv.x, 1.0)
|
||||
* step(0.0, luv.y) * step(luv.y, 1.0);
|
||||
vec4 logo = texture(logo_tex, luv);
|
||||
float logo_a = logo.a * in_box * logo_enabled;
|
||||
base = mix(base, logo.rgb, logo_a);
|
||||
|
||||
// Gentle toon shadow — subtle darkening on the shadow side.
|
||||
vec3 light_dir = normalize(vec3(0.3, -1.0, 0.5));
|
||||
float ndl = dot(NORMAL, -light_dir);
|
||||
float toon = smoothstep(shadow_threshold - 0.1, shadow_threshold + 0.1, ndl);
|
||||
ALBEDO = base * mix(1.0 - shadow_strength, 1.0, toon);
|
||||
}
|
||||