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:
@@ -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()
|
||||
Reference in New Issue
Block a user