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
+162
View File
@@ -0,0 +1,162 @@
## Tests for the T-1089 wardrobe engine: the toon_garment multi-region tint +
## logo pipeline and the proof garment (tshirt_modern).
##
## Covers what is verifiable headless:
## 1. The new toon_garment.gdshader loads/compiles.
## 2. The region-mask CHANNEL MATH — pure GDScript replication of the shader's
## RGBA→4-tint channel blend, asserting channel-blended (not indexed) routing.
## 3. The manifest schema extension (style tag + logo_capable flag).
## 4. coverage.json shape.
## 5. Every fitted body variant loads as a skinned garment mesh.
## 6. The logo + region-mask textures load.
##
## The shader itself runs on the GPU, so its arithmetic is re-implemented here in
## GDScript and asserted against the same convention the .gdshader uses:
## region_tint = m.r*t0 + m.g*t1 + m.b*t2 + m.a*t3
## Ref: T-1089 gaps G3 (multi-region tint) + G4 (logo decal), D-251.
class_name TestGarmentEngineT1089
extends GdUnitTestSuite
const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader"
const CLOTHING_DIR := "res://assets/characters/clothing/tshirt_modern/"
const LOGO_PATH := "res://assets/characters/logos/thrds.png"
const MANIFEST_PATH := "res://assets/characters/manifest.json"
const FITTED_BODIES := [
"average_m", "average_f", "muscular_m", "muscular_f", "teen_m", "teen_f",
]
# --- 1. Shader loads -------------------------------------------------------
func test_toon_garment_shader_loads() -> void:
assert_bool(ResourceLoader.exists(SHADER_PATH)).override_failure_message(
"toon_garment.gdshader missing at %s" % SHADER_PATH
).is_true()
var shader := load(SHADER_PATH) as Shader
assert_object(shader).is_not_null()
assert_str(shader.code).contains("region_mask")
assert_str(shader.code).contains("logo_tex")
assert_str(shader.code).contains("UV2")
func test_toon_garment_binds_to_material() -> void:
# A ShaderMaterial with all four tints + logo params must accept them.
var shader := load(SHADER_PATH) as Shader
var mat := ShaderMaterial.new()
mat.shader = shader
mat.set_shader_parameter("tint_0", Color.RED)
mat.set_shader_parameter("tint_1", Color.GREEN)
mat.set_shader_parameter("tint_2", Color.BLUE)
mat.set_shader_parameter("tint_3", Color.WHITE)
mat.set_shader_parameter("logo_enabled", 1.0)
assert_object(mat.get_shader_parameter("tint_2")).is_equal(Color.BLUE)
# --- 2. Region-mask channel math (the testable shader logic) ---------------
## Pure-GDScript replication of the shader's per-texel region tint selection.
func _region_tint(mask: Color, t0: Color, t1: Color, t2: Color, t3: Color) -> Color:
var r := t0 * mask.r + t1 * mask.g + t2 * mask.b + t3 * mask.a
return Color(r.r, r.g, r.b, 1.0)
func test_pure_channel_routes_to_its_tint() -> void:
var t0 := Color(0.1, 0.2, 0.3)
var t1 := Color(0.4, 0.5, 0.6)
var t2 := Color(0.7, 0.8, 0.9)
var t3 := Color(0.0, 0.0, 0.0)
# Pure collar (R) -> tint_0, body (G) -> tint_1, sleeve (B) -> tint_2
assert_bool(_region_tint(Color(1, 0, 0, 0), t0, t1, t2, t3).is_equal_approx(t0)).is_true()
assert_bool(_region_tint(Color(0, 1, 0, 0), t0, t1, t2, t3).is_equal_approx(t1)).is_true()
assert_bool(_region_tint(Color(0, 0, 1, 0), t0, t1, t2, t3).is_equal_approx(t2)).is_true()
func test_boundary_channel_blends_not_indexes() -> void:
# The whole point of G3: a bilinear boundary texel (R=0.5,G=0.5) must produce
# a MIDPOINT of the two tints — a crossfade — not snap to one index.
var blended := _region_tint(
Color(0.5, 0.5, 0.0, 0.0), Color(1, 0, 0), Color(0, 0, 1), Color.BLACK, Color.BLACK
)
assert_bool(blended.is_equal_approx(Color(0.5, 0.0, 0.5))).override_failure_message(
"boundary texel must crossfade tints, got %s" % blended
).is_true()
func test_zero_mask_contributes_no_tint() -> void:
var out := _region_tint(Color(0, 0, 0, 0), Color.RED, Color.GREEN, Color.BLUE, Color.WHITE)
assert_bool(out.is_equal_approx(Color.BLACK)).is_true()
# --- 3. Manifest schema extension ------------------------------------------
func test_manifest_has_tshirt_with_style_and_logo_flag() -> void:
var f := FileAccess.open(MANIFEST_PATH, FileAccess.READ)
assert_object(f).is_not_null()
var data: Variant = JSON.parse_string(f.get_as_text())
f.close()
assert_bool(data is Dictionary).is_true()
var clothing: Dictionary = data["clothing"]
assert_bool(clothing.has("tshirt_modern")).override_failure_message(
"manifest.clothing missing tshirt_modern entry"
).is_true()
var entry: Dictionary = clothing["tshirt_modern"]
assert_str(entry.get("slot", "")).is_equal("torso")
assert_str(entry.get("style", "")).is_equal("modern")
assert_bool(entry.get("logo_capable", false)).is_true()
# --- 4. coverage.json ------------------------------------------------------
func test_coverage_json_shape() -> void:
var path := CLOTHING_DIR + "coverage.json"
var f := FileAccess.open(path, FileAccess.READ)
assert_object(f).is_not_null()
var cov: Variant = JSON.parse_string(f.get_as_text())
f.close()
assert_bool(cov is Dictionary).is_true()
assert_bool((cov as Dictionary).has("hides")).is_true()
assert_array((cov as Dictionary)["hides"]).contains(["torso"])
assert_str((cov as Dictionary).get("torso_variant", "")).is_equal("full")
# --- 5. Fitted variants load as skinned garment meshes ---------------------
func test_all_fitted_bodies_load_skinned() -> void:
for body in FITTED_BODIES:
var glb := CLOTHING_DIR + "%s.glb" % body
assert_bool(ResourceLoader.exists(glb)).override_failure_message(
"fitted variant missing: %s" % glb
).is_true()
var scene := load(glb) as PackedScene
assert_object(scene).override_failure_message(
"variant %s failed to load as PackedScene" % body
).is_not_null()
var inst := scene.instantiate()
var skinned := _has_skinned_mesh(inst)
inst.queue_free()
assert_bool(skinned).override_failure_message(
"variant %s has no skinned MeshInstance3D (cannot animate)" % body
).is_true()
func _has_skinned_mesh(root: Node) -> bool:
var stack: Array[Node] = [root]
while not stack.is_empty():
var n: Node = stack.pop_back()
for c in n.get_children():
stack.append(c)
if n is MeshInstance3D and (n as MeshInstance3D).skin != null:
return true
return false
# --- 6. Decal + mask textures load -----------------------------------------
func test_logo_and_mask_textures_load() -> void:
assert_bool(ResourceLoader.exists(LOGO_PATH)).is_true()
assert_bool(ResourceLoader.exists(CLOTHING_DIR + "reference_mask.png")).is_true()
var logo := load(LOGO_PATH) as Texture2D
assert_object(logo).is_not_null()
var mask := load(CLOTHING_DIR + "reference_mask.png") as Texture2D
assert_object(mask).is_not_null()