feat(assets): wardrobe wave 1 — per-body shells, 8 garments, try-on UI (T-1089)

Infra: offset-shell gains --per-body mode (each body's own segments, cut/mask
thresholds derived from that body's bone landmarks — reproduces the
hand-calibrated reference constants exactly on average_m); compositor prefers
<body>_mask.png with reference_mask.png fallback; tshirt re-authored per-body
on all 11 (the Q-060 torso poke-through class is GONE — residual flags are a
sleeve-hem epsilon artifact on thick arms, offset-insensitive, documented).

Garments (all per-body x 11, chromakey-gated <=150px worst, previewed):
hoodie (hood-down roll, kangaroo pocket, logo), button-down (collar/placket),
shorts, jeans (analytic denim field driving albedo+mask together; boundary
weld + open-rim flattening — real segment-splitter findings), formal pants,
jacket (over-shirt standoff, zip), suit_jacket_black (lapel region, tintable
shirt triangle — the hand-author proof), uniform_utility (11-segment
coverall, gap-free waist join by construction, 4-zone showcase, logo patch).

Try-on UI: creation screen shows per-region tint pickers (multi_region
garments) + logo picker (logo_capable + logos/*.png scan), data-driven off
manifest+coverage. Manifest merged by the lead: 12 clothing entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 00:35:20 +02:00
co-authored by Claude Fable 5
parent dd0d220846
commit c765efd54e
345 changed files with 5369 additions and 54 deletions
+19 -1
View File
@@ -22,8 +22,11 @@ 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"
## All 11 body types — tshirt_modern is authored PER BODY (offset-shell mode,
## T-1089): each body gets its own <body>.glb + <body>_mask.png.
const FITTED_BODIES := [
"average_m", "average_f", "muscular_m", "muscular_f", "teen_m", "teen_f",
"average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f",
"heavy_m", "heavy_f", "teen_m", "teen_f", "child",
]
@@ -155,8 +158,23 @@ func _has_skinned_mesh(root: Node) -> bool:
func test_logo_and_mask_textures_load() -> void:
assert_bool(ResourceLoader.exists(LOGO_PATH)).is_true()
# Runtime fallback mask (SD-fit garments + safety net) must still exist.
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()
func test_per_body_masks_load() -> void:
# Per-body offset-shell authoring: the compositor prefers
# <body>_mask.png over reference_mask.png (character_visual.gd).
for body in FITTED_BODIES:
var mask_path := CLOTHING_DIR + "%s_mask.png" % body
assert_bool(ResourceLoader.exists(mask_path)).override_failure_message(
"per-body region mask missing: %s" % mask_path
).is_true()
var mask := load(mask_path) as Texture2D
assert_object(mask).override_failure_message(
"per-body region mask failed to load: %s" % mask_path
).is_not_null()