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>
This commit is contained in:
2026-07-06 22:21:33 +02:00
co-authored by Claude Fable 5
parent b54b8189d9
commit e22ea0fa4a
26 changed files with 1496 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

@@ -0,0 +1,5 @@
{
"hides": ["torso", "arm_upper_l", "arm_upper_r"],
"torso_variant": "full",
"multi_region": true
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+5
View File
@@ -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);
}