diff --git a/.pql/changelog/ticket_history/2026-07.sql b/.pql/changelog/ticket_history/2026-07.sql index 2894c3cc0..bc1445e8d 100644 --- a/.pql/changelog/ticket_history/2026-07.sql +++ b/.pql/changelog/ticket_history/2026-07.sql @@ -885,3 +885,93 @@ ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spik Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection — pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational — sleeveless arms, collars) vs clip_through (gating — skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa . Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA. ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py — route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) — offset-shell garments should be authored PER BODY (needs per-body masks + runtime _mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done — add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos.', NULL, '2026-07-06 20:22:08', '2026-07-06 20:22:08.752', '2026-07-06 20:22:08.752', NULL, 'b2e49bf9d2fcab1df28108db784e8b8f', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCPBF41RWK9PW9M22Q7961C', 'description', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth — varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes — colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked — difficulty tier). Engine deliverables: (1) multi-region tint masks — requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained — user purchase action); (2) logo decal region on garment chest UV — flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd). + +Correction (2026-07-06): Source tier is PARTIALLY owned already — docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] — the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups — no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use. + +Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN — near-future everyday civilian + corporate wear — NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) — no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand — thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) — so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only. + +Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition — fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern. + +Art direction CONSOLIDATED (user, 2026-07-06 — supersedes the two notes above): the Reach has no single clothing style — ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center — modern casual + corporate — because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low. + +Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only — cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope. + +Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary — ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX — extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips — no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands. + +Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline — the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine — (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof — non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding — Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset. + +ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS — (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author — with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience. + +Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection — pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational — sleeveless arms, collars) vs clip_through (gating — skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa . Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA. + +ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py — route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) — offset-shell garments should be authored PER BODY (needs per-body masks + runtime _mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done — add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos.', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth — varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes — colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked — difficulty tier). Engine deliverables: (1) multi-region tint masks — requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained — user purchase action); (2) logo decal region on garment chest UV — flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd). + +Correction (2026-07-06): Source tier is PARTIALLY owned already — docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] — the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups — no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use. + +Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN — near-future everyday civilian + corporate wear — NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) — no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand — thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) — so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only. + +Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition — fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern. + +Art direction CONSOLIDATED (user, 2026-07-06 — supersedes the two notes above): the Reach has no single clothing style — ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center — modern casual + corporate — because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low. + +Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only — cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope. + +Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary — ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX — extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips — no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands. + +Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline — the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine — (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof — non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding — Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset. + +ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS — (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author — with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience. + +Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection — pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational — sleeveless arms, collars) vs clip_through (gating — skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa . Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA. + +ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py — route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) — offset-shell garments should be authored PER BODY (needs per-body masks + runtime _mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done — add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos. + +Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basics across the board''): wave 2 after the in-flight wave 1 — FOOTWEAR: sneakers_modern (trainers, everyday default), shoes_formal (suit pairing), boots_modern (ankle height); SPORT: tank_top (sleeveless, logo-capable), joggers_modern (cuffed, track-stripe tint region), track_jacket (with joggers = the training suit, logo-capable); SWIM: swim_trunks (bright), swimsuit_onepiece (color-block regions). Foot garments follow the peasant_shoes both-feet convention; sole geometry as an offset-shell param extension. Total modern catalogue after wave 2: 17 garments across tops/bottoms/feet/full-body x casual/formal/sport/swim. DEFERRED deliberately: hats/accessories — accessory slot path is unexercised and T-721 flags a filter bug; needs daylight, not a 3am debug. Overnight sequence: wave 1 -> wave 2 -> full garment x 11-body fitting matrix (the ''T-1090 fittings'' check, fork bodies especially) -> commits -> gated pushes -> morning report.', NULL, '2026-07-06 21:19:48', '2026-07-06 21:19:48.840', '2026-07-06 21:19:48.840', NULL, '2e5f043ccaad01475ce7fd7972613565', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCPBF41RWK9PW9M22Q7961C', 'description', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth — varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes — colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked — difficulty tier). Engine deliverables: (1) multi-region tint masks — requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained — user purchase action); (2) logo decal region on garment chest UV — flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd). + +Correction (2026-07-06): Source tier is PARTIALLY owned already — docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] — the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups — no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use. + +Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN — near-future everyday civilian + corporate wear — NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) — no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand — thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) — so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only. + +Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition — fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern. + +Art direction CONSOLIDATED (user, 2026-07-06 — supersedes the two notes above): the Reach has no single clothing style — ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center — modern casual + corporate — because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low. + +Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only — cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope. + +Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary — ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX — extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips — no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands. + +Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline — the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine — (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof — non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding — Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset. + +ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS — (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author — with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience. + +Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection — pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational — sleeveless arms, collars) vs clip_through (gating — skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa . Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA. + +ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py — route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) — offset-shell garments should be authored PER BODY (needs per-body masks + runtime _mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done — add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos. + +Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basics across the board''): wave 2 after the in-flight wave 1 — FOOTWEAR: sneakers_modern (trainers, everyday default), shoes_formal (suit pairing), boots_modern (ankle height); SPORT: tank_top (sleeveless, logo-capable), joggers_modern (cuffed, track-stripe tint region), track_jacket (with joggers = the training suit, logo-capable); SWIM: swim_trunks (bright), swimsuit_onepiece (color-block regions). Foot garments follow the peasant_shoes both-feet convention; sole geometry as an offset-shell param extension. Total modern catalogue after wave 2: 17 garments across tops/bottoms/feet/full-body x casual/formal/sport/swim. DEFERRED deliberately: hats/accessories — accessory slot path is unexercised and T-721 flags a filter bug; needs daylight, not a 3am debug. Overnight sequence: wave 1 -> wave 2 -> full garment x 11-body fitting matrix (the ''T-1090 fittings'' check, fork bodies especially) -> commits -> gated pushes -> morning report.', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth — varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes — colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked — difficulty tier). Engine deliverables: (1) multi-region tint masks — requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained — user purchase action); (2) logo decal region on garment chest UV — flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd). + +Correction (2026-07-06): Source tier is PARTIALLY owned already — docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] — the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups — no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use. + +Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN — near-future everyday civilian + corporate wear — NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) — no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand — thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) — so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only. + +Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition — fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern. + +Art direction CONSOLIDATED (user, 2026-07-06 — supersedes the two notes above): the Reach has no single clothing style — ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center — modern casual + corporate — because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low. + +Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only — cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope. + +Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary — ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX — extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips — no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands. + +Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline — the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine — (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof — non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding — Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset. + +ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS — (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author — with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience. + +Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection — pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational — sleeveless arms, collars) vs clip_through (gating — skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa . Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA. + +ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py — route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) — offset-shell garments should be authored PER BODY (needs per-body masks + runtime _mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done — add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos. + +Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basics across the board''): wave 2 after the in-flight wave 1 — FOOTWEAR: sneakers_modern (trainers, everyday default), shoes_formal (suit pairing), boots_modern (ankle height); SPORT: tank_top (sleeveless, logo-capable), joggers_modern (cuffed, track-stripe tint region), track_jacket (with joggers = the training suit, logo-capable); SWIM: swim_trunks (bright), swimsuit_onepiece (color-block regions). Foot garments follow the peasant_shoes both-feet convention; sole geometry as an offset-shell param extension. Total modern catalogue after wave 2: 17 garments across tops/bottoms/feet/full-body x casual/formal/sport/swim. DEFERRED deliberately: hats/accessories — accessory slot path is unexercised and T-721 flags a filter bug; needs daylight, not a 3am debug. Overnight sequence: wave 1 -> wave 2 -> full garment x 11-body fitting matrix (the ''T-1090 fittings'' check, fork bodies especially) -> commits -> gated pushes -> morning report. + +Fill-in-the-blanks addendum (user bedtime directive 2 + lead judgment): wave 2 grows to 12 — adds sweater_modern (crew pullover), cargo_pants (utility/cyberpunk staple), parka_thrds (cold-weather was absent entirely; ships as the FIRST CANON-BRANDED garment — thrds is Braemar''s cold-weather fiber brand per wiki; hip-length to avoid the parked skirt-rigging tier), slides (swim footwear, near-free). PLUS outfit presets: data file of named complete looks (casual/business/sport/beach/worker/sleep as tint-palette presets over garments) authored by the lead at integration — makes the catalogue usable for NPC dressing and the shop as OUTFITS, not just items. Post-wave-2 catalogue: 21 modern pieces (tops 8 / bottoms 6 / feet 4 / full-body 2 / swim 3). Hats/accessories remain the deliberate daylight deferral (T-721).', NULL, '2026-07-06 21:20:30', '2026-07-06 21:20:30.325', '2026-07-06 21:20:30.325', NULL, '844a61fd59dbff78bb63a90f4399d5c9', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-07.sql b/.pql/changelog/tickets/2026-07.sql index 7ac670845..7b5d4bd1e 100644 --- a/.pql/changelog/tickets/2026-07.sql +++ b/.pql/changelog/tickets/2026-07.sql @@ -743,3 +743,51 @@ ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spik Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection — pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational — sleeveless arms, collars) vs clip_through (gating — skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa . Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA. ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py — route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) — offset-shell garments should be authored PER BODY (needs per-body masks + runtime _mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done — add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos.', 'backlog', 'high', NULL, 'client', NULL, '2026-07-06 07:34:18.656', '2026-07-06 20:22:08.752', NULL, 'a8211523c2bef72893601e640205216e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCPBF41RWK9PW9M22Q7961C', 'story', '06FB0TNSRXX29H5X0EB0F0GBW4', 'Clothing catalogue engine + basic set — recolorable, logo-capable everyday garments', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth — varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes — colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked — difficulty tier). Engine deliverables: (1) multi-region tint masks — requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained — user purchase action); (2) logo decal region on garment chest UV — flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd). + +Correction (2026-07-06): Source tier is PARTIALLY owned already — docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] — the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups — no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use. + +Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN — near-future everyday civilian + corporate wear — NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) — no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand — thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) — so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only. + +Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition — fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern. + +Art direction CONSOLIDATED (user, 2026-07-06 — supersedes the two notes above): the Reach has no single clothing style — ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center — modern casual + corporate — because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low. + +Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only — cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope. + +Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary — ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX — extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips — no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands. + +Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline — the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine — (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof — non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding — Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset. + +ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS — (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author — with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience. + +Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection — pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational — sleeveless arms, collars) vs clip_through (gating — skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa . Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA. + +ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py — route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) — offset-shell garments should be authored PER BODY (needs per-body masks + runtime _mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done — add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos. + +Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basics across the board''): wave 2 after the in-flight wave 1 — FOOTWEAR: sneakers_modern (trainers, everyday default), shoes_formal (suit pairing), boots_modern (ankle height); SPORT: tank_top (sleeveless, logo-capable), joggers_modern (cuffed, track-stripe tint region), track_jacket (with joggers = the training suit, logo-capable); SWIM: swim_trunks (bright), swimsuit_onepiece (color-block regions). Foot garments follow the peasant_shoes both-feet convention; sole geometry as an offset-shell param extension. Total modern catalogue after wave 2: 17 garments across tops/bottoms/feet/full-body x casual/formal/sport/swim. DEFERRED deliberately: hats/accessories — accessory slot path is unexercised and T-721 flags a filter bug; needs daylight, not a 3am debug. Overnight sequence: wave 1 -> wave 2 -> full garment x 11-body fitting matrix (the ''T-1090 fittings'' check, fork bodies especially) -> commits -> gated pushes -> morning report.', 'backlog', 'high', NULL, 'client', NULL, '2026-07-06 07:34:18.656', '2026-07-06 21:19:48.839', NULL, '9da9b8e46990fd320bbaf9897b77620b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCPBF41RWK9PW9M22Q7961C', 'story', '06FB0TNSRXX29H5X0EB0F0GBW4', 'Clothing catalogue engine + basic set — recolorable, logo-capable everyday garments', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth — varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes — colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked — difficulty tier). Engine deliverables: (1) multi-region tint masks — requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained — user purchase action); (2) logo decal region on garment chest UV — flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd). + +Correction (2026-07-06): Source tier is PARTIALLY owned already — docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] — the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups — no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use. + +Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN — near-future everyday civilian + corporate wear — NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) — no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand — thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) — so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only. + +Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition — fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern. + +Art direction CONSOLIDATED (user, 2026-07-06 — supersedes the two notes above): the Reach has no single clothing style — ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center — modern casual + corporate — because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low. + +Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only — cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope. + +Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary — ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX — extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips — no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands. + +Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline — the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine — (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof — non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding — Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset. + +ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS — (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author — with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience. + +Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection — pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational — sleeveless arms, collars) vs clip_through (gating — skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa . Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA. + +ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py — route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) — offset-shell garments should be authored PER BODY (needs per-body masks + runtime _mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done — add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos. + +Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basics across the board''): wave 2 after the in-flight wave 1 — FOOTWEAR: sneakers_modern (trainers, everyday default), shoes_formal (suit pairing), boots_modern (ankle height); SPORT: tank_top (sleeveless, logo-capable), joggers_modern (cuffed, track-stripe tint region), track_jacket (with joggers = the training suit, logo-capable); SWIM: swim_trunks (bright), swimsuit_onepiece (color-block regions). Foot garments follow the peasant_shoes both-feet convention; sole geometry as an offset-shell param extension. Total modern catalogue after wave 2: 17 garments across tops/bottoms/feet/full-body x casual/formal/sport/swim. DEFERRED deliberately: hats/accessories — accessory slot path is unexercised and T-721 flags a filter bug; needs daylight, not a 3am debug. Overnight sequence: wave 1 -> wave 2 -> full garment x 11-body fitting matrix (the ''T-1090 fittings'' check, fork bodies especially) -> commits -> gated pushes -> morning report. + +Fill-in-the-blanks addendum (user bedtime directive 2 + lead judgment): wave 2 grows to 12 — adds sweater_modern (crew pullover), cargo_pants (utility/cyberpunk staple), parka_thrds (cold-weather was absent entirely; ships as the FIRST CANON-BRANDED garment — thrds is Braemar''s cold-weather fiber brand per wiki; hip-length to avoid the parked skirt-rigging tier), slides (swim footwear, near-free). PLUS outfit presets: data file of named complete looks (casual/business/sport/beach/worker/sleep as tint-palette presets over garments) authored by the lead at integration — makes the catalogue usable for NPC dressing and the shop as OUTFITS, not just items. Post-wave-2 catalogue: 21 modern pieces (tops 8 / bottoms 6 / feet 4 / full-body 2 / swim 3). Hats/accessories remain the deliberate daylight deferral (T-721).', 'backlog', 'high', NULL, 'client', NULL, '2026-07-06 07:34:18.656', '2026-07-06 21:20:30.325', NULL, '680076b0a42604327f9ea5701b6355c5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/client/assets/characters/clothing/boots_modern/average_f.glb b/client/assets/characters/clothing/boots_modern/average_f.glb new file mode 100644 index 000000000..ca3b3fb9b Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/boots_modern/average_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/average_f_base_albedo.png new file mode 100644 index 000000000..03843f66d Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/average_f_mask.png b/client/assets/characters/clothing/boots_modern/average_f_mask.png new file mode 100644 index 000000000..3110dbcf8 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/average_m.glb b/client/assets/characters/clothing/boots_modern/average_m.glb new file mode 100644 index 000000000..13210fad6 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/boots_modern/average_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/average_m_base_albedo.png new file mode 100644 index 000000000..cd1364991 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/average_m_mask.png b/client/assets/characters/clothing/boots_modern/average_m_mask.png new file mode 100644 index 000000000..18c5bd846 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/base_albedo.png b/client/assets/characters/clothing/boots_modern/base_albedo.png new file mode 100644 index 000000000..cd1364991 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/child.glb b/client/assets/characters/clothing/boots_modern/child.glb new file mode 100644 index 000000000..07b5694c7 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/child.glb differ diff --git a/client/assets/characters/clothing/boots_modern/child_base_albedo.png b/client/assets/characters/clothing/boots_modern/child_base_albedo.png new file mode 100644 index 000000000..81c74c902 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/child_mask.png b/client/assets/characters/clothing/boots_modern/child_mask.png new file mode 100644 index 000000000..dd38b1937 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/coverage.json b/client/assets/characters/clothing/boots_modern/coverage.json new file mode 100644 index 000000000..35a2255ff --- /dev/null +++ b/client/assets/characters/clothing/boots_modern/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["foot_l", "foot_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/boots_modern/heavy_f.glb b/client/assets/characters/clothing/boots_modern/heavy_f.glb new file mode 100644 index 000000000..ebf4cf602 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/boots_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..2fabec760 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/heavy_f_mask.png b/client/assets/characters/clothing/boots_modern/heavy_f_mask.png new file mode 100644 index 000000000..d610c01cc Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/heavy_m.glb b/client/assets/characters/clothing/boots_modern/heavy_m.glb new file mode 100644 index 000000000..7e59f5c00 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/boots_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..2b242242e Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/heavy_m_mask.png b/client/assets/characters/clothing/boots_modern/heavy_m_mask.png new file mode 100644 index 000000000..21eb3e0d7 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/muscular_f.glb b/client/assets/characters/clothing/boots_modern/muscular_f.glb new file mode 100644 index 000000000..7f12de004 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/boots_modern/muscular_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/muscular_f_base_albedo.png new file mode 100644 index 000000000..0be274998 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/muscular_f_mask.png b/client/assets/characters/clothing/boots_modern/muscular_f_mask.png new file mode 100644 index 000000000..e9ce0aaa3 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/muscular_m.glb b/client/assets/characters/clothing/boots_modern/muscular_m.glb new file mode 100644 index 000000000..2f1177d97 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/boots_modern/muscular_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/muscular_m_base_albedo.png new file mode 100644 index 000000000..ed1c8cfc4 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/muscular_m_mask.png b/client/assets/characters/clothing/boots_modern/muscular_m_mask.png new file mode 100644 index 000000000..c3bee321d Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/reference_mask.png b/client/assets/characters/clothing/boots_modern/reference_mask.png new file mode 100644 index 000000000..18c5bd846 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/teen_f.glb b/client/assets/characters/clothing/boots_modern/teen_f.glb new file mode 100644 index 000000000..dd538c090 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/boots_modern/teen_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/teen_f_base_albedo.png new file mode 100644 index 000000000..423bf21c7 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/teen_f_mask.png b/client/assets/characters/clothing/boots_modern/teen_f_mask.png new file mode 100644 index 000000000..0c6c5ef30 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/teen_m.glb b/client/assets/characters/clothing/boots_modern/teen_m.glb new file mode 100644 index 000000000..25eaca185 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/boots_modern/teen_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/teen_m_base_albedo.png new file mode 100644 index 000000000..ccdd7c8be Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/teen_m_mask.png b/client/assets/characters/clothing/boots_modern/teen_m_mask.png new file mode 100644 index 000000000..ff1449d2a Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/thin_f.glb b/client/assets/characters/clothing/boots_modern/thin_f.glb new file mode 100644 index 000000000..1a24759c2 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/boots_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..8ce283c82 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/thin_f_mask.png b/client/assets/characters/clothing/boots_modern/thin_f_mask.png new file mode 100644 index 000000000..f7ccf44c2 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/boots_modern/thin_m.glb b/client/assets/characters/clothing/boots_modern/thin_m.glb new file mode 100644 index 000000000..f1514c056 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/boots_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..8ccfe8d70 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/thin_m_mask.png b/client/assets/characters/clothing/boots_modern/thin_m_mask.png new file mode 100644 index 000000000..408b4cf0d Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/average_f.glb b/client/assets/characters/clothing/cargo_pants/average_f.glb new file mode 100644 index 000000000..f6e6fa871 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/average_f.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/average_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/average_f_base_albedo.png new file mode 100644 index 000000000..4d66b7afd Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/average_f_mask.png b/client/assets/characters/clothing/cargo_pants/average_f_mask.png new file mode 100644 index 000000000..07463ebb4 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/average_f_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/average_m.glb b/client/assets/characters/clothing/cargo_pants/average_m.glb new file mode 100644 index 000000000..d92384e2c Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/average_m.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/average_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/average_m_base_albedo.png new file mode 100644 index 000000000..a57d39101 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/average_m_mask.png b/client/assets/characters/clothing/cargo_pants/average_m_mask.png new file mode 100644 index 000000000..3611fa343 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/average_m_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/base_albedo.png b/client/assets/characters/clothing/cargo_pants/base_albedo.png new file mode 100644 index 000000000..a57d39101 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/child.glb b/client/assets/characters/clothing/cargo_pants/child.glb new file mode 100644 index 000000000..195eec8a3 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/child.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/child_base_albedo.png b/client/assets/characters/clothing/cargo_pants/child_base_albedo.png new file mode 100644 index 000000000..6b84388a9 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/child_mask.png b/client/assets/characters/clothing/cargo_pants/child_mask.png new file mode 100644 index 000000000..85c017477 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/child_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/coverage.json b/client/assets/characters/clothing/cargo_pants/coverage.json new file mode 100644 index 000000000..00e827118 --- /dev/null +++ b/client/assets/characters/clothing/cargo_pants/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["hips", "leg_upper_l", "leg_upper_r", "leg_lower_l", "leg_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/cargo_pants/heavy_f.glb b/client/assets/characters/clothing/cargo_pants/heavy_f.glb new file mode 100644 index 000000000..5d7e32329 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/heavy_f.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/heavy_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/heavy_f_base_albedo.png new file mode 100644 index 000000000..5949ff2a8 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/heavy_f_mask.png b/client/assets/characters/clothing/cargo_pants/heavy_f_mask.png new file mode 100644 index 000000000..cf9c4bd9f Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/heavy_m.glb b/client/assets/characters/clothing/cargo_pants/heavy_m.glb new file mode 100644 index 000000000..a14fb6ea4 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/heavy_m.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/heavy_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/heavy_m_base_albedo.png new file mode 100644 index 000000000..0759bb1f7 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/heavy_m_mask.png b/client/assets/characters/clothing/cargo_pants/heavy_m_mask.png new file mode 100644 index 000000000..ffac57437 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/muscular_f.glb b/client/assets/characters/clothing/cargo_pants/muscular_f.glb new file mode 100644 index 000000000..bd7299742 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/muscular_f.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/muscular_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/muscular_f_base_albedo.png new file mode 100644 index 000000000..85c19e151 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/muscular_f_mask.png b/client/assets/characters/clothing/cargo_pants/muscular_f_mask.png new file mode 100644 index 000000000..40107eac8 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/muscular_m.glb b/client/assets/characters/clothing/cargo_pants/muscular_m.glb new file mode 100644 index 000000000..b4b227f31 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/muscular_m.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/muscular_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/muscular_m_base_albedo.png new file mode 100644 index 000000000..ad9a577c4 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/muscular_m_mask.png b/client/assets/characters/clothing/cargo_pants/muscular_m_mask.png new file mode 100644 index 000000000..2c0e5679d Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/reference_mask.png b/client/assets/characters/clothing/cargo_pants/reference_mask.png new file mode 100644 index 000000000..3611fa343 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/reference_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/teen_f.glb b/client/assets/characters/clothing/cargo_pants/teen_f.glb new file mode 100644 index 000000000..0dbfd1895 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/teen_f.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/teen_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/teen_f_base_albedo.png new file mode 100644 index 000000000..01c40004b Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/teen_f_mask.png b/client/assets/characters/clothing/cargo_pants/teen_f_mask.png new file mode 100644 index 000000000..cf02b43fa Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/teen_m.glb b/client/assets/characters/clothing/cargo_pants/teen_m.glb new file mode 100644 index 000000000..78ffacf8e Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/teen_m.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/teen_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/teen_m_base_albedo.png new file mode 100644 index 000000000..f165fd784 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/teen_m_mask.png b/client/assets/characters/clothing/cargo_pants/teen_m_mask.png new file mode 100644 index 000000000..1aba96de6 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/thin_f.glb b/client/assets/characters/clothing/cargo_pants/thin_f.glb new file mode 100644 index 000000000..547cc0085 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/thin_f.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/thin_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/thin_f_base_albedo.png new file mode 100644 index 000000000..042c7fb0f Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/thin_f_mask.png b/client/assets/characters/clothing/cargo_pants/thin_f_mask.png new file mode 100644 index 000000000..532433c43 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/cargo_pants/thin_m.glb b/client/assets/characters/clothing/cargo_pants/thin_m.glb new file mode 100644 index 000000000..e3edb8784 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/thin_m.glb differ diff --git a/client/assets/characters/clothing/cargo_pants/thin_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/thin_m_base_albedo.png new file mode 100644 index 000000000..92e990704 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/thin_m_mask.png b/client/assets/characters/clothing/cargo_pants/thin_m_mask.png new file mode 100644 index 000000000..a69d755df Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/average_f.glb b/client/assets/characters/clothing/joggers_modern/average_f.glb new file mode 100644 index 000000000..5dea6378a Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/average_f_average_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/average_f_average_f_base_albedo.png new file mode 100644 index 000000000..7f23332cd Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/average_f_average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/average_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/average_f_base_albedo.png new file mode 100644 index 000000000..7f23332cd Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/average_f_mask.png b/client/assets/characters/clothing/joggers_modern/average_f_mask.png new file mode 100644 index 000000000..441c8ecb4 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/average_m.glb b/client/assets/characters/clothing/joggers_modern/average_m.glb new file mode 100644 index 000000000..ace8166c4 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/average_m_average_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/average_m_average_m_base_albedo.png new file mode 100644 index 000000000..beea4ff85 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/average_m_average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/average_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/average_m_base_albedo.png new file mode 100644 index 000000000..beea4ff85 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/average_m_mask.png b/client/assets/characters/clothing/joggers_modern/average_m_mask.png new file mode 100644 index 000000000..86a23324d Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/base_albedo.png b/client/assets/characters/clothing/joggers_modern/base_albedo.png new file mode 100644 index 000000000..beea4ff85 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/child.glb b/client/assets/characters/clothing/joggers_modern/child.glb new file mode 100644 index 000000000..87d08ba37 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/child.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/child_base_albedo.png b/client/assets/characters/clothing/joggers_modern/child_base_albedo.png new file mode 100644 index 000000000..0299c43ae Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/child_child_base_albedo.png b/client/assets/characters/clothing/joggers_modern/child_child_base_albedo.png new file mode 100644 index 000000000..0299c43ae Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/child_child_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/child_mask.png b/client/assets/characters/clothing/joggers_modern/child_mask.png new file mode 100644 index 000000000..a9e074faf Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/coverage.json b/client/assets/characters/clothing/joggers_modern/coverage.json new file mode 100644 index 000000000..00e827118 --- /dev/null +++ b/client/assets/characters/clothing/joggers_modern/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["hips", "leg_upper_l", "leg_upper_r", "leg_lower_l", "leg_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/joggers_modern/heavy_f.glb b/client/assets/characters/clothing/joggers_modern/heavy_f.glb new file mode 100644 index 000000000..7e7ef0772 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..166649236 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/heavy_f_heavy_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/heavy_f_heavy_f_base_albedo.png new file mode 100644 index 000000000..166649236 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/heavy_f_heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/heavy_f_mask.png b/client/assets/characters/clothing/joggers_modern/heavy_f_mask.png new file mode 100644 index 000000000..d402e08f8 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/heavy_m.glb b/client/assets/characters/clothing/joggers_modern/heavy_m.glb new file mode 100644 index 000000000..848b5dba8 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..78599beea Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/heavy_m_heavy_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/heavy_m_heavy_m_base_albedo.png new file mode 100644 index 000000000..78599beea Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/heavy_m_heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/heavy_m_mask.png b/client/assets/characters/clothing/joggers_modern/heavy_m_mask.png new file mode 100644 index 000000000..1701bfa02 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/muscular_f.glb b/client/assets/characters/clothing/joggers_modern/muscular_f.glb new file mode 100644 index 000000000..8e91df920 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/muscular_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/muscular_f_base_albedo.png new file mode 100644 index 000000000..f0fdc88f7 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/muscular_f_mask.png b/client/assets/characters/clothing/joggers_modern/muscular_f_mask.png new file mode 100644 index 000000000..280a82f85 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/muscular_f_muscular_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/muscular_f_muscular_f_base_albedo.png new file mode 100644 index 000000000..f0fdc88f7 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/muscular_f_muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/muscular_m.glb b/client/assets/characters/clothing/joggers_modern/muscular_m.glb new file mode 100644 index 000000000..af703a196 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/muscular_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/muscular_m_base_albedo.png new file mode 100644 index 000000000..06f7c8be6 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/muscular_m_mask.png b/client/assets/characters/clothing/joggers_modern/muscular_m_mask.png new file mode 100644 index 000000000..a328ff258 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/muscular_m_muscular_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/muscular_m_muscular_m_base_albedo.png new file mode 100644 index 000000000..06f7c8be6 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/muscular_m_muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/reference_mask.png b/client/assets/characters/clothing/joggers_modern/reference_mask.png new file mode 100644 index 000000000..86a23324d Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/teen_f.glb b/client/assets/characters/clothing/joggers_modern/teen_f.glb new file mode 100644 index 000000000..4239959ae Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/teen_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/teen_f_base_albedo.png new file mode 100644 index 000000000..b5f463ece Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/teen_f_mask.png b/client/assets/characters/clothing/joggers_modern/teen_f_mask.png new file mode 100644 index 000000000..7dde55857 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/teen_f_teen_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/teen_f_teen_f_base_albedo.png new file mode 100644 index 000000000..b5f463ece Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/teen_f_teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/teen_m.glb b/client/assets/characters/clothing/joggers_modern/teen_m.glb new file mode 100644 index 000000000..1e2668a56 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/teen_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/teen_m_base_albedo.png new file mode 100644 index 000000000..dfd359af5 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/teen_m_mask.png b/client/assets/characters/clothing/joggers_modern/teen_m_mask.png new file mode 100644 index 000000000..f9c9b2fbf Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/teen_m_teen_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/teen_m_teen_m_base_albedo.png new file mode 100644 index 000000000..dfd359af5 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/teen_m_teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/thin_f.glb b/client/assets/characters/clothing/joggers_modern/thin_f.glb new file mode 100644 index 000000000..13f763496 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..4725e481f Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/thin_f_mask.png b/client/assets/characters/clothing/joggers_modern/thin_f_mask.png new file mode 100644 index 000000000..06b778987 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/thin_f_thin_f_base_albedo.png b/client/assets/characters/clothing/joggers_modern/thin_f_thin_f_base_albedo.png new file mode 100644 index 000000000..4725e481f Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/thin_f_thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/thin_m.glb b/client/assets/characters/clothing/joggers_modern/thin_m.glb new file mode 100644 index 000000000..3a256369b Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/joggers_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..c595b0ec6 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/joggers_modern/thin_m_mask.png b/client/assets/characters/clothing/joggers_modern/thin_m_mask.png new file mode 100644 index 000000000..774823655 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/joggers_modern/thin_m_thin_m_base_albedo.png b/client/assets/characters/clothing/joggers_modern/thin_m_thin_m_base_albedo.png new file mode 100644 index 000000000..c595b0ec6 Binary files /dev/null and b/client/assets/characters/clothing/joggers_modern/thin_m_thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/average_f.glb b/client/assets/characters/clothing/parka_thrds/average_f.glb new file mode 100644 index 000000000..7e6e64231 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/average_f.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/average_f_base_albedo.png b/client/assets/characters/clothing/parka_thrds/average_f_base_albedo.png new file mode 100644 index 000000000..b161caac3 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/average_f_mask.png b/client/assets/characters/clothing/parka_thrds/average_f_mask.png new file mode 100644 index 000000000..e4157c8c5 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/average_f_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/average_m.glb b/client/assets/characters/clothing/parka_thrds/average_m.glb new file mode 100644 index 000000000..a69f07ab8 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/average_m.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/average_m_base_albedo.png b/client/assets/characters/clothing/parka_thrds/average_m_base_albedo.png new file mode 100644 index 000000000..acccea0a8 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/average_m_mask.png b/client/assets/characters/clothing/parka_thrds/average_m_mask.png new file mode 100644 index 000000000..101cd9dd1 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/average_m_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/base_albedo.png b/client/assets/characters/clothing/parka_thrds/base_albedo.png new file mode 100644 index 000000000..acccea0a8 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/child.glb b/client/assets/characters/clothing/parka_thrds/child.glb new file mode 100644 index 000000000..745a9bcfb Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/child.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/child_base_albedo.png b/client/assets/characters/clothing/parka_thrds/child_base_albedo.png new file mode 100644 index 000000000..bef11466e Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/child_mask.png b/client/assets/characters/clothing/parka_thrds/child_mask.png new file mode 100644 index 000000000..dad24e85a Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/child_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/coverage.json b/client/assets/characters/clothing/parka_thrds/coverage.json new file mode 100644 index 000000000..9c2d866d2 --- /dev/null +++ b/client/assets/characters/clothing/parka_thrds/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/parka_thrds/heavy_f.glb b/client/assets/characters/clothing/parka_thrds/heavy_f.glb new file mode 100644 index 000000000..712f783ba Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/heavy_f.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/heavy_f_base_albedo.png b/client/assets/characters/clothing/parka_thrds/heavy_f_base_albedo.png new file mode 100644 index 000000000..1fe4f438e Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/heavy_f_mask.png b/client/assets/characters/clothing/parka_thrds/heavy_f_mask.png new file mode 100644 index 000000000..738a947d7 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/heavy_m.glb b/client/assets/characters/clothing/parka_thrds/heavy_m.glb new file mode 100644 index 000000000..4da811dbc Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/heavy_m.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/heavy_m_base_albedo.png b/client/assets/characters/clothing/parka_thrds/heavy_m_base_albedo.png new file mode 100644 index 000000000..1f197549f Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/heavy_m_mask.png b/client/assets/characters/clothing/parka_thrds/heavy_m_mask.png new file mode 100644 index 000000000..543e54826 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/muscular_f.glb b/client/assets/characters/clothing/parka_thrds/muscular_f.glb new file mode 100644 index 000000000..206d337b6 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/muscular_f.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/muscular_f_base_albedo.png b/client/assets/characters/clothing/parka_thrds/muscular_f_base_albedo.png new file mode 100644 index 000000000..328d8bfae Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/muscular_f_mask.png b/client/assets/characters/clothing/parka_thrds/muscular_f_mask.png new file mode 100644 index 000000000..4d7ee8361 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/muscular_m.glb b/client/assets/characters/clothing/parka_thrds/muscular_m.glb new file mode 100644 index 000000000..d3205f302 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/muscular_m.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/muscular_m_base_albedo.png b/client/assets/characters/clothing/parka_thrds/muscular_m_base_albedo.png new file mode 100644 index 000000000..c03728241 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/muscular_m_mask.png b/client/assets/characters/clothing/parka_thrds/muscular_m_mask.png new file mode 100644 index 000000000..86bc105d1 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/reference_mask.png b/client/assets/characters/clothing/parka_thrds/reference_mask.png new file mode 100644 index 000000000..101cd9dd1 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/reference_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/teen_f.glb b/client/assets/characters/clothing/parka_thrds/teen_f.glb new file mode 100644 index 000000000..01e499cb5 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/teen_f.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/teen_f_base_albedo.png b/client/assets/characters/clothing/parka_thrds/teen_f_base_albedo.png new file mode 100644 index 000000000..dfa0082ee Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/teen_f_mask.png b/client/assets/characters/clothing/parka_thrds/teen_f_mask.png new file mode 100644 index 000000000..70305dae0 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/teen_m.glb b/client/assets/characters/clothing/parka_thrds/teen_m.glb new file mode 100644 index 000000000..d60aa9c7c Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/teen_m.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/teen_m_base_albedo.png b/client/assets/characters/clothing/parka_thrds/teen_m_base_albedo.png new file mode 100644 index 000000000..365042d04 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/teen_m_mask.png b/client/assets/characters/clothing/parka_thrds/teen_m_mask.png new file mode 100644 index 000000000..050b9bfc6 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/thin_f.glb b/client/assets/characters/clothing/parka_thrds/thin_f.glb new file mode 100644 index 000000000..6326ec684 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/thin_f.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/thin_f_base_albedo.png b/client/assets/characters/clothing/parka_thrds/thin_f_base_albedo.png new file mode 100644 index 000000000..9227056a1 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/thin_f_mask.png b/client/assets/characters/clothing/parka_thrds/thin_f_mask.png new file mode 100644 index 000000000..755d09dfb Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/parka_thrds/thin_m.glb b/client/assets/characters/clothing/parka_thrds/thin_m.glb new file mode 100644 index 000000000..ace91a213 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/thin_m.glb differ diff --git a/client/assets/characters/clothing/parka_thrds/thin_m_base_albedo.png b/client/assets/characters/clothing/parka_thrds/thin_m_base_albedo.png new file mode 100644 index 000000000..705ce8a06 Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/parka_thrds/thin_m_mask.png b/client/assets/characters/clothing/parka_thrds/thin_m_mask.png new file mode 100644 index 000000000..42ec9eabc Binary files /dev/null and b/client/assets/characters/clothing/parka_thrds/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/average_f.glb b/client/assets/characters/clothing/shoes_formal/average_f.glb new file mode 100644 index 000000000..59f36d66b Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/average_f.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/average_f_base_albedo.png b/client/assets/characters/clothing/shoes_formal/average_f_base_albedo.png new file mode 100644 index 000000000..c13027444 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/average_f_mask.png b/client/assets/characters/clothing/shoes_formal/average_f_mask.png new file mode 100644 index 000000000..b771bef3c Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/average_f_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/average_m.glb b/client/assets/characters/clothing/shoes_formal/average_m.glb new file mode 100644 index 000000000..99c2e3da0 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/average_m.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/average_m_base_albedo.png b/client/assets/characters/clothing/shoes_formal/average_m_base_albedo.png new file mode 100644 index 000000000..055d31fb4 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/average_m_mask.png b/client/assets/characters/clothing/shoes_formal/average_m_mask.png new file mode 100644 index 000000000..f8a9cb0cf Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/average_m_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/base_albedo.png b/client/assets/characters/clothing/shoes_formal/base_albedo.png new file mode 100644 index 000000000..055d31fb4 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/child.glb b/client/assets/characters/clothing/shoes_formal/child.glb new file mode 100644 index 000000000..851ebabe7 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/child.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/child_base_albedo.png b/client/assets/characters/clothing/shoes_formal/child_base_albedo.png new file mode 100644 index 000000000..0336c3189 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/child_mask.png b/client/assets/characters/clothing/shoes_formal/child_mask.png new file mode 100644 index 000000000..2b46788ac Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/child_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/coverage.json b/client/assets/characters/clothing/shoes_formal/coverage.json new file mode 100644 index 000000000..88157c790 --- /dev/null +++ b/client/assets/characters/clothing/shoes_formal/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": [], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/shoes_formal/heavy_f.glb b/client/assets/characters/clothing/shoes_formal/heavy_f.glb new file mode 100644 index 000000000..a9b9c20f7 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/heavy_f.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/heavy_f_base_albedo.png b/client/assets/characters/clothing/shoes_formal/heavy_f_base_albedo.png new file mode 100644 index 000000000..a707381e9 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/heavy_f_mask.png b/client/assets/characters/clothing/shoes_formal/heavy_f_mask.png new file mode 100644 index 000000000..802df5329 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/heavy_m.glb b/client/assets/characters/clothing/shoes_formal/heavy_m.glb new file mode 100644 index 000000000..15016270c Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/heavy_m.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/heavy_m_base_albedo.png b/client/assets/characters/clothing/shoes_formal/heavy_m_base_albedo.png new file mode 100644 index 000000000..6d67847f3 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/heavy_m_mask.png b/client/assets/characters/clothing/shoes_formal/heavy_m_mask.png new file mode 100644 index 000000000..d32221422 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/muscular_f.glb b/client/assets/characters/clothing/shoes_formal/muscular_f.glb new file mode 100644 index 000000000..1b3ed4e44 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/muscular_f.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/muscular_f_base_albedo.png b/client/assets/characters/clothing/shoes_formal/muscular_f_base_albedo.png new file mode 100644 index 000000000..81b8d58dc Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/muscular_f_mask.png b/client/assets/characters/clothing/shoes_formal/muscular_f_mask.png new file mode 100644 index 000000000..c45962f38 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/muscular_m.glb b/client/assets/characters/clothing/shoes_formal/muscular_m.glb new file mode 100644 index 000000000..77be93bd7 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/muscular_m.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/muscular_m_base_albedo.png b/client/assets/characters/clothing/shoes_formal/muscular_m_base_albedo.png new file mode 100644 index 000000000..db219ee0a Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/muscular_m_mask.png b/client/assets/characters/clothing/shoes_formal/muscular_m_mask.png new file mode 100644 index 000000000..e0a53d0cf Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/reference_mask.png b/client/assets/characters/clothing/shoes_formal/reference_mask.png new file mode 100644 index 000000000..f8a9cb0cf Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/reference_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/teen_f.glb b/client/assets/characters/clothing/shoes_formal/teen_f.glb new file mode 100644 index 000000000..509e47d9a Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/teen_f.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/teen_f_base_albedo.png b/client/assets/characters/clothing/shoes_formal/teen_f_base_albedo.png new file mode 100644 index 000000000..702a32f46 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/teen_f_mask.png b/client/assets/characters/clothing/shoes_formal/teen_f_mask.png new file mode 100644 index 000000000..9af9398b1 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/teen_m.glb b/client/assets/characters/clothing/shoes_formal/teen_m.glb new file mode 100644 index 000000000..9f514829e Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/teen_m.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/teen_m_base_albedo.png b/client/assets/characters/clothing/shoes_formal/teen_m_base_albedo.png new file mode 100644 index 000000000..933266f22 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/teen_m_mask.png b/client/assets/characters/clothing/shoes_formal/teen_m_mask.png new file mode 100644 index 000000000..9cb11a2a2 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/thin_f.glb b/client/assets/characters/clothing/shoes_formal/thin_f.glb new file mode 100644 index 000000000..a2b713b7a Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/thin_f.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/thin_f_base_albedo.png b/client/assets/characters/clothing/shoes_formal/thin_f_base_albedo.png new file mode 100644 index 000000000..3be69c953 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/thin_f_mask.png b/client/assets/characters/clothing/shoes_formal/thin_f_mask.png new file mode 100644 index 000000000..b0bf1b70d Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/shoes_formal/thin_m.glb b/client/assets/characters/clothing/shoes_formal/thin_m.glb new file mode 100644 index 000000000..68db23ea1 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/thin_m.glb differ diff --git a/client/assets/characters/clothing/shoes_formal/thin_m_base_albedo.png b/client/assets/characters/clothing/shoes_formal/thin_m_base_albedo.png new file mode 100644 index 000000000..4d91874d1 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shoes_formal/thin_m_mask.png b/client/assets/characters/clothing/shoes_formal/thin_m_mask.png new file mode 100644 index 000000000..544a09203 Binary files /dev/null and b/client/assets/characters/clothing/shoes_formal/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/slides/average_f.glb b/client/assets/characters/clothing/slides/average_f.glb new file mode 100644 index 000000000..b5d43502c Binary files /dev/null and b/client/assets/characters/clothing/slides/average_f.glb differ diff --git a/client/assets/characters/clothing/slides/average_f_base_albedo.png b/client/assets/characters/clothing/slides/average_f_base_albedo.png new file mode 100644 index 000000000..ad856a82a Binary files /dev/null and b/client/assets/characters/clothing/slides/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/average_f_mask.png b/client/assets/characters/clothing/slides/average_f_mask.png new file mode 100644 index 000000000..fa29f5220 Binary files /dev/null and b/client/assets/characters/clothing/slides/average_f_mask.png differ diff --git a/client/assets/characters/clothing/slides/average_m.glb b/client/assets/characters/clothing/slides/average_m.glb new file mode 100644 index 000000000..442b94723 Binary files /dev/null and b/client/assets/characters/clothing/slides/average_m.glb differ diff --git a/client/assets/characters/clothing/slides/average_m_base_albedo.png b/client/assets/characters/clothing/slides/average_m_base_albedo.png new file mode 100644 index 000000000..80256e64e Binary files /dev/null and b/client/assets/characters/clothing/slides/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/average_m_mask.png b/client/assets/characters/clothing/slides/average_m_mask.png new file mode 100644 index 000000000..6aedd40cc Binary files /dev/null and b/client/assets/characters/clothing/slides/average_m_mask.png differ diff --git a/client/assets/characters/clothing/slides/base_albedo.png b/client/assets/characters/clothing/slides/base_albedo.png new file mode 100644 index 000000000..80256e64e Binary files /dev/null and b/client/assets/characters/clothing/slides/base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/child.glb b/client/assets/characters/clothing/slides/child.glb new file mode 100644 index 000000000..dbf1c411e Binary files /dev/null and b/client/assets/characters/clothing/slides/child.glb differ diff --git a/client/assets/characters/clothing/slides/child_base_albedo.png b/client/assets/characters/clothing/slides/child_base_albedo.png new file mode 100644 index 000000000..2d7d4af71 Binary files /dev/null and b/client/assets/characters/clothing/slides/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/child_mask.png b/client/assets/characters/clothing/slides/child_mask.png new file mode 100644 index 000000000..502522afd Binary files /dev/null and b/client/assets/characters/clothing/slides/child_mask.png differ diff --git a/client/assets/characters/clothing/slides/coverage.json b/client/assets/characters/clothing/slides/coverage.json new file mode 100644 index 000000000..88157c790 --- /dev/null +++ b/client/assets/characters/clothing/slides/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": [], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/slides/heavy_f.glb b/client/assets/characters/clothing/slides/heavy_f.glb new file mode 100644 index 000000000..810ad7012 Binary files /dev/null and b/client/assets/characters/clothing/slides/heavy_f.glb differ diff --git a/client/assets/characters/clothing/slides/heavy_f_base_albedo.png b/client/assets/characters/clothing/slides/heavy_f_base_albedo.png new file mode 100644 index 000000000..2c2208b63 Binary files /dev/null and b/client/assets/characters/clothing/slides/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/heavy_f_mask.png b/client/assets/characters/clothing/slides/heavy_f_mask.png new file mode 100644 index 000000000..f6f2bb352 Binary files /dev/null and b/client/assets/characters/clothing/slides/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/slides/heavy_m.glb b/client/assets/characters/clothing/slides/heavy_m.glb new file mode 100644 index 000000000..c44dcd230 Binary files /dev/null and b/client/assets/characters/clothing/slides/heavy_m.glb differ diff --git a/client/assets/characters/clothing/slides/heavy_m_base_albedo.png b/client/assets/characters/clothing/slides/heavy_m_base_albedo.png new file mode 100644 index 000000000..97c4ed05e Binary files /dev/null and b/client/assets/characters/clothing/slides/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/heavy_m_mask.png b/client/assets/characters/clothing/slides/heavy_m_mask.png new file mode 100644 index 000000000..d4d5c2e1b Binary files /dev/null and b/client/assets/characters/clothing/slides/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/slides/muscular_f.glb b/client/assets/characters/clothing/slides/muscular_f.glb new file mode 100644 index 000000000..d8ef24ec7 Binary files /dev/null and b/client/assets/characters/clothing/slides/muscular_f.glb differ diff --git a/client/assets/characters/clothing/slides/muscular_f_base_albedo.png b/client/assets/characters/clothing/slides/muscular_f_base_albedo.png new file mode 100644 index 000000000..5833c0163 Binary files /dev/null and b/client/assets/characters/clothing/slides/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/muscular_f_mask.png b/client/assets/characters/clothing/slides/muscular_f_mask.png new file mode 100644 index 000000000..ddc5934b3 Binary files /dev/null and b/client/assets/characters/clothing/slides/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/slides/muscular_m.glb b/client/assets/characters/clothing/slides/muscular_m.glb new file mode 100644 index 000000000..13fd9f92d Binary files /dev/null and b/client/assets/characters/clothing/slides/muscular_m.glb differ diff --git a/client/assets/characters/clothing/slides/muscular_m_base_albedo.png b/client/assets/characters/clothing/slides/muscular_m_base_albedo.png new file mode 100644 index 000000000..8047ca5d5 Binary files /dev/null and b/client/assets/characters/clothing/slides/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/muscular_m_mask.png b/client/assets/characters/clothing/slides/muscular_m_mask.png new file mode 100644 index 000000000..4555ea37c Binary files /dev/null and b/client/assets/characters/clothing/slides/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/slides/reference_mask.png b/client/assets/characters/clothing/slides/reference_mask.png new file mode 100644 index 000000000..6aedd40cc Binary files /dev/null and b/client/assets/characters/clothing/slides/reference_mask.png differ diff --git a/client/assets/characters/clothing/slides/teen_f.glb b/client/assets/characters/clothing/slides/teen_f.glb new file mode 100644 index 000000000..0f5ae57fb Binary files /dev/null and b/client/assets/characters/clothing/slides/teen_f.glb differ diff --git a/client/assets/characters/clothing/slides/teen_f_base_albedo.png b/client/assets/characters/clothing/slides/teen_f_base_albedo.png new file mode 100644 index 000000000..89ba2c991 Binary files /dev/null and b/client/assets/characters/clothing/slides/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/teen_f_mask.png b/client/assets/characters/clothing/slides/teen_f_mask.png new file mode 100644 index 000000000..324f19980 Binary files /dev/null and b/client/assets/characters/clothing/slides/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/slides/teen_m.glb b/client/assets/characters/clothing/slides/teen_m.glb new file mode 100644 index 000000000..1e226e659 Binary files /dev/null and b/client/assets/characters/clothing/slides/teen_m.glb differ diff --git a/client/assets/characters/clothing/slides/teen_m_base_albedo.png b/client/assets/characters/clothing/slides/teen_m_base_albedo.png new file mode 100644 index 000000000..1f5288202 Binary files /dev/null and b/client/assets/characters/clothing/slides/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/teen_m_mask.png b/client/assets/characters/clothing/slides/teen_m_mask.png new file mode 100644 index 000000000..ed3cd5c2a Binary files /dev/null and b/client/assets/characters/clothing/slides/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/slides/thin_f.glb b/client/assets/characters/clothing/slides/thin_f.glb new file mode 100644 index 000000000..e6d814f89 Binary files /dev/null and b/client/assets/characters/clothing/slides/thin_f.glb differ diff --git a/client/assets/characters/clothing/slides/thin_f_base_albedo.png b/client/assets/characters/clothing/slides/thin_f_base_albedo.png new file mode 100644 index 000000000..4bea5f819 Binary files /dev/null and b/client/assets/characters/clothing/slides/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/thin_f_mask.png b/client/assets/characters/clothing/slides/thin_f_mask.png new file mode 100644 index 000000000..d7233442b Binary files /dev/null and b/client/assets/characters/clothing/slides/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/slides/thin_m.glb b/client/assets/characters/clothing/slides/thin_m.glb new file mode 100644 index 000000000..c8c2c7d20 Binary files /dev/null and b/client/assets/characters/clothing/slides/thin_m.glb differ diff --git a/client/assets/characters/clothing/slides/thin_m_base_albedo.png b/client/assets/characters/clothing/slides/thin_m_base_albedo.png new file mode 100644 index 000000000..1b91c980a Binary files /dev/null and b/client/assets/characters/clothing/slides/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/slides/thin_m_mask.png b/client/assets/characters/clothing/slides/thin_m_mask.png new file mode 100644 index 000000000..70b3ea814 Binary files /dev/null and b/client/assets/characters/clothing/slides/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/average_f.glb b/client/assets/characters/clothing/sneakers_modern/average_f.glb new file mode 100644 index 000000000..82b6eba4b Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/average_f_average_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/average_f_average_f_base_albedo.png new file mode 100644 index 000000000..3639b19cd Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/average_f_average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/average_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/average_f_base_albedo.png new file mode 100644 index 000000000..3639b19cd Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/average_f_mask.png b/client/assets/characters/clothing/sneakers_modern/average_f_mask.png new file mode 100644 index 000000000..992e30c6c Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/average_m.glb b/client/assets/characters/clothing/sneakers_modern/average_m.glb new file mode 100644 index 000000000..c6a808f24 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/average_m_average_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/average_m_average_m_base_albedo.png new file mode 100644 index 000000000..af3d127a8 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/average_m_average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/average_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/average_m_base_albedo.png new file mode 100644 index 000000000..af3d127a8 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/average_m_mask.png b/client/assets/characters/clothing/sneakers_modern/average_m_mask.png new file mode 100644 index 000000000..1ec86a5fc Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/base_albedo.png b/client/assets/characters/clothing/sneakers_modern/base_albedo.png new file mode 100644 index 000000000..af3d127a8 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/child.glb b/client/assets/characters/clothing/sneakers_modern/child.glb new file mode 100644 index 000000000..cb7d7cce7 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/child.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/child_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/child_base_albedo.png new file mode 100644 index 000000000..11903d0cb Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/child_child_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/child_child_base_albedo.png new file mode 100644 index 000000000..11903d0cb Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/child_child_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/child_mask.png b/client/assets/characters/clothing/sneakers_modern/child_mask.png new file mode 100644 index 000000000..0fb99e1d8 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/coverage.json b/client/assets/characters/clothing/sneakers_modern/coverage.json new file mode 100644 index 000000000..35a2255ff --- /dev/null +++ b/client/assets/characters/clothing/sneakers_modern/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["foot_l", "foot_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/sneakers_modern/heavy_f.glb b/client/assets/characters/clothing/sneakers_modern/heavy_f.glb new file mode 100644 index 000000000..9a683db5c Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..a59acaf7f Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/heavy_f_heavy_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/heavy_f_heavy_f_base_albedo.png new file mode 100644 index 000000000..a59acaf7f Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/heavy_f_heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/heavy_f_mask.png b/client/assets/characters/clothing/sneakers_modern/heavy_f_mask.png new file mode 100644 index 000000000..bbb952a58 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/heavy_m.glb b/client/assets/characters/clothing/sneakers_modern/heavy_m.glb new file mode 100644 index 000000000..bb903ac76 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..fe3fa22c6 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/heavy_m_heavy_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/heavy_m_heavy_m_base_albedo.png new file mode 100644 index 000000000..fe3fa22c6 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/heavy_m_heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/heavy_m_mask.png b/client/assets/characters/clothing/sneakers_modern/heavy_m_mask.png new file mode 100644 index 000000000..cb08713ca Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/muscular_f.glb b/client/assets/characters/clothing/sneakers_modern/muscular_f.glb new file mode 100644 index 000000000..6bd392abe Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/muscular_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/muscular_f_base_albedo.png new file mode 100644 index 000000000..d520a413c Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/muscular_f_mask.png b/client/assets/characters/clothing/sneakers_modern/muscular_f_mask.png new file mode 100644 index 000000000..87e28b124 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/muscular_f_muscular_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/muscular_f_muscular_f_base_albedo.png new file mode 100644 index 000000000..d520a413c Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/muscular_f_muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/muscular_m.glb b/client/assets/characters/clothing/sneakers_modern/muscular_m.glb new file mode 100644 index 000000000..d91dd892a Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/muscular_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/muscular_m_base_albedo.png new file mode 100644 index 000000000..8ef1b3940 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/muscular_m_mask.png b/client/assets/characters/clothing/sneakers_modern/muscular_m_mask.png new file mode 100644 index 000000000..3680ca6ec Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/muscular_m_muscular_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/muscular_m_muscular_m_base_albedo.png new file mode 100644 index 000000000..8ef1b3940 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/muscular_m_muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/reference_mask.png b/client/assets/characters/clothing/sneakers_modern/reference_mask.png new file mode 100644 index 000000000..1ec86a5fc Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/teen_f.glb b/client/assets/characters/clothing/sneakers_modern/teen_f.glb new file mode 100644 index 000000000..c980599fb Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/teen_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/teen_f_base_albedo.png new file mode 100644 index 000000000..a16798ce8 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/teen_f_mask.png b/client/assets/characters/clothing/sneakers_modern/teen_f_mask.png new file mode 100644 index 000000000..9459fe196 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/teen_f_teen_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/teen_f_teen_f_base_albedo.png new file mode 100644 index 000000000..a16798ce8 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/teen_f_teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/teen_m.glb b/client/assets/characters/clothing/sneakers_modern/teen_m.glb new file mode 100644 index 000000000..43d6488d5 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/teen_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/teen_m_base_albedo.png new file mode 100644 index 000000000..722da97a1 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/teen_m_mask.png b/client/assets/characters/clothing/sneakers_modern/teen_m_mask.png new file mode 100644 index 000000000..006052031 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/teen_m_teen_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/teen_m_teen_m_base_albedo.png new file mode 100644 index 000000000..722da97a1 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/teen_m_teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/thin_f.glb b/client/assets/characters/clothing/sneakers_modern/thin_f.glb new file mode 100644 index 000000000..5996a32eb Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..72b61d2b2 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/thin_f_mask.png b/client/assets/characters/clothing/sneakers_modern/thin_f_mask.png new file mode 100644 index 000000000..78ce836b4 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/thin_f_thin_f_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/thin_f_thin_f_base_albedo.png new file mode 100644 index 000000000..72b61d2b2 Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/thin_f_thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/thin_m.glb b/client/assets/characters/clothing/sneakers_modern/thin_m.glb new file mode 100644 index 000000000..d994fabcb Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/sneakers_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..4a26c303e Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/thin_m_mask.png b/client/assets/characters/clothing/sneakers_modern/thin_m_mask.png new file mode 100644 index 000000000..49a10069a Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/sneakers_modern/thin_m_thin_m_base_albedo.png b/client/assets/characters/clothing/sneakers_modern/thin_m_thin_m_base_albedo.png new file mode 100644 index 000000000..4a26c303e Binary files /dev/null and b/client/assets/characters/clothing/sneakers_modern/thin_m_thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/average_f.glb b/client/assets/characters/clothing/sweater_modern/average_f.glb new file mode 100644 index 000000000..467650d01 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/average_f_base_albedo.png b/client/assets/characters/clothing/sweater_modern/average_f_base_albedo.png new file mode 100644 index 000000000..21f6b9093 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/average_f_mask.png b/client/assets/characters/clothing/sweater_modern/average_f_mask.png new file mode 100644 index 000000000..b1cd0b74d Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/average_m.glb b/client/assets/characters/clothing/sweater_modern/average_m.glb new file mode 100644 index 000000000..04efd1c76 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/average_m_base_albedo.png b/client/assets/characters/clothing/sweater_modern/average_m_base_albedo.png new file mode 100644 index 000000000..721a15089 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/average_m_mask.png b/client/assets/characters/clothing/sweater_modern/average_m_mask.png new file mode 100644 index 000000000..0f4389ce8 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/base_albedo.png b/client/assets/characters/clothing/sweater_modern/base_albedo.png new file mode 100644 index 000000000..721a15089 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/child.glb b/client/assets/characters/clothing/sweater_modern/child.glb new file mode 100644 index 000000000..01075d132 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/child.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/child_base_albedo.png b/client/assets/characters/clothing/sweater_modern/child_base_albedo.png new file mode 100644 index 000000000..02273998a Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/child_mask.png b/client/assets/characters/clothing/sweater_modern/child_mask.png new file mode 100644 index 000000000..4d737521f Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/coverage.json b/client/assets/characters/clothing/sweater_modern/coverage.json new file mode 100644 index 000000000..d72d0c9ac --- /dev/null +++ b/client/assets/characters/clothing/sweater_modern/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "torso_upper", "arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/sweater_modern/heavy_f.glb b/client/assets/characters/clothing/sweater_modern/heavy_f.glb new file mode 100644 index 000000000..cfa787b44 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/sweater_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..1b7c007e3 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/heavy_f_mask.png b/client/assets/characters/clothing/sweater_modern/heavy_f_mask.png new file mode 100644 index 000000000..c11c0e6c6 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/heavy_m.glb b/client/assets/characters/clothing/sweater_modern/heavy_m.glb new file mode 100644 index 000000000..c9e292aa1 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/sweater_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..e7a709a17 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/heavy_m_mask.png b/client/assets/characters/clothing/sweater_modern/heavy_m_mask.png new file mode 100644 index 000000000..5c7e03e38 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/muscular_f.glb b/client/assets/characters/clothing/sweater_modern/muscular_f.glb new file mode 100644 index 000000000..164cfc64c Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/muscular_f_base_albedo.png b/client/assets/characters/clothing/sweater_modern/muscular_f_base_albedo.png new file mode 100644 index 000000000..b81b839f1 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/muscular_f_mask.png b/client/assets/characters/clothing/sweater_modern/muscular_f_mask.png new file mode 100644 index 000000000..ff6981d49 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/muscular_m.glb b/client/assets/characters/clothing/sweater_modern/muscular_m.glb new file mode 100644 index 000000000..a16e07172 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/muscular_m_base_albedo.png b/client/assets/characters/clothing/sweater_modern/muscular_m_base_albedo.png new file mode 100644 index 000000000..bb3dc2252 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/muscular_m_mask.png b/client/assets/characters/clothing/sweater_modern/muscular_m_mask.png new file mode 100644 index 000000000..8308892d2 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/reference_mask.png b/client/assets/characters/clothing/sweater_modern/reference_mask.png new file mode 100644 index 000000000..0f4389ce8 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/teen_f.glb b/client/assets/characters/clothing/sweater_modern/teen_f.glb new file mode 100644 index 000000000..290cedcf4 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/teen_f_base_albedo.png b/client/assets/characters/clothing/sweater_modern/teen_f_base_albedo.png new file mode 100644 index 000000000..3d85c2735 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/teen_f_mask.png b/client/assets/characters/clothing/sweater_modern/teen_f_mask.png new file mode 100644 index 000000000..749436e85 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/teen_m.glb b/client/assets/characters/clothing/sweater_modern/teen_m.glb new file mode 100644 index 000000000..e20c82582 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/teen_m_base_albedo.png b/client/assets/characters/clothing/sweater_modern/teen_m_base_albedo.png new file mode 100644 index 000000000..4b35d5643 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/teen_m_mask.png b/client/assets/characters/clothing/sweater_modern/teen_m_mask.png new file mode 100644 index 000000000..b52ba1a8f Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/thin_f.glb b/client/assets/characters/clothing/sweater_modern/thin_f.glb new file mode 100644 index 000000000..916be4804 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/sweater_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..e2835c1f0 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/thin_f_mask.png b/client/assets/characters/clothing/sweater_modern/thin_f_mask.png new file mode 100644 index 000000000..1a033877d Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/sweater_modern/thin_m.glb b/client/assets/characters/clothing/sweater_modern/thin_m.glb new file mode 100644 index 000000000..73bf91a61 Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/sweater_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/sweater_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..cc990a2ce Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/sweater_modern/thin_m_mask.png b/client/assets/characters/clothing/sweater_modern/thin_m_mask.png new file mode 100644 index 000000000..de8394eef Binary files /dev/null and b/client/assets/characters/clothing/sweater_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/average_f.glb b/client/assets/characters/clothing/swim_trunks/average_f.glb new file mode 100644 index 000000000..93350ae8f Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/average_f.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/average_f_average_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/average_f_average_f_base_albedo.png new file mode 100644 index 000000000..1996c6943 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/average_f_average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/average_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/average_f_base_albedo.png new file mode 100644 index 000000000..1996c6943 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/average_f_mask.png b/client/assets/characters/clothing/swim_trunks/average_f_mask.png new file mode 100644 index 000000000..0b8e66c85 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/average_f_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/average_m.glb b/client/assets/characters/clothing/swim_trunks/average_m.glb new file mode 100644 index 000000000..e749decfd Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/average_m.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/average_m_average_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/average_m_average_m_base_albedo.png new file mode 100644 index 000000000..25da4d436 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/average_m_average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/average_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/average_m_base_albedo.png new file mode 100644 index 000000000..25da4d436 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/average_m_mask.png b/client/assets/characters/clothing/swim_trunks/average_m_mask.png new file mode 100644 index 000000000..6dead5f50 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/average_m_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/base_albedo.png b/client/assets/characters/clothing/swim_trunks/base_albedo.png new file mode 100644 index 000000000..25da4d436 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/child.glb b/client/assets/characters/clothing/swim_trunks/child.glb new file mode 100644 index 000000000..8999b5719 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/child.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/child_base_albedo.png b/client/assets/characters/clothing/swim_trunks/child_base_albedo.png new file mode 100644 index 000000000..23c9aeb33 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/child_child_base_albedo.png b/client/assets/characters/clothing/swim_trunks/child_child_base_albedo.png new file mode 100644 index 000000000..23c9aeb33 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/child_child_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/child_mask.png b/client/assets/characters/clothing/swim_trunks/child_mask.png new file mode 100644 index 000000000..d2717e767 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/child_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/coverage.json b/client/assets/characters/clothing/swim_trunks/coverage.json new file mode 100644 index 000000000..aa1db5d24 --- /dev/null +++ b/client/assets/characters/clothing/swim_trunks/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["hips", "leg_upper_l", "leg_upper_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/swim_trunks/heavy_f.glb b/client/assets/characters/clothing/swim_trunks/heavy_f.glb new file mode 100644 index 000000000..b7dff5a4d Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/heavy_f.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/heavy_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/heavy_f_base_albedo.png new file mode 100644 index 000000000..c3c437e30 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/heavy_f_heavy_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/heavy_f_heavy_f_base_albedo.png new file mode 100644 index 000000000..c3c437e30 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/heavy_f_heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/heavy_f_mask.png b/client/assets/characters/clothing/swim_trunks/heavy_f_mask.png new file mode 100644 index 000000000..084923baa Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/heavy_m.glb b/client/assets/characters/clothing/swim_trunks/heavy_m.glb new file mode 100644 index 000000000..0de0ecb44 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/heavy_m.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/heavy_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/heavy_m_base_albedo.png new file mode 100644 index 000000000..086446ebe Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/heavy_m_heavy_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/heavy_m_heavy_m_base_albedo.png new file mode 100644 index 000000000..086446ebe Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/heavy_m_heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/heavy_m_mask.png b/client/assets/characters/clothing/swim_trunks/heavy_m_mask.png new file mode 100644 index 000000000..cb64908a6 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/muscular_f.glb b/client/assets/characters/clothing/swim_trunks/muscular_f.glb new file mode 100644 index 000000000..b46eb9e3c Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/muscular_f.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/muscular_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/muscular_f_base_albedo.png new file mode 100644 index 000000000..7e0d596ef Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/muscular_f_mask.png b/client/assets/characters/clothing/swim_trunks/muscular_f_mask.png new file mode 100644 index 000000000..de7216a7d Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/muscular_f_muscular_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/muscular_f_muscular_f_base_albedo.png new file mode 100644 index 000000000..7e0d596ef Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/muscular_f_muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/muscular_m.glb b/client/assets/characters/clothing/swim_trunks/muscular_m.glb new file mode 100644 index 000000000..4fb04aee6 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/muscular_m.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/muscular_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/muscular_m_base_albedo.png new file mode 100644 index 000000000..ebcfe1adb Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/muscular_m_mask.png b/client/assets/characters/clothing/swim_trunks/muscular_m_mask.png new file mode 100644 index 000000000..17ce605f3 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/muscular_m_muscular_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/muscular_m_muscular_m_base_albedo.png new file mode 100644 index 000000000..ebcfe1adb Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/muscular_m_muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/reference_mask.png b/client/assets/characters/clothing/swim_trunks/reference_mask.png new file mode 100644 index 000000000..6dead5f50 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/reference_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/teen_f.glb b/client/assets/characters/clothing/swim_trunks/teen_f.glb new file mode 100644 index 000000000..05781d879 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/teen_f.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/teen_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/teen_f_base_albedo.png new file mode 100644 index 000000000..399188ccc Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/teen_f_mask.png b/client/assets/characters/clothing/swim_trunks/teen_f_mask.png new file mode 100644 index 000000000..373169b85 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/teen_f_teen_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/teen_f_teen_f_base_albedo.png new file mode 100644 index 000000000..399188ccc Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/teen_f_teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/teen_m.glb b/client/assets/characters/clothing/swim_trunks/teen_m.glb new file mode 100644 index 000000000..ebf2ca832 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/teen_m.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/teen_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/teen_m_base_albedo.png new file mode 100644 index 000000000..8fcfb5f6e Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/teen_m_mask.png b/client/assets/characters/clothing/swim_trunks/teen_m_mask.png new file mode 100644 index 000000000..5f1707bc7 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/teen_m_teen_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/teen_m_teen_m_base_albedo.png new file mode 100644 index 000000000..8fcfb5f6e Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/teen_m_teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/thin_f.glb b/client/assets/characters/clothing/swim_trunks/thin_f.glb new file mode 100644 index 000000000..138f2cfab Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/thin_f.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/thin_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/thin_f_base_albedo.png new file mode 100644 index 000000000..0c1e38cbe Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/thin_f_mask.png b/client/assets/characters/clothing/swim_trunks/thin_f_mask.png new file mode 100644 index 000000000..8163a89a3 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/thin_f_thin_f_base_albedo.png b/client/assets/characters/clothing/swim_trunks/thin_f_thin_f_base_albedo.png new file mode 100644 index 000000000..0c1e38cbe Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/thin_f_thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/thin_m.glb b/client/assets/characters/clothing/swim_trunks/thin_m.glb new file mode 100644 index 000000000..3e3a78da4 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/thin_m.glb differ diff --git a/client/assets/characters/clothing/swim_trunks/thin_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/thin_m_base_albedo.png new file mode 100644 index 000000000..ae8daeda3 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swim_trunks/thin_m_mask.png b/client/assets/characters/clothing/swim_trunks/thin_m_mask.png new file mode 100644 index 000000000..bbc599557 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/swim_trunks/thin_m_thin_m_base_albedo.png b/client/assets/characters/clothing/swim_trunks/thin_m_thin_m_base_albedo.png new file mode 100644 index 000000000..ae8daeda3 Binary files /dev/null and b/client/assets/characters/clothing/swim_trunks/thin_m_thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/average_f.glb b/client/assets/characters/clothing/swimsuit_onepiece/average_f.glb new file mode 100644 index 000000000..08eef5860 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/average_f.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/average_f_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/average_f_base_albedo.png new file mode 100644 index 000000000..a8b52a402 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/average_f_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/average_f_mask.png new file mode 100644 index 000000000..6b116af6f Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/average_f_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/average_m.glb b/client/assets/characters/clothing/swimsuit_onepiece/average_m.glb new file mode 100644 index 000000000..d3c7026bb Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/average_m.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/average_m_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/average_m_base_albedo.png new file mode 100644 index 000000000..39f552ad6 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/average_m_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/average_m_mask.png new file mode 100644 index 000000000..a5adfc937 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/average_m_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/base_albedo.png new file mode 100644 index 000000000..39f552ad6 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/child.glb b/client/assets/characters/clothing/swimsuit_onepiece/child.glb new file mode 100644 index 000000000..423b7e8a9 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/child.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/child_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/child_base_albedo.png new file mode 100644 index 000000000..97368c214 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/child_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/child_mask.png new file mode 100644 index 000000000..1bcf4f32e Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/child_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/coverage.json b/client/assets/characters/clothing/swimsuit_onepiece/coverage.json new file mode 100644 index 000000000..8149307db --- /dev/null +++ b/client/assets/characters/clothing/swimsuit_onepiece/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "torso_upper", "hips"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/swimsuit_onepiece/heavy_f.glb b/client/assets/characters/clothing/swimsuit_onepiece/heavy_f.glb new file mode 100644 index 000000000..a6ea83b7c Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/heavy_f.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/heavy_f_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/heavy_f_base_albedo.png new file mode 100644 index 000000000..953364059 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/heavy_f_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/heavy_f_mask.png new file mode 100644 index 000000000..80f1519db Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/heavy_m.glb b/client/assets/characters/clothing/swimsuit_onepiece/heavy_m.glb new file mode 100644 index 000000000..75a58393d Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/heavy_m.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/heavy_m_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/heavy_m_base_albedo.png new file mode 100644 index 000000000..0d2df2416 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/heavy_m_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/heavy_m_mask.png new file mode 100644 index 000000000..8ae928a24 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/muscular_f.glb b/client/assets/characters/clothing/swimsuit_onepiece/muscular_f.glb new file mode 100644 index 000000000..c8f799156 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/muscular_f.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/muscular_f_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/muscular_f_base_albedo.png new file mode 100644 index 000000000..c7c9b0c91 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/muscular_f_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/muscular_f_mask.png new file mode 100644 index 000000000..c1228faf6 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/muscular_m.glb b/client/assets/characters/clothing/swimsuit_onepiece/muscular_m.glb new file mode 100644 index 000000000..6ac90c73a Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/muscular_m.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/muscular_m_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/muscular_m_base_albedo.png new file mode 100644 index 000000000..51e232c63 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/muscular_m_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/muscular_m_mask.png new file mode 100644 index 000000000..b4c08da36 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/reference_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/reference_mask.png new file mode 100644 index 000000000..a5adfc937 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/reference_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/teen_f.glb b/client/assets/characters/clothing/swimsuit_onepiece/teen_f.glb new file mode 100644 index 000000000..47b3154b4 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/teen_f.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/teen_f_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/teen_f_base_albedo.png new file mode 100644 index 000000000..f7a022048 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/teen_f_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/teen_f_mask.png new file mode 100644 index 000000000..15347f296 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/teen_m.glb b/client/assets/characters/clothing/swimsuit_onepiece/teen_m.glb new file mode 100644 index 000000000..8ff9a252b Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/teen_m.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/teen_m_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/teen_m_base_albedo.png new file mode 100644 index 000000000..f964f3ba6 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/teen_m_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/teen_m_mask.png new file mode 100644 index 000000000..82109d90a Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/thin_f.glb b/client/assets/characters/clothing/swimsuit_onepiece/thin_f.glb new file mode 100644 index 000000000..fbe9209fc Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/thin_f.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/thin_f_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/thin_f_base_albedo.png new file mode 100644 index 000000000..ca9eb2891 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/thin_f_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/thin_f_mask.png new file mode 100644 index 000000000..de1f67a14 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/thin_m.glb b/client/assets/characters/clothing/swimsuit_onepiece/thin_m.glb new file mode 100644 index 000000000..7d57ef349 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/thin_m.glb differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/thin_m_base_albedo.png b/client/assets/characters/clothing/swimsuit_onepiece/thin_m_base_albedo.png new file mode 100644 index 000000000..cfa22deb6 Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/swimsuit_onepiece/thin_m_mask.png b/client/assets/characters/clothing/swimsuit_onepiece/thin_m_mask.png new file mode 100644 index 000000000..b0a88174e Binary files /dev/null and b/client/assets/characters/clothing/swimsuit_onepiece/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/average_f.glb b/client/assets/characters/clothing/tank_top/average_f.glb new file mode 100644 index 000000000..dce53d0f3 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/average_f.glb differ diff --git a/client/assets/characters/clothing/tank_top/average_f_base_albedo.png b/client/assets/characters/clothing/tank_top/average_f_base_albedo.png new file mode 100644 index 000000000..f97bc810f Binary files /dev/null and b/client/assets/characters/clothing/tank_top/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/average_f_mask.png b/client/assets/characters/clothing/tank_top/average_f_mask.png new file mode 100644 index 000000000..6e43bb837 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/average_f_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/average_m.glb b/client/assets/characters/clothing/tank_top/average_m.glb new file mode 100644 index 000000000..6cbd70ce2 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/average_m.glb differ diff --git a/client/assets/characters/clothing/tank_top/average_m_base_albedo.png b/client/assets/characters/clothing/tank_top/average_m_base_albedo.png new file mode 100644 index 000000000..9152ebb05 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/average_m_mask.png b/client/assets/characters/clothing/tank_top/average_m_mask.png new file mode 100644 index 000000000..f7c786f3b Binary files /dev/null and b/client/assets/characters/clothing/tank_top/average_m_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/base_albedo.png b/client/assets/characters/clothing/tank_top/base_albedo.png new file mode 100644 index 000000000..9152ebb05 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/child.glb b/client/assets/characters/clothing/tank_top/child.glb new file mode 100644 index 000000000..43ea354a7 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/child.glb differ diff --git a/client/assets/characters/clothing/tank_top/child_base_albedo.png b/client/assets/characters/clothing/tank_top/child_base_albedo.png new file mode 100644 index 000000000..72dc7ede3 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/child_mask.png b/client/assets/characters/clothing/tank_top/child_mask.png new file mode 100644 index 000000000..8932f88a1 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/child_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/coverage.json b/client/assets/characters/clothing/tank_top/coverage.json new file mode 100644 index 000000000..68800cd0f --- /dev/null +++ b/client/assets/characters/clothing/tank_top/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "torso_upper"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/tank_top/heavy_f.glb b/client/assets/characters/clothing/tank_top/heavy_f.glb new file mode 100644 index 000000000..b2ba9266e Binary files /dev/null and b/client/assets/characters/clothing/tank_top/heavy_f.glb differ diff --git a/client/assets/characters/clothing/tank_top/heavy_f_base_albedo.png b/client/assets/characters/clothing/tank_top/heavy_f_base_albedo.png new file mode 100644 index 000000000..100ebe613 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/heavy_f_mask.png b/client/assets/characters/clothing/tank_top/heavy_f_mask.png new file mode 100644 index 000000000..84faaeab8 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/heavy_m.glb b/client/assets/characters/clothing/tank_top/heavy_m.glb new file mode 100644 index 000000000..aba40aaa3 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/heavy_m.glb differ diff --git a/client/assets/characters/clothing/tank_top/heavy_m_base_albedo.png b/client/assets/characters/clothing/tank_top/heavy_m_base_albedo.png new file mode 100644 index 000000000..26f990dc9 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/heavy_m_mask.png b/client/assets/characters/clothing/tank_top/heavy_m_mask.png new file mode 100644 index 000000000..8ce1107ab Binary files /dev/null and b/client/assets/characters/clothing/tank_top/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/muscular_f.glb b/client/assets/characters/clothing/tank_top/muscular_f.glb new file mode 100644 index 000000000..ee0d0ca21 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/muscular_f.glb differ diff --git a/client/assets/characters/clothing/tank_top/muscular_f_base_albedo.png b/client/assets/characters/clothing/tank_top/muscular_f_base_albedo.png new file mode 100644 index 000000000..36ac8429a Binary files /dev/null and b/client/assets/characters/clothing/tank_top/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/muscular_f_mask.png b/client/assets/characters/clothing/tank_top/muscular_f_mask.png new file mode 100644 index 000000000..fda94b42a Binary files /dev/null and b/client/assets/characters/clothing/tank_top/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/muscular_m.glb b/client/assets/characters/clothing/tank_top/muscular_m.glb new file mode 100644 index 000000000..b55cdea04 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/muscular_m.glb differ diff --git a/client/assets/characters/clothing/tank_top/muscular_m_base_albedo.png b/client/assets/characters/clothing/tank_top/muscular_m_base_albedo.png new file mode 100644 index 000000000..ba2941c5b Binary files /dev/null and b/client/assets/characters/clothing/tank_top/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/muscular_m_mask.png b/client/assets/characters/clothing/tank_top/muscular_m_mask.png new file mode 100644 index 000000000..451ec35dc Binary files /dev/null and b/client/assets/characters/clothing/tank_top/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/reference_mask.png b/client/assets/characters/clothing/tank_top/reference_mask.png new file mode 100644 index 000000000..f7c786f3b Binary files /dev/null and b/client/assets/characters/clothing/tank_top/reference_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/teen_f.glb b/client/assets/characters/clothing/tank_top/teen_f.glb new file mode 100644 index 000000000..b6f2c244b Binary files /dev/null and b/client/assets/characters/clothing/tank_top/teen_f.glb differ diff --git a/client/assets/characters/clothing/tank_top/teen_f_base_albedo.png b/client/assets/characters/clothing/tank_top/teen_f_base_albedo.png new file mode 100644 index 000000000..b8a262ef8 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/teen_f_mask.png b/client/assets/characters/clothing/tank_top/teen_f_mask.png new file mode 100644 index 000000000..b2df00eed Binary files /dev/null and b/client/assets/characters/clothing/tank_top/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/teen_m.glb b/client/assets/characters/clothing/tank_top/teen_m.glb new file mode 100644 index 000000000..82983e369 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/teen_m.glb differ diff --git a/client/assets/characters/clothing/tank_top/teen_m_base_albedo.png b/client/assets/characters/clothing/tank_top/teen_m_base_albedo.png new file mode 100644 index 000000000..9e50a1cc6 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/teen_m_mask.png b/client/assets/characters/clothing/tank_top/teen_m_mask.png new file mode 100644 index 000000000..4255e774d Binary files /dev/null and b/client/assets/characters/clothing/tank_top/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/thin_f.glb b/client/assets/characters/clothing/tank_top/thin_f.glb new file mode 100644 index 000000000..ac53180bc Binary files /dev/null and b/client/assets/characters/clothing/tank_top/thin_f.glb differ diff --git a/client/assets/characters/clothing/tank_top/thin_f_base_albedo.png b/client/assets/characters/clothing/tank_top/thin_f_base_albedo.png new file mode 100644 index 000000000..14ae32022 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/thin_f_mask.png b/client/assets/characters/clothing/tank_top/thin_f_mask.png new file mode 100644 index 000000000..fdf28e876 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/tank_top/thin_m.glb b/client/assets/characters/clothing/tank_top/thin_m.glb new file mode 100644 index 000000000..2c9db1add Binary files /dev/null and b/client/assets/characters/clothing/tank_top/thin_m.glb differ diff --git a/client/assets/characters/clothing/tank_top/thin_m_base_albedo.png b/client/assets/characters/clothing/tank_top/thin_m_base_albedo.png new file mode 100644 index 000000000..d9a42c193 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/thin_m_mask.png b/client/assets/characters/clothing/tank_top/thin_m_mask.png new file mode 100644 index 000000000..ccd170c80 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/average_f.glb b/client/assets/characters/clothing/track_jacket/average_f.glb new file mode 100644 index 000000000..a6f018952 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/average_f.glb differ diff --git a/client/assets/characters/clothing/track_jacket/average_f_base_albedo.png b/client/assets/characters/clothing/track_jacket/average_f_base_albedo.png new file mode 100644 index 000000000..b5a646864 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/average_f_mask.png b/client/assets/characters/clothing/track_jacket/average_f_mask.png new file mode 100644 index 000000000..69d2eeaba Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/average_f_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/average_m.glb b/client/assets/characters/clothing/track_jacket/average_m.glb new file mode 100644 index 000000000..29f1f48a7 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/average_m.glb differ diff --git a/client/assets/characters/clothing/track_jacket/average_m_base_albedo.png b/client/assets/characters/clothing/track_jacket/average_m_base_albedo.png new file mode 100644 index 000000000..449630600 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/average_m_mask.png b/client/assets/characters/clothing/track_jacket/average_m_mask.png new file mode 100644 index 000000000..2decfdc5a Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/average_m_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/base_albedo.png b/client/assets/characters/clothing/track_jacket/base_albedo.png new file mode 100644 index 000000000..449630600 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/child.glb b/client/assets/characters/clothing/track_jacket/child.glb new file mode 100644 index 000000000..354fab6ed Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/child.glb differ diff --git a/client/assets/characters/clothing/track_jacket/child_base_albedo.png b/client/assets/characters/clothing/track_jacket/child_base_albedo.png new file mode 100644 index 000000000..7168819fa Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/child_mask.png b/client/assets/characters/clothing/track_jacket/child_mask.png new file mode 100644 index 000000000..a649a99ab Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/child_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/coverage.json b/client/assets/characters/clothing/track_jacket/coverage.json new file mode 100644 index 000000000..9c2d866d2 --- /dev/null +++ b/client/assets/characters/clothing/track_jacket/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/track_jacket/heavy_f.glb b/client/assets/characters/clothing/track_jacket/heavy_f.glb new file mode 100644 index 000000000..8bfbaaced Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/heavy_f.glb differ diff --git a/client/assets/characters/clothing/track_jacket/heavy_f_base_albedo.png b/client/assets/characters/clothing/track_jacket/heavy_f_base_albedo.png new file mode 100644 index 000000000..2d0e77c83 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/heavy_f_mask.png b/client/assets/characters/clothing/track_jacket/heavy_f_mask.png new file mode 100644 index 000000000..55e6e0535 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/heavy_m.glb b/client/assets/characters/clothing/track_jacket/heavy_m.glb new file mode 100644 index 000000000..dd4299e8f Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/heavy_m.glb differ diff --git a/client/assets/characters/clothing/track_jacket/heavy_m_base_albedo.png b/client/assets/characters/clothing/track_jacket/heavy_m_base_albedo.png new file mode 100644 index 000000000..76360ae35 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/heavy_m_mask.png b/client/assets/characters/clothing/track_jacket/heavy_m_mask.png new file mode 100644 index 000000000..1bd80afa3 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/muscular_f.glb b/client/assets/characters/clothing/track_jacket/muscular_f.glb new file mode 100644 index 000000000..ea9fb8ea2 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/muscular_f.glb differ diff --git a/client/assets/characters/clothing/track_jacket/muscular_f_base_albedo.png b/client/assets/characters/clothing/track_jacket/muscular_f_base_albedo.png new file mode 100644 index 000000000..0623c082c Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/muscular_f_mask.png b/client/assets/characters/clothing/track_jacket/muscular_f_mask.png new file mode 100644 index 000000000..2a071c263 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/muscular_m.glb b/client/assets/characters/clothing/track_jacket/muscular_m.glb new file mode 100644 index 000000000..063795690 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/muscular_m.glb differ diff --git a/client/assets/characters/clothing/track_jacket/muscular_m_base_albedo.png b/client/assets/characters/clothing/track_jacket/muscular_m_base_albedo.png new file mode 100644 index 000000000..7d7514d5f Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/muscular_m_mask.png b/client/assets/characters/clothing/track_jacket/muscular_m_mask.png new file mode 100644 index 000000000..2b262ddbc Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/reference_mask.png b/client/assets/characters/clothing/track_jacket/reference_mask.png new file mode 100644 index 000000000..2decfdc5a Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/reference_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/teen_f.glb b/client/assets/characters/clothing/track_jacket/teen_f.glb new file mode 100644 index 000000000..cb00ec4e6 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/teen_f.glb differ diff --git a/client/assets/characters/clothing/track_jacket/teen_f_base_albedo.png b/client/assets/characters/clothing/track_jacket/teen_f_base_albedo.png new file mode 100644 index 000000000..fb0c34578 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/teen_f_mask.png b/client/assets/characters/clothing/track_jacket/teen_f_mask.png new file mode 100644 index 000000000..726d3f136 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/teen_m.glb b/client/assets/characters/clothing/track_jacket/teen_m.glb new file mode 100644 index 000000000..aa41ca0eb Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/teen_m.glb differ diff --git a/client/assets/characters/clothing/track_jacket/teen_m_base_albedo.png b/client/assets/characters/clothing/track_jacket/teen_m_base_albedo.png new file mode 100644 index 000000000..5bdf626ab Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/teen_m_mask.png b/client/assets/characters/clothing/track_jacket/teen_m_mask.png new file mode 100644 index 000000000..7105c433e Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/thin_f.glb b/client/assets/characters/clothing/track_jacket/thin_f.glb new file mode 100644 index 000000000..4b2fdfa44 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/thin_f.glb differ diff --git a/client/assets/characters/clothing/track_jacket/thin_f_base_albedo.png b/client/assets/characters/clothing/track_jacket/thin_f_base_albedo.png new file mode 100644 index 000000000..ab2cfcc07 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/thin_f_mask.png b/client/assets/characters/clothing/track_jacket/thin_f_mask.png new file mode 100644 index 000000000..f71aa302a Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/track_jacket/thin_m.glb b/client/assets/characters/clothing/track_jacket/thin_m.glb new file mode 100644 index 000000000..36585c64a Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/thin_m.glb differ diff --git a/client/assets/characters/clothing/track_jacket/thin_m_base_albedo.png b/client/assets/characters/clothing/track_jacket/thin_m_base_albedo.png new file mode 100644 index 000000000..6f57e685c Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/track_jacket/thin_m_mask.png b/client/assets/characters/clothing/track_jacket/thin_m_mask.png new file mode 100644 index 000000000..5bfcc3074 Binary files /dev/null and b/client/assets/characters/clothing/track_jacket/thin_m_mask.png differ diff --git a/client/assets/characters/manifest.json b/client/assets/characters/manifest.json index 0ce29da1b..2469df06c 100644 --- a/client/assets/characters/manifest.json +++ b/client/assets/characters/manifest.json @@ -137,6 +137,209 @@ "#d18c2e", "#d9dee0" ] + }, + "tank_top": { + "slot": "torso", + "style": "modern", + "logo_capable": true, + "regions": { + "R": "collar_band", + "G": "body", + "B": "armhole_trim" + }, + "default_tints": [ + "#f5f2eb", + "#e8693b", + "#f5f2eb", + "#ffffff" + ] + }, + "sweater_modern": { + "slot": "torso", + "style": "modern", + "logo_capable": false, + "regions": { + "R": "collar", + "G": "body", + "B": "cuffs_hem" + }, + "default_tints": [ + "#946b4f", + "#b8916e", + "#946b4f", + "#ffffff" + ] + }, + "track_jacket": { + "slot": "torso", + "style": "sport", + "logo_capable": true, + "multi_region": true, + "regions": { + "R": "stand_collar", + "G": "body", + "B": "sleeve_stripes", + "A": "cuffs_hem_band" + }, + "default_tints": [ + "#ebedf2", + "#294d9e", + "#f0f2f7", + "#1a2652" + ] + }, + "joggers_modern": { + "slot": "legs", + "style": "modern", + "logo_capable": false + }, + "cargo_pants": { + "slot": "legs", + "style": "modern", + "logo_capable": false + }, + "swim_trunks": { + "slot": "legs", + "style": "modern", + "logo_capable": false + }, + "swimsuit_onepiece": { + "swimsuit_onepiece": { + "slot": "full_body", + "style": "modern", + "logo_capable": false, + "regions": { + "R": "straps_trim", + "G": "body", + "B": "side_blocks" + }, + "default_tints": [ + "#f2f0e8", + "#e34a33", + "#1f7a8c", + "#ffffff" + ] + } + }, + "sneakers_modern": { + "slot": "feet", + "style": "modern", + "logo_capable": false, + "multi_region": true, + "regions": { + "R": "sole", + "G": "upper", + "B": "laces_trim" + }, + "default_tints": [ + "#bdc0c4", + "#ebebee", + "#e1e1e6" + ] + }, + "shoes_formal": { + "slot": "feet", + "style": "formal", + "logo_capable": false, + "regions": { + "R": "sole", + "G": "upper", + "B": "toe_cap" + }, + "default_tints": [ + [ + 0.1, + 0.1, + 0.11, + 1 + ], + [ + 0.07, + 0.07, + 0.08, + 1 + ], + [ + 0.07, + 0.07, + 0.08, + 1 + ], + [ + 1, + 1, + 1, + 1 + ] + ] + }, + "boots_modern": { + "slot": "feet", + "style": "modern", + "logo_capable": false, + "regions": { + "R": "sole", + "G": "upper_shaft", + "B": "laces_eyelets" + }, + "default_tints": [ + "#292624", + "#704a2e", + "#382e26", + "#ffffff" + ] + }, + "slides": { + "slot": "feet", + "style": "modern", + "logo_capable": false, + "regions": { + "R": "sole", + "G": "strap" + }, + "default_tints": [ + [ + 0.82, + 0.83, + 0.85, + 1 + ], + [ + 0.93, + 0.35, + 0.2, + 1 + ], + [ + 1, + 1, + 1, + 1 + ], + [ + 1, + 1, + 1, + 1 + ] + ] + }, + "parka_thrds": { + "slot": "torso", + "style": "modern", + "logo_capable": true, + "regions": { + "R": "hood_roll_collar", + "G": "body", + "B": "sleeves", + "A": "pocket_quilt_trim" + }, + "default_tints": [ + "#384257", + "#4d5973", + "#455269", + "#788599" + ] } }, "accessories": [] diff --git a/client/tools/garment_preview/load_check.gd b/client/tools/garment_preview/load_check.gd new file mode 100644 index 000000000..b2213e9f0 --- /dev/null +++ b/client/tools/garment_preview/load_check.gd @@ -0,0 +1,49 @@ +extends SceneTree +## Headless load check: verify garment GLB scenes load and their albedo +## texture resolves. Usage: +## godot4 --path client --headless --script res://tools/garment_preview/load_check.gd +## Temporary tool script — safe to delete after wave-2 integration. + +const ITEMS := [ + "res://assets/characters/clothing/jeans_modern/average_m.glb", + "res://assets/characters/clothing/swim_trunks/average_m.glb", + "res://assets/characters/clothing/swim_trunks/heavy_m.glb", +] + + +func _init() -> void: + for path in ITEMS: + _check(path) + quit(0) + + +func _check(path: String) -> void: + if not ResourceLoader.exists(path): + print("loadcheck: MISSING ", path) + return + var ps := load(path) as PackedScene + if ps == null: + print("loadcheck: LOAD FAILED ", path) + return + var inst := ps.instantiate() + var found := false + var stack: Array[Node] = [inst] + while not stack.is_empty(): + var n: Node = stack.pop_back() + for c in n.get_children(): + stack.append(c) + if n is MeshInstance3D: + var mi := n as MeshInstance3D + if mi.mesh == null: + continue + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + var tex := (m as BaseMaterial3D).albedo_texture + if tex != null: + print("loadcheck: OK ", path, " albedo=", tex.get_class(), + " ", tex.get_width(), "x", tex.get_height()) + found = true + if not found: + print("loadcheck: NO ALBEDO ", path) + inst.free() diff --git a/client/tools/garment_preview/preview_boots_modern.gd b/client/tools/garment_preview/preview_boots_modern.gd new file mode 100644 index 000000000..40aa11f24 --- /dev/null +++ b/client/tools/garment_preview/preview_boots_modern.gd @@ -0,0 +1,198 @@ +extends Node3D +## boots_modern preview capture (T-1089 wave 2, footwear set) — parameterized +## copy of preview_swim_trunks.gd for a FOOTWEAR 3-region garment: per-body +## region mask (reference fallback), no logo, camera framed low on the feet + +## boot shafts so the chunky sole, lace bars/eyelets and the shaft rim read. +## Front + 3/4 Walk renders. Temporary tool scene — safe to delete after +## captures. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "boots_modern" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/boots_modern" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Brown/black work-street default (spec) — matches the manifest default_tints +# so tinted and untinted renders agree: sole near-black rubber, upper work +# brown, laces/eyelets dark. +const TINT_SOLE := Color(0.16, 0.15, 0.14) # R region, near-black rubber +const TINT_UPPER := Color(0.44, 0.29, 0.18) # G region, work brown leather +const TINT_LACES := Color(0.22, 0.18, 0.15) # B region, dark laces + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "boots_modern") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/boots_modern") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 40.0 + add_child(_cam) + # Frame low on the feet + boot shafts (rim ~0.15-0.22 m): sole slab, lace + # panel and shaft rim must all read; slight top-down angle shows the sole + # lip. Distance leaves room for the Walk stride toward the camera. + _cam.position = Vector3(0.0, 0.42, 1.35) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.16, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body region mask (offset-shell route); reference fallback mirrors + # the runtime compositor. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a mid-stride Walk pose so the sole, laces and shaft rim read + # under motion (front foot planted, back heel lifting). + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task deliverable: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_SOLE) + mat.set_shader_parameter("tint_1", TINT_UPPER) + mat.set_shader_parameter("tint_2", TINT_LACES) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_boots_modern.tscn b/client/tools/garment_preview/preview_boots_modern.tscn new file mode 100644 index 000000000..be50abe92 --- /dev/null +++ b/client/tools/garment_preview/preview_boots_modern.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_boots_modern.gd" id="1_prevbt"] + +[node name="GarmentPreviewBootsModern" type="Node3D"] +script = ExtResource("1_prevbt") diff --git a/client/tools/garment_preview/preview_cargo_pants.gd b/client/tools/garment_preview/preview_cargo_pants.gd new file mode 100644 index 000000000..e6f81aac1 --- /dev/null +++ b/client/tools/garment_preview/preview_cargo_pants.gd @@ -0,0 +1,195 @@ +extends Node3D +## cargo_pants preview capture (T-1089 wave 2) — parameterized copy of +## preview_shorts_modern.gd for a LOWER-BODY 3-region garment: per-body region +## mask (reference fallback), no logo, camera framed on the full leg span +## (waistband to ankle hem) so the thigh cargo pockets and straight fit read. +## Front + 3/4 Walk renders. Temporary tool scene — safe to delete after +## captures. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "cargo_pants" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/cargo_pants" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Utility palette: charcoal waistband, olive legs, grey-olive pockets+flaps +# (tint_2 demonstrates the B region recolors independently). tint_3 unused. +const TINT_WAISTBAND := Color(0.13, 0.14, 0.16) # R region +const TINT_LEGS := Color(0.45, 0.47, 0.38) # G region +const TINT_POCKETS := Color(0.36, 0.37, 0.33) # B region + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "cargo_pants") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/cargo_pants") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame the full leg span (waistband ~1.03 down to the ankle hem) so the + # thigh cargo pockets and the straight-fit silhouette both read. + _cam.position = Vector3(0.0, 0.72, 2.05) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.60, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body region mask (offset-shell route); reference fallback mirrors + # the runtime compositor. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a mid-stride Walk pose so pockets + hem read under motion. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task deliverable: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_WAISTBAND) + mat.set_shader_parameter("tint_1", TINT_LEGS) + mat.set_shader_parameter("tint_2", TINT_POCKETS) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_cargo_pants.tscn b/client/tools/garment_preview/preview_cargo_pants.tscn new file mode 100644 index 000000000..1a651dc28 --- /dev/null +++ b/client/tools/garment_preview/preview_cargo_pants.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_cargo_pants.gd" id="1_prevcargo"] + +[node name="GarmentPreviewCargoPants" type="Node3D"] +script = ExtResource("1_prevcargo") diff --git a/client/tools/garment_preview/preview_joggers_modern.gd b/client/tools/garment_preview/preview_joggers_modern.gd new file mode 100644 index 000000000..c6283255c --- /dev/null +++ b/client/tools/garment_preview/preview_joggers_modern.gd @@ -0,0 +1,193 @@ +extends Node3D +## joggers_modern preview capture (T-1089 wave 2) — parameterized copy of +## preview_shorts_modern.gd for a LOWER-BODY 4-region garment: per-body region +## mask (reference fallback), no logo, camera framed waist to feet so the +## waistband, side stripe and ankle cuffs all read, front + 3/4 Walk renders. +## Temporary tool scene — safe to delete after captures. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "joggers_modern" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/joggers_modern" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Athletic palette: charcoal band + cuffs, slate legs, white track stripe. +const TINT_WAISTBAND := Color(0.16, 0.17, 0.19) # R region +const TINT_LEGS := Color(0.30, 0.34, 0.42) # G region +const TINT_STRIPE := Color(0.92, 0.93, 0.95) # B region +const TINT_CUFFS := Color(0.16, 0.17, 0.19) # A region + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "joggers_modern") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/joggers_modern") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame waist -> feet so waistband, stripe run and ankle cuffs all read. + _cam.position = Vector3(0.0, 0.62, 2.2) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.60, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body region mask (offset-shell route); reference fallback mirrors + # the runtime compositor. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a mid-stride Walk pose so taper + cuffs read under motion. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task deliverable: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_WAISTBAND) + mat.set_shader_parameter("tint_1", TINT_LEGS) + mat.set_shader_parameter("tint_2", TINT_STRIPE) + mat.set_shader_parameter("tint_3", TINT_CUFFS) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_joggers_modern.tscn b/client/tools/garment_preview/preview_joggers_modern.tscn new file mode 100644 index 000000000..abbe791f8 --- /dev/null +++ b/client/tools/garment_preview/preview_joggers_modern.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_joggers_modern.gd" id="1_prevj"] + +[node name="GarmentPreviewJoggers" type="Node3D"] +script = ExtResource("1_prevj") diff --git a/client/tools/garment_preview/preview_parka_thrds.gd b/client/tools/garment_preview/preview_parka_thrds.gd new file mode 100644 index 000000000..a2fe833c2 --- /dev/null +++ b/client/tools/garment_preview/preview_parka_thrds.gd @@ -0,0 +1,209 @@ +extends Node3D +## parka_thrds preview capture (T-1089 wave 2 — first canon-branded garment). +## +## Torso-family variant of preview_scene.gd for the hip-length thrds parka: +## collar-to-hip camera framing, per-body region mask preference +## (_mask.png, per-body offset-shell convention) and the deep blue-grey +## cold-weather default palette on the four recolor zones: +## R hood roll + collar = darker blue-grey +## G body = deep blue-grey +## B sleeves = deep blue-grey (a shade apart) +## A pocket / quilt trim = pale steel accent +## The left-breast chest patch carries the thrds logo (canon brand, +## wiki/star-systems/GJ-475 — Braemar fiber cooperative). +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "parka_thrds" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/parka_thrds" +## GARMENT_PREVIEW_LOGO default thrds +## +## Run: godot4 --path client --rendering-driver opengl3 \ +## res://tools/garment_preview/preview_parka_thrds.tscn + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const LOGO_PATH := "res://assets/characters/logos/thrds.png" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Deep blue-grey cold-weather default palette (parka_thrds spec). +const TINT_ROLL := Color(0.22, 0.26, 0.34) # R: hood roll + collar +const TINT_BODY := Color(0.30, 0.35, 0.45) # G: body +const TINT_SLEEVES := Color(0.27, 0.32, 0.41) # B: sleeves +const TINT_TRIM := Color(0.47, 0.52, 0.60) # A: pocket / quilt trim + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "parka_thrds") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/parka_thrds") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Collar-to-hip framing — the parka runs from the hood roll to the hip hem. + _cam.position = Vector3(0.0, 1.22, 2.4) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 1.12, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body mask first (offset-shell per-body convention), then fallback. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + var logo_path := _env("GARMENT_PREVIEW_LOGO", LOGO_PATH) + var logo := _try_load(logo_path) + for mi in meshes: + _apply_garment_shader(mi, shader, mask, logo) + + # Freeze mid-walk so the parka reads under a real pose. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 — the two requested preview angles. + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D, logo: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + if logo: + mat.set_shader_parameter("logo_tex", logo) + mat.set_shader_parameter("logo_enabled", 1.0) + mat.set_shader_parameter("tint_0", TINT_ROLL) + mat.set_shader_parameter("tint_1", TINT_BODY) + mat.set_shader_parameter("tint_2", TINT_SLEEVES) + mat.set_shader_parameter("tint_3", TINT_TRIM) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_parka_thrds.tscn b/client/tools/garment_preview/preview_parka_thrds.tscn new file mode 100644 index 000000000..a66f2d35b --- /dev/null +++ b/client/tools/garment_preview/preview_parka_thrds.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_parka_thrds.gd" id="1_prevp"] + +[node name="ParkaThrdsPreview" type="Node3D"] +script = ExtResource("1_prevp") diff --git a/client/tools/garment_preview/preview_shoes_formal.gd b/client/tools/garment_preview/preview_shoes_formal.gd new file mode 100644 index 000000000..ea57fc323 --- /dev/null +++ b/client/tools/garment_preview/preview_shoes_formal.gd @@ -0,0 +1,198 @@ +extends Node3D +## shoes_formal preview capture (T-1089 wave 2, footwear set) — parameterized +## copy of preview_swim_trunks.gd for a FOOTWEAR 3-region garment: per-body +## region mask (reference fallback), no logo, camera framed low on the feet so +## the thin flat sole, painted toe-cap line and topline stitch read. Black +## default tints (manifest defaults) — identity carried by painted-seam luma. +## Front + 3/4 Walk renders. Temporary tool scene — safe to delete after +## captures. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "shoes_formal" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/shoes_formal" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(1024, 768) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Black formal default (matches the manifest default_tints): sole a hair +# lighter than the upper so the welt line reads; toe cap same as upper — the +# painted cap-line stitch carries the boundary, not a colour step. +const TINT_SOLE := Color(0.10, 0.10, 0.11) # R region +const TINT_UPPER := Color(0.07, 0.07, 0.08) # G region +const TINT_TOECAP := Color(0.07, 0.07, 0.08) # B region + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "shoes_formal") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/shoes_formal") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 40.0 + add_child(_cam) + # Frame the feet + lower shin from slightly above ground level so the + # flat sole, toe-cap line and topline opening all read. + _cam.position = Vector3(0.0, 0.28, 0.85) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.09, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body region mask (offset-shell route); reference fallback mirrors + # the runtime compositor. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a mid-stride Walk pose so the sole lift + toe cap read under + # motion (one foot planted, one rolling off the toes). + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task deliverable: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_SOLE) + mat.set_shader_parameter("tint_1", TINT_UPPER) + mat.set_shader_parameter("tint_2", TINT_TOECAP) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_shoes_formal.tscn b/client/tools/garment_preview/preview_shoes_formal.tscn new file mode 100644 index 000000000..116167c55 --- /dev/null +++ b/client/tools/garment_preview/preview_shoes_formal.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_shoes_formal.gd" id="1_prevsf"] + +[node name="GarmentPreviewShoesFormal" type="Node3D"] +script = ExtResource("1_prevsf") diff --git a/client/tools/garment_preview/preview_slides.gd b/client/tools/garment_preview/preview_slides.gd new file mode 100644 index 000000000..a8633ac17 --- /dev/null +++ b/client/tools/garment_preview/preview_slides.gd @@ -0,0 +1,194 @@ +extends Node3D +## slides preview capture (T-1089 wave 2, swim set) — parameterized copy of +## preview_swim_trunks.gd for the FOOTWEAR slot, 2-region garment: per-body +## region mask (reference fallback), no logo, camera framed low on the feet so +## the sole slab and midfoot strap band read. Front + 3/4 Walk renders. +## Temporary tool scene — safe to delete after captures. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "slides" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/slides" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(1024, 768) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Bright swim palette matching the painted albedo (sole=R off-white foam, +# strap=G coral) so tinted and untinted renders agree. +const TINT_SOLE := Color(0.82, 0.83, 0.85) # R region, off-white foam +const TINT_STRAP := Color(0.93, 0.35, 0.20) # G region, bright coral +const TINT_UNUSED := Color.WHITE + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "slides") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/slides") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 40.0 + add_child(_cam) + # Low, close framing on the feet: the sole slab, the strap band and the + # open toe/heel must all read. Slight downward angle shows the footbed. + _cam.position = Vector3(0.0, 0.42, 1.15) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.10, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body region mask (offset-shell route); reference fallback mirrors + # the runtime compositor. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a mid-stride Walk pose so the strap and sole read under motion. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task deliverable: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_SOLE) + mat.set_shader_parameter("tint_1", TINT_STRAP) + mat.set_shader_parameter("tint_2", TINT_UNUSED) + mat.set_shader_parameter("tint_3", TINT_UNUSED) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_slides.tscn b/client/tools/garment_preview/preview_slides.tscn new file mode 100644 index 000000000..02c7243c9 --- /dev/null +++ b/client/tools/garment_preview/preview_slides.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_slides.gd" id="1_prevsl"] + +[node name="GarmentPreviewSlides" type="Node3D"] +script = ExtResource("1_prevsl") diff --git a/client/tools/garment_preview/preview_sneakers_modern.gd b/client/tools/garment_preview/preview_sneakers_modern.gd new file mode 100644 index 000000000..fd50ccb2c --- /dev/null +++ b/client/tools/garment_preview/preview_sneakers_modern.gd @@ -0,0 +1,195 @@ +extends Node3D +## sneakers_modern preview capture (T-1089 wave 2, footwear set) — parameterized +## copy of preview_swim_trunks.gd for a FEET-slot 3-region garment (both feet in +## one garment, peasant_shoes convention): per-body region mask (reference +## fallback), no logo, camera framed LOW on the feet so the sole slab, painted +## laces, toe cap and panel lines read. Front + 3/4 Walk renders. +## Temporary tool scene — safe to delete after captures. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "sneakers_modern" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/sneakers_modern" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(1024, 768) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Everyday default: white/grey trainers (spec) — matches the painted albedo +# tones so tinted and untinted renders agree. +const TINT_SOLE := Color(0.74, 0.75, 0.77) # R region, grey sole slab +const TINT_UPPER := Color(0.92, 0.92, 0.93) # G region, white upper +const TINT_TRIM := Color(0.88, 0.88, 0.90) # B region, laces + trim + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "sneakers_modern") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/sneakers_modern") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 40.0 + add_child(_cam) + # Low frame on the feet: the sole slab, laces, toe box and panel lines + # must read; mid-stride Walk spreads the feet ~0.5 m front-to-back. + _cam.position = Vector3(0.0, 0.28, 1.05) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.09, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body region mask (offset-shell route); reference fallback mirrors + # the runtime compositor. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a mid-stride Walk pose so the sole + toe box read under motion. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task deliverable: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_SOLE) + mat.set_shader_parameter("tint_1", TINT_UPPER) + mat.set_shader_parameter("tint_2", TINT_TRIM) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_sneakers_modern.tscn b/client/tools/garment_preview/preview_sneakers_modern.tscn new file mode 100644 index 000000000..1866f8eec --- /dev/null +++ b/client/tools/garment_preview/preview_sneakers_modern.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_sneakers_modern.gd" id="1_prevsn"] + +[node name="GarmentPreviewSneakersModern" type="Node3D"] +script = ExtResource("1_prevsn") diff --git a/client/tools/garment_preview/preview_sweater_modern.gd b/client/tools/garment_preview/preview_sweater_modern.gd new file mode 100644 index 000000000..b6df70f2c --- /dev/null +++ b/client/tools/garment_preview/preview_sweater_modern.gd @@ -0,0 +1,187 @@ +extends Node3D +## Sweater preview capture (T-1089 wave 2 sweater_modern) — per-garment copy of +## preview_scene.gd (same convention as preview_buttondown_modern.gd): warm +## muted knit palette (deeper collar/cuffs/hem over an oat-rust body), body- +## aware mask loading (_mask.png first, mirroring runtime), no logo +## (logo_capable OFF), front + 3/4 walk yaws only. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "sweater_modern" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, +} + +# Warm muted knit: oat-rust body, deeper clay collar + cuffs/hem bands. +const TINT_COLLAR := Color(0.58, 0.42, 0.31) # R region (crew collar) +const TINT_BODY := Color(0.72, 0.57, 0.43) # G region (body knit) +const TINT_CUFF := Color(0.58, 0.42, 0.31) # B region (cuffs + hem) + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "sweater_modern") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame torso + full sleeves so collar, cuffs and the hip-length hem read. + _cam.position = Vector3(0.0, 1.22, 1.75) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 1.12, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body-authored garment: prefer _mask.png (runtime behaviour). + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a walk pose so the knit reads in motion. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task spec: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_COLLAR) + mat.set_shader_parameter("tint_1", TINT_BODY) + mat.set_shader_parameter("tint_2", TINT_CUFF) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("logo_enabled", 0.0) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_sweater_modern.tscn b/client/tools/garment_preview/preview_sweater_modern.tscn new file mode 100644 index 000000000..f410643a0 --- /dev/null +++ b/client/tools/garment_preview/preview_sweater_modern.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_sweater_modern.gd" id="1_swprev"] + +[node name="SweaterPreview" type="Node3D"] +script = ExtResource("1_swprev") diff --git a/client/tools/garment_preview/preview_swim_trunks.gd b/client/tools/garment_preview/preview_swim_trunks.gd new file mode 100644 index 000000000..34fcb0250 --- /dev/null +++ b/client/tools/garment_preview/preview_swim_trunks.gd @@ -0,0 +1,194 @@ +extends Node3D +## swim_trunks preview capture (T-1089 wave 2, swim set) — parameterized copy of +## preview_shorts_modern.gd for a LOWER-BODY 3-region garment: per-body region +## mask (reference fallback), no logo, camera framed on hips + upper thighs so +## the drawstring, waistband and side panel read. Front + 3/4 Walk renders. +## Temporary tool scene — safe to delete after captures. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "swim_trunks" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/swim_trunks" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Bright saturated swim palette (spec: coral/teal default) — matches the +# painted albedo hues so tinted and untinted renders agree. +const TINT_WAISTBAND := Color(0.87, 0.40, 0.31) # R region, deep coral +const TINT_BODY := Color(0.95, 0.45, 0.35) # G region, coral +const TINT_PANEL := Color(0.13, 0.68, 0.64) # B region, teal + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "swim_trunks") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/swim_trunks") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame hips + upper thighs so the drawstring, waistband and the + # above-the-knee hem read clearly. + _cam.position = Vector3(0.0, 0.90, 1.5) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.80, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body region mask (offset-shell route); reference fallback mirrors + # the runtime compositor. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a mid-stride Walk pose so hem + drawstring read under motion. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task deliverable: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_WAISTBAND) + mat.set_shader_parameter("tint_1", TINT_BODY) + mat.set_shader_parameter("tint_2", TINT_PANEL) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_swim_trunks.tscn b/client/tools/garment_preview/preview_swim_trunks.tscn new file mode 100644 index 000000000..9942eb302 --- /dev/null +++ b/client/tools/garment_preview/preview_swim_trunks.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_swim_trunks.gd" id="1_prevsw"] + +[node name="GarmentPreviewSwimTrunks" type="Node3D"] +script = ExtResource("1_prevsw") diff --git a/client/tools/garment_preview/preview_swimsuit_onepiece.gd b/client/tools/garment_preview/preview_swimsuit_onepiece.gd new file mode 100644 index 000000000..2b5cdc4a3 --- /dev/null +++ b/client/tools/garment_preview/preview_swimsuit_onepiece.gd @@ -0,0 +1,198 @@ +extends Node3D +## One-piece swimsuit preview capture (T-1089 wave 2, swimsuit_onepiece) — +## per-garment copy of preview_scene.gd (same convention as the other wave +## companions): saturated colour-block palette (off-white straps/trim over a +## vivid coral body with deep-teal side blocks, matching the manifest +## default_tints), body-aware mask loading (_mask.png first, mirroring +## runtime), NO logo (garment is not logo-capable — UV2 ships fully parked), +## front + 3/4 walk yaws. Default body is average_f; override via env. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "swimsuit_onepiece" +## GARMENT_PREVIEW_BODY default "average_f" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Saturated colour-block default (matches the manifest default_tints): +# off-white straps + opening trim, vivid coral body, deep teal side blocks. +const TINT_TRIM := Color(0.95, 0.94, 0.91) # R region (straps + edge trim) +const TINT_BODY := Color(0.89, 0.29, 0.20) # G region (main body) +const TINT_BLOCK := Color(0.12, 0.48, 0.55) # B region (side colour-block) + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "swimsuit_onepiece") + var body := _env("GARMENT_PREVIEW_BODY", "average_f") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame the whole garment span (shoulder straps down past the high-cut + # hips) with bare limbs in view — the suit reads by its openings. + _cam.position = Vector3(0.0, 1.02, 2.6) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.95, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + # The male buzzed/regular combo renders mottled on female bodies at + # preview camera range; pick female variants there (cosmetic only). + var fem := body.ends_with("_f") + desc.hair_id = "bob" if fem else "buzzed" + desc.eyebrow_id = "female" if fem else "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body-authored garment: prefer _mask.png (runtime behaviour). + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a walk pose so the suit reads in motion (bare limbs swinging). + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task spec: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_TRIM) + mat.set_shader_parameter("tint_1", TINT_BODY) + mat.set_shader_parameter("tint_2", TINT_BLOCK) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_swimsuit_onepiece.tscn b/client/tools/garment_preview/preview_swimsuit_onepiece.tscn new file mode 100644 index 000000000..65d8b56b6 --- /dev/null +++ b/client/tools/garment_preview/preview_swimsuit_onepiece.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_swimsuit_onepiece.gd" id="1_swsprev"] + +[node name="SwimsuitOnepiecePreview" type="Node3D"] +script = ExtResource("1_swsprev") diff --git a/client/tools/garment_preview/preview_tank_top.gd b/client/tools/garment_preview/preview_tank_top.gd new file mode 100644 index 000000000..52616a743 --- /dev/null +++ b/client/tools/garment_preview/preview_tank_top.gd @@ -0,0 +1,199 @@ +extends Node3D +## Tank top preview capture (T-1089 wave 2, tank_top) — per-garment copy of +## preview_scene.gd (same convention as the other wave companions): bright +## sport palette (off-white collar/armhole bindings over bright orange body, +## matching the manifest default_tints), body-aware mask loading +## (_mask.png first, mirroring runtime), thrds chest logo ON +## (logo_capable garment — proves the UV2 channel), front + 3/4 walk yaws. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "tank_top" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview" +## GARMENT_PREVIEW_LOGO default thrds.png + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const LOGO_PATH := "res://assets/characters/logos/thrds.png" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +# Bright sport default (matches the manifest default_tints for tank_top): +# off-white bindings, bright orange body. +const TINT_COLLAR := Color(0.96, 0.95, 0.92) # R region (collar band) +const TINT_BODY := Color(0.91, 0.41, 0.23) # G region (body) +const TINT_ARMHOLE := Color(0.96, 0.95, 0.92) # B region (armhole trim) + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "tank_top") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame the upper body so scoop/straps/armholes + chest logo read clearly. + _cam.position = Vector3(0.0, 1.25, 1.7) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 1.15, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body-authored garment: prefer _mask.png (runtime behaviour). + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + var logo := _try_load(_env("GARMENT_PREVIEW_LOGO", LOGO_PATH)) + for mi in meshes: + _apply_garment_shader(mi, shader, mask, logo) + + # Freeze on a walk pose so the tank reads in motion (bare arms swinging). + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task spec: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D, logo: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + if logo: + mat.set_shader_parameter("logo_tex", logo) + mat.set_shader_parameter("logo_enabled", 1.0) + mat.set_shader_parameter("tint_0", TINT_COLLAR) + mat.set_shader_parameter("tint_1", TINT_BODY) + mat.set_shader_parameter("tint_2", TINT_ARMHOLE) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_tank_top.tscn b/client/tools/garment_preview/preview_tank_top.tscn new file mode 100644 index 000000000..70b40461a --- /dev/null +++ b/client/tools/garment_preview/preview_tank_top.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_tank_top.gd" id="1_tankprev"] + +[node name="TankTopPreview" type="Node3D"] +script = ExtResource("1_tankprev") diff --git a/client/tools/garment_preview/preview_track_jacket.gd b/client/tools/garment_preview/preview_track_jacket.gd new file mode 100644 index 000000000..55476d814 --- /dev/null +++ b/client/tools/garment_preview/preview_track_jacket.gd @@ -0,0 +1,194 @@ +extends Node3D +## Track jacket preview capture (T-1089 wave 2 — track_jacket) — per-garment +## copy of preview_scene.gd (same convention as preview_buttondown_modern.gd): +## team palette (white stand collar + white sleeve stripes over royal-blue +## body, navy cuff/hem trim), body-aware mask loading (_mask.png first, +## mirroring runtime), thrds chest logo (logo_capable ON, left-breast patch), +## front + 3/4 walk yaws only. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "track_jacket" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview" +## GARMENT_PREVIEW_LOGO default thrds.png + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const LOGO_PATH := "res://assets/characters/logos/thrds.png" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, +} + +# Team kit: white stand collar, royal-blue body, white stripes, navy trim. +const TINT_COLLAR := Color(0.92, 0.93, 0.95) # R region +const TINT_BODY := Color(0.16, 0.30, 0.62) # G region +const TINT_STRIPE := Color(0.94, 0.95, 0.97) # B region (team stripe) +const TINT_TRIM := Color(0.10, 0.15, 0.32) # A region (cuffs + hem band) + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "track_jacket") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame the upper body so collar/zip/stripes/logo read clearly. + _cam.position = Vector3(0.0, 1.25, 1.7) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 1.15, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body-authored garment: prefer _mask.png (runtime behaviour). + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + var logo := _try_load(_env("GARMENT_PREVIEW_LOGO", LOGO_PATH)) + for mi in meshes: + _apply_garment_shader(mi, shader, mask, logo) + + # Freeze on a walk pose so the jacket reads in motion. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task spec: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).instantiate() + var stack: Array[Node] = [inst] + 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: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D, logo: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + if logo: + mat.set_shader_parameter("logo_tex", logo) + mat.set_shader_parameter("logo_enabled", 1.0) + mat.set_shader_parameter("tint_0", TINT_COLLAR) + mat.set_shader_parameter("tint_1", TINT_BODY) + mat.set_shader_parameter("tint_2", TINT_STRIPE) + mat.set_shader_parameter("tint_3", TINT_TRIM) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_track_jacket.tscn b/client/tools/garment_preview/preview_track_jacket.tscn new file mode 100644 index 000000000..fb6e7a6dc --- /dev/null +++ b/client/tools/garment_preview/preview_track_jacket.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_track_jacket.gd" id="1_tjprev"] + +[node name="TrackJacketPreview" type="Node3D"] +script = ExtResource("1_tjprev") diff --git a/tooling/garment-fit/blender_author_boots.py b/tooling/garment-fit/blender_author_boots.py new file mode 100644 index 000000000..ab04fff52 --- /dev/null +++ b/tooling/garment-fit/blender_author_boots.py @@ -0,0 +1,740 @@ +""" +blender_author_boots.py (T-1089 wave 2, boots_modern + ankle-boot family) + +Authors ANKLE BOOTS as per-body offset shells: both feet PLUS the lower calf +(seg_leg_lower cut to a boot shaft just above the ankle), with a chunky sole +extension and painted eyelets/laces up the shaft. Reuses +blender_author_offset_shell.py (via blender_author_denim_pants.py, imported as +a module) for the shared machinery — scene build, join, offset, solidify, +albedo material, GLB export — plus the two denim REQUIRED PRACTICES for +bottoms/footwear: + + * boundary WELD of coincident segment-seam rings (denim.weld_boundaries): + the ankle joins (foot<->leg_lower) duplicate a coincident vert band per + segment; offsetting un-welded rings along diverging normals opens cracks. + * open-rim FLATTENING: the shaft cut leaves a jagged "teeth" ring on each + calf; both rims are pulled down onto one clean shared plane (the deeper + valley of the two, so the boots match) before offsetting. + +What this companion adds, as reusable parameters (not hacks): + + * --shaft-frac: boot shaft height as a fraction of the ankle->knee calf + span (0.35 cut => rim lands on the lower calf after rim flattening; + larger values give combat/riding variants). + * TOE-BOX MERGE: the skin mesh has individual toes; a boot must not. The + foot region is Laplacian-smoothed pre-offset (mild on the whole foot, + aggressive forward of the ball joint) so the toes fuse into one rounded + leather toe box and anatomical detail (ankle knobs, heel tendon) reads + as boot, not foot. Weights/UVs ride along untouched. + * SKIN CONTAINMENT CLAMP: smoothing can pull the shell inside the skin it + no longer follows (melted toe box vs real toes). A BVH of the ORIGINAL + welded skin is kept, and after the offset every foot-region vert whose + signed distance to the skin is below a minimum clearance is pushed back + out along the skin normal — standoff guaranteed by construction, which + is what the chromakey QA gates on (the runtime does not hide segments). + * FOOT WEIGHT RE-BIND: smoothing + clamping RELOCATE shell verts but leave + them carrying their ORIGIN vertex's bone weights, so material that ends + up hovering over toe N flexes with the bone of toe M — under ball-joint + flexion (Walk push-off, crouch) the shell diverges from the skin beneath + it and the toes poke through (QA/preview evidence, wave 2 run 1/2). + After shaping, every foot-region vert re-copies its vertex-group weights + from the nearest vert of the original welded skin, so the boot flexes + exactly with the anatomy each patch of leather actually covers. + * SOLE geometry as an offset-shell param extension (--sole-drop): the + under-foot surface created by the outward offset is flattened onto a slab + plane sole_drop below the skin sole (solidify adds its thickness back, + so the shipped slab bottom sits exactly sole_drop under the skin), and a + feathered radial LIP (~5 mm) bulges the sole band outward for the chunky + work-boot silhouette. + * --shaft-flare: extra feathered radial stand-off toward the shaft rim. + Serves the same crouch-fold purpose as the denim waist flare AND buys + clearance so full-length pant hems (jeans/formal, 12 mm standoff) tuck + INSIDE the boot shaft instead of z-fighting with it. + * TEXEL-level feature painting (denim technique, boot field): one analytic + field evaluation drives BOTH the painted albedo and the region mask so + they always agree — lace bars + eyelet dots up the front of the shaft, + welt stitching above the sole, padded collar shading at the rim. + Regions (spec): sole -> R, upper + shaft -> G, laces + eyelets -> B. + * a parked logo_uv TEXCOORD_1 layer (boots are not logo-capable, but + toon_garment.gdshader samples UV2 unconditionally). + +All cut/mask/paint parameters derive PER BODY from that body's own calf bone +landmarks and measured skin-sole plane, scaled by the calf-span ratio against +average_m (clamped so the child keeps believable, not clown, proportions) — +the same proportional-ratio philosophy as base.derive_thresholds. Per-body +mode only (offset shells author per body, Q-060). + +Style pin (T-1089): modern only; identity carried by the texture. The albedo +is painted at mid luma so toon_garment.gdshader's luma-preserving recolor +stays faithful; the brown/black work-street default lives in the manifest +default_tints (sole near-black, upper work brown, laces dark). + +Usage (boots_modern reference invocation): + tooling/blender --background --python \ + tooling/garment-fit/blender_author_boots.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/boots_modern \ + [--bodies average_m,child,...] [--offset 0.013] [--shaft-frac 0.30] \ + [--shaft-flare 0.008] [--sole-drop 0.020] [--base-rgb r,g,b] [--plain] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house +wardrobe), Q-060 (per-body offset shells). +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# -------------------------------------------------------------------------- +# Import the denim companion as a library (which itself imports the base +# offset-shell module). Boot reuse: base machinery + denim's weld/parked-UV2. +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_spec = importlib.util.spec_from_file_location( + "denim_pants_lib", os.path.join(_HERE, "blender_author_denim_pants.py")) +denim = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(denim) + +base = denim.base +log = base.log +FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base) + +# -------------------------------------------------------------------------- +# Parameters +# -------------------------------------------------------------------------- +COVERED_SEGMENTS = [ + "seg_foot_l", "seg_foot_r", + "seg_leg_lower_l", "seg_leg_lower_r", +] + +SHAFT_FRAC = 0.35 # boot shaft = this fraction of the ankle->knee span +BOOT_OFFSET_M = 0.013 # standoff along normals (leather sits off the skin) +SHAFT_FLARE_M = 0.008 # extra radial stand-off at the shaft rim (feathered) +SOLE_DROP_M = 0.020 # chunky sole slab depth below the skin sole (spec) +SOLE_LIP_M = 0.008 # radial sole bulge (chunky work-boot lip) +SOLE_LIP_TOP_M = 0.020 # lip feather reaches this far above the skin sole +TEX_SIZE = 1024 # albedo + mask resolution (painted laces need >512) +NOISE_SEED = 3089 + +# Toe-box merge (pre-offset Laplacian smoothing). +FOOT_SMOOTH_ITERS = 4 # mild pass over the whole foot (below the ankle) +FOOT_SMOOTH_FACTOR = 0.5 +TOE_SMOOTH_ITERS = 10 # aggressive pass forward of the ball joint +TOE_SMOOTH_FACTOR = 1.0 +TOE_ZONE_BALL_FRAC = 0.80 # toe zone starts at this fraction of the ball-y + +# Skin containment clamp (post-offset). +CLAMP_CLEAR_FRAC = 1.0 # minimum clearance as a fraction of the offset. + # At 1.0 the clamp restores the full standoff: the + # clamp moves VERTS, and a face spanning two clamped + # verts can still dip toward the high-frequency skin + # toes between them (QA/preview evidence: toe tips + # poked the melted toe box at 0.75). Higher values + # (1.15 tried) buy little once the foot weight + # re-bind aligns flexion, and read as a lumpy box. +CLAMP_ITERS = 3 + +# Painted-detail metrics (metres on average_m; scaled by the calf-span ratio). +LACE_PITCH_M = 0.017 # vertical distance between lace bars +LACE_BAR_HW_M = 0.0026 # half-height of a painted lace bar +LACE_PANEL_HW_M = 0.015 # half arc-width of the lace panel +EYELET_R_M = 0.0032 # eyelet dot radius (at the panel edges) +LACE_LO_FRAC = 0.0 # panel starts at the ankle joint (shaft only) +COLLAR_H_M = 0.014 # padded collar band at the shaft rim (albedo shade) +SOLE_TOP_M = 0.014 # R region reaches this far above the skin sole +WELT_OFF_M = 0.0035 # welt stitch line offset above the sole top +SEAM_HW_M = 0.0018 # painted stitch line half-width +TOECAP_STITCH = True # painted toe-cap stitch line at the ball joint + +# Albedo tones (sRGB floats). Mid-luma leather so the toon_garment luma +# recolor keeps its dynamic range; hue defaults come from manifest tints. +LEATHER_RGB = (0.58, 0.44, 0.32) # light tan work leather +SOLE_RGB = (0.30, 0.29, 0.28) # darker rubber (low luma -> reads black) +LACE_RGB = (0.24, 0.21, 0.19) # dark laces +EYELET_RGB = (0.85, 0.78, 0.60) # bright metal eyelet glint +WELT_RGB = (0.78, 0.62, 0.40) # welt stitching thread +PANEL_SHADE = 0.90 # lace-panel background darkening +COLLAR_SHADE = 0.86 # padded collar darkening +ALBEDO_NOISE = 0.020 # +/- grain jitter +PLAIN = False # --plain: skip laces/eyelets/welt paint + +# Reference proportions (average_m) the scale ratio is anchored to. +_REF_CALF_SPAN = 0.4559 # calf head z (0.5424) - calf tail z (0.0865) + + +# -------------------------------------------------------------------------- +# Per-body landmarks +# -------------------------------------------------------------------------- + +class BootLandmarks: + """Cut/mask/paint parameters from one body's calf bones + skin-sole plane.""" + + def __init__(self, armature, skin_min_z): + bones = armature.data.bones + calf_l = bones.get("calf_l") + calf_r = bones.get("calf_r") + if calf_l is None or calf_r is None: + raise RuntimeError("calf_l/calf_r missing — not the 65-bone rig?") + self.ankle_z = (calf_l.tail_local.z + calf_r.tail_local.z) / 2.0 + self.knee_z = (calf_l.head_local.z + calf_r.head_local.z) / 2.0 + self.skin_min_z = skin_min_z + self.calf_span = self.knee_z - self.ankle_z + # Detail scale: proportional to the calf span, clamped so small bodies + # keep believable (not clown, not doll) sole/lace metrics. + self.s = min(max(self.calf_span / _REF_CALF_SPAN, 0.55), 1.15) + # Per-z leg axis control points (ankle -> knee) for the +x leg; + # the right leg mirrors via the x sign. np.interp clamps outside. + self.leg_z_pts = np.array([calf_l.tail_local.z, calf_l.head_local.z]) + self.leg_x_pts = np.array( + [abs(calf_l.tail_local.x), abs(calf_l.head_local.x)]) + self.leg_y_pts = np.array([calf_l.tail_local.y, calf_l.head_local.y]) + self.shaft_top_z = self.ankle_z + SHAFT_FRAC * self.calf_span + self.rim_z = self.shaft_top_z # finalized after rim flattening + # Ball joint (toe-box hinge): forward distance on the front axis. + ball = bones.get("ball_l") + self.ball_front = (ball.head_local.y * FRONT_Y_SIGN + if ball is not None else None) + + def finalize(self, rim_z): + self.rim_z = rim_z + self.sole_top_z = self.skin_min_z + SOLE_TOP_M * self.s + self.lace_lo = self.ankle_z - LACE_LO_FRAC * (self.ankle_z - self.skin_min_z) + self.lace_hi = self.rim_z - 0.7 * COLLAR_H_M * self.s + log(f"landmarks: ankle={self.ankle_z:.3f} knee={self.knee_z:.3f} " + f"sole={self.skin_min_z:.3f} rim={self.rim_z:.3f} s={self.s:.2f} " + f"sole_top={self.sole_top_z:.3f} " + f"laces z=[{self.lace_lo:.3f},{self.lace_hi:.3f}]") + + +# -------------------------------------------------------------------------- +# Geometry: shaft cut + rim flatten + flare + sole +# -------------------------------------------------------------------------- + +def build_skin_bvh(shell): + """BVH of the welded skin BEFORE any shaping — the containment reference. + + Returns (bvh, bm); the bmesh must stay alive as long as the BVH is used. + """ + from mathutils.bvhtree import BVHTree + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.normal_update() + return BVHTree.FromBMesh(bm), bm + + +def containment_clamp(shell, skin_bvh, min_clear, z_max): + """Push shell verts back outside the original skin (post-offset). + + The toe-box merge can leave the smoothed shell INSIDE the real skin toes; + any vert (below z_max) whose signed distance to the skin surface is less + than min_clear is moved to min_clear along the skin normal. Iterated, + because in concave spots (toe crevices) the first push can land near + another skin face.""" + me = shell.data + total = 0 + for it in range(CLAMP_ITERS): + moved = 0 + for v in me.vertices: + if v.co.z > z_max: + continue + loc, nrm, _idx, _dist = skin_bvh.find_nearest(v.co) + if loc is None: + continue + if (v.co - loc).dot(nrm) < min_clear: + v.co = loc + nrm * min_clear + moved += 1 + total += moved + if moved == 0: + break + me.update() + log(f"containment clamp: {total} vert pushes " + f"(min clearance {min_clear * 1000:.1f} mm)") + + +def transfer_foot_weights(shell, skin_bm, skin_bvh, z_max): + """Re-copy foot-region vertex weights from the nearest ORIGINAL skin vert. + + Smoothing + clamping relocate shell verts while they keep their origin + vert's weights, so leather hovering over toe N can flex with toe M's bone + — under ball flexion the shell then diverges from the skin it covers and + the toes poke through. Copying each relocated vert's weights from the + nearest vert of the original welded skin re-aligns material to anatomy. + Group indices match by construction (same object, groups untouched). + """ + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + dl = bm.verts.layers.deform.verify() + sdl = skin_bm.verts.layers.deform.verify() + skin_bm.faces.ensure_lookup_table() + rebound = 0 + for v in bm.verts: + if v.co.z > z_max: + continue + loc, _nrm, fidx, _dist = skin_bvh.find_nearest(v.co) + if loc is None: + continue + face = skin_bm.faces[fidx] + sv = min(face.verts, key=lambda fv: (fv.co - v.co).length_squared) + src = sv[sdl] + dst = v[dl] + dst.clear() + for g, w in src.items(): + dst[g] = w + rebound += 1 + bm.to_mesh(me) + bm.free() + me.update() + log(f"foot weight re-bind: {rebound} verts re-copied from nearest skin") + + +def toe_merge(shell, lm): + """Fuse the skin toes into one rounded boot toe box (pre-offset). + + Two Laplacian passes: a mild one over the whole foot (below the ankle) + rounds anatomical detail into leather, an aggressive one forward of the + ball joint melts the individual toes together. Vertex weights and UVs are + untouched, so skinning and texel painting are unaffected; the outward + offset afterwards restores the lost girth.""" + bm = bmesh.new() + bm.from_mesh(shell.data) + foot = [v for v in bm.verts if v.co.z < lm.ankle_z] + for _ in range(FOOT_SMOOTH_ITERS): + bmesh.ops.smooth_vert(bm, verts=foot, factor=FOOT_SMOOTH_FACTOR, + use_axis_x=True, use_axis_y=True, use_axis_z=True) + n_toe = 0 + if lm.ball_front is not None: + toe_y = lm.ball_front * TOE_ZONE_BALL_FRAC + toes = [v for v in foot if v.co.y * FRONT_Y_SIGN > toe_y] + n_toe = len(toes) + for _ in range(TOE_SMOOTH_ITERS): + bmesh.ops.smooth_vert(bm, verts=toes, factor=TOE_SMOOTH_FACTOR, + use_axis_x=True, use_axis_y=True, + use_axis_z=True) + else: + log("WARNING: ball_l bone missing — toe box keeps skin toes") + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"toe merge: smoothed {len(foot)} foot verts " + f"({FOOT_SMOOTH_ITERS}x{FOOT_SMOOTH_FACTOR}), " + f"{n_toe} toe verts ({TOE_SMOOTH_ITERS}x{TOE_SMOOTH_FACTOR})") + + +def shaft_cut(shell, lm): + """Trim the calves above the boot-shaft plane (bone-derived).""" + bm = bmesh.new() + bm.from_mesh(shell.data) + doomed = [v for v in bm.verts if v.co.z > lm.shaft_top_z] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"shaft cut at z={lm.shaft_top_z:.3f}: removed {len(doomed)} verts") + + +def flatten_shaft_rims(shell, lm): + """Pull both shaft-rim teeth rings onto ONE shared clean plane (pre-offset). + + The plane cut follows mesh topology, so each rim is a jagged ring of + teeth. Both rings flatten DOWN to the common valley (deepest notch of + either ring) — a clean straight rim, identical height on both boots, no + invented coverage. Any stray open-boundary verts below the shaft (weld + leftovers) are left alone and reported. Returns the rim plane z. + """ + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + boundary = set() + for e in bm.edges: + if len(e.link_faces) == 1: + boundary.update(v.index for v in e.verts) + low_thresh = lm.ankle_z + 0.25 * (lm.shaft_top_z - lm.ankle_z) + rims = [i for i in boundary if bm.verts[i].co.z > low_thresh] + stray = len(boundary) - len(rims) + if not rims: + bm.free() + raise RuntimeError("no shaft rim boundary verts found") + valley = min(bm.verts[i].co.z for i in rims) + teeth = max(bm.verts[i].co.z for i in rims) - valley + for i in rims: + bm.verts[i].co.z = valley + bm.to_mesh(me) + bm.free() + me.update() + log(f"flattened shaft rims: {len(rims)} verts, teeth {teeth:.3f} m " + f"-> z={valley:.3f}" + + (f" (WARNING: {stray} stray low boundary verts left)" if stray else "")) + return valley + + +def shaft_flare(shell, lm, rim_z, flare): + """Extra RADIAL stand-off toward the shaft rim, feathered from the ankle + (post-offset, pre-solidify). Buys pant-hem clearance + chunky read.""" + if flare <= 0.0: + return + span = max(rim_z - lm.ankle_z, 1e-6) + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.normal_update() + n = 0 + for v in bm.verts: + if v.co.z > lm.ankle_z: + t = min((v.co.z - lm.ankle_z) / span, 1.0) + nx, ny = v.normal.x, v.normal.y + mag = (nx * nx + ny * ny) ** 0.5 + if mag > 1e-6: + v.co.x += flare * t * nx / mag + v.co.y += flare * t * ny / mag + n += 1 + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"shaft flare: {n} verts, +{flare * 1000:.1f} mm radial at rim") + + +def sole_shape(shell, lm, offset, sole_drop): + """Chunky sole (post-offset, pre-solidify): feathered radial LIP around + the sole band, then the under-foot offset surface flattened onto a slab + plane. Solidify(use_rim) then grows its thickness outward (downward + there), landing the shipped slab bottom exactly sole_drop below the skin + sole.""" + lip_top = lm.skin_min_z + SOLE_LIP_TOP_M * lm.s + lip = SOLE_LIP_M * lm.s + slab_z = lm.skin_min_z - (sole_drop * lm.s - base.CLOTH_THICKNESS_M) + feather = max(lip_top - lm.skin_min_z, 1e-6) + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.normal_update() + n_lip = 0 + for v in bm.verts: + if v.co.z < lip_top: + t = min((lip_top - v.co.z) / feather, 1.0) + nx, ny = v.normal.x, v.normal.y + mag = (nx * nx + ny * ny) ** 0.5 + if mag > 1e-6: + v.co.x += lip * t * nx / mag + v.co.y += lip * t * ny / mag + n_lip += 1 + n_flat = 0 + for v in bm.verts: + if v.co.z < lm.skin_min_z: + v.co.z = slab_z + n_flat += 1 + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"sole: lip +{lip * 1000:.1f} mm on {n_lip} verts, " + f"{n_flat} under-sole verts flattened -> z={slab_z:.3f} " + f"(shipped bottom {sole_drop * lm.s * 1000:.0f} mm below skin sole)") + + +def clamp_rim_residue(shell, rim_z): + """Post-solidify safety clamp: verts still above the rim plane (solidify + displacement on multi-triangle teeth) get squashed onto it.""" + me = shell.data + n = 0 + for v in me.vertices: + if v.co.z > rim_z: + v.co.z = rim_z + n += 1 + me.update() + if n: + log(f"clamped {n} residual rim verts -> {rim_z:.3f}") + + +# -------------------------------------------------------------------------- +# Boot feature field (texel-level; drives albedo AND mask together) +# -------------------------------------------------------------------------- + +def _boot_field(px, py, pz, lm): + """Evaluate boot features at texel 3D positions (numpy arrays).""" + s = lm.s + side = np.where(px >= 0.0, 1.0, -1.0) + cx = side * np.interp(pz, lm.leg_z_pts, lm.leg_x_pts) + cy = np.interp(pz, lm.leg_z_pts, lm.leg_y_pts) + dx = px - cx + dy = py - cy + r = np.hypot(dx, dy) + 1e-9 + # Arc distance from the front meridian around the per-z leg axis — + # constant metric width at any girth, mirrors correctly on both boots. + arc_front = np.arccos(np.clip(dy * FRONT_Y_SIGN / r, -1.0, 1.0)) * r + + in_sole = pz <= lm.sole_top_z + in_lace_z = (pz > lm.lace_lo) & (pz < lm.lace_hi) & ~in_sole + + panel = in_lace_z & (arc_front < LACE_PANEL_HW_M * s) + pitch = LACE_PITCH_M * s + ph = np.mod(pz - lm.lace_lo, pitch) + laces = panel & (np.abs(ph - 0.5 * pitch) < LACE_BAR_HW_M * s) + eyelets = ( + in_lace_z + & (np.abs(arc_front - LACE_PANEL_HW_M * s) < EYELET_R_M * s) + & (np.abs(ph - 0.5 * pitch) < EYELET_R_M * s) + ) + welt = (~in_sole) & ( + np.abs(pz - (lm.sole_top_z + WELT_OFF_M * s)) < SEAM_HW_M * s) + if TOECAP_STITCH and lm.ball_front is not None: + welt = welt | ( + (~in_sole) & (pz < lm.lace_lo) + & (np.abs(py * FRONT_Y_SIGN - lm.ball_front) < SEAM_HW_M * s)) + collar = (~in_sole) & (pz > lm.rim_z - COLLAR_H_M * s) + return in_sole, panel, laces, eyelets, welt, collar + + +def _paint_texels(px, py, pz, noise, lm): + """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions.""" + n = px.shape[0] + in_sole, panel, laces, eyelets, welt, collar = _boot_field(px, py, pz, lm) + + # --- albedo ------------------------------------------------------------ + alb = np.empty((n, 4), dtype=np.float32) + for c in range(3): + alb[:, c] = LEATHER_RGB[c] + noise + alb[:, 3] = 1.0 + alb[collar, :3] *= COLLAR_SHADE + if not PLAIN: + alb[panel, :3] *= PANEL_SHADE + for c in range(3): + alb[welt, c] = WELT_RGB[c] + for c in range(3): + alb[in_sole, c] = SOLE_RGB[c] + noise[in_sole] + if not PLAIN: + for c in range(3): + alb[laces, c] = LACE_RGB[c] + alb[eyelets, c] = EYELET_RGB[c] + + # --- region mask: sole R / upper+shaft G / laces+eyelets B --------------- + mask = np.zeros((n, 4), dtype=np.float32) + is_b = (laces | eyelets) & ~in_sole if not PLAIN else np.zeros(n, dtype=bool) + is_g = ~(in_sole | is_b) + mask[in_sole, 0] = 1.0 + mask[is_b, 2] = 1.0 + mask[is_g, 1] = 1.0 + return alb, mask + + +# -------------------------------------------------------------------------- +# UV0 rasterization (denim technique: one pass paints albedo + mask) +# -------------------------------------------------------------------------- + +def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H): + """Barycentric texel fill of one UV triangle: interpolate 3D positions, + evaluate the boot field, write albedo + mask together.""" + a, b, c = uvs + A, B, C = cos + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + pxg = xs + 0.5 + pyg = ys + 0.5 + w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom + w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + w0i, w1i, w2i = w0[inside], w1[inside], w2[inside] + px3 = w0i * A.x + w1i * B.x + w2i * C.x + py3 = w0i * A.y + w1i * B.y + w2i * C.y + pz3 = w0i * A.z + w1i * B.z + w2i * C.z + ysin = ys[inside] + xsin = xs[inside] + alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm) + alb_buf[ysin, xsin] = alb + mask_buf[ysin, xsin] = mask + + +def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body): + """Rasterize all UV0 triangles once, producing the painted albedo and the + region mask from one shared feature-field evaluation per texel.""" + W = H = TEX_SIZE + rng = np.random.default_rng(NOISE_SEED) + noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5) + * 2.0 * ALBEDO_NOISE) + + alb_buf = np.empty((H, W, 4), dtype=np.float32) + for c in range(3): + alb_buf[:, :, c] = LEATHER_RGB[c] + noise_buf + alb_buf[:, :, 3] = 1.0 + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 # background = upper green (bleed-safe) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + if not len(bm.loops.layers.uv): + raise RuntimeError("no UV layer for albedo/mask paint") + uv_layer = bm.loops.layers.uv[0] + + tri_count = 0 + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv.copy() for loop in loops] + cos = [loop.vert.co.copy() for loop in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_paint( + alb_buf, mask_buf, noise_buf, + (uvs[0], uvs[i], uvs[i + 1]), + (cos[0], cos[i], cos[i + 1]), + lm, W, H) + tri_count += 1 + bm.free() + log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})") + + def _save(buf, name, path): + img = bpy.data.images.new(name, W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + albedo_img = _save(alb_buf, f"boot_albedo_{body}", albedo_path) + _save(mask_buf, f"boot_mask_{body}", mask_path) + log(f"saved albedo -> {albedo_path}") + log(f"saved mask -> {mask_path}") + return albedo_img + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_boot_shell(body_dir, out_dir, body, offset, sole_drop): + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + denim.weld_boundaries(shell) # ankle seams: foot <-> leg_lower + + skin_min_z = min(v.co.z for v in shell.data.vertices) + lm = BootLandmarks(armature, skin_min_z) + + # Containment reference: the ORIGINAL welded skin (real toes), captured + # before any shaping. The bmesh must outlive the BVH queries. + skin_bvh, skin_bm = build_skin_bvh(shell) + + toe_merge(shell, lm) + shaft_cut(shell, lm) + rim_z = flatten_shaft_rims(shell, lm) + + base.offset_outward(shell, offset) + # Toe-box merge can leave the smoothed+offset shell inside the real skin + # toes; push foot-region verts back out to the minimum clearance, then + # re-align their bone weights to the anatomy they now cover (flex fix). + containment_clamp(shell, skin_bvh, CLAMP_CLEAR_FRAC * offset, lm.ankle_z) + transfer_foot_weights(shell, skin_bm, skin_bvh, lm.ankle_z) + skin_bm.free() + shaft_flare(shell, lm, rim_z, SHAFT_FLARE_M * lm.s) + sole_shape(shell, lm, offset, sole_drop) + base.solidify(shell, base.CLOTH_THICKNESS_M) + clamp_rim_residue(shell, rim_z) + + lm.finalize(rim_z) + denim.author_parked_uv2(shell) # boots are not logo-capable + + albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png") + mask_path = os.path.join(out_dir, f"{body}_mask.png") + albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + global SHAFT_FRAC, SHAFT_FLARE_M, LEATHER_RGB, PLAIN + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--shaft-frac F] [--shaft-flare M] " + "[--sole-drop M] [--base-rgb r,g,b] [--plain]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = BOOT_OFFSET_M + sole_drop = SOLE_DROP_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--shaft-frac" in argv: + SHAFT_FRAC = float(argv[argv.index("--shaft-frac") + 1]) + if "--shaft-flare" in argv: + SHAFT_FLARE_M = float(argv[argv.index("--shaft-flare") + 1]) + if "--sole-drop" in argv: + sole_drop = float(argv[argv.index("--sole-drop") + 1]) + if "--base-rgb" in argv: + LEATHER_RGB = tuple( + float(v) for v in argv[argv.index("--base-rgb") + 1].split(",")) + if "--plain" in argv: + PLAIN = True + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"boots per-body mode: {len(bodies)} bodies, offset " + f"{offset * 1000:.0f} mm, shaft-frac {SHAFT_FRAC}, " + f"sole-drop {sole_drop * 1000:.0f} mm, plain={PLAIN}") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_boot_shell(body_dir, out_dir, body, offset, sole_drop) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_cargo_pants.py b/tooling/garment-fit/blender_author_cargo_pants.py new file mode 100644 index 000000000..e567d6ef1 --- /dev/null +++ b/tooling/garment-fit/blender_author_cargo_pants.py @@ -0,0 +1,502 @@ +""" +blender_author_cargo_pants.py (T-1089 wave 2, cargo_pants) + +Authors full-length CARGO pants as per-body offset shells. Reuses +blender_author_offset_shell.py as the base library (scene build, join, +offset, solidify, GLB export) and blender_author_denim_pants.py as the +proven bottoms companion (boundary WELD of coincident segment-seam rings, +open-rim FLATTENING onto clean planes, per-body LegLandmarks, waist flare, +hem cut, parked logo UV2) — the denim script's utilities are imported as a +module, not re-derived. + +What cargo adds, as reusable parameters: + + * --straight: STRAIGHT-FIT boost — extra radial stand-off away from the + per-z leg axis, ramping 0 at the knee to the full value at the ankle, so + the lower leg does not taper with the calf (straight silhouette; also + buys lower-leg clip clearance). Applied post-offset, pre-solidify. + * TEXEL-level cargo feature painting (same one-field-drives-both design as + denim: painted albedo and region mask always agree): + - albedo: large outer-thigh CARGO POCKETS with button-down FLAPS + (filled panels, border stitching, centre pleat, two flap + buttons), outseam/inseam side seams, centre-front fly + stitch, belt loops + waist button, waistband and hem border + stitching — flat tone-on-tone utility look, identity + carried by the texture (style pin: modern only). + - mask: waistband -> R, legs -> G, pockets + flaps -> B + (spec: waistband=R, legs=G, pockets+flaps=B). + A SIGNED azimuth arc around the per-z leg axis (0 at the outer side, + positive toward the front) places the pockets slightly forward-of-side + and the flap buttons symmetrically — the same analytic-field approach as + the denim script, generalised from unsigned to signed arc. + * pocket proportions anchor to each body's own THIGH span (crotch -> + knee) and hip half-width, so the pockets stay proportional across all + 11 bodies (child included) — the base.derive_thresholds philosophy. + +Covered segments: seg_hips + seg_leg_upper_l/r + seg_leg_lower_l/r; natural +boundaries give the waist opening and ankle hems. Regions are painted, not +modelled — no 3D pocket geometry (texture carries identity). + +Usage (cargo_pants reference invocation): + tooling/blender --background --python \ + tooling/garment-fit/blender_author_cargo_pants.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/cargo_pants \ + [--bodies average_m,child,...] [--offset 0.013] [--hem-frac 1.0] \ + [--waist-flare 0.007] [--straight 0.010] [--base-rgb 0.36,0.37,0.29] \ + [--plain] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house +wardrobe), Q-060 (per-body offset shells). +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# -------------------------------------------------------------------------- +# Import the base offset-shell module + the denim bottoms companion +# (weld / rim-flatten / flare / landmarks utilities live there) +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _load(name, fname): + spec = importlib.util.spec_from_file_location( + name, os.path.join(_HERE, fname)) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +base = _load("offset_shell_base", "blender_author_offset_shell.py") +denim = _load("denim_pants_lib", "blender_author_denim_pants.py") + +log = base.log +FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base) + +# -------------------------------------------------------------------------- +# Parameters +# -------------------------------------------------------------------------- +COVERED_SEGMENTS = [ + "seg_hips", + "seg_leg_upper_l", "seg_leg_upper_r", + "seg_leg_lower_l", "seg_leg_lower_r", +] + +HEM_FRAC = 1.0 # 1.0 = full leg to the ankle +OFFSET_M = 0.013 # cargo sits a touch looser than jeans (0.012) +STRAIGHT_M = 0.010 # straight-fit radial boost at the ankle (0 at knee) +WAIST_FLARE_M = 0.007 # waistband rim stand-off (deep-crouch mitigation, + # proven on jeans) +TEX_SIZE = 1024 # painted pockets/seams need >512 + +# Vertical proportions — fractions of the garment span (waist_z - hem_z), +# via denim.LegLandmarks.finalize (BAND/CUFF/SEAM fractions proven on jeans). +# Pocket proportions — fractions of the THIGH span (crotch_z - knee_z). +FLAP_TOP_FRAC = 0.18 # flap top below the crotch +FLAP_H_FRAC = 0.14 # flap height +POCKET_BOT_FRAC = 0.74 # pocket bottom below the crotch + +# Horizontal proportions — signed azimuth ARC around the per-z leg axis +# (metres on the reference body, scaled by the body's hip ratio lm.sh). +POCKET_HALF_ARC_M = 0.058 # pocket half-width +FLAP_EXTRA_ARC_M = 0.007 # flap overhangs the pocket by this much per side +POCKET_FWD_BIAS_M = 0.010 # pocket centre sits slightly forward of the outseam +BUTTON_R_M = 0.0075 # flap button radius +LOOP_X_FRACS = (0.55, 1.30) # belt-loop |x| positions (fractions of hip_x) +LOOP_W_M = 0.016 # belt loop width (m, scaled by hip ratio) + +# Utility olive/grey style (sRGB floats; flat toon-friendly, tone-on-tone). +CARGO_RGB = (0.360, 0.370, 0.290) # utility olive-grey +THREAD_RGB = (0.225, 0.235, 0.185) # tone-on-tone darker stitching +BUTTON_RGB = (0.170, 0.160, 0.140) # matte button +ALBEDO_NOISE = 0.020 # +/- woven jitter +POCKET_SHADE = 0.93 # pocket panel albedo darkening +FLAP_SHADE = 0.85 # flap panel albedo darkening +LOOP_SHADE = 0.80 # belt-loop albedo darkening +PLAIN = False # --plain: skip pockets/stitch/button paint +NOISE_SEED = 3089 + + +# -------------------------------------------------------------------------- +# Geometry: straight-fit boost (cargo's reusable fit parameter) +# -------------------------------------------------------------------------- + +def straight_boost(shell, lm, boost): + """Extra RADIAL stand-off away from the per-z leg axis below the knee, + ramping linearly from 0 at the knee to `boost` at the ankle. The body + calf tapers; pushing the shell out progressively keeps the pant leg + straight (cargo silhouette) instead of hugging the calf. Applied + post-offset, pre-solidify; weights/UVs are untouched.""" + if boost <= 0.0: + return + knee_z = float(lm.leg_z_pts[1]) + ankle_z = float(lm.leg_z_pts[0]) + span = max(knee_z - ankle_z, 1e-6) + bm = bmesh.new() + bm.from_mesh(shell.data) + n = 0 + for v in bm.verts: + z = v.co.z + if z >= knee_z: + continue + t = min((knee_z - z) / span, 1.0) + side = 1.0 if v.co.x >= 0.0 else -1.0 + cx = side * float(np.interp(z, lm.leg_z_pts, lm.leg_x_pts)) + cy = float(np.interp(z, lm.leg_z_pts, lm.leg_y_pts)) + dx = v.co.x - cx + dy = v.co.y - cy + r = (dx * dx + dy * dy) ** 0.5 + if r > 1e-6: + v.co.x += boost * t * dx / r + v.co.y += boost * t * dy / r + n += 1 + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"straight-fit boost: {n} verts, +{boost * 1000:.1f} mm radial at " + f"ankle (ramped from knee z={knee_z:.3f})") + + +# -------------------------------------------------------------------------- +# Cargo feature field (texel-level; drives albedo AND mask together) +# -------------------------------------------------------------------------- + +def _cargo_field(px, py, pz, lm): + """Evaluate cargo features at texel 3D positions (numpy arrays). + + Returns a dict of bool arrays. `pocket`/`flap` feed the mask B channel; + the stitch/fill features feed the albedo only. + """ + w2 = lm.seam_w * 0.5 + front = py * FRONT_Y_SIGN > 0.004 + in_band = pz >= lm.band_z + mid = (~in_band) & (pz > lm.hem_z + 0.5 * lm.cuff_h) + + # Per-z leg axis, mirrored by x sign; SIGNED azimuth arc around it + # (0 at the outer side, positive toward the front, +/-pi*r at the inseam). + side = np.where(px >= 0.0, 1.0, -1.0) + cx = side * np.interp(pz, lm.leg_z_pts, lm.leg_x_pts) + cy = np.interp(pz, lm.leg_z_pts, lm.leg_y_pts) + dx = px - cx + dy = py - cy + r = np.hypot(dx, dy) + 1e-9 + theta = np.arctan2(dy * FRONT_Y_SIGN / r, np.clip(dx * side / r, -1.0, 1.0)) + sarc = theta * r # signed arc distance from the outer direction + + # Side seams: outseam on the outer azimuth; inseam opposite, below crotch. + outseam = mid & (np.abs(sarc) < w2) + arc_in = (np.pi - np.abs(theta)) * r + inseam = mid & (arc_in < w2) \ + & (pz < lm.crotch_z - 0.01 * lm.span / denim._REF_SPAN) + + # Centre-front fly stitch (slightly off-centre, like the jeans J-front). + fly = front & mid & (np.abs(px - 0.012 * lm.sh) < w2) \ + & (pz > lm.crotch_z + 0.015 * lm.span / denim._REF_SPAN) + + # --- cargo pocket + flap (outer thigh, slightly forward-of-side) -------- + knee_z = float(lm.leg_z_pts[1]) + thigh = max(lm.crotch_z - knee_z, 1e-6) + flap_top = lm.crotch_z - FLAP_TOP_FRAC * thigh + flap_bot = flap_top - FLAP_H_FRAC * thigh + pocket_bot = lm.crotch_z - POCKET_BOT_FRAC * thigh + p_arc = POCKET_HALF_ARC_M * lm.sh + f_arc = p_arc + FLAP_EXTRA_ARC_M * lm.sh + a = sarc - POCKET_FWD_BIAS_M * lm.sh # pocket-centred signed arc + + pocket = (np.abs(a) < p_arc) & (pz <= flap_top) & (pz >= pocket_bot) + flap = (np.abs(a) < f_arc) & (pz <= flap_top) & (pz >= flap_bot) + + # Pocket border stitching (sides + bottom), centre pleat, flap edge. + p_side = (np.abs(np.abs(a) - p_arc) < w2) & (pz <= flap_top) \ + & (pz >= pocket_bot) + p_bottom = (np.abs(pz - pocket_bot) < w2) & (np.abs(a) < p_arc) + pleat = (np.abs(a) < w2) & (pz < flap_bot - 2.0 * w2) & (pz >= pocket_bot) + f_edge = flap & ((np.abs(np.abs(a) - f_arc) < w2) + | (np.abs(pz - flap_bot) < w2)) + pkt_stitch = p_side | p_bottom | pleat | f_edge + + # Two flap buttons, symmetric about the pocket centre. + btn_r = BUTTON_R_M * lm.sh + btn_z = flap_bot + 0.30 * (flap_top - flap_bot) + buttons = np.zeros_like(front) + for bx in (-0.45 * f_arc, 0.45 * f_arc): + buttons |= np.hypot(a - bx, pz - btn_z) < btn_r + + # Border stitching: waistband seam + hem stitch. + wstitch = np.abs(pz - lm.band_z) < w2 + hemstitch = np.abs(pz - lm.cuff_top) < w2 + + return { + "front": front, "in_band": in_band, + "seams": outseam | inseam | fly, + "wstitch": wstitch, "hemstitch": hemstitch, + "pocket": pocket, "flap": flap, + "pkt_stitch": pkt_stitch, "buttons": buttons, + } + + +def _paint_texels(px, py, pz, noise, lm): + """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions.""" + n = px.shape[0] + f = _cargo_field(px, py, pz, lm) + + # --- albedo ------------------------------------------------------------- + alb = np.empty((n, 4), dtype=np.float32) + for c in range(3): + alb[:, c] = CARGO_RGB[c] + noise + alb[:, 3] = 1.0 + + if not PLAIN: + # Belt loops: darkened bands on the waistband. + loop_w = LOOP_W_M * lm.sh + loops = np.zeros(n, dtype=bool) + for fx in LOOP_X_FRACS: + loops |= np.abs(np.abs(px) - fx * lm.hip_x) < loop_w * 0.5 + loops |= (~f["front"]) & (np.abs(px) < loop_w * 0.5) # centre-back + loops &= f["in_band"] + alb[loops, :3] *= LOOP_SHADE + + # Panel fills first, stitch lines on top. + alb[f["pocket"] & ~f["flap"], :3] *= POCKET_SHADE + alb[f["flap"], :3] *= FLAP_SHADE + + thread = f["seams"] | f["wstitch"] | f["hemstitch"] | f["pkt_stitch"] + alb[thread, 0] = THREAD_RGB[0] + alb[thread, 1] = THREAD_RGB[1] + alb[thread, 2] = THREAD_RGB[2] + + # Flap buttons + waist button. + btn = f["buttons"] | (f["front"] & ( + np.hypot(px, pz - (lm.band_z + 0.5 * lm.band_h)) < 0.009 * lm.sh)) + alb[btn, 0] = BUTTON_RGB[0] + alb[btn, 1] = BUTTON_RGB[1] + alb[btn, 2] = BUTTON_RGB[2] + + # --- region mask: waistband R / legs G / pockets+flaps B ----------------- + mask = np.zeros((n, 4), dtype=np.float32) + is_b = (f["pocket"] | f["flap"]) & ~f["in_band"] + is_g = ~(f["in_band"] | is_b) + mask[f["in_band"], 0] = 1.0 + mask[is_b, 2] = 1.0 + mask[is_g, 1] = 1.0 + return alb, mask + + +def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H): + """Barycentric texel fill of one UV triangle: interpolate 3D positions, + evaluate the cargo field, write albedo + mask together (same rasterizer + contract as the denim companion, pointed at the cargo painter).""" + a, b, c = uvs + A, B, C = cos + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + pxg = xs + 0.5 + pyg = ys + 0.5 + w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom + w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + w0i, w1i, w2i = w0[inside], w1[inside], w2[inside] + px3 = w0i * A.x + w1i * B.x + w2i * C.x + py3 = w0i * A.y + w1i * B.y + w2i * C.y + pz3 = w0i * A.z + w1i * B.z + w2i * C.z + ysin = ys[inside] + xsin = xs[inside] + alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm) + alb_buf[ysin, xsin] = alb + mask_buf[ysin, xsin] = mask + + +def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body): + """Rasterize all UV0 triangles once, producing the painted albedo and the + region mask from one shared feature-field evaluation per texel.""" + W = H = TEX_SIZE + rng = np.random.default_rng(NOISE_SEED) + noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5) + * 2.0 * ALBEDO_NOISE) + + alb_buf = np.empty((H, W, 4), dtype=np.float32) + for c in range(3): + alb_buf[:, :, c] = CARGO_RGB[c] + noise_buf + alb_buf[:, :, 3] = 1.0 + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 # background = legs green (bleed-safe) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + if not len(bm.loops.layers.uv): + raise RuntimeError("no UV layer for albedo/mask paint") + uv_layer = bm.loops.layers.uv[0] + + tri_count = 0 + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv.copy() for loop in loops] + cos = [loop.vert.co.copy() for loop in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_paint( + alb_buf, mask_buf, noise_buf, + (uvs[0], uvs[i], uvs[i + 1]), + (cos[0], cos[i], cos[i + 1]), + lm, W, H) + tri_count += 1 + bm.free() + log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})") + + def _save(buf, name, path): + img = bpy.data.images.new(name, W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + albedo_img = _save(alb_buf, f"cargo_albedo_{body}", albedo_path) + _save(mask_buf, f"cargo_mask_{body}", mask_path) + log(f"saved albedo -> {albedo_path}") + log(f"saved mask -> {mask_path}") + return albedo_img + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_cargo_shell(body_dir, out_dir, body, offset, hem_frac): + crotch_z, _hips_top = denim.probe_hips_bounds(body_dir) + + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + denim.weld_boundaries(shell) + + zs = [v.co.z for v in shell.data.vertices] + waist_z, ankle_z = max(zs), min(zs) + lm = denim.LegLandmarks(armature, waist_z, ankle_z, crotch_z + 0.005) + + hem_z = denim.hem_cut(shell, lm, hem_frac) + ankle_plane = float(lm.leg_z_pts[0]) if hem_frac >= 0.999 else hem_z + waist_plane, ankle_plane = denim.flatten_open_rims(shell, ankle_plane) + + base.offset_outward(shell, offset) + straight_boost(shell, lm, STRAIGHT_M) + denim.waist_flare(shell, waist_plane, + denim.BAND_FRAC * (waist_plane - ankle_plane), + WAIST_FLARE_M) + base.solidify(shell, base.CLOTH_THICKNESS_M) + denim.clamp_waist_residue(shell, waist_plane) + + # Landmarks reference the CLEAN rims (band under the flattened waist edge, + # hem stitch above the flattened ankle rim). + lm.waist_z = waist_plane + lm.finalize(ankle_plane) + denim.author_parked_uv2(shell) + + albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png") + mask_path = os.path.join(out_dir, f"{body}_mask.png") + albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + global HEM_FRAC, CARGO_RGB, PLAIN, WAIST_FLARE_M, STRAIGHT_M + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--hem-frac F] [--waist-flare M] [--straight M] " + "[--base-rgb r,g,b] [--plain]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--hem-frac" in argv: + HEM_FRAC = float(argv[argv.index("--hem-frac") + 1]) + if "--waist-flare" in argv: + WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1]) + if "--straight" in argv: + STRAIGHT_M = float(argv[argv.index("--straight") + 1]) + if "--base-rgb" in argv: + CARGO_RGB = tuple( + float(v) for v in argv[argv.index("--base-rgb") + 1].split(",")) + if "--plain" in argv: + PLAIN = True + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"cargo per-body mode: {len(bodies)} bodies, offset " + f"{offset * 1000:.0f} mm, straight {STRAIGHT_M * 1000:.0f} mm, " + f"hem-frac {HEM_FRAC}, plain={PLAIN}") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_cargo_shell(body_dir, out_dir, body, offset, HEM_FRAC) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_joggers.py b/tooling/garment-fit/blender_author_joggers.py new file mode 100644 index 000000000..051ce1941 --- /dev/null +++ b/tooling/garment-fit/blender_author_joggers.py @@ -0,0 +1,511 @@ +""" +blender_author_joggers.py (T-1089 wave 2, joggers_modern + sweatpant family) + +Authors JOGGERS/sweatpants as per-body offset shells: hips + full legs, +tapered toward the ankle into snug cuff bands, painted waist drawstring, and +a track SIDE STRIPE carried as its own tint region. Reuses +blender_author_offset_shell.py (via blender_author_denim_pants.py's module +instance) for scene build / join / solidify / export, and reuses the denim +companion's REQUIRED lower-body practices directly (not re-derived): + + * boundary WELD of coincident segment-seam rings before offsetting + (denim.weld_boundaries) — un-welded rings offset apart along diverging + normals and open cracks at the waist/knee joins. + * open-rim FLATTENING (denim.flatten_open_rims) — the segment splitter + leaves 4.5-7.6 cm jagged teeth at the waist/ankle rims; boundary verts are + pulled onto clean planes pre-offset. The flattened ankle plane IS the + jogger cuff hem. + * waist flare + post-solidify residue clamp (denim.waist_flare, + denim.clamp_waist_residue) — deep-crouch waist-fold clip mitigation. + * per-body leg-axis landmarks (denim.LegLandmarks) — thigh_l/calf_l control + points drive azimuth math for the side stripe and the cuff/band rib paint, + so every parameter derives from the body's own bones (Q-060 per-body mode). + +What this companion adds, as reusable parameters (not hacks): + + * TAPERED variable offset — constant baggy standoff (--offset) above the + knee, smoothstep taper to --mid-offset at the cuff top, then a short + feathered step down to the snug --cuff-offset inside the cuff band: the + sweatpant silhouette (baggy thigh -> tapered shin -> gripping cuff). + * ankle CUFF band (--cuff-frac of the garment span) — its own mask region + (A channel) with painted knit RIB (azimuth-arc alternating shades, constant + metric rib width via the per-z leg axis). + * SIDE STRIPE (--stripe-width metres, scaled by hip width) — constant-width + azimuth band along the outer leg from waistband to cuff, painted bright + AND masked to its own region (B channel) for independent tinting. + * painted elastic waistband (subtle vertical rib) + centre-front DRAWSTRING: + two eyelets and two hanging cords with a slight outward slant (albedo + only; dark cords stay legible under the luma-scaled region tint). + +Regions (RGBA mask, toon_garment.gdshader): waistband -> R (tint_0), +legs -> G (tint_1), side stripe -> B (tint_2), ankle cuffs -> A (tint_3). +A parked logo_uv TEXCOORD_1 layer is authored (denim.author_parked_uv2) — +not logo-capable, but the shader samples UV2 unconditionally. + +Covered segments: seg_hips + seg_leg_upper_l/r + seg_leg_lower_l/r. Natural +boundaries give the waist opening and ankle hems for free. Per-body mode only +(offset shells author per body, Q-060). + +Usage (joggers_modern reference invocation): + tooling/blender --background --python \ + tooling/garment-fit/blender_author_joggers.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/joggers_modern \ + [--bodies average_m,child,...] [--offset 0.014] [--mid-offset 0.010] \ + [--cuff-offset 0.0065] [--band-frac 0.055] [--cuff-frac 0.075] \ + [--stripe-width 0.034] [--fabric 0.55,0.56,0.58] \ + [--stripe-rgb 0.88,0.89,0.91] [--waist-flare 0.007] [--seed N] [--plain] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house +wardrobe), Q-060 (per-body offset shells). +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# -------------------------------------------------------------------------- +# Import the denim companion as a module — it carries the shared lower-body +# practices (weld, rim flatten, waist flare, clamp, landmarks, parked UV2) and +# its own instance of the base offset-shell library. +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_spec = importlib.util.spec_from_file_location( + "denim_pants", os.path.join(_HERE, "blender_author_denim_pants.py")) +denim = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(denim) + +base = denim.base # single shared base-module instance +log = base.log +FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base) + +# -------------------------------------------------------------------------- +# Parameters (defaults = joggers_modern) +# -------------------------------------------------------------------------- +COVERED_SEGMENTS = [ + "seg_hips", + "seg_leg_upper_l", "seg_leg_upper_r", + "seg_leg_lower_l", "seg_leg_lower_r", +] + +TEX_SIZE = 1024 # albedo + mask resolution (rib + cords need >512) +WAIST_FLARE_M = 0.007 # denim-proven deep-crouch waist-fold mitigation + +# Tapered-offset profile (metres). +OFFSET_HI_M = 0.014 # hips/thigh standoff — baggy sweatpant volume +OFFSET_MID_M = 0.010 # taper target at the cuff top (shin) +OFFSET_CUFF_M = 0.0065 # snug cuff standoff (ankles barely deform) +CUFF_STEP_W_M = 0.010 # feather width of the taper->cuff step + +# Vertical proportions — fractions of the garment span (waist_z - hem_z). +BAND_FRAC = 0.055 # elastic waistband height (R region) +CUFF_FRAC = 0.075 # knit cuff band height (A region) — taller than a hem +SEAM_W_FRAC = 0.008 # painted border-stitch line width +CORD_DROP_FRAC = 0.050 # drawstring cord length below the waistband +CORD_SLANT = 0.18 # outward cord slant (dx per unit hang depth) + +# Horizontal / arc-metric proportions (scaled by hip half-width ratio lm.sh). +STRIPE_W_M = 0.034 # side stripe metric width (B region) +RIB_PERIOD_M = 0.009 # cuff knit-rib period (arc metres) +BAND_RIB_PERIOD_M = 0.006 # waistband elastic-rib period (arc metres) +CORD_X_M = 0.016 # drawstring eyelet/cord |x| at the band +CORD_W_M = 0.005 # cord width +EYELET_R_M = 0.0045 # eyelet dot radius + +# Fleece style (sRGB floats; flat toon-friendly, texture carries identity). +FLEECE_RGB = (0.55, 0.56, 0.58) # heather grey, luma ~0.56 (tint-faithful) +STRIPE_RGB = (0.88, 0.89, 0.91) # near-white track stripe +CORD_RGB = (0.17, 0.17, 0.19) # charcoal drawstring +ALBEDO_NOISE = 0.020 # +/- fleece jitter +BAND_SHADE = 0.94 # waistband overall darkening +CUFF_SHADE = 0.96 # cuff overall darkening +RIB_LO = 0.86 # cuff rib dark-stripe factor +BAND_RIB_LO = 0.94 # waistband rib dark-stripe factor +STITCH_SHADE = 0.80 # band/cuff border-stitch darkening +PLAIN = False # --plain: skip drawstring/rib/stitch paint +NOISE_SEED = 2090 # distinct from denim (2089) + + +# -------------------------------------------------------------------------- +# Joggers landmark finalization (denim.LegLandmarks carries the leg axis; the +# band/cuff fractions here are jogger parameters, not denim's) +# -------------------------------------------------------------------------- + +def finalize_jogger_landmarks(lm, waist_plane, ankle_plane, + band_frac, cuff_frac): + lm.waist_z = waist_plane + lm.hem_z = ankle_plane + lm.span = waist_plane - ankle_plane + lm.sh = lm.hip_x / denim._REF_HIP_X + lm.band_h = band_frac * lm.span + lm.cuff_h = cuff_frac * lm.span + lm.seam_w = SEAM_W_FRAC * lm.span + lm.band_z = waist_plane - lm.band_h + lm.cuff_top = ankle_plane + lm.cuff_h + log(f"landmarks: waist={lm.waist_z:.3f} hem={lm.hem_z:.3f} " + f"span={lm.span:.3f} hip_x={lm.hip_x:.3f} band_z={lm.band_z:.3f} " + f"cuff_top={lm.cuff_top:.3f} sh={lm.sh:.2f}") + + +# -------------------------------------------------------------------------- +# Tapered offset — the jogger silhouette +# -------------------------------------------------------------------------- + +def _smoothstep(u): + u = min(max(u, 0.0), 1.0) + return u * u * (3.0 - 2.0 * u) + + +def tapered_offset(shell, knee_z, cuff_top, off_hi, off_mid, off_cuff, + step_w): + """Per-vertex outward offset along smoothed normals: `off_hi` above the + knee, smoothstep taper to `off_mid` at the cuff top, then a feathered step + down to the snug `off_cuff` inside the cuff band. The step reads as the + cuff ledge; the A-region tint + painted rib carry the rest.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.normal_update() + denom = max(knee_z - cuff_top, 1e-6) + lo = hi = None + for v in bm.verts: + z = v.co.z + t = off_hi + (off_mid - off_hi) * _smoothstep((knee_z - z) / denom) + s = _smoothstep((cuff_top + step_w - z) / step_w) + t = t + (off_cuff - t) * s + v.co += v.normal * t + lo = t if lo is None else min(lo, t) + hi = t if hi is None else max(hi, t) + bm.to_mesh(me) + bm.free() + me.update() + log(f"tapered offset: {hi * 1000:.1f} mm (thigh) -> {lo * 1000:.1f} mm " + f"(cuff); knee_z={knee_z:.3f} cuff_top={cuff_top:.3f}") + + +# -------------------------------------------------------------------------- +# Jogger feature field (texel-level; drives albedo AND mask together) +# -------------------------------------------------------------------------- + +def _jogger_field(px, py, pz, lm): + """Evaluate jogger features at texel 3D positions (numpy arrays). + + Returns (front, in_band, in_cuff, stripe, arc): `stripe` feeds the mask B + channel, `in_band`/`in_cuff` feed R/A, `arc` is the azimuth arc-length + coordinate around the per-z leg axis used by the rib paint. + """ + front = py * FRONT_Y_SIGN > 0.004 + in_band = pz >= lm.band_z + in_cuff = pz <= lm.cuff_top + mid = (~in_band) & (~in_cuff) + + # Per-z leg axis, mirrored by x sign (denim.LegLandmarks control points). + side = np.where(px >= 0.0, 1.0, -1.0) + cx = side * np.interp(pz, lm.leg_z_pts, lm.leg_x_pts) + cy = np.interp(pz, lm.leg_z_pts, lm.leg_y_pts) + dx = px - cx + dy = py - cy + r = np.hypot(dx, dy) + 1e-9 + + # Side stripe: azimuth toward the outer (+/-x) direction; constant metric + # width via arc distance (same construction as the denim outseam). + arc_out = np.arccos(np.clip(dx * side / r, -1.0, 1.0)) * r + stripe = mid & (arc_out < 0.5 * STRIPE_W_M * lm.sh) + + # Rib coordinate: signed azimuth arc length (0 at the outseam; the +/-pi + # wrap sits at the inner ankle / body centre where it cannot be seen). + arc = np.arctan2(dy, dx * side) * r + return front, in_band, in_cuff, stripe, arc + + +def _paint_texels(px, py, pz, noise, lm): + """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions.""" + n = px.shape[0] + front, in_band, in_cuff, stripe, arc = _jogger_field(px, py, pz, lm) + + # --- albedo ------------------------------------------------------------ + alb = np.empty((n, 4), dtype=np.float32) + for c in range(3): + alb[:, c] = FLEECE_RGB[c] + noise + alb[:, 3] = 1.0 + + # Side stripe: bright, low-noise (crisp edge carries the sport read). + for c in range(3): + alb[stripe, c] = STRIPE_RGB[c] + 0.5 * noise[stripe] + + if not PLAIN: + # Cuff knit rib: alternating shade stripes, constant metric width. + rib_dark = (np.floor(arc / (RIB_PERIOD_M * lm.sh)).astype(np.int64) + % 2) == 0 + cuff_f = np.where(rib_dark, RIB_LO, 1.0) * CUFF_SHADE + alb[in_cuff, :3] *= cuff_f[in_cuff, None] + + # Waistband elastic rib: same construction, subtler. + band_dark = (np.floor(arc / (BAND_RIB_PERIOD_M * lm.sh)) + .astype(np.int64) % 2) == 0 + band_f = np.where(band_dark, BAND_RIB_LO, 1.0) * BAND_SHADE + alb[in_band, :3] *= band_f[in_band, None] + + # Border stitching: darker lines under the band and above the cuff. + w2 = lm.seam_w * 0.5 + stitch = (np.abs(pz - lm.band_z) < w2) | (np.abs(pz - lm.cuff_top) < w2) + alb[stitch, :3] *= STITCH_SHADE + + # Drawstring: two eyelets on the band + two slanted hanging cords. + ex = CORD_X_M * lm.sh + eyelet_z = lm.band_z + 0.35 * lm.band_h + eyelets = front & ( + np.hypot(np.abs(px) - ex, pz - eyelet_z) < EYELET_R_M * lm.sh) + drop = CORD_DROP_FRAC * lm.span + hang = np.clip(eyelet_z - pz, 0.0, None) + cord_cx = ex + CORD_SLANT * hang + cords = front & (pz > eyelet_z - drop) & (pz <= eyelet_z) \ + & (np.abs(np.abs(px) - cord_cx) < 0.5 * CORD_W_M * lm.sh) + for c in range(3): + alb[eyelets | cords, c] = CORD_RGB[c] + else: + alb[in_cuff, :3] *= CUFF_SHADE + alb[in_band, :3] *= BAND_SHADE + + # --- region mask: band R / legs G / stripe B / cuffs A ------------------- + mask = np.zeros((n, 4), dtype=np.float32) + is_b = stripe & ~in_band & ~in_cuff + is_g = ~(in_band | in_cuff | is_b) + mask[in_band, 0] = 1.0 + mask[is_g, 1] = 1.0 + mask[is_b, 2] = 1.0 + mask[in_cuff, 3] = 1.0 + return alb, mask + + +def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H): + """Barycentric texel fill of one UV triangle: interpolate 3D positions, + evaluate the jogger field, write albedo + mask together (same rasterizer + construction as the denim companion; the paint field is what differs).""" + a, b, c = uvs + A, B, C = cos + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + pxg = xs + 0.5 + pyg = ys + 0.5 + w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom + w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + w0i, w1i, w2i = w0[inside], w1[inside], w2[inside] + px3 = w0i * A.x + w1i * B.x + w2i * C.x + py3 = w0i * A.y + w1i * B.y + w2i * C.y + pz3 = w0i * A.z + w1i * B.z + w2i * C.z + ysin = ys[inside] + xsin = xs[inside] + alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm) + alb_buf[ysin, xsin] = alb + mask_buf[ysin, xsin] = mask + + +def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body): + """Rasterize all UV0 triangles once, producing the painted albedo and the + region mask from one shared feature-field evaluation per texel.""" + W = H = TEX_SIZE + rng = np.random.default_rng(NOISE_SEED) + noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5) + * 2.0 * ALBEDO_NOISE) + + alb_buf = np.empty((H, W, 4), dtype=np.float32) + for c in range(3): + alb_buf[:, :, c] = FLEECE_RGB[c] + noise_buf + alb_buf[:, :, 3] = 1.0 + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 # background = legs green (bleed-safe) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + if not len(bm.loops.layers.uv): + raise RuntimeError("no UV layer for albedo/mask paint") + uv_layer = bm.loops.layers.uv[0] + + tri_count = 0 + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv.copy() for loop in loops] + cos = [loop.vert.co.copy() for loop in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_paint( + alb_buf, mask_buf, noise_buf, + (uvs[0], uvs[i], uvs[i + 1]), + (cos[0], cos[i], cos[i + 1]), + lm, W, H) + tri_count += 1 + bm.free() + log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})") + + def _save(buf, name, path): + img = bpy.data.images.new(name, W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + albedo_img = _save(alb_buf, f"jogger_albedo_{body}", albedo_path) + _save(mask_buf, f"jogger_mask_{body}", mask_path) + log(f"saved albedo -> {albedo_path}") + log(f"saved mask -> {mask_path}") + return albedo_img + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_jogger_shell(body_dir, out_dir, body): + crotch_z, _hips_top = denim.probe_hips_bounds(body_dir) + + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + denim.weld_boundaries(shell) + + zs = [v.co.z for v in shell.data.vertices] + waist_z, ankle_z = max(zs), min(zs) + lm = denim.LegLandmarks(armature, waist_z, ankle_z, crotch_z + 0.005) + + # Full-length hem AT the ankle joint (calf tail); rim teeth flattened onto + # clean planes pre-offset (denim practice — no jagged silhouette). + ankle_plane = float(lm.leg_z_pts[0]) + waist_plane, ankle_plane = denim.flatten_open_rims(shell, ankle_plane) + + span = waist_plane - ankle_plane + knee_z = float(lm.leg_z_pts[1]) # calf head + cuff_top = ankle_plane + CUFF_FRAC * span + tapered_offset(shell, knee_z, cuff_top, OFFSET_HI_M, OFFSET_MID_M, + OFFSET_CUFF_M, CUFF_STEP_W_M) + denim.waist_flare(shell, waist_plane, BAND_FRAC * span, WAIST_FLARE_M) + base.solidify(shell, base.CLOTH_THICKNESS_M) + denim.clamp_waist_residue(shell, waist_plane) + + # Landmarks reference the CLEAN rims for the paint/mask pass. + finalize_jogger_landmarks(lm, waist_plane, ankle_plane, BAND_FRAC, + CUFF_FRAC) + denim.author_parked_uv2(shell) + + albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png") + mask_path = os.path.join(out_dir, f"{body}_mask.png") + albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + global OFFSET_HI_M, OFFSET_MID_M, OFFSET_CUFF_M, BAND_FRAC, CUFF_FRAC + global STRIPE_W_M, FLEECE_RGB, STRIPE_RGB, WAIST_FLARE_M, NOISE_SEED, PLAIN + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--mid-offset M] [--cuff-offset M] " + "[--band-frac F] [--cuff-frac F] [--stripe-width M] " + "[--fabric r,g,b] [--stripe-rgb r,g,b] [--waist-flare M] " + "[--seed N] [--plain]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + if "--offset" in argv: + OFFSET_HI_M = float(argv[argv.index("--offset") + 1]) + if "--mid-offset" in argv: + OFFSET_MID_M = float(argv[argv.index("--mid-offset") + 1]) + if "--cuff-offset" in argv: + OFFSET_CUFF_M = float(argv[argv.index("--cuff-offset") + 1]) + if "--band-frac" in argv: + BAND_FRAC = float(argv[argv.index("--band-frac") + 1]) + if "--cuff-frac" in argv: + CUFF_FRAC = float(argv[argv.index("--cuff-frac") + 1]) + if "--stripe-width" in argv: + STRIPE_W_M = float(argv[argv.index("--stripe-width") + 1]) + if "--fabric" in argv: + FLEECE_RGB = tuple( + float(v) for v in argv[argv.index("--fabric") + 1].split(",")) + if "--stripe-rgb" in argv: + STRIPE_RGB = tuple( + float(v) for v in argv[argv.index("--stripe-rgb") + 1].split(",")) + if "--waist-flare" in argv: + WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1]) + if "--seed" in argv: + NOISE_SEED = int(argv[argv.index("--seed") + 1]) + if "--plain" in argv: + PLAIN = True + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"joggers per-body mode: {len(bodies)} bodies, offset " + f"{OFFSET_HI_M * 1000:.0f}->{OFFSET_CUFF_M * 1000:.1f} mm, " + f"band_frac={BAND_FRAC} cuff_frac={CUFF_FRAC} " + f"stripe={STRIPE_W_M * 1000:.0f} mm, plain={PLAIN}") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_jogger_shell(body_dir, out_dir, body) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_parka.py b/tooling/garment-fit/blender_author_parka.py new file mode 100644 index 000000000..efea2fb63 --- /dev/null +++ b/tooling/garment-fit/blender_author_parka.py @@ -0,0 +1,726 @@ +""" +blender_author_parka.py (T-1089 wave 2 — parka_thrds, hip-length insulated parka) + +Cold-weather parka authored as per-body offset shells — the FIRST CANON-BRANDED +garment (thrds, the Braemar fiber cooperative, wiki/star-systems/GJ-475). +Companion to blender_author_offset_shell.py (imported as a library): reuses the +base module's segment join / bone-ratio thresholds / offset / solidify / +logo-UV2 / export machinery and layers the parka geometry + texture identity on +top, combining the proven practices of the wave-1 family: + + * HIP-LENGTH — covers torso + torso_upper + FULL arms + the TOP of the hips + zone: seg_hips joins the shell, the coincident torso|hips overlap band is + WELDED before offsetting (denim practice — un-welded rings offset along + diverging normals and open cracks), and the hem is cut at a bone-derived + plane between the pelvis head and spine_01 head (no skirt panel below). + The vert-threshold hem cut leaves jagged teeth, so the open hem rim is + FLATTENED onto its own valley plane (denim flatten practice) for a clean + straight hem, which Solidify(use_rim) then caps. + * HOOD-DOWN ROLL — the hoodie's rolled-collar inflation with bulkier parka + parameters (radial bulge away from the neck axis, back-biased, rim lift). + * CHUNKY INSULATED READ — painted, not modelled: horizontal quilt channel + lines (constant-z on the body, constant-|x| around the sleeves) with a + soft per-channel "puff" luminance gradient; big front patch pockets with + flaps; a storm-flap front placket with bright zip-teeth dashes; hem + drawcord band; sleeve cuff bands. All identity is carried as LUMINANCE so + the luma-preserving toon_garment recolor keeps it under any tint. + * REGIONS (spec): R = hood roll + collar, G = body, B = sleeves, + A = pocket / quilt trim (quilt lines, pockets, storm flap, cuff bands). + Albedo and mask come from ONE shared per-texel feature-field evaluation + (denim practice) so they always agree. The mask ships CHANNEL_PACKED with + an alpha floor (coverall lesson: Godot's fix_alpha_border pass + Blender's + straight-alpha PNG save both destroy a plain A channel). + * LOGO-CAPABLE — left-breast chest patch (the storm flap owns the centre + line), same bone-ratio box as jacket_modern; painted as a brighter patch + rectangle kept in the G region so the decal reads on the body tint. + +Per-body only (Q-060: offset shells author per body, never SD-fit): + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_parka.py -- \ + client/assets/characters/bodies client/assets/characters/clothing/parka_thrds \ + [--bodies a,b,c] [--offset 0.026] [--hem-frac 0.15] + +Writes per body: /.glb (skinned, painted albedo embedded) + /_mask.png (RGBA region mask, that body's UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060. +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np +from mathutils import Vector + +# -------------------------------------------------------------------------- +# Load the base offset-shell module (shared engine machinery). +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_spec = importlib.util.spec_from_file_location( + "offset_shell_base", os.path.join(_HERE, "blender_author_offset_shell.py")) +base = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(base) + + +def log(msg): + print(f"[parka] {msg}") + + +# -------------------------------------------------------------------------- +# PARAMS — parka_thrds. A parka-family variant should only touch this block. +# -------------------------------------------------------------------------- + +GARMENT_ID = "parka_thrds" + +SEGMENTS = [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", + "seg_hips", +] + +OFFSET_M = 0.026 # chunkiest standoff of the tops family — an insulated + # parka worn over layers (jacket_modern is 0.022) +THICKNESS_M = 0.008 # insulated cloth (jacket 5 mm, hoodie 6 mm) +WELD_DIST = 5e-4 # coincident segment-seam weld tolerance (denim) + +# Hem: fraction of the pelvis->spine_01 span ABOVE the pelvis head. 0.15 lands +# just below the hip joint (thigh head) on every body — hip-length, no skirt. +HEM_PELVIS_FRAC = 0.15 +HEM_ALLOW_FRAC = 0.50 # hem-ring search band above the cut plane (same span) + +# Sleeves: wrist cut + cuff band (jacket/hoodie practice — cuff anchored to the +# ACTUAL post-cut mesh reach, the jagged cut stops 1-3 cm short of the plane). +CUFF_KEEP_FRAC = 0.88 # fraction of the lowerarm kept +CUFF_LEN_FRAC = 0.14 # cuff band length as fraction of the lowerarm + +# Collar / hood-down roll (fractions of neck_01 length unless noted). +COLLAR_DROP_NECK_FRAC = 0.55 # collar band starts this far below the neck head +COLLAR_X_MULT = 1.20 # parka collar wider than the tee band +ROLL_OUT_FRAC = 0.50 # radial bulge magnitude (hoodie 0.40 — bulkier) +ROLL_LIFT_FRAC = 0.16 # upward lift at the rim +ROLL_Z_START_FRAC = 0.50 # roll influence starts this far below collar_z_min +ROLL_REACH_COLLAR_X = 1.80 # candidate radius around neck axis (x tee collar_x) +ROLL_FRONT_GAIN = 0.60 # bulge scale at the front... +ROLL_BACK_GAIN = 1.25 # ...and the back (hood mass hangs behind) +ROLL_EXPONENT = 1.80 # falloff sharpness toward the rim +R_Z_DROP_FRONT = 0.20 # R region drop below collar_z_min (x neck_len) +R_Z_DROP_BACK = 0.60 # asymmetric — the roll drapes lower on the back + +# Storm flap + painted zip (metric refs scale by shoulder |x| / 0.1919). +FLAP_HALF_FRAC = 0.026 / 0.1919 # storm-flap half-width, frac of shoulder |x| +TEETH_HALF_M_REF = 0.0065 # zip teeth strip half-width +TEETH_PERIOD_M_REF = 0.026 # dash period along Z +TEETH_DUTY_M_REF = 0.015 # bright dash length within a period + +# Big front patch pockets (x fracs of shoulder |x|, z fracs of garment span). +POCKET_CX_FRAC = 0.44 +POCKET_HW_FRAC = 0.25 +POCKET_Z0_FRAC = 0.06 # clear of the hem drawcord band (HEM_BAND_FRAC 0.045) +POCKET_Z1_FRAC = 0.255 +POCKET_FLAP_FRAC = 0.30 # top fraction of the pocket = flap + +# Quilt channels (fracs of the garment span = collar_z_min - hem). +QUILT_PERIOD_FRAC = 0.13 +QUILT_HALFW_FRAC = 0.0070 # painted line half-width +HEM_BAND_FRAC = 0.045 # drawcord hem band height +PUFF_AMP = 0.045 # per-channel puff luminance amplitude + +# Left-breast logo patch (character-left = +X; the flap owns the centre line). +# Same average_m absolutes as jacket_modern (0.035..0.115 m, z 1.30..1.42 m). +CHEST_X_LO_FRAC = 0.035 / 0.1919 +CHEST_X_HI_FRAC = 0.115 / 0.1919 +CHEST_Z_LO_FRAC = (1.30 - 1.072) / (1.5205 - 1.072) +CHEST_Z_HI_FRAC = (1.42 - 1.072) / (1.5205 - 1.072) + +# Painted luma palette (toon_garment recolors by luma: tint * (luma*1.5+0.2)). +BASE_LUMA = 0.55 +QUILT_LUMA = 0.40 # quilt channel lines +ROLL_LUMA = 0.62 # hood roll reads lighter/lofted +FLAP_LUMA = 0.50 # storm flap band +POCKET_LUMA = 0.60 # pocket patch fill +POCKET_FLAP_LUMA = 0.47 # pocket flap +PATCH_LUMA = 0.68 # chest logo patch backing +CUFF_LUMA = 0.46 # sleeve cuff bands +HEM_LUMA = 0.48 # hem drawcord band +STITCH_LUMA = 0.33 # stitch / outline lines +TEETH_LUMA = 0.88 # bright zip teeth dashes +TEETH_GAP_LUMA = 0.30 # dark zip tape between dashes +ALBEDO_NOISE = 0.02 # woven jitter +FRONT_EPS_M = 0.004 # front-facing gate on |y| (denim practice) + +# Deep blue-grey cold-weather hue (visible when the region mask is absent; +# with the mask bound, identity is carried by luma alone). +FABRIC_HUE = np.array([0.62, 0.70, 0.88], dtype=np.float32) + +TEX_SIZE = 1024 # albedo + mask (painted quilt lines need > 512) +NOISE_SEED = 2093 + +_REF_SHOULDER_X = 0.1919 # average_m anchor (same as base/jacket) + +# Region labels -> mask channels (spec: roll+collar=R, body=G, sleeves=B, trim=A). +LBL_G, LBL_R, LBL_B, LBL_A = 0, 1, 2, 3 +LABEL_NAMES = ["body", "roll", "sleeve", "trim"] +LABEL_RGBA_ARR = np.array([ + (0.0, 1.0, 0.0, 0.0), # G body + (1.0, 0.0, 0.0, 0.0), # R hood roll + collar + (0.0, 0.0, 1.0, 0.0), # B sleeves + (0.0, 0.0, 0.0, 1.0), # A pocket / quilt trim +], dtype=np.float32) + + +# -------------------------------------------------------------------------- +# Thresholds — base derivation + parka extras from the same armature +# -------------------------------------------------------------------------- + +def parka_thresholds(armature): + thr = base.derive_thresholds(armature) + bones = armature.data.bones + + def bone(name): + b = bones.get(name) + if b is None: + raise RuntimeError(f"landmark bone {name} missing") + return b + + ua_l = bone("upperarm_l") + ua_r = bone("upperarm_r") + neck = bone("neck_01") + pelvis = bone("pelvis") + spine01 = bone("spine_01") + shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 + scale = shoulder_x / _REF_SHOULDER_X + + # Collar band (taller + wider than the tee) and hood-roll geometry. + neck_len = neck.tail_local.z - neck.head_local.z + tee_collar_x = thr["collar_x_abs"] # before the parka widening + thr["collar_z_min"] = neck.head_local.z - COLLAR_DROP_NECK_FRAC * neck_len + thr["collar_x_abs"] = tee_collar_x * COLLAR_X_MULT + thr["neck_y"] = neck.head_local.y + thr["neck_len"] = neck_len + thr["roll_out"] = ROLL_OUT_FRAC * neck_len + thr["roll_lift"] = ROLL_LIFT_FRAC * neck_len + thr["roll_reach"] = ROLL_REACH_COLLAR_X * tee_collar_x + + # Hip-length hem plane + ring-search allowance (bone-derived, per body). + pz, sz = pelvis.head_local.z, spine01.head_local.z + thr["hem_z"] = pz + HEM_PELVIS_FRAC * (sz - pz) + thr["hem_allow"] = HEM_ALLOW_FRAC * (sz - pz) + + # Wrist cut planes on the lowerarm bones (jacket practice). + cut_planes = [] + lowerarm_len = 0.0 + for bone_name, sign in [("lowerarm_l", +1), ("lowerarm_r", -1)]: + b = bone(bone_name) + head_x, tail_x = b.head_local.x, b.tail_local.x + cut_planes.append((sign, head_x + CUFF_KEEP_FRAC * (tail_x - head_x))) + lowerarm_len = abs(tail_x - head_x) + thr["wrist_cut_planes"] = cut_planes + thr["cuff_len"] = CUFF_LEN_FRAC * lowerarm_len + + # Storm flap + zip teeth. + thr["flap_half"] = shoulder_x * FLAP_HALF_FRAC + thr["teeth_half"] = TEETH_HALF_M_REF * scale + thr["teeth_period"] = TEETH_PERIOD_M_REF * scale + thr["teeth_duty"] = TEETH_DUTY_M_REF * scale + + # Pockets (x now; z after the span is known in finalize_thresholds). + thr["pocket_cx"] = POCKET_CX_FRAC * shoulder_x + thr["pocket_hw"] = POCKET_HW_FRAC * shoulder_x + + # Left-breast logo patch (replaces the base full-chest box). + thr["chest_x"] = (shoulder_x * CHEST_X_LO_FRAC, shoulder_x * CHEST_X_HI_FRAC) + spine_lo = spine01.head_local.z + span = neck.head_local.z - spine_lo + thr["chest_z"] = (spine_lo + CHEST_Z_LO_FRAC * span, + spine_lo + CHEST_Z_HI_FRAC * span) + + log(f"thresholds: hem z={thr['hem_z']:.3f} collar z>={thr['collar_z_min']:.3f} " + f"|x|<{thr['collar_x_abs']:.3f} roll out={thr['roll_out']*1000:.0f}mm " + f"reach={thr['roll_reach']:.3f} flap half={thr['flap_half']:.3f} " + f"pocket cx={thr['pocket_cx']:.3f} hw={thr['pocket_hw']:.3f} " + f"chest x=({thr['chest_x'][0]:.3f},{thr['chest_x'][1]:.3f}) " + f"z=({thr['chest_z'][0]:.3f},{thr['chest_z'][1]:.3f})") + return thr + + +def finalize_thresholds(shell, thr): + """Mesh-measured extras after cuts/flatten/offset/roll: the garment span + (drives quilt/pocket/hem-band proportions) and the cuff anchor from the + sleeves' ACTUAL post-cut reach (hoodie practice).""" + span = thr["collar_z_min"] - thr["hem_z"] + thr["span"] = span + thr["quilt_period"] = QUILT_PERIOD_FRAC * span + thr["quilt_halfw"] = QUILT_HALFW_FRAC * span + thr["hem_band_h"] = HEM_BAND_FRAC * span + thr["pocket_z0"] = thr["hem_z"] + POCKET_Z0_FRAC * span + thr["pocket_z1"] = thr["hem_z"] + POCKET_Z1_FRAC * span + thr["pocket_flap_h"] = POCKET_FLAP_FRAC * (thr["pocket_z1"] - thr["pocket_z0"]) + wrist_x = max((abs(v.co.x) for v in shell.data.vertices), default=0.0) + thr["cuff_x0"] = wrist_x - thr["cuff_len"] + log(f"finalized: span={span:.3f} quilt period={thr['quilt_period']*1000:.0f}mm " + f"pocket z=({thr['pocket_z0']:.3f},{thr['pocket_z1']:.3f}) " + f"cuff |x|>={thr['cuff_x0']:.3f} (reach {wrist_x:.3f})") + + +# -------------------------------------------------------------------------- +# Geometry: weld, cuts, hem flatten, hood roll +# -------------------------------------------------------------------------- + +def weld_boundaries(shell): + """Merge coincident segment-boundary verts (denim practice) so the offset + can't open cracks at the torso|hips / torso|arm seams. Weights are + identical by origin, so skinning is unaffected.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + before = len(bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=WELD_DIST) + merged = before - len(bm.verts) + bm.to_mesh(me) + bm.free() + me.update() + log(f"welded segment boundaries: {merged} verts merged " + f"({before} -> {len(me.vertices)})") + + +def wrist_cut(shell, thr): + """Delete forearm verts beyond the wrist plane on each side (jacket).""" + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + to_delete = [] + for v in bm.verts: + for sign, thr_x in thr["wrist_cut_planes"]: + if sign > 0 and v.co.x > thr_x: + to_delete.append(v) + break + if sign < 0 and v.co.x < thr_x: + to_delete.append(v) + break + bmesh.ops.delete(bm, geom=to_delete, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"wrist cut removed {len(to_delete)} verts; " + f"{len(shell.data.vertices)} remain") + + +def hem_cut(shell, thr): + """Trim everything below the hip-length hem plane (removes the seg_hips + lower boundary, crotch and leg openings entirely).""" + hem_z = thr["hem_z"] + bm = bmesh.new() + bm.from_mesh(shell.data) + doomed = [v for v in bm.verts if v.co.z < hem_z] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"hem cut at z={hem_z:.3f}: removed {len(doomed)} verts") + + +def flatten_hem_rim(shell, thr): + """Pull the open hem-ring verts onto the ring's own valley plane (denim + flatten practice) — the vert-threshold cut leaves jagged teeth; a clean + straight hem is what Solidify(use_rim) then caps. Only boundary verts in + the hem band move; the neck and wrist rims sit far above `hem_allow`.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + limit = thr["hem_z"] + thr["hem_allow"] + ring = set() + for e in bm.edges: + if len(e.link_faces) == 1: # open boundary + for v in e.verts: + if v.co.z < limit: + ring.add(v.index) + if not ring: + log("WARNING: no hem-ring verts found — hem left jagged") + bm.free() + return + zs = [bm.verts[i].co.z for i in ring] + valley = min(zs) + teeth = max(zs) - valley + for i in ring: + bm.verts[i].co.z = valley + bm.to_mesh(me) + bm.free() + me.update() + thr["hem_z"] = valley # paint anchors reference the clean rim + log(f"flattened hem rim: {len(ring)} verts, teeth {teeth:.3f} m " + f"-> plane z={valley:.3f}") + + +def hood_roll(shell, thr): + """Inflate collar-band verts radially away from the neck axis to read as a + rolled-down hood (hoodie practice, parka-bulk parameters). Runs after the + outward offset and before solidify so the roll gets cloth thickness.""" + import math + z_start = thr["collar_z_min"] - ROLL_Z_START_FRAC * thr["neck_len"] + reach = thr["roll_reach"] + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + + candidates = [] + z_rim = z_start + for v in bm.verts: + if v.co.z < z_start: + continue + hd = math.hypot(v.co.x, v.co.y - thr["neck_y"]) + if hd > reach or hd < 1e-6: + continue + candidates.append((v, hd)) + z_rim = max(z_rim, v.co.z) + if z_rim <= z_start or not candidates: + log("WARNING: no hood-roll candidates found — roll skipped") + bm.free() + return + + moved = 0 + for v, hd in candidates: + t = (v.co.z - z_start) / (z_rim - z_start) + w = max(0.0, min(1.0, t)) ** ROLL_EXPONENT + if w <= 0.0: + continue + dir_h = Vector((v.co.x, v.co.y - thr["neck_y"], 0.0)) / hd + backness = 0.5 * (1.0 + dir_h.y * -base.FRONT_Y_SIGN) # +Y = back + gain = ROLL_FRONT_GAIN + (ROLL_BACK_GAIN - ROLL_FRONT_GAIN) * backness + v.co += dir_h * (thr["roll_out"] * w * gain) + v.co.z += thr["roll_lift"] * w + moved += 1 + bm.to_mesh(me) + bm.free() + me.update() + log(f"hood roll: {moved} verts inflated (rim z {z_rim:.3f}, " + f"start z {z_start:.3f})") + + +# -------------------------------------------------------------------------- +# Feature field — ONE evaluation drives albedo luma AND region label together +# -------------------------------------------------------------------------- + +def parka_field(px, py, pz, thr): + """Evaluate the parka feature field at texel 3D positions (numpy arrays). + + Returns (label uint8 array indexing LABEL_RGBA_ARR, luma float32 array). + """ + n = px.shape[0] + label = np.full(n, LBL_G, dtype=np.uint8) + luma = np.full(n, BASE_LUMA, dtype=np.float32) + ax = np.abs(px) + front = py * base.FRONT_Y_SIGN > FRONT_EPS_M + ew = thr["quilt_halfw"] # shared edge/stitch line half-width + + on_sleeve = ax >= thr["sleeve_x_abs"] + label[on_sleeve] = LBL_B + + # --- zones ------------------------------------------------------------- + hem = thr["hem_z"] + hem_top = hem + thr["hem_band_h"] + hem_band = pz <= hem_top + cuff = ax >= thr["cuff_x0"] + flap = front & (ax <= thr["flap_half"]) + pocket_dx = np.abs(ax - thr["pocket_cx"]) + in_pocket_z = (pz >= thr["pocket_z0"]) & (pz <= thr["pocket_z1"]) + pocket = front & in_pocket_z & (pocket_dx <= thr["pocket_hw"]) + m = ew * 3.0 # quilt keep-out margin around painted features + pocket_pad = front & (pz >= thr["pocket_z0"] - m) & (pz <= thr["pocket_z1"] + m) \ + & (pocket_dx <= thr["pocket_hw"] + m) + x0, x1 = thr["chest_x"] + z0, z1 = thr["chest_z"] + patch = front & (px >= x0) & (px <= x1) & (pz >= z0) & (pz <= z1) + patch_pad = front & (px >= x0 - m) & (px <= x1 + m) \ + & (pz >= z0 - m) & (pz <= z1 + m) + hd = np.hypot(px, py - thr["neck_y"]) + roll_zmin = thr["collar_z_min"] \ + - np.where(front, R_Z_DROP_FRONT, R_Z_DROP_BACK) * thr["neck_len"] + # Sleeve texels never join the roll (hoodie precedence: sleeve check first) + # — without the gate the deltoid caps classify as R on every body. + roll = (pz >= roll_zmin) & ~on_sleeve \ + & (hd <= thr["roll_reach"] + thr["roll_out"] + 0.01) + + # --- quilt channels (constant-z on the body, constant-|x| on sleeves) --- + coord = np.where(on_sleeve, ax - thr["sleeve_x_abs"], pz - hem) + period = thr["quilt_period"] + tph = np.mod(coord, period) / period + edge_d = np.minimum(tph, 1.0 - tph) * period + quilt_ok = ~(hem_band | cuff | flap | pocket_pad | patch_pad | roll) + qline = quilt_ok & (edge_d <= ew) + puff = 4.0 * tph * (1.0 - tph) + + # --- luma (paint order = precedence, later writes win) ------------------ + luma += np.where(quilt_ok, PUFF_AMP * (puff - 0.5) * 2.0, 0.0) + luma[qline] = QUILT_LUMA + luma[hem_band] = HEM_LUMA + hemline = (np.abs(pz - hem_top) <= ew) & ~on_sleeve + luma[hemline] = STITCH_LUMA + luma[cuff] = CUFF_LUMA + cuffline = np.abs(ax - thr["cuff_x0"]) <= ew + luma[cuffline] = STITCH_LUMA + + # Big patch pockets: fill, flap, stitch outline. + luma[pocket] = POCKET_LUMA + flap_z = thr["pocket_z1"] - thr["pocket_flap_h"] + luma[pocket & (pz >= flap_z)] = POCKET_FLAP_LUMA + p_outline = pocket & ( + (pocket_dx >= thr["pocket_hw"] - ew) + | (pz <= thr["pocket_z0"] + ew) + | (pz >= thr["pocket_z1"] - ew) + | (np.abs(pz - flap_z) <= ew) + ) + luma[p_outline] = STITCH_LUMA + + # Chest logo patch backing (stays in G so the decal reads on body tint). + luma[patch] = PATCH_LUMA + patch_edge = patch & ( + (px <= x0 + ew) | (px >= x1 - ew) | (pz <= z0 + ew) | (pz >= z1 - ew) + ) + luma[patch_edge] = STITCH_LUMA + + # Storm flap + zip teeth (teeth stop under the hood roll). + luma[flap] = FLAP_LUMA + flap_edge = front & (np.abs(ax - thr["flap_half"]) <= ew * 0.8) + luma[flap_edge] = STITCH_LUMA + teeth = front & (ax <= thr["teeth_half"]) & ~roll + dash = np.mod(pz, thr["teeth_period"]) < thr["teeth_duty"] + luma[teeth & dash] = TEETH_LUMA + luma[teeth & ~dash] = TEETH_GAP_LUMA + + # Hood roll last: lofted read + a collar seam line right under it. + rollseam = (np.abs(pz - roll_zmin) <= ew) & ~on_sleeve \ + & (hd <= thr["roll_reach"] + thr["roll_out"] + 0.02) + luma[rollseam & ~roll] = STITCH_LUMA + luma[roll] = ROLL_LUMA + + # --- region labels (same masks; precedence: trim, then roll wins) ------- + label[qline] = LBL_A + label[cuff] = LBL_A + label[pocket] = LBL_A + label[flap] = LBL_A + label[roll] = LBL_R + return label, luma + + +# -------------------------------------------------------------------------- +# Combined albedo + mask bake (per-texel, pre-solidify — coverall practice) +# -------------------------------------------------------------------------- + +def _tri_texels(a, b, c, W, H): + """Texels covered by UV triangle (a,b,c) with barycentric weights + (coverall rasterizer: returns ys, xs, w0, w1, w2).""" + empty = (np.empty(0, int),) * 2 + (np.empty(0, np.float32),) * 3 + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return empty + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return empty + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + pxg = xs + 0.5 + pyg = ys + 0.5 + w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom + w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return empty + return ( + ys[inside], xs[inside], + w0[inside].astype(np.float32), + w1[inside].astype(np.float32), + w2[inside].astype(np.float32), + ) + + +def bake_albedo_and_mask(shell, thr, out_dir, body, seed): + """One pass over the UV0 faces: albedo and mask from a single shared + feature-field evaluation per texel (denim practice). Baked PRE-solidify so + there is exactly one face per texel (coverall practice).""" + W = H = TEX_SIZE + rng = np.random.default_rng(seed) + noise_buf = (rng.random((H, W), dtype=np.float32) - 0.5) * 2.0 * ALBEDO_NOISE + + alb = np.empty((H, W, 4), dtype=np.float32) + alb[:, :, 0:3] = np.clip( + (BASE_LUMA + noise_buf)[:, :, None] * FABRIC_HUE[None, None, :], 0.0, 1.0) + alb[:, :, 3] = 1.0 + mask = np.zeros((H, W, 4), dtype=np.float32) + mask[:, :, 1] = 1.0 # body-green background (bilinear-bleed safe) + label_map = np.full((H, W), LBL_G, dtype=np.uint8) + covered = np.zeros((H, W), dtype=bool) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + if not len(bm.loops.layers.uv): + raise RuntimeError("no UV layer for albedo/mask bake") + uv_layer = bm.loops.layers.uv[0] + + tri_count = 0 + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv for loop in loops] + pos = [loop.vert.co for loop in loops] + for i in range(1, len(loops) - 1): + tri = (0, i, i + 1) + ys, xs, w0, w1, w2 = _tri_texels( + uvs[tri[0]], uvs[tri[1]], uvs[tri[2]], W, H) + if ys.size == 0: + continue + px3 = w0 * pos[tri[0]].x + w1 * pos[tri[1]].x + w2 * pos[tri[2]].x + py3 = w0 * pos[tri[0]].y + w1 * pos[tri[1]].y + w2 * pos[tri[2]].y + pz3 = w0 * pos[tri[0]].z + w1 * pos[tri[1]].z + w2 * pos[tri[2]].z + lab, lum = parka_field(px3, py3, pz3, thr) + lum = np.clip(lum + noise_buf[ys, xs], 0.0, 1.0) + alb[ys, xs, 0:3] = np.clip( + lum[:, None] * FABRIC_HUE[None, :], 0.0, 1.0) + mask[ys, xs] = LABEL_RGBA_ARR[lab] + label_map[ys, xs] = lab + covered[ys, xs] = True + tri_count += 1 + bm.free() + + total = max(int(covered.sum()), 1) + counts = np.bincount(label_map[covered], minlength=len(LABEL_NAMES)) + log(f"painted {tri_count} UV triangles; region texels: " + " ".join( + f"{LABEL_NAMES[i]}={int(c)} ({100.0 * c / total:.1f}%)" + for i, c in enumerate(counts))) + + # Alpha floor (coverall lesson): Godot's fix_alpha_border import pass + # rewrites RGB of fully-transparent texels — floor A at 2/255 everywhere. + mask[:, :, 3] = np.maximum(mask[:, :, 3], 2.0 / 255.0) + + mask_path = os.path.join(out_dir, f"{body}_mask.png") + img_mask = bpy.data.images.new(f"{GARMENT_ID}_mask_{body}", W, H, alpha=True) + # Channel-packed DATA, not imagery: a straight-alpha PNG save would zero + # the A (trim) channel (coverall lesson, verified there). + img_mask.alpha_mode = 'CHANNEL_PACKED' + img_mask.pixels.foreach_set(mask.reshape(-1)) + img_mask.update() + img_mask.filepath_raw = mask_path + img_mask.file_format = 'PNG' + img_mask.save() + log(f"baked region mask -> {mask_path}") + + img_alb = bpy.data.images.new(f"{GARMENT_ID}_albedo_{body}", W, H, alpha=False) + img_alb.pixels.foreach_set(alb.reshape(-1)) + img_alb.update() + return img_alb + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_parka(body_dir, out_dir, body, offset, seed): + base.clear_scene() + base.COVERED_SEGMENTS = SEGMENTS # build_covered_mesh reads this + shell, armature = base.build_covered_mesh(body_dir) + weld_boundaries(shell) + thr = parka_thresholds(armature) + + wrist_cut(shell, thr) + hem_cut(shell, thr) + flatten_hem_rim(shell, thr) + base.offset_outward(shell, offset) + hood_roll(shell, thr) + finalize_thresholds(shell, thr) + + # UV2 + bake BEFORE solidify (coverall practice: one face per texel). + base.author_logo_uv(shell, thr) + albedo_img = bake_albedo_and_mask(shell, thr, out_dir, body, seed) + + base.solidify(shell, THICKNESS_M) + base.assign_fabric_material(shell, albedo_img) + + # Shared sidecar name before export: the glTF exporter names the embedded + # image after the filepath basename and Godot extracts it as + # _.png — this lands on the tshirt-convention + # _base_albedo.png (coverall/jacket practice). + base.save_albedo_sidecar(albedo_img, os.path.join(out_dir, "base_albedo.png")) + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + shutil.copy2(os.path.join(out_dir, "base_albedo.png"), + os.path.join(out_dir, f"{body}_base_albedo.png")) + + +def main(): + global HEM_PELVIS_FRAC + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--hem-frac F]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--hem-frac" in argv: + HEM_PELVIS_FRAC = float(argv[argv.index("--hem-frac") + 1]) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"parka per-body mode: {len(bodies)} bodies, offset {offset*1000:.0f} mm, " + f"hem-frac {HEM_PELVIS_FRAC}") + + results = [] + for i, body in enumerate(bodies): + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_parka(body_dir, out_dir, body, offset, seed=NOISE_SEED + i) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + # Deterministic shared sidecars mirror the reference body (average_m). + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_shoes_formal.py b/tooling/garment-fit/blender_author_shoes_formal.py new file mode 100644 index 000000000..7a81618af --- /dev/null +++ b/tooling/garment-fit/blender_author_shoes_formal.py @@ -0,0 +1,755 @@ +""" +blender_author_shoes_formal.py (T-1089 wave 2, footwear: shoes_formal) + +Authors FORMAL SHOES (both feet, one garment — the peasant_shoes convention) +as per-body offset shells, reusing blender_author_offset_shell.py as a library +(scene build, join, offset, solidify, GLB export) and the denim companion's +required bottoms/footwear practices (boundary WELD, parked logo UV2). + +What footwear adds, as reusable parameters (sneakers/boots can re-drive this): + + * both-feet build: seg_foot_l + seg_foot_r joined into ONE skinned mesh; + the two shells stay disjoint (weights by construction, one draw call). + * boundary WELD is load-bearing here, not just hygienic: a raw seg_foot is + 3-8 mesh islands (upper + a separate flat SOLE PLATE sheet + ankle-band + slivers). remove_doubles(0.5 mm) zips them into one shell per foot whose + only open boundary is the ankle ring (verified on average_m + child). + * ankle TOPLINE cut: the welded ankle ring is jagged weight-threshold teeth + (~5 cm on average_m). Verts above the ring's own valley (optionally + +--topline-lift) are deleted and the fresh boundary is flattened UP onto + the topline plane — a clean horizontal low-profile shoe opening. Guard + rails clamp the topline to [0.70, 0.95] x the foot-bone head height. + The rim ring is then RELAXED in XY (neighbour averaging, z pinned) so the + throat reads as a smooth loafer opening instead of a jagged U. + * TOE-BOX smoothing: the skin mesh has individual toe bumps; offsetting + them verbatim yields a five-finger foot-glove. Forefoot verts are + Laplacian-smoothed in feathered bands (mild at the metatarsal, strong at + the toes) into one smooth formal toe box, then given a small extra + standoff (--toe-extra) to buy back the clearance smoothing costs over + the toe bumps. Band reps are deliberately MODERATE: smoothing migrates + shell verts away from the skin they are weighted like, and under toe + flex (Walk heel-strike / Sprint push-off) the migration error scales + with sin(flex angle) — first authoring pass used (2,4,8) reps and the + animated toes visibly overtook the toe box. + * CLEARANCE ENFORCEMENT (the fix that guards all of the above): a BVH + snapshot of the post-cut, PRE-smoothing skin surface; after offset + + toe-extra, every shell vert closer than the shell offset to the skin + (signed, along the skin normal) is pushed out to exactly that standoff. + Guaranteed rest-pose clearance by construction, no matter how far the + toe smoothing migrated verts. + * SOLE as an offset-shell param extension: pre-offset, downward-facing + verts (normal.z < -0.5) are recorded; post-offset they are flattened onto + a plane so the finished exterior bottom sits --sole-mm (default 8 mm) + below the skin's lowest point once Solidify adds cloth thickness. A final + clamp keeps every vert on/above the plane — thin flat sole. + * UV0 NORMALIZATION: feet occupy a tiny corner of the body texture atlas; + since BOTH the painted albedo and the region mask are authored here on + UV0 (skin textures are dropped), the used UV bbox is rescaled to fill + [0,1] — ~10x texel density for the painted seams at no cost. Mirrored + L/R UV islands may overlap; all painted features are x-symmetric, so + overlap is harmless by construction. + * texel-level feature painting (denim's albedo+mask-from-one-field idea): + - albedo: painted TOE CAP LINE across the vamp, sole-edge welt stitch, + topline edge stitch, heel counter seam — flat toon-friendly, + identity carried by luminance (the toon_garment shader tints + by luma, so lines survive a black default tint). + - mask: sole -> R, upper -> G, toe cap -> B (spec regions). + * parked logo UV2 (shoes are not logo-capable; shader samples UV2 anyway). + +All parameters derive PER BODY from that body's own bone landmarks (foot_l / +ball_l) and measured mesh extents (toe/heel y, skin bottom z, per-foot ankle +ring valley), scaled by foot length against the average_m reference — the +same proportional-ratio philosophy as base.derive_thresholds. Per-body mode +only (offset shells author per body, Q-060). + +Usage (shoes_formal reference invocation): + tooling/blender --background --python \ + tooling/garment-fit/blender_author_shoes_formal.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/shoes_formal \ + [--bodies average_m,child,...] [--offset 0.006] [--sole-mm 0.008] \ + [--topline-lift 0.0] [--cap-frac 0.35] [--base-rgb r,g,b] \ + [--sole-rgb r,g,b] [--plain] [--no-uv-normalize] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Coverage note (the peasant_shoes convention): footwear ships hides: [] — the +foot skin stays VISIBLE at runtime because the shoe throat legitimately shows +the instep (hiding the feet would open a see-through hole there). The +chromakey QA still gates: its config passes an explicit "covers": +["foot_l","foot_r"] so the feet are keyed even though coverage.json hides +nothing. + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house +wardrobe), Q-060 (per-body offset shells). +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# -------------------------------------------------------------------------- +# Import the base offset-shell module + the denim companion (shared utilities) +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _load(mod_name, fname): + spec = importlib.util.spec_from_file_location( + mod_name, os.path.join(_HERE, fname)) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +base = _load("offset_shell_base", "blender_author_offset_shell.py") +denim = _load("denim_pants_lib", "blender_author_denim_pants.py") + +log = base.log +FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y; toes point -Y + +# -------------------------------------------------------------------------- +# Parameters (all reusable across the footwear family) +# -------------------------------------------------------------------------- +COVERED_SEGMENTS = ["seg_foot_l", "seg_foot_r"] + +SHELL_OFFSET_M = 0.006 # slim low-profile standoff (weights exact per body) +SOLE_TOTAL_M = 0.008 # finished exterior sole drop below the skin bottom +TOPLINE_LIFT_M = 0.0 # extra height above the ankle-ring valley +TOPLINE_FLOOR_FRAC = 0.70 # topline >= this frac of foot-bone head z +TOPLINE_CEIL_FRAC = 0.95 # topline <= this frac of foot-bone head z +CAP_BALL_FRAC = 0.35 # toe-cap line along the ball bone (head -> tail) +SOLE_BAND_M = 0.010 # sole side band height on average_m (R region) +SEAM_W_M = 0.0030 # painted seam width on average_m +SEAM_W_MIN_M = 0.0016 # floor so child seams don't alias away +HEEL_SEAM_FRAC = 0.16 # heel counter seam, fraction of foot len from heel +MIN_ISLAND_VERTS = 10 # post-cut sliver cleanup threshold +TOE_SMOOTH_REPS = (2, 3, 4) # feathered smoothing bands, metatarsal -> toes + # (moderate: heavy reps migrate verts off their + # weight-source skin and animated toes poke out) +TOE_EXTRA_OFFSET_M = 0.005 # extra forefoot standoff after smoothing +RIM_RELAX_PASSES = 2 # XY neighbour-average passes on the topline ring +TEX_SIZE = 1024 +UV_NORMALIZE = True # rescale used UV bbox to fill [0,1] +PLAIN = False # --plain: skip painted stitch lines + +# Formal leather style (luma carries the detail; runtime tints recolor). +LEATHER_RGB = (0.320, 0.315, 0.330) # upper mid-grey leather +SOLE_RGB = (0.235, 0.230, 0.240) # sole band slightly darker +STITCH_RGB = (0.560, 0.550, 0.570) # painted seam thread (lighter luma) +ALBEDO_NOISE = 0.015 +NOISE_SEED = 3089 + +# Reference proportions (average_m) the fractions were calibrated against. +_REF_FOOT_LEN = 0.2704 # heel y (+0.1374) - toe y (-0.1330) + + +# -------------------------------------------------------------------------- +# Per-body landmarks +# -------------------------------------------------------------------------- + +class FootLandmarks: + """Cut/mask/paint parameters derived from one body's bones + mesh.""" + + def __init__(self, armature, mesh): + bones = armature.data.bones + foot = bones.get("foot_l") + ball = bones.get("ball_l") + if foot is None or ball is None: + raise RuntimeError("foot_l/ball_l missing — not the 65-bone rig?") + self.ankle_z = foot.head_local.z + ys = [v.co.y for v in mesh.vertices] + zs = [v.co.z for v in mesh.vertices] + self.toe_y = min(ys) # toes point -Y (FRONT_Y_SIGN) + self.heel_y = max(ys) + self.skin_min_z = min(zs) + self.foot_len = self.heel_y - self.toe_y + self.s = self.foot_len / _REF_FOOT_LEN + # Toe-cap line sits along the ball bone (metatarsal -> toes). + self.ball_head_y = ball.head_local.y + self.ball_tail_y = ball.tail_local.y + self.cap_y = self.ball_head_y + CAP_BALL_FRAC * ( + self.ball_tail_y - self.ball_head_y) + self.heel_seam_y = self.heel_y - HEEL_SEAM_FRAC * self.foot_len + # Geometry planes: Solidify adds CLOTH_THICKNESS_M outward (down at + # the sole), so the pre-solidify flatten plane sits thickness higher. + self.sole_plane = (self.skin_min_z - SOLE_TOTAL_M + + base.CLOTH_THICKNESS_M) + self.sole_top = self.sole_plane + SOLE_BAND_M * self.s + self.seam_w = max(SEAM_W_M * self.s, SEAM_W_MIN_M) + self.topline = {} # per foot side ('L'/'R'), set by ankle cut + log(f"landmarks: ankle_z={self.ankle_z:.4f} foot_len={self.foot_len:.4f} " + f"(s={self.s:.3f}) cap_y={self.cap_y:.4f} " + f"heel_seam_y={self.heel_seam_y:.4f} skin_min_z={self.skin_min_z:.4f} " + f"sole_plane={self.sole_plane:.4f} sole_top={self.sole_top:.4f} " + f"seam_w={self.seam_w * 1000:.1f}mm") + + +# -------------------------------------------------------------------------- +# Geometry +# -------------------------------------------------------------------------- + +def recalc_normals(shell): + """Consistent outward face normals pre-offset (the raw foot carries a + separately-authored sole plate whose orientation is not guaranteed).""" + bm = bmesh.new() + bm.from_mesh(shell.data) + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log("recalculated outward face normals") + + +def _side(x): + return "L" if x >= 0.0 else "R" + + +def ankle_cut_and_flatten(shell, lm, lift): + """Per foot: topline = clamp(ankle-ring valley + lift); delete verts above + it; drop disconnected slivers; flatten the fresh boundary UP onto the + plane. Lifting (not lowering) is safe for footwear: the rim sits a full + shell-offset OUTSIDE the skin, so raising it only deepens the overlap + with the (visible) ankle skin — cloth over skin, never a gap.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + + # 1. Per-foot ankle-ring valley (the welded shell's only open boundary). + ring_z = {"L": [], "R": []} + for e in bm.edges: + if len(e.link_faces) == 1: + for v in e.verts: + ring_z[_side(v.co.x)].append(v.co.z) + floor_z = TOPLINE_FLOOR_FRAC * lm.ankle_z + ceil_z = TOPLINE_CEIL_FRAC * lm.ankle_z + for side in ("L", "R"): + if not ring_z[side]: + raise RuntimeError(f"no ankle boundary ring on side {side}") + valley = min(ring_z[side]) + lm.topline[side] = min(max(valley + lift, floor_z), ceil_z) + log(f"topline {side}: valley={valley:.4f} -> {lm.topline[side]:.4f} " + f"(guards [{floor_z:.4f}, {ceil_z:.4f}], ring teeth " + f"{max(ring_z[side]) - valley:.4f} m)") + + # 2. Cut above the topline. + doomed = [v for v in bm.verts if v.co.z > lm.topline[_side(v.co.x)]] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + log(f"ankle cut removed {len(doomed)} verts") + + # 3. Sliver cleanup: islands the cut disconnected. + bm.verts.ensure_lookup_table() + seen = set() + doomed_isl = [] + for v in bm.verts: + if v.index in seen: + continue + stack, isl = [v], set() + while stack: + cur = stack.pop() + if cur.index in isl: + continue + isl.add(cur.index) + for e in cur.link_edges: + o = e.other_vert(cur) + if o.index not in isl: + stack.append(o) + seen |= isl + if len(isl) < MIN_ISLAND_VERTS: + doomed_isl.extend(isl) + if doomed_isl: + bm.verts.ensure_lookup_table() + bmesh.ops.delete(bm, geom=[bm.verts[i] for i in doomed_isl], + context='VERTS') + log(f"removed {len(doomed_isl)} sliver-island verts") + + # 4. Flatten the fresh jagged boundary UP onto the topline plane. + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + lifted = 0 + for e in bm.edges: + if len(e.link_faces) == 1: + for v in e.verts: + tl = lm.topline[_side(v.co.x)] + if v.co.z != tl: + v.co.z = tl + lifted += 1 + bm.to_mesh(me) + bm.free() + me.update() + log(f"flattened ankle rims: {lifted} boundary verts -> topline planes") + + +def relax_rim(shell, lm, passes): + """XY neighbour-averaging over the topline boundary ring (z pinned to the + topline) so the throat opening reads smooth. Mild by design: the ring + sits a full shell-offset outside the skin, and 2 half-weight passes stay + well inside that budget.""" + if passes <= 0: + return + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + adj = {} + for e in bm.edges: + if len(e.link_faces) == 1: + a, b = e.verts + adj.setdefault(a.index, []).append(b.index) + adj.setdefault(b.index, []).append(a.index) + for _ in range(passes): + new_xy = {} + for i, nbrs in adj.items(): + if not nbrs: + continue + ax = sum(bm.verts[j].co.x for j in nbrs) / len(nbrs) + ay = sum(bm.verts[j].co.y for j in nbrs) / len(nbrs) + v = bm.verts[i] + new_xy[i] = (0.5 * v.co.x + 0.5 * ax, 0.5 * v.co.y + 0.5 * ay) + for i, (x, y) in new_xy.items(): + bm.verts[i].co.x = x + bm.verts[i].co.y = y + bm.to_mesh(me) + bm.free() + me.update() + log(f"relaxed topline rim: {len(adj)} verts, {passes} XY passes") + + +def smooth_toe_box(shell, lm): + """Feathered Laplacian smoothing over the forefoot so the individual toe + bumps merge into one formal toe box. Bands (mild -> strong toward the + toes) avoid a crease at the smoothing frontier.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + margin = 0.012 * lm.s + bands = [ + (lm.ball_head_y + margin, TOE_SMOOTH_REPS[0]), + (lm.ball_head_y, TOE_SMOOTH_REPS[1]), + ((lm.ball_head_y + lm.ball_tail_y) * 0.5, TOE_SMOOTH_REPS[2]), + ] + total = 0 + for y_max, reps in bands: + verts = [v for v in bm.verts if v.co.y < y_max] + if not verts: + continue + for _ in range(reps): + bmesh.ops.smooth_vert(bm, verts=verts, factor=0.5, + use_axis_x=True, use_axis_y=True, + use_axis_z=True) + total += len(verts) * reps + bm.to_mesh(me) + bm.free() + me.update() + log(f"smoothed toe box: bands at y<{[f'{b:.3f}' for b, _ in bands]} " + f"({total} vert-passes)") + + +def toe_extra_offset(shell, lm, extra): + """Extra forefoot standoff (feathered along y) to buy back the clearance + the toe smoothing costs over the toe bumps.""" + if extra <= 0.0: + return + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.normal_update() + margin = 0.012 * lm.s + y0 = lm.ball_head_y + margin # feather start (0 extra) + y1 = lm.ball_tail_y # full extra from the ball line on + n = 0 + for v in bm.verts: + if v.co.y < y0: + t = min((y0 - v.co.y) / max(y0 - y1, 1e-6), 1.0) + v.co += v.normal * (extra * t) + n += 1 + bm.to_mesh(me) + bm.free() + me.update() + log(f"toe extra offset: {n} verts, +{extra * 1000:.1f} mm feathered") + + +def snapshot_skin_bvh(shell): + """BVH of the current (post-cut, pre-smoothing) skin surface — at this + stage the shell verts still ARE the skin verts, so this is the reference + every later deformation is measured against.""" + import mathutils.bvhtree + bm = bmesh.new() + bm.from_mesh(shell.data) + bvh = mathutils.bvhtree.BVHTree.FromBMesh(bm) + bm.free() + log("snapshotted skin surface BVH (clearance reference)") + return bvh + + +def enforce_clearance(shell, skin_bvh, min_clearance): + """Push any shell vert closer than `min_clearance` to the skin snapshot + out to exactly that standoff (along the skin normal). This is what makes + the toe smoothing safe: Laplacian migration can leave the toe box inside + the toe bumps; here every vert gets its rest-pose clearance back by + construction.""" + me = shell.data + pushed = 0 + worst = 0.0 + for v in me.vertices: + hit = skin_bvh.find_nearest(v.co) + if hit is None or hit[0] is None: + continue + location, normal, _idx, _dist = hit + signed = (v.co - location).dot(normal) + if signed < min_clearance: + v.co = location + normal * min_clearance + pushed += 1 + worst = max(worst, min_clearance - signed) + me.update() + log(f"enforced clearance {min_clearance * 1000:.1f} mm: pushed {pushed} " + f"verts (worst deficit {worst * 1000:.1f} mm)") + + +def classify_sole_verts(shell): + """Indices of downward-facing verts (the foot underside), pre-offset.""" + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.normal_update() + idx = [v.index for v in bm.verts if v.normal.z < -0.5] + bm.free() + log(f"classified {idx and len(idx) or 0} sole (downward-normal) verts") + return idx + + +def flatten_sole(shell, sole_idx, sole_plane): + """Post-offset: pull the underside onto the flat sole plane, then clamp + everything on/above it (toe rounding can dip below after the offset).""" + me = shell.data + for i in sole_idx: + me.vertices[i].co.z = sole_plane + clamped = 0 + for v in me.vertices: + if v.co.z < sole_plane: + v.co.z = sole_plane + clamped += 1 + me.update() + log(f"flattened sole: {len(sole_idx)} verts -> z={sole_plane:.4f} " + f"(+{clamped} clamped)") + + +def clamp_topline_residue(shell, lm): + """Post-solidify safety clamp: rim-adjacent verts the Solidify pushed + above the topline get squashed back onto it (denim waist pattern).""" + me = shell.data + n = 0 + for v in me.vertices: + tl = lm.topline[_side(v.co.x)] + if v.co.z > tl: + v.co.z = tl + n += 1 + me.update() + if n: + log(f"clamped {n} residual topline verts") + + +def normalize_uv0(shell): + """Rescale the used UV0 bbox to fill [0,1] (uniform scale, aspect kept). + Feet use a tiny corner of the body atlas; both the albedo and the mask + are authored here on UV0, so reclaiming the space is free texel density.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + uvl = bm.loops.layers.uv[0] + us, vs = [], [] + for f in bm.faces: + for lo in f.loops: + us.append(lo[uvl].uv.x) + vs.append(lo[uvl].uv.y) + u0, u1, v0, v1 = min(us), max(us), min(vs), max(vs) + span = max(u1 - u0, v1 - v0) + if span < 1e-6: + bm.free() + log("WARNING: degenerate UV bbox — normalization skipped") + return + scale = 0.96 / span + for f in bm.faces: + for lo in f.loops: + uv = lo[uvl].uv + uv.x = 0.02 + (uv.x - u0) * scale + uv.y = 0.02 + (uv.y - v0) * scale + bm.to_mesh(me) + bm.free() + me.update() + log(f"normalized UV0: bbox ({u0:.3f},{v0:.3f})..({u1:.3f},{v1:.3f}) " + f"-> [0.02,0.98] (x{scale:.1f} density)") + + +# -------------------------------------------------------------------------- +# Feature field (texel-level; drives albedo AND mask together) +# -------------------------------------------------------------------------- + +def _paint_texels(px, py, pz, noise, lm): + """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions. + + Regions (spec): sole -> R, upper -> G, toe cap -> B. + Painted lines (albedo only): toe cap line, sole welt stitch, topline edge + stitch, heel counter seam. + """ + n = px.shape[0] + tl = np.where(px >= 0.0, lm.topline.get("L", 1.0), lm.topline.get("R", 1.0)) + w2 = lm.seam_w * 0.5 + + sole = pz < lm.sole_top + cap = (~sole) & (py <= lm.cap_y) + + # --- albedo ------------------------------------------------------------- + alb = np.empty((n, 4), dtype=np.float32) + for c in range(3): + alb[:, c] = LEATHER_RGB[c] + noise + alb[:, 3] = 1.0 + for c in range(3): + alb[sole, c] = SOLE_RGB[c] + noise[sole] + + if not PLAIN: + capline = (~sole) & (np.abs(py - lm.cap_y) < w2) + welt = np.abs(pz - lm.sole_top) < w2 + topstitch = (~sole) & (np.abs(pz - (tl - 3.0 * w2)) < w2) + heelseam = (~sole) & (py > 0.0) \ + & (np.abs(py - lm.heel_seam_y) < w2) + thread = capline | welt | topstitch | heelseam + for c in range(3): + alb[thread, c] = STITCH_RGB[c] + + # --- region mask: sole R / upper G / toe cap B --------------------------- + mask = np.zeros((n, 4), dtype=np.float32) + mask[sole, 0] = 1.0 + mask[cap, 2] = 1.0 + mask[~(sole | cap), 1] = 1.0 + return alb, mask + + +def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H): + """Barycentric texel fill of one UV triangle: interpolate 3D positions, + evaluate the footwear field, write albedo + mask together.""" + a, b, c = uvs + A, B, C = cos + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + pxg = xs + 0.5 + pyg = ys + 0.5 + w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom + w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + w0i, w1i, w2i = w0[inside], w1[inside], w2[inside] + px3 = w0i * A.x + w1i * B.x + w2i * C.x + py3 = w0i * A.y + w1i * B.y + w2i * C.y + pz3 = w0i * A.z + w1i * B.z + w2i * C.z + ysin = ys[inside] + xsin = xs[inside] + alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm) + alb_buf[ysin, xsin] = alb + mask_buf[ysin, xsin] = mask + + +def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body): + """Rasterize all UV0 triangles once, producing the painted albedo and the + region mask from one shared feature-field evaluation per texel.""" + W = H = TEX_SIZE + rng = np.random.default_rng(NOISE_SEED) + noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5) + * 2.0 * ALBEDO_NOISE) + + alb_buf = np.empty((H, W, 4), dtype=np.float32) + for c in range(3): + alb_buf[:, :, c] = LEATHER_RGB[c] + noise_buf + alb_buf[:, :, 3] = 1.0 + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 # background = upper green (bleed-safe) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + if not len(bm.loops.layers.uv): + raise RuntimeError("no UV layer for albedo/mask paint") + uv_layer = bm.loops.layers.uv[0] + + tri_count = 0 + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv.copy() for loop in loops] + cos = [loop.vert.co.copy() for loop in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_paint( + alb_buf, mask_buf, noise_buf, + (uvs[0], uvs[i], uvs[i + 1]), + (cos[0], cos[i], cos[i + 1]), + lm, W, H) + tri_count += 1 + bm.free() + log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})") + + def _save(buf, name, path): + img = bpy.data.images.new(name, W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + albedo_img = _save(alb_buf, f"shoes_albedo_{body}", albedo_path) + _save(mask_buf, f"shoes_mask_{body}", mask_path) + log(f"saved albedo -> {albedo_path}") + log(f"saved mask -> {mask_path}") + + # Re-save the albedo under the SHARED sidecar name and leave the image + # datablock pointing there: the glTF exporter derives the embedded image + # name from the filepath basename, so the GLB carries "base_albedo" and + # Godot's extract-on-import lands exactly on _base_albedo.png (the + # wave-1 convention) instead of doubling to __base_albedo.png. + # The pixels at base_albedo.png are THIS body's during its export; main() + # restores the reference body's copy after the loop. + shared_path = os.path.join(os.path.dirname(albedo_path), "base_albedo.png") + albedo_img.filepath_raw = shared_path + albedo_img.save() + return albedo_img + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_shoes(body_dir, out_dir, body, offset, topline_lift): + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + denim.weld_boundaries(shell) # zips upper + sole plate + ankle slivers + recalc_normals(shell) + + lm = FootLandmarks(armature, shell.data) + ankle_cut_and_flatten(shell, lm, topline_lift) + relax_rim(shell, lm, RIM_RELAX_PASSES) + skin_bvh = snapshot_skin_bvh(shell) # BEFORE smoothing moves anything + smooth_toe_box(shell, lm) + + sole_idx = classify_sole_verts(shell) + base.offset_outward(shell, offset) + toe_extra_offset(shell, lm, TOE_EXTRA_OFFSET_M) + enforce_clearance(shell, skin_bvh, offset) + flatten_sole(shell, sole_idx, lm.sole_plane) + base.solidify(shell, base.CLOTH_THICKNESS_M) + clamp_topline_residue(shell, lm) + + if UV_NORMALIZE: + normalize_uv0(shell) + denim.author_parked_uv2(shell) # shoes are not logo-capable + + albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png") + mask_path = os.path.join(out_dir, f"{body}_mask.png") + albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + global CAP_BALL_FRAC, LEATHER_RGB, SOLE_RGB, SOLE_TOTAL_M, PLAIN + global UV_NORMALIZE + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--sole-mm M] [--topline-lift M] [--cap-frac F] " + "[--base-rgb r,g,b] [--sole-rgb r,g,b] [--plain] " + "[--no-uv-normalize]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = SHELL_OFFSET_M + topline_lift = TOPLINE_LIFT_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--sole-mm" in argv: + SOLE_TOTAL_M = float(argv[argv.index("--sole-mm") + 1]) + if "--topline-lift" in argv: + topline_lift = float(argv[argv.index("--topline-lift") + 1]) + if "--cap-frac" in argv: + CAP_BALL_FRAC = float(argv[argv.index("--cap-frac") + 1]) + if "--base-rgb" in argv: + LEATHER_RGB = tuple( + float(v) for v in argv[argv.index("--base-rgb") + 1].split(",")) + if "--sole-rgb" in argv: + SOLE_RGB = tuple( + float(v) for v in argv[argv.index("--sole-rgb") + 1].split(",")) + if "--plain" in argv: + PLAIN = True + if "--no-uv-normalize" in argv: + UV_NORMALIZE = False + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"shoes per-body mode: {len(bodies)} bodies, offset " + f"{offset * 1000:.0f} mm, sole {SOLE_TOTAL_M * 1000:.0f} mm, " + f"plain={PLAIN}") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_shoes(body_dir, out_dir, body, offset, topline_lift) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_slides.py b/tooling/garment-fit/blender_author_slides.py new file mode 100644 index 000000000..b1a7f2382 --- /dev/null +++ b/tooling/garment-fit/blender_author_slides.py @@ -0,0 +1,676 @@ +""" +blender_author_slides.py (T-1089 wave 2, slides — open swim footwear) + +Authors SLIDES (open sandal: flat sole slab + one broad strap band across the +midfoot) as per-body offset shells, reusing blender_author_offset_shell.py as +a library (scene build/join, offset, solidify, GLB export) and the denim +companion's required bottoms/footwear practices (boundary WELD of coincident +segment-seam verts before offsetting; the parked logo UV2 layer). + +First offset-shell FOOTWEAR: follows the peasant_shoes both-feet convention +(one garment covers seg_foot_l + seg_foot_r), and adds what feet need that no +torso/leg companion provides, as reusable parameters: + + * STRAP band cut — the foot shell is cut down to just the midfoot band + between two Y planes derived from that body's own foot bones + (fractions of the ball_l/r head -> foot_l/r head span, identical 65-bone + rig on all 11 bodies). Probe evidence: the seg_foot ankle rim (the + weight-threshold splitter's jagged boundary, 3.3-8.3 cm teeth post-weld) + stays ABOVE y-fraction ~0.62 of that span on every body, so a band cut at + <= 0.58 removes the entire jagged rim by construction. The band keeps the + full cross-section ring (including under-foot skin) so the strap-to-sole + join is gap-free by construction (the coverall waist-join pattern); the + hidden under-foot part is swallowed by the sole slab. + * clean strap rims by PLANE BISECT — the wave-1 delete-then-flatten rim + practice is too crude for the foot's large instep triangles (it notches + the strap crest); the band is instead cut with two exact bisect planes, + which yields the same clean-plane end state the denim flatten was after, + by construction (bmesh interpolates weights/UVs on the new edge verts). + A rim check verifies every open-edge vert sits on a cut plane. + * SOLE slab construction — per foot, the 2D convex hull (monotone chain) of + that foot's full footprint, expanded radially by a margin, extruded into + a prism from below the skin's lowest point to just above it (the foot + visually rests IN the footbed). Sole verts receive skin weights by + nearest-vertex transfer from the pre-cut foot snapshot, so the sole bends + with the foot/ball bones during Walk/Sprint toe-off. A FLEX CREASE ring + is bisected into each prism at the ball-joint line so the slab hinges + where the foot hinges (QA evidence: without it, toe skin dips through + the linearly-interpolated top face in deep-crouch toe-off). + * under-sole clamp — strap ring verts that offset/solidify pushed below the + sole interior are clamped onto a plane inside the slab (invisible), so the + strap never pokes out of the sole bottom. + * deterministic region UVs — the garment is fully re-UV'd (the foot's body + atlas layout is useless for garment paint): strap faces pack into the left + half of UV space, sole faces into the right half, each planar-projected by + dominant normal axis into three stacked tiles. Faces of one region may + overlap in UV (they share one flat colour), but strap and sole texels are + DISJOINT by construction — no cross-region contamination, no operator + (bpy.ops.uv.*) dependency in background mode. + * albedo + region mask painted together per face: sole -> R (tint_0), + strap -> G (tint_1); bright default albedo (spec: swim family, bright). + Painted texels are dilated outward so bilinear/mip bleed at tile edges + never lands on an untinted texel. + +Usage (per-body only — offset shells author per body, Q-060): + tooling/blender --background --python \ + tooling/garment-fit/blender_author_slides.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/slides \ + [--bodies average_m,child,...] [--offset 0.005] \ + [--strap-y0 0.10] [--strap-y1 0.58] \ + [--sole-margin 0.007] [--sole-embed 0.009] [--sole-drop 0.010] \ + [--strap-rgb 0.93,0.35,0.20] [--sole-rgb 0.82,0.83,0.85] [--seed N] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house +wardrobe), Q-060 (per-body offset shells). +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# -------------------------------------------------------------------------- +# Import the base offset-shell module + the denim companion (shared machinery) +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _load(name, fname): + spec = importlib.util.spec_from_file_location(name, os.path.join(_HERE, fname)) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +base = _load("offset_shell_base", "blender_author_offset_shell.py") +denim = _load("denim_pants_lib", "blender_author_denim_pants.py") + +log = base.log + +# -------------------------------------------------------------------------- +# Parameters +# -------------------------------------------------------------------------- +COVERED_SEGMENTS = ["seg_foot_l", "seg_foot_r"] + +STRAP_OFFSET_M = 0.005 # strap standoff from skin (slides hug the foot) +STRAP_THICKNESS_M = 0.005 # Solidify thickness (chunky foam strap) + +# Strap band window as fractions of the ball-head -> foot(ankle)-head Y span, +# measured from the ball. Probe: the jagged seg_foot ankle rim starts at +# fraction ~0.62 on every body, so y1 <= 0.58 excludes it by construction. +STRAP_Y0_FRAC = 0.10 +STRAP_Y1_FRAC = 0.58 + +# Sole slab (metres on average_m; scaled by each body's foot-length ratio). +SOLE_MARGIN_M = 0.007 # radial footprint expansion beyond the skin hull +SOLE_EMBED_M = 0.009 # slab top above the foot's lowest skin point +SOLE_DROP_M = 0.010 # slab bottom below the foot's lowest skin point +CLAMP_INSET_M = 0.003 # strap under-foot verts clamped this far above + # the slab bottom (kept inside the sole) + +# Bright default (spec: swim family). Texture carries identity; the runtime +# toon_garment.gdshader recolors per region via luma, so these tones ARE the +# default look and default_tints should match them. +STRAP_RGB = (0.93, 0.35, 0.20) # bright coral strap (G region, tint_1) +SOLE_RGB = (0.82, 0.83, 0.85) # off-white foam sole (R region, tint_0) +ALBEDO_NOISE = 0.018 # +/- jitter, subtle foam/EVA feel +NOISE_SEED = 3089 + +TEX_SIZE = 512 # slides are small; 512 is plenty +DILATE_PX = 6 # painted-texel dilation into the background + +_REF_FOOT_SPAN = 0.2211 # average_m: foot_l head y (0.0875) - ball_l tail y (-0.1336) + + +# -------------------------------------------------------------------------- +# Per-body landmarks +# -------------------------------------------------------------------------- + +class FootLandmarks: + """Per-side cut/sole parameters from one body's own foot bones + mesh.""" + + def __init__(self, armature): + bones = armature.data.bones + self.sides = {} + spans = [] + for side, sign in (("l", +1), ("r", -1)): + foot = bones.get(f"foot_{side}") + ball = bones.get(f"ball_{side}") + if foot is None or ball is None: + raise RuntimeError( + f"foot_{side}/ball_{side} missing — not the 65-bone rig?") + ankle_y = foot.head_local.y + ball_y = ball.head_local.y + toe_y = ball.tail_local.y + span = ankle_y - toe_y + spans.append(span) + self.sides[sign] = { + "y0": ball_y + STRAP_Y0_FRAC * (ankle_y - ball_y), + "y1": ball_y + STRAP_Y1_FRAC * (ankle_y - ball_y), + "ball_y": ball_y, + } + self.scale = (sum(spans) / len(spans)) / _REF_FOOT_SPAN + for sign in (+1, -1): + s = self.sides[sign] + log(f"landmarks side {'L' if sign > 0 else 'R'}: " + f"strap y[{s['y0']:.4f},{s['y1']:.4f}]") + log(f"foot scale vs average_m: {self.scale:.3f}") + + +# -------------------------------------------------------------------------- +# Geometry: snapshot, strap cut, rim flatten, sole build +# -------------------------------------------------------------------------- + +def snapshot_skin(shell): + """Record post-weld skin verts: positions + per-vertex group weights. + + Used later for the sole's nearest-vertex weight transfer, after the strap + cut has thrown most of the foot away. + """ + me = shell.data + pos = np.array([(v.co.x, v.co.y, v.co.z) for v in me.vertices], + dtype=np.float64) + weights = [ + [(g.group, g.weight) for g in v.groups if g.weight > 0.0] + for v in me.vertices + ] + return pos, weights + + +def strap_cut(shell, lm): + """Cut the foot shell down to the strap band with two exact plane bisects. + + The wave-1 bottoms practice (delete-then-flatten) is too crude here: the + foot mesh's instep triangles are large relative to the 5-7 cm band, so + vertex deletion notches the strap crest and rim-snapping folds it. Bisect + planes give clean straight rims BY CONSTRUCTION (bmesh interpolates + weights/UVs on the new edge verts), which is the same end state the denim + flatten was after. The rig's feet are mirrored, so one Y window (side + average) serves both feet. + """ + y0 = (lm.sides[+1]["y0"] + lm.sides[-1]["y0"]) / 2.0 + y1 = (lm.sides[+1]["y1"] + lm.sides[-1]["y1"]) / 2.0 + bm = bmesh.new() + bm.from_mesh(shell.data) + before = len(bm.verts) + bmesh.ops.bisect_plane( + bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], + plane_co=(0.0, y0, 0.0), plane_no=(0.0, 1.0, 0.0), + clear_inner=True) # drop y < y0 (toe side) + bmesh.ops.bisect_plane( + bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], + plane_co=(0.0, y1, 0.0), plane_no=(0.0, 1.0, 0.0), + clear_outer=True) # drop y > y1 (ankle side, incl. the jagged rim) + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"strap band bisect y[{y0:.4f},{y1:.4f}]: {before} -> " + f"{len(shell.data.vertices)} verts") + if len(shell.data.vertices) == 0: + raise RuntimeError("strap cut removed everything — window wrong?") + + +def check_strap_rims(shell, lm): + """Verify the band's open edges sit ON the two cut planes (bisect gives + this by construction; residue means ankle-rim leakage into the window).""" + y0 = (lm.sides[+1]["y0"] + lm.sides[-1]["y0"]) / 2.0 + y1 = (lm.sides[+1]["y1"] + lm.sides[-1]["y1"]) / 2.0 + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + boundary = set() + for e in bm.edges: + if len(e.link_faces) == 1: + boundary.update(v.index for v in e.verts) + off = [i for i in boundary + if min(abs(bm.verts[i].co.y - y0), abs(bm.verts[i].co.y - y1)) > 1e-4] + bm.free() + log(f"strap rims: {len(boundary)} boundary verts, {len(off)} off-plane " + f"(expected 0)") + if off: + log("WARNING: off-plane rim verts — ankle-rim residue inside the " + "band window; lower STRAP_Y1_FRAC") + + +def _convex_hull_2d(points): + """Andrew's monotone chain; returns CCW hull points (numpy (H,2)).""" + pts = np.unique(np.round(points, 6), axis=0) + order = np.lexsort((pts[:, 1], pts[:, 0])) + pts = pts[order] + if len(pts) < 3: + raise RuntimeError("degenerate footprint for convex hull") + + def cross(o, a, b): + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) + + lower = [] + for p in pts: + while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0: + lower.pop() + lower.append(p) + upper = [] + for p in pts[::-1]: + while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0: + upper.pop() + upper.append(p) + return np.array(lower[:-1] + upper[:-1], dtype=np.float64) + + +def build_soles(shell, skin_pos, lm, margin, embed, drop): + """Append one sole prism per foot; returns (sole_start_index, sole_info). + + Footprint = expanded convex hull of that foot's FULL skin xy (pre-cut + snapshot); prism spans z in [skin_min - drop, skin_min + embed]. + + FLEX CREASE: each prism is bisected (no material removed) by a Y plane at + that side's ball-joint line. Without it the long heel->toe top face + interpolates skinning linearly across the whole span while the foot + creases sharply at the ball during Sprint/Crouch toe-off — the slab sags + below the bend crest and toe skin dips through (QA evidence, wave-2 + slides). The crease ring picks up ball-blended weights from the + nearest-vertex transfer, so the slab hinges where the foot hinges. + """ + me = shell.data + sole_start = len(me.vertices) + bm = bmesh.new() + bm.from_mesh(me) + + info = {} + for sign in (+1, -1): + if sign > 0: + side_pos = skin_pos[skin_pos[:, 0] >= 0.0] + else: + side_pos = skin_pos[skin_pos[:, 0] < 0.0] + if len(side_pos) == 0: + raise RuntimeError("no skin verts on one side for sole build") + hull = _convex_hull_2d(side_pos[:, :2]) + centroid = hull.mean(axis=0) + d = hull - centroid + n = d / np.linalg.norm(d, axis=1, keepdims=True) + hull = hull + n * margin + z_min = float(side_pos[:, 2].min()) + z_bot, z_top = z_min - drop, z_min + embed + info[sign] = {"z_bot": z_bot, "z_top": z_top} + + bot = [bm.verts.new((p[0], p[1], z_bot)) for p in hull] + top = [bm.verts.new((p[0], p[1], z_top)) for p in hull] + new_faces = [] + h = len(hull) + for i in range(h): + j = (i + 1) % h + new_faces.append(bm.faces.new((bot[i], bot[j], top[j], top[i]))) + new_faces.append(bm.faces.new(top)) + new_faces.append(bm.faces.new(tuple(reversed(bot)))) + bmesh.ops.recalc_face_normals(bm, faces=new_faces) + + # Flex crease at the ball line (cut only, keep both sides). + crease_verts = set() + for f in new_faces: + crease_verts.update(f.verts) + crease_edges = {e for v in crease_verts for e in v.link_edges} + res = bmesh.ops.bisect_plane( + bm, + geom=list(crease_verts) + list(crease_edges) + new_faces, + plane_co=(0.0, lm.sides[sign]["ball_y"], 0.0), + plane_no=(0.0, 1.0, 0.0), + clear_inner=False, clear_outer=False) + cut = sum(1 for g in res["geom_cut"] if isinstance(g, bmesh.types.BMVert)) + log(f"sole {'L' if sign > 0 else 'R'}: hull {h} pts, " + f"z[{z_bot:.4f},{z_top:.4f}], ball crease " + f"y={lm.sides[sign]['ball_y']:.4f} ({cut} crease verts)") + + bm.to_mesh(me) + bm.free() + me.update() + log(f"soles appended: verts {sole_start} -> {len(me.vertices)}") + return sole_start, info + + +def transfer_sole_weights(shell, skin_pos, skin_weights, sole_start): + """Nearest-vertex weight transfer (same-side skin snapshot) for sole verts.""" + me = shell.data + left = skin_pos[:, 0] >= 0.0 + idx_by_side = {+1: np.where(left)[0], -1: np.where(~left)[0]} + transferred = 0 + for vi in range(sole_start, len(me.vertices)): + co = me.vertices[vi].co + side = +1 if co.x >= 0.0 else -1 + cand = idx_by_side[side] + d2 = ((skin_pos[cand] - np.array([co.x, co.y, co.z])) ** 2).sum(axis=1) + src = int(cand[int(np.argmin(d2))]) + for gi, w in skin_weights[src]: + shell.vertex_groups[gi].add([vi], w, 'REPLACE') + transferred += 1 + log(f"sole weights transferred: {transferred} verts (nearest skin vert)") + + +def clamp_strap_under_sole(shell, sole_start, sole_info): + """Clamp strap verts that dipped below the sole interior back inside it.""" + me = shell.data + n = 0 + for vi in range(sole_start): + v = me.vertices[vi] + side = +1 if v.co.x >= 0.0 else -1 + floor_z = sole_info[side]["z_bot"] + CLAMP_INSET_M + if v.co.z < floor_z: + v.co.z = floor_z + n += 1 + me.update() + if n: + log(f"clamped {n} strap verts above the sole bottom (hidden in slab)") + + +# -------------------------------------------------------------------------- +# Deterministic region UVs (no bpy.ops dependency) +# -------------------------------------------------------------------------- + +def _dominant_axis(normal): + a = (abs(normal.x), abs(normal.y), abs(normal.z)) + return a.index(max(a)) + + +def author_region_uvs(shell, sole_start): + """Re-UV the garment: strap faces -> left half, sole faces -> right half, + each split into three stacked tiles by dominant normal axis (planar + projection). Faces within one (region, axis) tile may overlap — harmless, + they share one flat colour — but strap/sole texels never mix.""" + me = shell.data + while len(me.uv_layers) > 1: + me.uv_layers.remove(me.uv_layers[-1]) + if len(me.uv_layers) == 0: + me.uv_layers.new(name="UVMap") + + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + bm.normal_update() + uvl = bm.loops.layers.uv[0] + + # region 0 = strap (u 0.02..0.48), region 1 = sole (u 0.52..0.98) + u_ranges = {0: (0.02, 0.48), 1: (0.52, 0.98)} + proj = {0: (1, 2), 1: (0, 2), 2: (0, 1)} # axis -> (coord_a, coord_b) + + buckets = {} + for face in bm.faces: + region = 1 if all(v.index >= sole_start for v in face.verts) else 0 + axis = _dominant_axis(face.normal) + buckets.setdefault((region, axis), []).append(face) + + for (region, axis), faces in buckets.items(): + ca, cb = proj[axis] + pts = [] + for f in faces: + for lo in f.loops: + pts.append((lo.vert.co[ca], lo.vert.co[cb])) + pts = np.array(pts) + lo_a, hi_a = float(pts[:, 0].min()), float(pts[:, 0].max()) + lo_b, hi_b = float(pts[:, 1].min()), float(pts[:, 1].max()) + da = max(hi_a - lo_a, 1e-6) + db = max(hi_b - lo_b, 1e-6) + u0, u1 = u_ranges[region] + v0 = 0.02 + axis * (1.0 / 3.0) + v1 = v0 + (1.0 / 3.0) - 0.04 + for f in faces: + for lo in f.loops: + a = (lo.vert.co[ca] - lo_a) / da + b = (lo.vert.co[cb] - lo_b) / db + lo[uvl].uv = (u0 + a * (u1 - u0), v0 + b * (v1 - v0)) + + bm.to_mesh(me) + bm.free() + me.update() + log(f"region UVs authored: {len(buckets)} (region,axis) tiles") + + +# -------------------------------------------------------------------------- +# Paint albedo + mask (per-face flat colours, shared rasterization) +# -------------------------------------------------------------------------- + +def _raster_tri_multi(bufs_colors, a, b, c, W, H): + """Barycentric fill of one UV triangle into several (buf, color) pairs.""" + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + px = xs + 0.5 + py = ys + 0.5 + w0 = ((by - cy) * (px - cx) + (cx - bx) * (py - cy)) / denom + w1 = ((cy - ay) * (px - cx) + (ax - cx) * (py - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + for buf, color in bufs_colors: + region = buf[miny:maxy + 1, minx:maxx + 1] + region[inside] = np.array(color, dtype=buf.dtype) + + +def _dilate_painted(alb, mask, flag, iters): + """Grow painted texels into the background so bilinear/mip bleed at tile + edges picks up real region colours, not the background fill.""" + H, W = flag.shape + for _ in range(iters): + grew = np.zeros_like(flag) + for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)): + src = np.zeros_like(flag) + sy0, sy1 = max(dy, 0), H + min(dy, 0) + ty0, ty1 = max(-dy, 0), H + min(-dy, 0) + sx0, sx1 = max(dx, 0), W + min(dx, 0) + tx0, tx1 = max(-dx, 0), W + min(-dx, 0) + src[ty0:ty1, tx0:tx1] = flag[sy0:sy1, sx0:sx1] + fill = (~flag) & (~grew) & src + if not fill.any(): + continue + alb_src = np.zeros_like(alb) + alb_src[ty0:ty1, tx0:tx1] = alb[sy0:sy1, sx0:sx1] + mask_src = np.zeros_like(mask) + mask_src[ty0:ty1, tx0:tx1] = mask[sy0:sy1, sx0:sx1] + alb[fill] = alb_src[fill] + mask[fill] = mask_src[fill] + grew |= fill + flag |= grew + + +def paint_albedo_and_mask(shell, sole_start, albedo_path, mask_path, body): + """Rasterize all UV0 triangles once: sole -> R + sole tone, strap -> G + + strap tone. One classification drives both outputs (denim practice).""" + W = H = TEX_SIZE + alb_buf = np.empty((H, W, 4), dtype=np.float32) + for c in range(3): + alb_buf[:, :, c] = STRAP_RGB[c] + alb_buf[:, :, 3] = 1.0 + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 # background = strap green (bleed-safe default) + flag_buf = np.zeros((H, W), dtype=bool) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + uvl = bm.loops.layers.uv[0] + + counts = {"sole": 0, "strap": 0} + for face in bm.faces: + is_sole = all(v.index >= sole_start for v in face.verts) + counts["sole" if is_sole else "strap"] += 1 + alb_rgb = SOLE_RGB if is_sole else STRAP_RGB + mask_rgba = (1.0, 0.0, 0.0, 0.0) if is_sole else (0.0, 1.0, 0.0, 0.0) + alb_rgba = (alb_rgb[0], alb_rgb[1], alb_rgb[2], 1.0) + uvs = [lo[uvl].uv.copy() for lo in face.loops] + flag_view = flag_buf[:, :, None] # view — writes reach flag_buf + for i in range(1, len(uvs) - 1): + _raster_tri_multi( + [(alb_buf, alb_rgba), (mask_buf, mask_rgba), + (flag_view, True)], + uvs[0], uvs[i], uvs[i + 1], W, H) + bm.free() + total = max(sum(counts.values()), 1) + log("region faces: " + " ".join( + f"{k}={v} ({100.0 * v / total:.1f}%)" for k, v in counts.items())) + + _dilate_painted(alb_buf, mask_buf, flag_buf, DILATE_PX) + + rng = np.random.default_rng(NOISE_SEED) + noise = ((rng.random((H, W, 1), dtype=np.float32) - 0.5) + * 2.0 * ALBEDO_NOISE) + alb_buf[:, :, :3] = np.clip(alb_buf[:, :, :3] + noise, 0.0, 1.0) + + def _save(buf, name, path): + img = bpy.data.images.new(name, W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + albedo_img = _save(alb_buf, f"slides_albedo_{body}", albedo_path) + _save(mask_buf, f"slides_mask_{body}", mask_path) + log(f"saved albedo -> {albedo_path}") + log(f"saved mask -> {mask_path}") + # The glTF exporter names the embedded image after the file basename, and + # the Godot import EXTRACTS it as _.png. Point the image at + # the shared base_albedo.png so the extraction lands exactly on the sidecar + # saved above (_base_albedo.png, same pixels — hoodie/tshirt + # convention), instead of a doubled __base_albedo.png whose + # deletion would break the imported scene. The shared file is re-pointed + # to the reference body's paint at the end of the run (main()). + albedo_img.filepath_raw = os.path.join(os.path.dirname(albedo_path), + "base_albedo.png") + albedo_img.save() + return albedo_img + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_slides(body_dir, out_dir, body, offset): + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + denim.weld_boundaries(shell) # merge UV-seam/segment duplicate verts + + lm = FootLandmarks(armature) + skin_pos, skin_weights = snapshot_skin(shell) + + strap_cut(shell, lm) + check_strap_rims(shell, lm) + base.offset_outward(shell, offset) + base.solidify(shell, STRAP_THICKNESS_M) + + sole_start, sole_info = build_soles( + shell, skin_pos, lm, + SOLE_MARGIN_M * lm.scale, SOLE_EMBED_M * lm.scale, + SOLE_DROP_M * lm.scale) + transfer_sole_weights(shell, skin_pos, skin_weights, sole_start) + clamp_strap_under_sole(shell, sole_start, sole_info) + + author_region_uvs(shell, sole_start) + denim.author_parked_uv2(shell) # slides are not logo-capable + + albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png") + mask_path = os.path.join(out_dir, f"{body}_mask.png") + albedo_img = paint_albedo_and_mask(shell, sole_start, albedo_path, + mask_path, body) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + global STRAP_Y0_FRAC, STRAP_Y1_FRAC, STRAP_RGB, SOLE_RGB + global SOLE_MARGIN_M, SOLE_EMBED_M, SOLE_DROP_M, NOISE_SEED + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--strap-y0 F] [--strap-y1 F] [--sole-margin M] " + "[--sole-embed M] [--sole-drop M] [--strap-rgb r,g,b] " + "[--sole-rgb r,g,b] [--seed N]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + + def _f(flag, default): + return float(argv[argv.index(flag) + 1]) if flag in argv else default + + def _rgb(flag, default): + if flag not in argv: + return default + return tuple(float(v) for v in argv[argv.index(flag) + 1].split(",")) + + offset = _f("--offset", STRAP_OFFSET_M) + STRAP_Y0_FRAC = _f("--strap-y0", STRAP_Y0_FRAC) + STRAP_Y1_FRAC = _f("--strap-y1", STRAP_Y1_FRAC) + SOLE_MARGIN_M = _f("--sole-margin", SOLE_MARGIN_M) + SOLE_EMBED_M = _f("--sole-embed", SOLE_EMBED_M) + SOLE_DROP_M = _f("--sole-drop", SOLE_DROP_M) + NOISE_SEED = int(_f("--seed", NOISE_SEED)) + STRAP_RGB = _rgb("--strap-rgb", STRAP_RGB) + SOLE_RGB = _rgb("--sole-rgb", SOLE_RGB) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"slides per-body mode: {len(bodies)} bodies, strap offset " + f"{offset * 1000:.0f} mm, band frac [{STRAP_Y0_FRAC},{STRAP_Y1_FRAC}]") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_slides(body_dir, out_dir, body, offset) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_sneakers.py b/tooling/garment-fit/blender_author_sneakers.py new file mode 100644 index 000000000..72481eb3f --- /dev/null +++ b/tooling/garment-fit/blender_author_sneakers.py @@ -0,0 +1,765 @@ +""" +blender_author_sneakers.py (T-1089 wave 2, sneakers_modern + trainer family) + +Authors low-top TRAINERS as per-body offset shells over BOTH feet (seg_foot_l + +seg_foot_r joined into ONE garment — the peasant_shoes both-feet convention), +reusing blender_author_offset_shell.py as a library (scene build, join, offset, +solidify, GLB export) and blender_author_denim_pants.py's boundary weld. + +What footwear needs beyond the bottoms family, as reusable parameters: + + * boundary WELD first (denim practice) — the raw foot segment is NOT one + surface: the sole is a separate coincident-vert patch and the ankle cut + leaves floating shards (probe: average_m = 4 islands, muscular_m/child = 8). + remove_doubles at 0.5 mm fuses everything into one watertight-except-ankle + shell per foot; weights identical by origin, so skinning is unaffected. + * COLLAR cut + rim flatten — the segment splitter's ankle boundary is jagged + weight-threshold teeth (3.3-8.3 cm across bodies, back-biased). Verts above + the collar plane (a fraction of the ankle-joint height) are deleted, then + every remaining boundary vert is pulled ONTO the collar plane — a clean + horizontal low-top opening. Re-flattened after the outward offset because + rim-vert normals have a +z bias that would lift the rim. + * SOLE slab (this family's new offset-shell param) — the rim-flatten practice + applied to the ground plane, built as cut + flatten + extrude + fill: the + shell's underside band is cut away (with the toe-knuckle lobes that survive + smoothing), the open rim is flattened onto the cut plane and its outline + relaxed, then extruded straight down to a plane `--sole-drop` (scaled per + body) below the body's own foot-bottom and closed with a flat bottom — a + clean prism slab; Solidify thickens it and a final clamp guarantees the + outer sole is planar. (Snapping the band in place instead collapses mesh + rows into crumpled slivers — melted-wax scallops on the probe renders.) + * TOE-BOX SMOOTHING + ROUNDING — the body feet have INDIVIDUAL TOES; a raw + offset shell reads as a foot-shaped slipper (verified on average_m). Heavy + iterative vertex smoothing over the toe region (feathered, boundary rim + pinned) melts the toe creases into one volume, a light global pass + de-lumps the ankle anatomy, then an extra normal-along inflation feathered + toward the toe tip restores the lost volume as a rounded sneaker toe box. + * COLLAR FLARE — small feathered radial stand-off at the opening (the jeans + waist-flare practice) for ankle-flex clearance under Walk/Crouch. + * TEXEL-level feature painting (denim practice): one analytic field drives + BOTH the albedo and the region mask, so they always agree: + - albedo: painted lace cross-straps over a darker tongue panel, toe cap + + border line, foxing stripe at the sole top, heel tab, collar band, + vamp + heel-counter panel lines (swoosh-free — no brand marks). + Everyday default: white/grey, flat toon-friendly tones. + - mask: sole -> R, upper -> G, laces + trim (collar band, toe cap, + heel tab) -> B (spec: sole=R, upper=G, laces+trim=B). + * a parked logo_uv TEXCOORD_1 layer (all UVs at (2,2)) — not logo-capable, + but toon_garment.gdshader samples UV2 unconditionally. + +All parameters derive PER BODY from that body's own bone landmarks (foot_l / +ball_l) and measured mesh extents (foot length, half-width, ground plane), +scaled by foot length against the hand-calibrated average_m reference — the +same proportional-ratio philosophy as base.derive_thresholds. Per-body mode +only (offset shells author per body, Q-060). Left/right feet share body-atlas +UV space whose islands may overlap; every painted feature is |x|-mirror +symmetric, so overlapping texels agree by construction. + +Usage (sneakers_modern reference invocation): + tooling/blender --background --python \ + tooling/garment-fit/blender_author_sneakers.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/sneakers_modern \ + [--bodies average_m,child,...] [--offset 0.009] [--sole-drop 0.015] \ + [--sole-snap-frac 0.55] [--collar-frac 0.95] [--collar-flare 0.002] \ + [--toe-round 0.004] [--toe-smooth 12] [--laces 4] [--plain] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house +wardrobe), Q-060 (per-body offset shells). +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# -------------------------------------------------------------------------- +# Import the base offset-shell module + the denim module (weld utility) +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _load(mod_name, file_name): + spec = importlib.util.spec_from_file_location( + mod_name, os.path.join(_HERE, file_name)) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +base = _load("offset_shell_base", "blender_author_offset_shell.py") +denim = _load("denim_pants_lib", "blender_author_denim_pants.py") + +log = base.log +FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (toes point -Y) + +# -------------------------------------------------------------------------- +# Parameters (CLI-overridable ones are module globals) +# -------------------------------------------------------------------------- +COVERED_SEGMENTS = ["seg_foot_l", "seg_foot_r"] + +OFFSET_M = 0.009 # shoe standoff — snugger than cloth (12 mm) +SOLE_DROP_M = 0.015 # sole slab depth below the body's own foot bottom + # (scaled by foot length per body) +SOLE_SNAP_FRAC = 0.55 # sole CUT height as a fraction of the sole rise + # (ground -> foxing top): everything below is cut + # away and rebuilt as an extruded prism slab (see + # build_sole_slab) — kills the toe-knuckle underside + # lobes that survive smoothing (probe, average_m) +COLLAR_FRAC = 0.95 # collar plane as fraction of ankle-joint height +COLLAR_FLARE_M = 0.002 # radial stand-off at the opening (ankle-flex room) +TOE_ROUND_M = 0.004 # extra feathered inflation at the toe box +SMOOTH_GLOBAL_ITERS = 2 # light whole-shell de-lumping passes +SMOOTH_TOE_ITERS = 18 # heavy toe-crease melting passes (12 left visible + # knuckle grooves on the probe renders) +SMOOTH_FACTOR = 0.5 +N_LACES = 4 # painted cross-straps +PLAIN = False # --plain: flat upper, no painted features +TEX_SIZE = 1024 # painted lace/panel lines need > 512 +NOISE_SEED = 3089 + +# Vertical proportions — fractions of the collar height above the ground. +SOLE_TOP_FRAC = 0.30 # sole sidewall (foxing) top -> R region below this +COLLAR_BAND_FRAC = 0.15 # collar trim band height (B region) +VAMP_LINE_FRAC = 0.42 # side panel line height between sole top and collar + +# Foot-axis proportions — fractions of foot length / half-width. +TOE_CAP_FRAC = 0.18 # toe cap depth from the toe tip (B region) +HEEL_LINE_FRAC = 0.22 # heel-counter panel line from the heel tip +LACE_T0, LACE_T1 = 0.18, 0.80 # lace panel span along the foot bone +LACE_HALFW_FRAC = 0.40 # lace panel half-width, of foot half-width +LACE_STRIPE_DUTY = 0.44 # stripe thickness as a fraction of stripe spacing +LINE_W_M = 0.0035 # painted panel/border line half-width (scaled) +HEEL_TAB_HALFW_M = 0.012 # heel tab half-width (scaled) + +# Everyday default: white/grey, flat toon-friendly tones (sRGB floats). +UPPER_RGB = (0.880, 0.880, 0.890) +SOLE_RGB = (0.780, 0.790, 0.800) +TREAD_RGB = (0.450, 0.460, 0.480) # below-ground outsole +TOE_RGB = (0.920, 0.920, 0.930) # toe bumper +LACE_RGB = (0.960, 0.960, 0.965) +TONGUE_SHADE = 0.90 # lace-zone panel behind the straps +COLLAR_SHADE = 0.88 # collar band darkening +HEEL_TAB_SHADE = 0.72 +LINE_SHADE = 0.74 # painted panel/border lines +ALBEDO_NOISE = 0.015 + +# Reference proportions (average_m) the fractions were calibrated against. +_REF_FOOT_LEN = 0.2704 # heel y (0.1374) - toe tip y (-0.1330) + + +# -------------------------------------------------------------------------- +# Per-body landmarks +# -------------------------------------------------------------------------- + +class FootLandmarks: + """Cut/mask/paint parameters from one body's foot bones + measured mesh.""" + + def __init__(self, armature, shell): + bones = armature.data.bones + foot = bones.get("foot_l") + ball = bones.get("ball_l") + if foot is None or ball is None: + raise RuntimeError("foot_l/ball_l missing — not the 65-bone rig?") + # Bone landmarks (left foot; the right mirrors via |x|). + self.ankle_y = foot.head_local.y + self.ankle_z = foot.head_local.z + self.ball_y = foot.tail_local.y + self.ball_z = foot.tail_local.z + self.toe_y = ball.tail_local.y + # Measured mesh extents (left-foot verts; feet are x-mirror symmetric). + lx = [v.co.x for v in shell.data.vertices if v.co.x > 0.0] + ly = [v.co.y for v in shell.data.vertices if v.co.x > 0.0] + zs = [v.co.z for v in shell.data.vertices] + self.foot_cx = (min(lx) + max(lx)) / 2.0 + self.half_w = (max(lx) - min(lx)) / 2.0 + self.heel_y = max(ly) + self.toe_tip_y = min(ly) + self.ground_z = min(zs) + self.foot_len = self.heel_y - self.toe_tip_y + self.s = self.foot_len / _REF_FOOT_LEN + + self.collar_z = self.ground_z + COLLAR_FRAC * (self.ankle_z - self.ground_z) + self.sole_drop = SOLE_DROP_M * self.s + self.sole_bottom = self.ground_z - self.sole_drop + rise = self.collar_z - self.ground_z + self.sole_top = self.ground_z + SOLE_TOP_FRAC * rise + self.band_h = COLLAR_BAND_FRAC * rise + self.vamp_z = self.sole_top + VAMP_LINE_FRAC * (self.collar_z - self.sole_top) + self.cap_y = self.toe_tip_y + TOE_CAP_FRAC * self.foot_len + self.heel_line_y = self.heel_y - HEEL_LINE_FRAC * self.foot_len + self.lace_halfw = LACE_HALFW_FRAC * self.half_w + self.line_w = LINE_W_M * self.s + log(f"landmarks: ankle_z={self.ankle_z:.4f} collar_z={self.collar_z:.4f} " + f"ground={self.ground_z:.4f} sole_bottom={self.sole_bottom:.4f} " + f"sole_top={self.sole_top:.4f} foot_len={self.foot_len:.4f} " + f"half_w={self.half_w:.4f} cx={self.foot_cx:.4f} s={self.s:.3f}") + + +# -------------------------------------------------------------------------- +# Geometry: collar cut + flatten, sole slab, toe round, collar flare +# -------------------------------------------------------------------------- + +def make_normals_consistent(shell): + """Outward-consistent normals BEFORE the offset — the raw foot's sole patch + winds independently of the upper, so post-weld normals need one recalc or + the offset would pull the sole inward.""" + bpy.ops.object.select_all(action='DESELECT') + shell.select_set(True) + bpy.context.view_layer.objects.active = shell + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.normals_make_consistent(inside=False) + bpy.ops.object.mode_set(mode='OBJECT') + log("recalculated outward-consistent normals") + + +def collar_cut(shell, collar_z): + """Delete verts above the collar plane (bone-plane-cut practice).""" + bm = bmesh.new() + bm.from_mesh(shell.data) + doomed = [v for v in bm.verts if v.co.z > collar_z] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"collar cut at z={collar_z:.4f}: removed {len(doomed)} verts") + + +def flatten_collar_rims(shell, collar_z, label): + """Pull every open-boundary vert ONTO the collar plane (rim-flatten + practice). After the weld + collar cut the only open boundary is the two + collar rims, so a single pass flattens both feet.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + boundary = set() + for e in bm.edges: + if len(e.link_faces) == 1: + boundary.update(v.index for v in e.verts) + if not boundary: + bm.free() + raise RuntimeError("no open collar boundary found after cut") + zs = [bm.verts[i].co.z for i in boundary] + for i in boundary: + bm.verts[i].co.z = collar_z + bm.to_mesh(me) + bm.free() + me.update() + log(f"flattened collar rims ({label}): {len(boundary)} verts, " + f"z {min(zs):.4f}..{max(zs):.4f} -> {collar_z:.4f}") + + +def smooth_shell(shell, lm): + """Melt the individual toes into one toe box and de-lump the ankle + anatomy: iterative vertex smoothing, heavier toward the toe, with the open + collar boundary pinned so the flattened rim stays put. Runs pre-offset; + the 9 mm standoff + toe inflation restore the smoothing's volume loss.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + boundary = set() + for e in bm.edges: + if len(e.link_faces) == 1: + boundary.update(v.index for v in e.verts) + interior = [v for v in bm.verts if v.index not in boundary] + toe_lim = lm.cap_y + 0.10 * lm.foot_len + toe_verts = [v for v in interior if v.co.y < toe_lim] + for _ in range(SMOOTH_GLOBAL_ITERS): + bmesh.ops.smooth_vert(bm, verts=interior, factor=SMOOTH_FACTOR, + use_axis_x=True, use_axis_y=True, use_axis_z=True) + for _ in range(SMOOTH_TOE_ITERS): + bmesh.ops.smooth_vert(bm, verts=toe_verts, factor=SMOOTH_FACTOR, + use_axis_x=True, use_axis_y=True, use_axis_z=True) + bm.to_mesh(me) + bm.free() + me.update() + log(f"smoothed shell: {SMOOTH_GLOBAL_ITERS} global passes " + f"({len(interior)} verts), {SMOOTH_TOE_ITERS} toe passes " + f"({len(toe_verts)} verts, y < {toe_lim:.4f})") + + +def toe_round(shell, lm, amount): + """Extra normal-along inflation feathered toward the toe tip — swallows + the foot's toe detail into one rounded sneaker toe box.""" + if amount <= 0.0: + return + span = lm.cap_y - lm.toe_tip_y + if span <= 1e-6: + return + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.normal_update() + n = 0 + for v in bm.verts: + if v.co.y < lm.cap_y: + t = min((lm.cap_y - v.co.y) / span, 1.0) + v.co += v.normal * (amount * t) + n += 1 + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"toe rounding: {n} verts, +{amount * 1000:.1f} mm feathered") + + +RIM_RELAX_ITERS = 3 # along-ring XY relaxation of the cut rim outline + + +def build_sole_slab(shell, lm): + """Rim-flatten practice applied to the GROUND plane, done PROPERLY as a + cut + flatten + extrude (snapping a whole z-band onto the plane collapses + multiple mesh rows into crumpled slivers that read as melted-wax scallops + — probe renders v3-v5): + + 1. DELETE everything below the cut height (SOLE_SNAP_FRAC of the sole + rise) — removes the toe-knuckle underside lobes outright. + 2. RIM-FLATTEN the resulting open bottom boundary onto the cut plane + (exactly the denim ankle practice), then relax the ring outline in + XY along the ring only — a smooth footprint curve, rounded toe. + 3. EXTRUDE the ring straight down to the sole plane — a clean vertical + prism wall (extruded verts inherit the rim verts' deform weights, + so the sole still flexes at the ball joint). + 4. FILL the bottom ring with faces — a closed flat underside. + + Solidify then grows the outer surface CLOTH_THICKNESS_M further down, + landing the visible outsole on lm.sole_bottom (guaranteed by + clamp_residue after solidify).""" + plane = lm.sole_bottom + base.CLOTH_THICKNESS_M + hi = lm.ground_z + SOLE_SNAP_FRAC * (lm.sole_top - lm.ground_z) + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + + # 1. cut + doomed = [v for v in bm.verts if v.co.z < hi] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + + # 2. rim-flatten (the collar rim is also open — split boundaries by z) + boundary = {v for e in bm.edges if len(e.link_faces) == 1 + for v in e.verts} + z_mid = (lm.collar_z + hi) / 2.0 + sole_rim = {v for v in boundary if v.co.z < z_mid} + if not sole_rim: + bm.free() + raise RuntimeError("no sole rim found after cut — check cut height") + for v in sole_rim: + v.co.z = hi + # along-ring XY relax: average each rim vert with its ring neighbours + # only (bmesh smooth_vert would pull toward the upper rows and shrink) + for _ in range(RIM_RELAX_ITERS): + new_pos = {} + for v in sole_rim: + ring_nbrs = [e.other_vert(v) for e in v.link_edges + if len(e.link_faces) == 1 + and e.other_vert(v) in sole_rim] + if len(ring_nbrs) >= 2: + ax = sum(n.co.x for n in ring_nbrs) / len(ring_nbrs) + ay = sum(n.co.y for n in ring_nbrs) / len(ring_nbrs) + new_pos[v] = (v.co.x + 0.5 * (ax - v.co.x), + v.co.y + 0.5 * (ay - v.co.y)) + for v, (x, y) in new_pos.items(): + v.co.x = x + v.co.y = y + + # 3. extrude the rim edges straight down to the sole plane + rim_edges = [e for e in bm.edges if len(e.link_faces) == 1 + and e.verts[0] in sole_rim and e.verts[1] in sole_rim] + ret = bmesh.ops.extrude_edge_only(bm, edges=rim_edges) + new_verts = [g for g in ret["geom"] + if isinstance(g, bmesh.types.BMVert)] + for v in new_verts: + v.co.z = plane + + # 4. close the bottom + bottom_edges = [e for e in bm.edges if len(e.link_faces) == 1 + and all(abs(v.co.z - plane) < 1e-6 for v in e.verts)] + filled = bmesh.ops.holes_fill(bm, edges=bottom_edges, sides=0) + + # 5. UV-park the new faces. The fill n-gon's default loop UVs span the + # whole enclosed UV region — its texel bake overwrites painted islands + # (probe v6: mottled patches, features erased); wall quads' copied UVs + # sit ON the island boundary where bilinear sampling picks up + # background. ONE park point per foot (per-face points sample texels of + # varying paint state and stripe the wall — probe v7): all new faces on + # a side park on the UV centre of that side's LARGEST-UV-area + # rim-adjacent face — big enough that the bake reliably rasterizes its + # interior, and its centre lies in the sole band (z <= sole_top), so + # the sampled texel is the flat sole tone with an R-region mask. + uv_layer = bm.loops.layers.uv[0] if len(bm.loops.layers.uv) else None + if uv_layer is not None: + new_faces = [g for g in ret["geom"] + if isinstance(g, bmesh.types.BMFace)] + new_faces += list(filled["faces"]) + new_face_set = set(new_faces) + best = {} # x-sign side -> (uv_area, centre uv); feet never cross x=0 + for v in sole_rim: + side = 1 if v.co.x >= 0.0 else -1 + for loop in v.link_loops: + f = loop.face + if f in new_face_set: + continue + us = [lp[uv_layer].uv for lp in f.loops] + area = 0.0 + for i in range(len(us)): + j = (i + 1) % len(us) + area += us[i].x * us[j].y - us[j].x * us[i].y + area = abs(area) * 0.5 + if side not in best or area > best[side][0]: + best[side] = (area, + (sum(u.x for u in us) / len(us), + sum(u.y for u in us) / len(us))) + for f in new_faces: + side = 1 if sum(v.co.x for v in f.verts) >= 0.0 else -1 + if side not in best: + side = -side + uv = best[side][1] + for loop in f.loops: + loop[uv_layer].uv = uv + + bm.to_mesh(me) + bm.free() + me.update() + log(f"sole slab: cut {len(doomed)} verts (z < {hi:.4f}), rim " + f"{len(sole_rim)} verts -> z={hi:.4f}, extruded {len(new_verts)} " + f"verts -> z={plane:.4f}, filled {len(filled['faces'])} bottom faces") + + +def collar_flare(shell, lm, flare): + """Feathered radial stand-off at the opening (waist-flare practice) — + ankle-flex clearance under Walk/Crouch.""" + if flare <= 0.0: + return + z0 = lm.collar_z - 2.0 * lm.band_h + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.normal_update() + n = 0 + for v in bm.verts: + if v.co.z > z0: + t = min((v.co.z - z0) / (2.0 * lm.band_h), 1.0) + nx, ny = v.normal.x, v.normal.y + mag = (nx * nx + ny * ny) ** 0.5 + if mag > 1e-6: + v.co.x += flare * t * nx / mag + v.co.y += flare * t * ny / mag + n += 1 + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"collar flare: {n} verts, +{flare * 1000:.1f} mm radial at rim") + + +def clamp_residue(shell, lm): + """Post-solidify safety clamps: the rim cap can push verts above the + collar plane, and blended edge normals leave the outsole slightly uneven — + squash both back onto their planes.""" + me = shell.data + n_top = n_bot = 0 + for v in me.vertices: + if v.co.z > lm.collar_z: + v.co.z = lm.collar_z + n_top += 1 + elif v.co.z < lm.sole_bottom: + v.co.z = lm.sole_bottom + n_bot += 1 + me.update() + log(f"clamped residue: {n_top} collar verts -> {lm.collar_z:.4f}, " + f"{n_bot} sole verts -> {lm.sole_bottom:.4f}") + + +# -------------------------------------------------------------------------- +# Sneaker feature field (texel-level; drives albedo AND mask together). +# Every feature is |x|-mirror symmetric so the (possibly overlapping) +# left/right UV islands paint identical values. +# -------------------------------------------------------------------------- + +def _paint_texels(px, py, pz, noise, lm): + """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions.""" + n = px.shape[0] + side = np.where(px >= 0.0, 1.0, -1.0) + dx = px - side * lm.foot_cx # signed offset from the foot centre + adx = np.abs(dx) + + in_sole = pz <= lm.sole_top + # Tread = the flat underside plane only (never visible from the side). + # The sole SIDE WALL is snapped geometry whose UV triangles are stretched + # slivers — any tonal variation there (noise, a dark tread band) smears + # into vertical streaks under bilinear magnification, so the whole band + # above the underside is painted ONE flat tone. + in_tread = pz <= lm.sole_bottom + 0.0015 + in_band = (pz >= lm.collar_z - lm.band_h) & ~in_sole + + # Lace panel: param t along the foot bone (ankle head -> ball tail) in + # the (y,z) plane; texels above the bone line, near the centreline. + dy_ax = lm.ball_y - lm.ankle_y + dz_ax = lm.ball_z - lm.ankle_z + l2 = dy_ax * dy_ax + dz_ax * dz_ax + t = ((py - lm.ankle_y) * dy_ax + (pz - lm.ankle_z) * dz_ax) / l2 + above_bone = pz > (lm.ankle_z + t * dz_ax + 0.002 * lm.s) + in_tongue = (~in_sole & above_bone & (adx < lm.lace_halfw) + & (t >= LACE_T0) & (t <= LACE_T1)) + frac = (t - LACE_T0) / (LACE_T1 - LACE_T0) + stripe_pos = frac * N_LACES + stripe_d = np.abs(stripe_pos - (np.floor(stripe_pos) + 0.5)) + laces = in_tongue & (stripe_d < 0.5 * LACE_STRIPE_DUTY) + + toe_cap = (py < lm.cap_y) & ~in_sole + heel_tab = ((adx < HEEL_TAB_HALFW_M * lm.s) & ~in_sole + & (py > lm.ankle_y + 0.55 * (lm.heel_y - lm.ankle_y))) + + # --- albedo ------------------------------------------------------------- + alb = np.empty((n, 4), dtype=np.float32) + for c in range(3): + alb[:, c] = UPPER_RGB[c] + noise + alb[:, 3] = 1.0 + for c in range(3): + alb[in_sole, c] = SOLE_RGB[c] # flat, noise-free (sliver UVs) + alb[in_tread, c] = TREAD_RGB[c] # underside plane only + + if not PLAIN: + for c in range(3): + alb[toe_cap & ~in_sole, c] = TOE_RGB[c] + noise[toe_cap & ~in_sole] + alb[in_tongue, :3] *= TONGUE_SHADE + for c in range(3): + alb[laces, c] = LACE_RGB[c] + noise[laces] + alb[in_band & ~laces, :3] *= COLLAR_SHADE + alb[heel_tab & ~in_band, :3] *= HEEL_TAB_SHADE + + # Painted panel lines (albedo only — swoosh-free). + foxing = np.abs(pz - lm.sole_top) < lm.line_w + cap_border = (np.abs(py - lm.cap_y) < lm.line_w) & ~in_sole + vamp = ((np.abs(pz - lm.vamp_z) < lm.line_w) & ~in_sole + & (adx > lm.lace_halfw * 0.8) + & (py > lm.cap_y) & (py < lm.heel_line_y)) + heel_ctr = ((np.abs(py - lm.heel_line_y) < lm.line_w) & ~in_sole + & (pz < lm.collar_z - lm.band_h)) + lines = (foxing | cap_border | vamp | heel_ctr) & ~laces + alb[lines, :3] *= LINE_SHADE + + # --- region mask: sole R / upper G / laces+trim B ------------------------- + mask = np.zeros((n, 4), dtype=np.float32) + is_b = (laces | toe_cap | heel_tab | in_band) & ~in_sole + mask[in_sole, 0] = 1.0 + mask[is_b, 2] = 1.0 + mask[~(in_sole | is_b), 1] = 1.0 + return alb, mask + + +def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H): + """Barycentric texel fill of one UV triangle: interpolate 3D positions, + evaluate the sneaker field, write albedo + mask together.""" + a, b, c = uvs + A, B, C = cos + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + pxg = xs + 0.5 + pyg = ys + 0.5 + w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom + w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + w0i, w1i, w2i = w0[inside], w1[inside], w2[inside] + px3 = w0i * A.x + w1i * B.x + w2i * C.x + py3 = w0i * A.y + w1i * B.y + w2i * C.y + pz3 = w0i * A.z + w1i * B.z + w2i * C.z + ysin = ys[inside] + xsin = xs[inside] + alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm) + alb_buf[ysin, xsin] = alb + mask_buf[ysin, xsin] = mask + + +def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body): + """Rasterize all UV0 triangles once, producing the painted albedo and the + region mask from one shared feature-field evaluation per texel.""" + W = H = TEX_SIZE + rng = np.random.default_rng(NOISE_SEED) + noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5) + * 2.0 * ALBEDO_NOISE) + + alb_buf = np.empty((H, W, 4), dtype=np.float32) + for c in range(3): + alb_buf[:, :, c] = UPPER_RGB[c] + noise_buf + alb_buf[:, :, 3] = 1.0 + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 # background = upper green (bleed-safe) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + if not len(bm.loops.layers.uv): + raise RuntimeError("no UV layer for albedo/mask paint") + uv_layer = bm.loops.layers.uv[0] + + tri_count = 0 + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv.copy() for loop in loops] + cos = [loop.vert.co.copy() for loop in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_paint( + alb_buf, mask_buf, noise_buf, + (uvs[0], uvs[i], uvs[i + 1]), + (cos[0], cos[i], cos[i + 1]), + lm, W, H) + tri_count += 1 + bm.free() + counts = [float(mask_buf[:, :, c].sum()) for c in range(3)] + total = max(sum(counts), 1.0) + log(f"painted {tri_count} UV triangles ({W}x{H}); mask texels " + f"R={100 * counts[0] / total:.1f}% G={100 * counts[1] / total:.1f}% " + f"B={100 * counts[2] / total:.1f}%") + + def _save(buf, name, path): + img = bpy.data.images.new(name, W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + albedo_img = _save(alb_buf, f"sneaker_albedo_{body}", albedo_path) + _save(mask_buf, f"sneaker_mask_{body}", mask_path) + log(f"saved albedo -> {albedo_path}") + log(f"saved mask -> {mask_path}") + return albedo_img + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_sneaker_shell(body_dir, out_dir, body, offset): + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + denim.weld_boundaries(shell) # fuse sole patch + ankle shards + make_normals_consistent(shell) + + lm = FootLandmarks(armature, shell) + + collar_cut(shell, lm.collar_z) + smooth_shell(shell, lm) + flatten_collar_rims(shell, lm.collar_z, "pre-offset") + base.offset_outward(shell, offset) + flatten_collar_rims(shell, lm.collar_z, "post-offset") # rim normals lift it + toe_round(shell, lm, TOE_ROUND_M * lm.s) + build_sole_slab(shell, lm) + collar_flare(shell, lm, COLLAR_FLARE_M * lm.s) + base.solidify(shell, base.CLOTH_THICKNESS_M) + clamp_residue(shell, lm) + + denim.author_parked_uv2(shell) # not logo-capable; shader needs UV2 + + albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png") + mask_path = os.path.join(out_dir, f"{body}_mask.png") + albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + global OFFSET_M, SOLE_DROP_M, COLLAR_FRAC, COLLAR_FLARE_M + global TOE_ROUND_M, N_LACES, PLAIN, SOLE_SNAP_FRAC, SMOOTH_TOE_ITERS + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--sole-drop M] [--sole-snap-frac F] " + "[--collar-frac F] [--collar-flare M] [--toe-round M] " + "[--toe-smooth N] [--laces N] [--plain]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + if "--offset" in argv: + OFFSET_M = float(argv[argv.index("--offset") + 1]) + if "--sole-drop" in argv: + SOLE_DROP_M = float(argv[argv.index("--sole-drop") + 1]) + if "--sole-snap-frac" in argv: + SOLE_SNAP_FRAC = float(argv[argv.index("--sole-snap-frac") + 1]) + if "--toe-smooth" in argv: + SMOOTH_TOE_ITERS = int(argv[argv.index("--toe-smooth") + 1]) + if "--collar-frac" in argv: + COLLAR_FRAC = float(argv[argv.index("--collar-frac") + 1]) + if "--collar-flare" in argv: + COLLAR_FLARE_M = float(argv[argv.index("--collar-flare") + 1]) + if "--toe-round" in argv: + TOE_ROUND_M = float(argv[argv.index("--toe-round") + 1]) + if "--laces" in argv: + N_LACES = int(argv[argv.index("--laces") + 1]) + if "--plain" in argv: + PLAIN = True + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"sneaker per-body mode: {len(bodies)} bodies, offset " + f"{OFFSET_M * 1000:.0f} mm, sole-drop {SOLE_DROP_M * 1000:.0f} mm, " + f"collar-frac {COLLAR_FRAC}, laces {N_LACES}, plain={PLAIN}") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_sneaker_shell(body_dir, out_dir, body, OFFSET_M) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_sweater.py b/tooling/garment-fit/blender_author_sweater.py new file mode 100644 index 000000000..6f35b22d1 --- /dev/null +++ b/tooling/garment-fit/blender_author_sweater.py @@ -0,0 +1,597 @@ +""" +blender_author_sweater.py (T-1089 wave 2 — sweater_modern, per-body offset shell) + +Crew-neck pullover authored per body via the offset-shell route. Imports +blender_author_offset_shell.py as the shared library (segment join, bone-ratio +thresholds, offset, solidify, logo UV2, GLB export) and +blender_author_denim_pants.py for the wave-1-proven boundary practices +(coincident segment-seam WELD, seg_hips crotch probe). What the sweater adds, +as reusable parameters: + + * coverage: torso + torso_upper + FULL arms (cuffs at the wrist) + the UPPER + seg_hips band. Including seg_hips is the hem answer for tops: the natural + seg_torso bottom boundary is a 9-14 cm jagged tooth ring (probe evidence, + all bodies) and flattening it up/down either opens skin holes (hips skin + fails to reach the teeth tops in 2/24 azimuth bins) or invents cloth off + the body surface. Instead the shell continues into real hip geometry and + is hem-CUT at a clean plane derived from bone landmarks (spine_01 -> + crotch fraction), then the cut ring is flattened onto that plane + (denim-pants ankle practice; pull distance ~ one face, not full teeth). + Hips stay UNHIDDEN in coverage.json — skin continues under the hem, so no + hole is geometrically possible (uniform_utility waist-join precedent). + * crew-neck CUT + rim CIRCULARIZATION — the seg_torso_upper neckline is a + jagged tooth ring whose VALLEY sits at neck_z + ~0.23*neck_len on every + body (probe evidence, 6 bodies) — exactly crew height — while its teeth + reach ~72% up the neck. Raising the rim to the teeth max (the first + draft) reads as a chin-high mock-neck, not the crew the spec asks. So: + delete the neck-tube verts ABOVE the valley within the ring's own + measured hd_max * CUT_R_MUL (per-body self-calibrating radius — the + neck-trap flare shell probes strictly outside it), then flatten the new + boundary onto the valley plane: verts within r_med * CREW_RIM_R_MUL of + the neck axis are circularized to the ring median radius (clean crew + rim); flare verts beyond only drop to the plane (no radial pinch, which + would fold cloth on wide-trap bodies). Skin-hole-safe: seg_neck skin + reaches down exactly to the valley (probe), and runtime never hides + segments anyway. + * wrist rim flattening onto the cut plane (denim ankle practice on the arm + axis) — the vert-threshold cut leaves 1-3 cm teeth; the painted ribbed + cuff needs a clean edge. + * cuff PINCH — per-vert offset scale tapering to CUFF_PINCH at the wrist so + the ribbed cuffs read cinched (knit hugs the wrist). + * painted knit identity, driven per texel from final 3D positions + (buttondown per-pixel bake machinery): ribbed collar / cuffs / hem — + azimuthal rib stripes around the neck axis, arm axis, and torso axis — + plus border stitch lines. All LUMINANCE detail, so the luma-preserving + toon_garment recolor keeps it under any tint. Warm muted default tints + live in the manifest entry, not the albedo. + * region mask (spec): collar=R (tint_0), body=G (tint_1), cuffs+hem=B + (tint_2). A unused. Logo-capable OFF per spec, but the UV2 chest channel + is still authored (costs nothing; buttondown precedent). + +PER-BODY ONLY (Q-060: offset shells are authored per body, never SD-fit): + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_sweater.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/sweater_modern \ + [--bodies average_m,child,...] [--offset 0.014] [--hem-drop 0.30] \ + [--rib-period 0.011] [--base-rgb 0.62,0.585,0.545] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060. +""" + +import math +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import blender_author_offset_shell as base # noqa: E402 +import blender_author_denim_pants as denim # noqa: E402 (weld + hips probe) +import blender_author_buttondown as bd # noqa: E402 (per-pixel bake helpers) + +# -------------------------------------------------------------------------- +# Parameters (the reusable seam — override for cardigan/turtleneck variants) +# -------------------------------------------------------------------------- + +GARMENT_ID = "sweater_modern" + +SEGMENTS = [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", + "seg_hips", +] + +OFFSET_M = 0.014 # knit-weight standoff (tee 12 / hoodie 16) +THICKNESS_M = 0.005 # knit cloth thickness (tee 4 / hoodie 6) +WRIST_FRAC = 0.94 # fraction of lowerarm kept — cuffs AT the wrist +CUFF_PINCH = 0.75 # offset scale at the wrist rim (ribbing cinches) +CUFF_LEN_FRAC = 0.18 # ribbed cuff length, fraction of lowerarm length +HEM_DROP_FRAC = 0.30 # hem below the spine_01 waistline, fraction of + # (waistline - crotch) — hip-length pullover +HEM_BAND_FRAC = 0.085 # ribbed hem band, fraction of (collar_z - hem_z) +COLLAR_REACH_FRAC = 1.45 # collar region radial reach, x collar_x_abs +CUT_R_MUL = 1.05 # crew cut radius, x the neck ring's measured hd_max +CREW_RIM_R_MUL = 1.6 # circularize rim verts within this x ring r_med +COLLAR_BAND_FRAC = 0.32 # ribbed collar band height below the crew rim, + # x neck_len (~2.5 cm on average_m) — anchored to + # the MEASURED rim, not collar_z_min, so the band + # reads as a crew rib, not a chest yoke + +# Knit paint (luminance detail; absolute mm scaled by the body's shoulder +# ratio so the rib gauge reads identical from child to heavy_m). +RIB_PERIOD_M = 0.011 # one rib pair (dark+light) around the band +RIB_DARK = 0.85 # dark rib stripe multiplier +BAND_MUL = 0.94 # overall band shade vs body knit +LINE_MUL = 0.70 # border stitch line multiplier +LINE_HALF_M = 0.0022 # border stitch line half-width + +# Warm muted base fabric (default tints in the manifest carry the colour; +# the albedo carries luma detail + the untinted fallback tone). +FABRIC_RGB = (0.620, 0.585, 0.545) +FABRIC_NOISE = 0.030 +ALBEDO_SEED = 2093 + +MASK_SIZE = 512 +ALBEDO_SIZE = 1024 + + +def log(msg): + print(f"[sweater] {msg}") + + +# -------------------------------------------------------------------------- +# Landmarks (all bone-proportional; same philosophy as base.derive_thresholds) +# -------------------------------------------------------------------------- + +def derive_landmarks(armature, thr, crotch_z): + bones = armature.data.bones + neck = bones.get("neck_01") + la_l = bones.get("lowerarm_l") + la_r = bones.get("lowerarm_r") + spine01 = bones.get("spine_01") + ua_l = bones.get("upperarm_l") + ua_r = bones.get("upperarm_r") + if not all([neck, la_l, la_r, spine01, ua_l, ua_r]): + raise RuntimeError("landmark bones missing (neck_01/lowerarm/spine_01)") + + shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 + s = shoulder_x / base._REF_SHOULDER_X + + def along(b, frac): + return b.head_local.x + frac * (b.tail_local.x - b.head_local.x) + + cut_l = along(la_l, WRIST_FRAC) # left arm +x: delete x > cut_l + cut_r = along(la_r, WRIST_FRAC) # right arm -x: delete x < cut_r + lowerarm_len = abs(la_l.tail_local.x - la_l.head_local.x) + wrist_x_abs = (abs(cut_l) + abs(cut_r)) / 2.0 + + waist_z = spine01.head_local.z + hem_z = waist_z - HEM_DROP_FRAC * (waist_z - crotch_z) + + # Arm rib axis (y, z) — midpoint of the lowerarm bone, shared by both + # sides (the rig mirrors in x only). + az_y = (la_l.head_local.y + la_l.tail_local.y) / 2.0 + az_z = (la_l.head_local.z + la_l.tail_local.z) / 2.0 + + lm = { + "style": s, + "neck_y": neck.head_local.y, + "neck_len": neck.tail_local.z - neck.head_local.z, + "cut_l": cut_l, + "cut_r": cut_r, + "wrist_x_abs": wrist_x_abs, + "cuff_x0": wrist_x_abs - CUFF_LEN_FRAC * lowerarm_len, + "arm_axis_yz": (az_y, az_z), + "waist_z": waist_z, + "crotch_z": crotch_z, + "hem_z": hem_z, + "collar_reach": COLLAR_REACH_FRAC * thr["collar_x_abs"], + "rib_period": RIB_PERIOD_M * s, + "line_half": LINE_HALF_M * s, + } + lm["hem_top"] = hem_z + HEM_BAND_FRAC * (thr["collar_z_min"] - hem_z) + log(f"landmarks: style {s:.3f} wrist cuts ({cut_l:.3f},{cut_r:.3f}) " + f"cuff_x0 {lm['cuff_x0']:.3f} hem {hem_z:.3f} (waist {waist_z:.3f}, " + f"crotch {crotch_z:.3f}) hem_top {lm['hem_top']:.3f} " + f"collar reach {lm['collar_reach']:.3f}") + return lm + + +# -------------------------------------------------------------------------- +# Geometry: wrist + hem cuts, rim flattening, crew-collar circularization +# -------------------------------------------------------------------------- + +def wrist_cut(shell, lm): + """Trim the sleeve tubes at the wrist plane (hoodie/jacket pattern).""" + bm = bmesh.new() + bm.from_mesh(shell.data) + doomed = [v for v in bm.verts + if v.co.x > lm["cut_l"] or v.co.x < lm["cut_r"]] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"wrist cut removed {len(doomed)} verts; " + f"{len(shell.data.vertices)} remain") + + +def hem_cut(shell, lm): + """Trim the hips continuation below the hem plane (real hip geometry, so + the surviving cloth conforms to the body — no invented coverage).""" + bm = bmesh.new() + bm.from_mesh(shell.data) + doomed = [v for v in bm.verts if v.co.z < lm["hem_z"]] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"hem cut at z={lm['hem_z']:.3f}: removed {len(doomed)} verts") + + +def crew_cut(shell, thr, lm): + """Cut the neck tube down to crew height (probe-driven, per body). + + The seg_torso_upper neck boundary is a jagged tooth ring: valley at + ~neck_z + 0.23*neck_len (crew height), teeth up to ~72% of the neck. + Measure the ring (valley z, median + max horizontal distance to the neck + axis), then delete every vert ABOVE the valley within hd_max * CUT_R_MUL + of the axis — the tube and its teeth, and provably not the neck-trap + flare shell (probe: flare hd sits well outside hd_max on all bodies). + Stores crew_z / crew_r in lm for the flatten pass. + """ + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + boundary = set() + for e in bm.edges: + if len(e.link_faces) == 1: + boundary.update(v.index for v in e.verts) + + mid_l = 0.5 * (thr["sleeve_x_abs"] + lm["cut_l"]) + mid_r = -0.5 * (thr["sleeve_x_abs"] + abs(lm["cut_r"])) + z_split = 0.5 * (lm["hem_z"] + thr["collar_z_min"]) + ring = [] + for i in boundary: + v = bm.verts[i] + if v.co.x > mid_l or v.co.x < mid_r or v.co.z <= z_split: + continue + hd = math.hypot(v.co.x, v.co.y - lm["neck_y"]) + if hd <= lm["collar_reach"] * 1.5: + ring.append((hd, v.co.z)) + if not ring: + bm.free() + raise RuntimeError("no neck boundary ring found for crew cut") + lm["crew_z"] = min(z for _, z in ring) + lm["crew_r"] = float(np.median([hd for hd, _ in ring])) + hd_max = max(hd for hd, _ in ring) + r_cut = hd_max * CUT_R_MUL + + doomed = [v for v in bm.verts + if v.co.z > lm["crew_z"] + and math.hypot(v.co.x, v.co.y - lm["neck_y"]) <= r_cut] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(me) + bm.free() + me.update() + lm["collar_band_z0"] = lm["crew_z"] - COLLAR_BAND_FRAC * lm["neck_len"] + log(f"crew cut: ring valley z={lm['crew_z']:.3f} r_med={lm['crew_r']:.3f} " + f"hd_max={hd_max:.3f} -> removed {len(doomed)} tube verts " + f"(r_cut {r_cut:.3f}); collar band z0={lm['collar_band_z0']:.3f}") + + +def flatten_rims_and_collar(shell, thr, lm): + """Clean every open rim (adapted from the denim flatten_open_rims + practice; only boundary verts move, weights/UVs ride along). + + wrist rings -> the wrist cut planes (x per side) + hem ring -> the hem plane (z) + neck ring -> the crew plane (crew_z from crew_cut): verts within + crew_r * CREW_RIM_R_MUL of the neck axis circularize to + the ring median radius (clean crew rim); flare verts + beyond only drop onto the plane — no radial pinch, which + would fold cloth on wide-trap bodies. + """ + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + boundary = set() + for e in bm.edges: + if len(e.link_faces) == 1: + boundary.update(v.index for v in e.verts) + + mid_l = 0.5 * (thr["sleeve_x_abs"] + lm["cut_l"]) + mid_r = -0.5 * (thr["sleeve_x_abs"] + abs(lm["cut_r"])) + z_split = 0.5 * (lm["hem_z"] + thr["collar_z_min"]) + + wrist_n = hem_n = rim_n = flare_n = 0 + stray = 0 + for i in boundary: + v = bm.verts[i] + if v.co.x > mid_l: + v.co.x = lm["cut_l"] + wrist_n += 1 + elif v.co.x < mid_r: + v.co.x = lm["cut_r"] + wrist_n += 1 + elif v.co.z <= z_split: + v.co.z = lm["hem_z"] + hem_n += 1 + else: + hd = math.hypot(v.co.x, v.co.y - lm["neck_y"]) + if hd <= lm["crew_r"] * CREW_RIM_R_MUL: + if hd > 1e-6: + f = lm["crew_r"] / hd + v.co.x *= f + v.co.y = lm["neck_y"] + (v.co.y - lm["neck_y"]) * f + v.co.z = lm["crew_z"] + rim_n += 1 + elif hd <= lm["collar_reach"] * 1.5: + v.co.z = lm["crew_z"] + flare_n += 1 + else: + stray += 1 + + if rim_n == 0: + log("WARNING: no crew rim verts found — collar left ragged") + log(f"flattened rims: wrist {wrist_n} verts -> cut planes, " + f"hem {hem_n} -> z={lm['hem_z']:.3f}, crew rim {rim_n} -> circle " + f"r={lm['crew_r']:.3f} z={lm['crew_z']:.3f}, flare {flare_n} -> " + f"plane, stray {stray}") + bm.to_mesh(me) + bm.free() + me.update() + + +def offset_with_cuff_pinch(shell, offset, lm): + """Outward normal offset with the standoff tapering to CUFF_PINCH over + the ribbed cuff band (knit cuffs hug the wrist).""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.normal_update() + span = max(lm["wrist_x_abs"] - lm["cuff_x0"], 1e-6) + pinched = 0 + for v in bm.verts: + s = 1.0 + ax = abs(v.co.x) + if ax > lm["cuff_x0"]: + t = min((ax - lm["cuff_x0"]) / span, 1.0) + s = 1.0 - (1.0 - CUFF_PINCH) * t + pinched += 1 + v.co += v.normal * (offset * s) + bm.to_mesh(me) + bm.free() + me.update() + log(f"offset {offset*1000:.0f} mm outward " + f"({pinched} cuff verts pinched to {CUFF_PINCH:.2f}x)") + + +# -------------------------------------------------------------------------- +# Per-pixel knit paint + region mask (buttondown bake machinery) +# -------------------------------------------------------------------------- + +def _rib_stripes(arc, period): + """Bool array: dark rib stripe (half of each rib pair).""" + return np.mod(arc / period, 1.0) < 0.5 + + +def _classify_px(pos, thr, lm): + """Region mask rows: collar=R, body=G, cuffs+hem=B (spec).""" + x, y, z = pos[:, 0], pos[:, 1], pos[:, 2] + ax = np.abs(x) + hd = np.hypot(x, y - lm["neck_y"]) + collar = (z >= lm["collar_band_z0"]) & (hd <= lm["collar_reach"]) + band_b = (~collar) & ((ax >= lm["cuff_x0"]) | (z <= lm["hem_top"])) + rgba = np.zeros((len(x), 4), dtype=np.float32) + rgba[:, 1] = 1.0 # default: body -> G + rgba[collar] = (1.0, 0.0, 0.0, 0.0) # crew collar -> R + rgba[band_b] = (0.0, 0.0, 1.0, 0.0) # cuffs + hem -> B + return rgba + + +def _paint_px(pos, rows, thr, lm): + """Knit luminance detail: ribbed collar/cuff/hem + border stitch lines.""" + x, y, z = pos[:, 0], pos[:, 1], pos[:, 2] + ax = np.abs(x) + hd = np.hypot(x, y - lm["neck_y"]) + period = lm["rib_period"] + line = lm["line_half"] + + collar = (z >= lm["collar_band_z0"]) & (hd <= lm["collar_reach"]) + cuff = (~collar) & (ax >= lm["cuff_x0"]) + hem = (~collar) & (~cuff) & (z <= lm["hem_top"]) + + mul = np.ones(len(x), dtype=np.float32) + mul[collar | cuff | hem] = BAND_MUL + + # Collar ribs: azimuth around the neck axis. atan2's +-pi discontinuity + # sits where the 2nd arg is negative — (y-cy)*FRONT_Y_SIGN < 0 is the + # BACK on this rig, so the rib wrap seam hides at centre-back. + arc = np.arctan2(x, (y - lm["neck_y"]) * base.FRONT_Y_SIGN) * hd + sel = collar & _rib_stripes(arc, period) + mul[sel] *= RIB_DARK + + # Cuff ribs: azimuth around the arm axis (wrap seam under the arm). + ay, az_ = lm["arm_axis_yz"] + r_arm = np.hypot(y - ay, z - az_) + arc = np.arctan2(y - ay, z - az_) * r_arm + sel = cuff & _rib_stripes(arc, period) + mul[sel] *= RIB_DARK + + # Hem ribs: azimuth around the torso axis (wrap seam at centre-back). + r_t = np.hypot(x, y - lm["hem_cy"]) + arc = np.arctan2(x, (y - lm["hem_cy"]) * base.FRONT_Y_SIGN) * r_t + sel = hem & _rib_stripes(arc, period) + mul[sel] *= RIB_DARK + + # Border stitch lines at each band's inner edge. + mul[(np.abs(z - lm["collar_band_z0"]) <= line) + & (hd <= lm["collar_reach"] * 1.1)] = LINE_MUL + mul[np.abs(ax - lm["cuff_x0"]) <= line] = LINE_MUL + mul[(np.abs(z - lm["hem_top"]) <= line) & (~cuff)] = LINE_MUL + + out = rows * mul[:, None] + np.clip(out, 0.0, 1.0, out) + return out + + +def bake_maps(shell, thr, lm, mask_path): + """One pass over UV0 triangles: bake _mask.png + painted albedo.""" + tris = bd._gather_tris(shell) + + mbuf = np.zeros((MASK_SIZE, MASK_SIZE, 4), dtype=np.float32) + mbuf[:, :, 1] = 1.0 # body-green background (bilinear-bleed safe) + + rng = np.random.default_rng(ALBEDO_SEED) + fabric = np.array(FABRIC_RGB, dtype=np.float32) + noise = (rng.random((ALBEDO_SIZE, ALBEDO_SIZE, 1), dtype=np.float32) + - 0.5) * 2.0 * FABRIC_NOISE + abuf = np.clip(fabric[None, None, :] + noise, 0.0, 1.0) + + # Hem torso-axis centre from the final mesh (paint anchor). + hem_ys = [v.co.y for v in shell.data.vertices if v.co.z <= lm["hem_top"]] + lm["hem_cy"] = float(np.mean(hem_ys)) if hem_ys else 0.0 + + for uv_a, uv_b, uv_c, co_a, co_b, co_c in tris: + cover = bd._tri_cover(uv_a, uv_b, uv_c, MASK_SIZE, MASK_SIZE) + if cover is not None: + pos = bd._interp_pos(cover, co_a, co_b, co_c) + mbuf[cover[0], cover[1], :] = _classify_px(pos, thr, lm) + cover = bd._tri_cover(uv_a, uv_b, uv_c, ALBEDO_SIZE, ALBEDO_SIZE) + if cover is not None: + pos = bd._interp_pos(cover, co_a, co_b, co_c) + abuf[cover[0], cover[1], :] = _paint_px( + pos, abuf[cover[0], cover[1], :], thr, lm) + + tot = MASK_SIZE * MASK_SIZE + log("mask texels: collar={:.1f}% body={:.1f}% cuffs+hem={:.1f}%".format( + 100.0 * float((mbuf[:, :, 0] > 0.5).sum()) / tot, + 100.0 * float((mbuf[:, :, 1] > 0.5).sum()) / tot, + 100.0 * float((mbuf[:, :, 2] > 0.5).sum()) / tot)) + + mask_img = bpy.data.images.new(f"{GARMENT_ID}_mask", MASK_SIZE, MASK_SIZE, + alpha=True) + mask_img.pixels.foreach_set(mbuf.reshape(-1)) + mask_img.update() + mask_img.filepath_raw = mask_path + mask_img.file_format = 'PNG' + mask_img.save() + log(f"baked region mask -> {mask_path}") + + argba = np.concatenate( + [abuf, np.ones((ALBEDO_SIZE, ALBEDO_SIZE, 1), dtype=np.float32)], + axis=2) + albedo_img = bpy.data.images.new(f"{GARMENT_ID}_albedo", ALBEDO_SIZE, + ALBEDO_SIZE, alpha=False) + albedo_img.pixels.foreach_set(argba.reshape(-1)) + albedo_img.update() + return albedo_img + + +def save_albedo(albedo_img, out_dir, body): + """Sidecar + embed naming (hoodie convention): the glTF exporter names the + embedded image after the file basename and Godot extracts it as + _.png, so pointing the image at base_albedo.png makes the + extraction land exactly on _base_albedo.png.""" + sidecar = os.path.join(out_dir, f"{body}_base_albedo.png") + albedo_img.filepath_raw = sidecar + albedo_img.file_format = 'PNG' + albedo_img.save() + log(f"painted albedo -> {sidecar}") + albedo_img.filepath_raw = os.path.join(out_dir, "base_albedo.png") + albedo_img.save() + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_sweater(body_dir, out_dir, body, offset): + crotch_z, _hips_top = denim.probe_hips_bounds(body_dir) + + base.clear_scene() + base.COVERED_SEGMENTS = SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + thr = base.derive_thresholds(armature) + lm = derive_landmarks(armature, thr, crotch_z) + + denim.weld_boundaries(shell) + wrist_cut(shell, lm) + hem_cut(shell, lm) + crew_cut(shell, thr, lm) + flatten_rims_and_collar(shell, thr, lm) + offset_with_cuff_pinch(shell, offset, lm) + base.solidify(shell, THICKNESS_M) + + base.author_logo_uv(shell, thr) # UV2 before bakes (mask/albedo use UV0) + albedo_img = bake_maps(shell, thr, lm, + os.path.join(out_dir, f"{body}_mask.png")) + save_albedo(albedo_img, out_dir, body) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + global OFFSET_M, HEM_DROP_FRAC, RIB_PERIOD_M, FABRIC_RGB + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--hem-drop F] [--rib-period M] " + "[--base-rgb r,g,b]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--hem-drop" in argv: + HEM_DROP_FRAC = float(argv[argv.index("--hem-drop") + 1]) + if "--rib-period" in argv: + RIB_PERIOD_M = float(argv[argv.index("--rib-period") + 1]) + if "--base-rgb" in argv: + FABRIC_RGB = tuple( + float(v) for v in argv[argv.index("--base-rgb") + 1].split(",")) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"per-body sweater: {len(bodies)} bodies, offset {offset*1000:.0f} mm, " + f"hem-drop {HEM_DROP_FRAC}") + + avg_albedo_stash = os.path.join(out_dir, "_tmp_avg_base_albedo.png") + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_sweater(body_dir, out_dir, body, offset) + if body == base.REFERENCE_BODY: + shutil.copy2(os.path.join(out_dir, "base_albedo.png"), + avg_albedo_stash) + results.append((body, "ok")) + except Exception as exc: # noqa: BLE001 — per-body isolation + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + # Runtime fallbacks mirror the reference body (average_m). + ref_mask = os.path.join(out_dir, f"{base.REFERENCE_BODY}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log("copied reference_mask.png fallback") + if os.path.isfile(avg_albedo_stash): + shutil.move(avg_albedo_stash, os.path.join(out_dir, "base_albedo.png")) + log(f"base_albedo.png = {base.REFERENCE_BODY}'s painted albedo") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_swim_trunks.py b/tooling/garment-fit/blender_author_swim_trunks.py new file mode 100644 index 000000000..a4eb4644d --- /dev/null +++ b/tooling/garment-fit/blender_author_swim_trunks.py @@ -0,0 +1,472 @@ +""" +blender_author_swim_trunks.py (T-1089 wave 2, swim set — swim_trunks) + +Authors bright SWIM TRUNKS as per-body offset shells: hips + upper legs with +the hem clearly ABOVE the knee (shorter than shorts_modern's 0.78 thigh +fraction), a painted centre-front drawstring (knot + two hanging cords) and a +lateral SIDE PANEL colour block down each leg. + +Regions (RGBA mask, toon_garment.gdshader): + waistband -> R (tint_0), body -> G (tint_1), side panel -> B (tint_2) + +Companion to blender_author_offset_shell.py (imported as a library — scene +build/join, offset, solidify, GLB export). The bottoms-specific practices are +REUSED from the proven wave-1 companions, not rediscovered: + + * denim (blender_author_denim_pants.py): boundary WELD of coincident + segment-seam rings before offsetting (un-welded rings offset apart along + diverging normals -> cracks), open-rim FLATTENING onto clean planes (the + segment splitter leaves 4.5-7.6 cm jagged teeth at the waist/hem rims), + the feathered --waist-flare deep-crouch mitigation, the post-solidify + waist residue clamp, and the parked logo TEXCOORD_1 layer (trunks are not + logo-capable, but toon_garment.gdshader samples UV2 unconditionally). + * legs (blender_author_offset_shell_legs.py): the bone-plane hem cut. + +Like the denim script, albedo and region mask are painted TEXEL-level from +ONE analytic feature-field evaluation per texel (UV0 triangles rasterized +with barycentric-interpolated 3D positions), so the painted drawstring/panel +and the recolor regions always agree: + - albedo: bright saturated default (coral body, teal side panel), painted + drawstring knot + hanging cords (centre front), waistband border + + hem border stitches, side-panel edge piping — flat and + toon-friendly, identity carried by the texture (style pin: + modern only). + - mask: waistband R, body G, side panel B. + +All cut/mask/paint parameters derive PER BODY from that body's own bone +landmarks (thigh_l/r) and measured mesh extents (waist rim valley, hem +plane), scaled by the body's garment span and hip half-width — the same +proportional-ratio philosophy as base.derive_thresholds. Per-body mode only +(offset shells author per body, Q-060). + +Usage (swim_trunks reference invocation): + tooling/blender --background --python \ + tooling/garment-fit/blender_author_swim_trunks.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/swim_trunks \ + [--bodies average_m,child,...] [--offset 0.012] [--hem-frac 0.60] \ + [--band-frac 0.14] [--panel-frac 0.42] [--waist-flare 0.007] \ + [--base-rgb 0.95,0.45,0.35] [--panel-rgb 0.13,0.68,0.64] [--plain] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house +wardrobe), Q-060 (per-body offset shells). +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# -------------------------------------------------------------------------- +# Import the wave-1 modules (shared machinery — reused, not copied) +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _load(mod_name, file_name): + spec = importlib.util.spec_from_file_location( + mod_name, os.path.join(_HERE, file_name)) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +base = _load("offset_shell_base", "blender_author_offset_shell.py") +denim = _load("denim_pants", "blender_author_denim_pants.py") +legs = _load("offset_shell_legs", "blender_author_offset_shell_legs.py") + +log = base.log +FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base) + +# -------------------------------------------------------------------------- +# Parameters (defaults = swim_trunks) +# -------------------------------------------------------------------------- +COVERED_SEGMENTS = ["seg_hips", "seg_leg_upper_l", "seg_leg_upper_r"] + +HEM_FRAC = 0.60 # fraction of the thigh bone KEPT below its head. + # 0.60 puts the hem ~17 cm above the knee on + # average_m — clearly shorter than shorts_modern's + # 0.78 (~9 cm above the knee), per the swim spec. +TEX_SIZE = 1024 # albedo + mask resolution (painted details need >512) +WAIST_FLARE_M = 0.007 # feathered radial stand-off at the waistband rim + # (deep-crouch waist-fold mitigation — QA evidence + # from the peasant + jeans sets, reused from denim) + +# Vertical proportions — fractions of the garment span (waist_z - hem_z). +BAND_FRAC = 0.14 # waistband height (R region) (~4.4 cm on average_m) +SEAM_W_FRAC = 0.019 # painted stitch/piping line width (~6 mm on average_m) +HEM_STITCH_FRAC = 0.028 # hem border stitch centre height above the hem +CORD_LEN_FRAC = 0.20 # drawstring cord drop below the knot (~6.3 cm) + +# Horizontal proportions — fractions of the hip half-width (|thigh head x|). +PANEL_HW_FRAC = 0.42 # side-panel half ARC width (~3.8 cm -> 7.6 cm panel) + +# Drawstring metrics in metres on average_m, scaled by the body's hip ratio. +KNOT_R_M = 0.009 # knot blob radius +CORD_W_M = 0.0055 # cord line width +CORD_X0_M = 0.011 # cord |x| offset just below the knot +CORD_SLANT = 0.28 # outward drift of the cords per metre of drop + +# Swim style (sRGB floats; saved as-is — matches the proven base pipeline). +# Bright saturated default per spec: coral body / teal side panel. Luma of +# both stays near the toon_garment.gdshader recolor sweet spot (~0.5-0.6). +CORAL_RGB = (0.95, 0.45, 0.35) # body + waistband base (luma ~0.59) +TEAL_RGB = (0.13, 0.68, 0.64) # side panel (luma ~0.51) +TRIM_RGB = (0.97, 0.95, 0.90) # drawstring + stitches/piping (cream) +BAND_SHADE = 0.93 # waistband albedo darkening (reads untinted) +ALBEDO_NOISE = 0.020 # +/- woven jitter +PLAIN = False # --plain: skip drawstring/stitch/piping paint +NOISE_SEED = 3089 + +# Reference proportions (average_m) the fractions were calibrated against: +# waist rim valley 1.0312, hem(0.60) 0.7139 -> span 0.3173; |thigh head x|. +_REF_HIP_X = 0.0906 + + +# -------------------------------------------------------------------------- +# Per-body landmarks +# -------------------------------------------------------------------------- + +class TrunkLandmarks: + """Cut/mask/paint parameters derived from one body's bones + mesh.""" + + def __init__(self, armature): + bones = armature.data.bones + thigh_l = bones.get("thigh_l") + thigh_r = bones.get("thigh_r") + if thigh_l is None or thigh_r is None: + raise RuntimeError("thigh_l/thigh_r missing — not the 65-bone rig?") + self.hip_x = (abs(thigh_l.head_local.x) + abs(thigh_r.head_local.x)) / 2.0 + # Hem plane: fraction of the thigh bone kept below its head (averaged + # over both sides — they are symmetric on every shipped body). + self.hem_z = ( + thigh_l.head_local.z + HEM_FRAC * (thigh_l.tail_local.z - thigh_l.head_local.z) + + thigh_r.head_local.z + HEM_FRAC * (thigh_r.tail_local.z - thigh_r.head_local.z) + ) / 2.0 + # Leg axis control points (z-increasing: knee -> hip) for the azimuth + # side-panel placement. x values are the +x (left) leg; the right leg + # mirrors via sign. + self.leg_z_pts = np.array([thigh_l.tail_local.z, thigh_l.head_local.z]) + self.leg_x_pts = np.array( + [abs(thigh_l.tail_local.x), abs(thigh_l.head_local.x)]) + self.leg_y_pts = np.array([thigh_l.tail_local.y, thigh_l.head_local.y]) + log(f"landmarks: hem plane z={self.hem_z:.3f} " + f"(thigh head {thigh_l.head_local.z:.3f} -> knee " + f"{thigh_l.tail_local.z:.3f}, keep {HEM_FRAC:.2f}) " + f"hip_x={self.hip_x:.3f}") + + def finalize(self, waist_plane, hem_plane): + """Derive paint metrics from the CLEAN (flattened) rims.""" + self.waist_z = waist_plane + self.hem_plane = hem_plane + self.span = waist_plane - hem_plane + self.sh = self.hip_x / _REF_HIP_X + self.band_h = BAND_FRAC * self.span + self.band_z = waist_plane - self.band_h + self.seam_w = SEAM_W_FRAC * self.span + self.panel_hw = PANEL_HW_FRAC * self.hip_x + self.hem_stitch_z = hem_plane + HEM_STITCH_FRAC * self.span + self.knot_z = self.band_z + 0.55 * self.band_h + self.knot_r = KNOT_R_M * self.sh + self.cord_len = CORD_LEN_FRAC * self.span + self.cord_w = CORD_W_M * self.sh + self.cord_x0 = CORD_X0_M * self.sh + log(f"finalized: waist={self.waist_z:.3f} hem={self.hem_plane:.3f} " + f"span={self.span:.3f} band_z={self.band_z:.3f} " + f"panel_hw={self.panel_hw * 100:.1f}cm " + f"seam_w={self.seam_w * 1000:.1f}mm knot_z={self.knot_z:.3f}") + + +# -------------------------------------------------------------------------- +# Swim feature field (texel-level; drives albedo AND mask together) +# -------------------------------------------------------------------------- + +def _trunk_field(px, py, pz, lm): + """Evaluate swim-trunk features at texel 3D positions (numpy arrays). + + Returns bool arrays (in_band, panel, trim): + `in_band`/`panel` feed the mask R/B channels; `trim` is every painted + cream detail (drawstring, border stitches, panel edge piping). + """ + w2 = lm.seam_w * 0.5 + front = py * FRONT_Y_SIGN > 0.004 + in_band = pz >= lm.band_z + mid = ~in_band + + # Per-z leg axis, mirrored by x sign (clamps to the hip values above the + # thigh head, so the panel runs straight up through the hip to the band). + side = np.where(px >= 0.0, 1.0, -1.0) + cx = side * np.interp(pz, lm.leg_z_pts, lm.leg_x_pts) + cy = np.interp(pz, lm.leg_z_pts, lm.leg_y_pts) + dx = px - cx + dy = py - cy + r = np.hypot(dx, dy) + 1e-9 + + # Side panel: azimuth toward the outer (+/-x) direction; constant metric + # width via arc distance (same construction as the denim outseam). + arc_out = np.arccos(np.clip(dx * side / r, -1.0, 1.0)) * r + panel = mid & (arc_out < lm.panel_hw) + + # Painted trim: panel edge piping, waistband border, hem border. + piping = mid & (np.abs(arc_out - lm.panel_hw) < w2 * 0.8) + wstitch = np.abs(pz - lm.band_z) < w2 * 0.7 + hstitch = np.abs(pz - lm.hem_stitch_z) < w2 * 0.7 + + # Drawstring: knot blob + two cords hanging from it, centre front, + # drifting slightly outward as they drop. + knot = front & (np.hypot(px, pz - lm.knot_z) < lm.knot_r) + drop = np.clip(lm.knot_z - pz, 0.0, None) + cord_sep = lm.cord_x0 + CORD_SLANT * drop + cords = front & (pz < lm.knot_z) & (pz > lm.knot_z - lm.cord_len) \ + & (np.abs(np.abs(px) - cord_sep) < lm.cord_w * 0.5) + + trim = piping | wstitch | hstitch | knot | cords + return in_band, panel, trim + + +def _paint_texels(px, py, pz, noise, lm): + """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions.""" + n = px.shape[0] + in_band, panel, trim = _trunk_field(px, py, pz, lm) + + # --- albedo: coral base, shaded band, teal panel, cream trim ------------ + alb = np.empty((n, 4), dtype=np.float32) + for c in range(3): + alb[:, c] = CORAL_RGB[c] + noise + alb[:, 3] = 1.0 + alb[in_band, :3] *= BAND_SHADE + for c in range(3): + alb[panel, c] = TEAL_RGB[c] + noise[panel] + if not PLAIN: + alb[trim, 0] = TRIM_RGB[0] + alb[trim, 1] = TRIM_RGB[1] + alb[trim, 2] = TRIM_RGB[2] + np.clip(alb, 0.0, 1.0, out=alb) + + # --- region mask: waistband R / body G / side panel B -------------------- + mask = np.zeros((n, 4), dtype=np.float32) + mask[in_band, 0] = 1.0 + mask[panel, 2] = 1.0 + mask[~(in_band | panel), 1] = 1.0 + return alb, mask + + +def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H): + """Barycentric texel fill of one UV triangle: interpolate 3D positions, + evaluate the swim field, write albedo + mask together (same rasterizer + contract as the denim companion, driving this garment's field).""" + a, b, c = uvs + A, B, C = cos + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + pxg = xs + 0.5 + pyg = ys + 0.5 + w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom + w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + w0i, w1i, w2i = w0[inside], w1[inside], w2[inside] + px3 = w0i * A.x + w1i * B.x + w2i * C.x + py3 = w0i * A.y + w1i * B.y + w2i * C.y + pz3 = w0i * A.z + w1i * B.z + w2i * C.z + ysin = ys[inside] + xsin = xs[inside] + alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm) + alb_buf[ysin, xsin] = alb + mask_buf[ysin, xsin] = mask + + +def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body): + """Rasterize all UV0 triangles once, producing the painted albedo and the + region mask from one shared feature-field evaluation per texel.""" + W = H = TEX_SIZE + rng = np.random.default_rng(NOISE_SEED) + noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5) + * 2.0 * ALBEDO_NOISE) + + alb_buf = np.empty((H, W, 4), dtype=np.float32) + for c in range(3): + alb_buf[:, :, c] = CORAL_RGB[c] + noise_buf + alb_buf[:, :, 3] = 1.0 + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 # background = body green (bilinear-bleed safe) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + if not len(bm.loops.layers.uv): + raise RuntimeError("no UV layer for albedo/mask paint") + uv_layer = bm.loops.layers.uv[0] + + tri_count = 0 + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv.copy() for loop in loops] + cos = [loop.vert.co.copy() for loop in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_paint( + alb_buf, mask_buf, noise_buf, + (uvs[0], uvs[i], uvs[i + 1]), + (cos[0], cos[i], cos[i + 1]), + lm, W, H) + tri_count += 1 + bm.free() + + total = float(W * H) + r_pct = 100.0 * float((mask_buf[:, :, 0] > 0.5).sum()) / total + b_pct = 100.0 * float((mask_buf[:, :, 2] > 0.5).sum()) / total + log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H}); " + f"mask texels: R(waistband)={r_pct:.1f}% B(panel)={b_pct:.1f}% " + f"(rest G/background)") + + def _save(buf, name, path): + img = bpy.data.images.new(name, W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + albedo_img = _save(alb_buf, f"swim_albedo_{body}", albedo_path) + _save(mask_buf, f"swim_mask_{body}", mask_path) + log(f"saved albedo -> {albedo_path}") + log(f"saved mask -> {mask_path}") + return albedo_img + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_trunks_shell(body_dir, out_dir, body, offset): + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + denim.weld_boundaries(shell) + + lm = TrunkLandmarks(armature) + legs.hem_cut(shell, lm.hem_z) + waist_plane, hem_plane = denim.flatten_open_rims(shell, lm.hem_z) + + base.offset_outward(shell, offset) + denim.waist_flare(shell, waist_plane, + BAND_FRAC * (waist_plane - hem_plane), WAIST_FLARE_M) + base.solidify(shell, base.CLOTH_THICKNESS_M) + denim.clamp_waist_residue(shell, waist_plane) + + # Paint metrics reference the CLEAN rims (band under the flattened waist + # edge, hem stitch above the flattened hem). + lm.finalize(waist_plane, hem_plane) + denim.author_parked_uv2(shell) + + albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png") + mask_path = os.path.join(out_dir, f"{body}_mask.png") + albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + global HEM_FRAC, BAND_FRAC, PANEL_HW_FRAC, WAIST_FLARE_M + global CORAL_RGB, TEAL_RGB, PLAIN + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--hem-frac F] [--band-frac F] [--panel-frac F] " + "[--waist-flare M] [--base-rgb r,g,b] [--panel-rgb r,g,b] " + "[--plain]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = base.PER_BODY_OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--hem-frac" in argv: + HEM_FRAC = float(argv[argv.index("--hem-frac") + 1]) + if "--band-frac" in argv: + BAND_FRAC = float(argv[argv.index("--band-frac") + 1]) + if "--panel-frac" in argv: + PANEL_HW_FRAC = float(argv[argv.index("--panel-frac") + 1]) + if "--waist-flare" in argv: + WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1]) + if "--base-rgb" in argv: + CORAL_RGB = tuple( + float(v) for v in argv[argv.index("--base-rgb") + 1].split(",")) + if "--panel-rgb" in argv: + TEAL_RGB = tuple( + float(v) for v in argv[argv.index("--panel-rgb") + 1].split(",")) + if "--plain" in argv: + PLAIN = True + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"swim-trunks per-body mode: {len(bodies)} bodies, offset " + f"{offset * 1000:.0f} mm, hem-frac {HEM_FRAC}, band-frac {BAND_FRAC}, " + f"panel-frac {PANEL_HW_FRAC}, plain={PLAIN}") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_trunks_shell(body_dir, out_dir, body, offset) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_swimsuit.py b/tooling/garment-fit/blender_author_swimsuit.py new file mode 100644 index 000000000..5ca8d636d --- /dev/null +++ b/tooling/garment-fit/blender_author_swimsuit.py @@ -0,0 +1,649 @@ +""" +blender_author_swimsuit.py (T-1089 wave 2, swimsuit_onepiece — swim family) + +Authors a one-piece SWIMSUIT as per-body offset shells, reusing +blender_author_offset_shell.py as a library (scene build, join, offset, +solidify, GLB export) and blender_author_offset_coverall.py's per-texel +rasterizer (_tri_texels) for crisp mask/albedo boundaries. Coverage: torso + +torso_upper + hips — NO arms, NO legs. The torso_upper is cut down to narrow +SHOULDER STRAPS (front scoop + back scoop + open armholes); the bottom gets +HIGH-CUT leg openings that rise from the crotch gusset to the hip line at the +sides. What this companion adds, as reusable parameters: + + * seg_leg_upper_l/r are INCLUDED in the covered set purely as cut stock: + the seg_hips lower boundary is splitter teeth (probe: 9-13 cm jag, + z 0.884..1.015 on average_m — worse than the waist teeth denim flattens). + Welding the legs in makes that seam interior, so the high-cut leg cut + slices through CLEAN thigh geometry and no natural teeth survive; the + tubes below the cut are deleted whole. + * measurement-guarded straps: the strap corridor is proportional + (STRAP_X0/X1_FRAC of shoulder |x|) but clamped per body against the + MEASURED neck-seam ring (max |x| + guard) and armscye ring (min |x| − + guard), because the armscye reaches |x| = 0.79..0.92 x shoulder depending + on body fork (probe) and a fixed fraction would slice into the seam. + * measurement-derived armhole plane: z_arm = (armscye ring min z) − drop, + so the whole jagged arm-seam ring is guaranteed deleted on every body. + * analytic high-cut leg surface z_leg(x, y): crotch gusset (below-crotch at + |x| < gusset half-width, so the gusset never opens), rising outward to + the thigh-head line, with a front/back blend that keeps the seat covered + (RISE_BACK < RISE_FRONT). The SAME function drives the cut, the rim + flattening and the trim band in the mask, so they always agree. + * open-rim FLATTENING onto the analytic lines (denim practice, post-offset): + scoop rims -> scoop plane, armhole rims -> z_arm, leg rims -> z_leg, + strap side edges -> |x| snapped to the exact strap planes. + +Regions (RGBA mask, toon_garment.gdshader; spec: straps+trim=R, body=G, +side color-block=B): + R = shoulder straps + edge trim along every opening (scoop/armhole/leg) + G = main body fabric + B = side color-block panels (normal-gated: |n.x| >= BLOCK_NX_MIN, below the + armholes) — the saturated-color-block default lives in the tints. + +Painted albedo: flat per-region luminance + woven noise + dark stitch lines on +region boundaries (toon-friendly; identity carried by the texture). A parked +logo_uv TEXCOORD_1 layer ships for channel consistency (not logo-capable). + +Per-body mode only (offset shells author per body, Q-060): + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_swimsuit.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/swimsuit_onepiece \ + [--bodies average_m,child,...] [--offset 0.012] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house +wardrobe), Q-060 (per-body offset shells). +""" + +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# Make the sibling modules importable when Blender runs this file directly. +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import blender_author_offset_shell as base # noqa: E402 +import blender_author_offset_coverall as cov # noqa: E402 (per-texel rasterizer) + +log = base.log +FRONT = base.FRONT_Y_SIGN # bodies face -Y (verified in base) + +# -------------------------------------------------------------------------- +# Parameters +# -------------------------------------------------------------------------- + +COVERED_SEGMENTS = [ + "seg_torso", "seg_torso_upper", "seg_hips", + # cut stock only — welded in so the hips|leg seam teeth become interior, + # then everything below the high-cut line is deleted. + "seg_leg_upper_l", "seg_leg_upper_r", +] + +WELD_DIST = 5e-4 # boundary weld tolerance (0.5 mm, denim practice) +MIN_LOOP_VERTS = 6 # ignore sliver boundary loops when measuring rings + +# Straps — proportional corridor over the shoulder top, clamped per body +# against the measured neck ring (outside it) and armscye ring (inside it). +STRAP_X0_FRAC = 0.52 # inner strap edge, fraction of shoulder |x| +STRAP_X1_FRAC = 0.74 # outer strap edge +STRAP_MIN_W_FRAC = 0.10 # minimum acceptable strap width (of shoulder |x|) +NECK_GUARD_M = 0.004 # keep this far outside the neck-seam ring +ARM_GUARD_M = 0.004 # keep this far inside the armscye ring + +# Necklines — fractions of the spine_01->neck_01 span above spine_01 head. +FRONT_SCOOP_FRAC = 0.66 # front neckline (covers the seg_torso top band) +BACK_SCOOP_FRAC = 0.54 # back scoop, slightly deeper +ARMHOLE_DROP_M_REF = 0.015 # armhole plane below the measured armscye min z + +# High-cut leg openings — all spans derive from (thigh head z − crotch z). +GUSSET_HALF_FRAC = 0.50 # crotch gusset half-width, fraction of thigh |x| +LEG_OUT_X_FRAC = 1.55 # |x| where the rise reaches its max (of thigh |x|) +LEG_RISE_FRONT_FRAC = 1.00 # front/side rise: up to the thigh-head line +LEG_RISE_BACK_FRAC = 0.58 # back rise: lower, keeps the seat covered +LEG_RISE_POW = 1.35 # >1 = convex sweep (classic high-cut) +LEG_YBLEND_FRAC = 0.35 # front/back blend half-width (of thigh |x|) +GUSSET_DROP_M_REF = 0.004 # gusset cut sits below the crotch -> never opens + +# Region mask / painted albedo. +TRIM_W_M_REF = 0.016 # edge-trim band width along the openings +BLOCK_NX_MIN = 0.74 # side panel: |normal.x| threshold (normal-gated) +ALBEDO_LUMA = {"body": 0.62, "trim": 0.56, "block": 0.67} +STITCH_LUMA = 0.30 +ALBEDO_NOISE = 0.02 +NOISE_SEED = 3089 + +LABELS = ["body", "trim", "block"] +LABEL_ID = {name: i for i, name in enumerate(LABELS)} +LABEL_RGBA_ARR = np.array( + [ + (0.0, 1.0, 0.0, 0.0), # body -> G + (1.0, 0.0, 0.0, 0.0), # trim -> R (straps + edge trim) + (0.0, 0.0, 1.0, 0.0), # block -> B (side color-block) + ], + dtype=np.float32, +) +LABEL_LUMA_ARR = np.array([ALBEDO_LUMA[n] for n in LABELS], dtype=np.float32) + +_REF_SHOULDER_X = 0.1919 # average_m upperarm head |x| (same anchor as base) + + +# -------------------------------------------------------------------------- +# Per-body landmarks + measured rings +# -------------------------------------------------------------------------- + +class SuitLandmarks: + """Cut/mask parameters from one body's bones, crotch probe and rings.""" + + def __init__(self, armature, crotch_z): + bones = armature.data.bones + + def bone(name): + b = bones.get(name) + if b is None: + raise RuntimeError(f"landmark bone {name} missing") + return b + + ua_l, ua_r = bone("upperarm_l"), bone("upperarm_r") + self.shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 + self.scale = self.shoulder_x / _REF_SHOULDER_X + neck = bone("neck_01") + spine01 = bone("spine_01") + span = neck.head_local.z - spine01.head_local.z + self.scoop_front = spine01.head_local.z + FRONT_SCOOP_FRAC * span + self.scoop_back = spine01.head_local.z + BACK_SCOOP_FRAC * span + + thigh = bone("thigh_l") + self.thigh_x = abs(thigh.head_local.x) + self.thigh_z = thigh.head_local.z + self.crotch_z = crotch_z + self.rise_span = self.thigh_z - self.crotch_z + self.gx = GUSSET_HALF_FRAC * self.thigh_x + self.out_x = LEG_OUT_X_FRAC * self.thigh_x + self.yb = LEG_YBLEND_FRAC * self.thigh_x + self.gusset_drop = GUSSET_DROP_M_REF * self.scale + self.trim_w = TRIM_W_M_REF * self.scale + + # Filled by apply_ring_measurements(): + self.sx0 = STRAP_X0_FRAC * self.shoulder_x + self.sx1 = STRAP_X1_FRAC * self.shoulder_x + self.z_arm = self.scoop_front # placeholder until rings are measured + + def apply_ring_measurements(self, neck_max_ax, arm_min_ax, arm_min_z): + self.sx0 = max(STRAP_X0_FRAC * self.shoulder_x, + neck_max_ax + NECK_GUARD_M * self.scale) + self.sx1 = min(STRAP_X1_FRAC * self.shoulder_x, + arm_min_ax - ARM_GUARD_M * self.scale) + min_w = STRAP_MIN_W_FRAC * self.shoulder_x + if self.sx1 - self.sx0 < min_w: + log(f"WARNING: strap corridor pinched " + f"({(self.sx1 - self.sx0) * 1000:.1f} mm) — widening inward") + self.sx0 = max(neck_max_ax + NECK_GUARD_M * self.scale, + self.sx1 - min_w) + self.z_arm = arm_min_z - ARMHOLE_DROP_M_REF * self.scale + log(f"landmarks: straps |x|=[{self.sx0:.3f},{self.sx1:.3f}] " + f"scoop_f={self.scoop_front:.3f} scoop_b={self.scoop_back:.3f} " + f"z_arm={self.z_arm:.3f} crotch={self.crotch_z:.3f} " + f"thigh_z={self.thigh_z:.3f} gusset<|x|<{self.gx:.3f} " + f"trim={self.trim_w * 1000:.1f}mm") + + # ---- analytic surfaces (numpy-vectorised; scalars work too) ---------- + + def z_leg(self, x, y): + """High-cut leg-opening surface: gusset floor below the crotch, + rising outward to the thigh-head line; back rises less (seat).""" + t = np.clip((np.abs(x) - self.gx) / max(self.out_x - self.gx, 1e-6), + 0.0, 1.0) ** LEG_RISE_POW + f = np.clip((np.asarray(y) * FRONT + self.yb) / (2.0 * self.yb), + 0.0, 1.0) + rise = self.rise_span * ( + LEG_RISE_BACK_FRAC + (LEG_RISE_FRONT_FRAC - LEG_RISE_BACK_FRAC) * f) + return (self.crotch_z - self.gusset_drop) + t * rise + + def scoop(self, y): + """Neckline level: front scoop on the front side, back scoop behind.""" + return np.where(np.asarray(y) * FRONT > 0.0, + self.scoop_front, self.scoop_back) + + +def probe_crotch(body_dir): + """Import seg_hips alone to measure the crotch (its lowest point) exactly + (denim practice — the joined mesh's min z is the knee cut stock).""" + base.clear_scene() + objs = base.import_glb(os.path.join(body_dir, "seg_hips.glb")) + zs = [] + for o in objs: + if base.is_body_mesh(o): + zs.extend(v.co.z for v in o.data.vertices) + if not zs: + raise RuntimeError("seg_hips.glb yielded no skinned mesh") + return min(zs) + + +# -------------------------------------------------------------------------- +# Geometry: weld, ring measurement, cuts, rim flattening +# -------------------------------------------------------------------------- + +def weld_boundaries(shell): + """Merge coincident segment-boundary verts so the offset can't open cracks + (weights/UVs identical on coincident verts, so skinning is unaffected).""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + before = len(bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=WELD_DIST) + bm.to_mesh(me) + bm.free() + me.update() + log(f"welded segment boundaries: {before} -> {len(me.vertices)} verts") + + +def _boundary_loops(bm): + """Connected open-boundary loops as lists of vert indices.""" + adj = {} + for e in bm.edges: + if len(e.link_faces) != 1: + continue + a, b = e.verts[0].index, e.verts[1].index + adj.setdefault(a, set()).add(b) + adj.setdefault(b, set()).add(a) + seen = set() + loops = [] + for start in adj: + if start in seen: + continue + stack, comp = [start], [] + while stack: + v = stack.pop() + if v in seen: + continue + seen.add(v) + comp.append(v) + stack.extend(adj[v] - seen) + loops.append(comp) + return loops + + +def measure_rings(shell, lm): + """Locate the neck-seam and armscye boundary rings on the welded shell. + + Expected loops: neck ring, 2 armscye rings, 2 knee rings (cut stock; + median z below the crotch — ignored). Sliver loops (< MIN_LOOP_VERTS) + are skipped. Returns (neck_max_ax, arm_min_ax, arm_min_z). + """ + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + candidates = [] + for comp in _boundary_loops(bm): + if len(comp) < MIN_LOOP_VERTS: + continue + zs = sorted(bm.verts[i].co.z for i in comp) + med_z = zs[len(zs) // 2] + if med_z < lm.crotch_z: + continue # knee ring on the leg cut stock + axs = [abs(bm.verts[i].co.x) for i in comp] + candidates.append({ + "med_ax": sorted(axs)[len(axs) // 2], + "max_ax": max(axs), + "min_ax": min(axs), + "min_z": min(bm.verts[i].co.z for i in comp), + "n": len(comp), + }) + bm.free() + if len(candidates) < 3: + raise RuntimeError( + f"expected neck + 2 armscye rings, found {len(candidates)}") + candidates.sort(key=lambda c: c["med_ax"]) + neck = candidates[0] + arms = candidates[-2:] + arm_min_ax = min(a["min_ax"] for a in arms) + arm_min_z = min(a["min_z"] for a in arms) + log(f"rings: neck max|x|={neck['max_ax']:.3f} ({neck['n']}v) " + f"armscye min|x|={arm_min_ax:.3f} min z={arm_min_z:.3f}") + return neck["max_ax"], arm_min_ax, arm_min_z + + +def suit_cuts(shell, lm): + """Delete everything the swimsuit doesn't cover: + - neckline scoops between the straps (|x| < sx0, z above scoop level) + - armholes outside the straps (|x| > sx1, z above the armhole plane) + - legs below the analytic high-cut surface z_leg(x, y) + The strap corridor (sx0 <= |x| <= sx1) survives over the shoulder.""" + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + doomed = [] + for v in bm.verts: + x, y, z = v.co.x, v.co.y, v.co.z + ax = abs(x) + if z < float(lm.z_leg(x, y)): + doomed.append(v) + elif ax < lm.sx0 and z > float(lm.scoop(y)): + doomed.append(v) + elif ax > lm.sx1 and z > lm.z_arm: + doomed.append(v) + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"suit cuts removed {len(doomed)} verts; " + f"{len(shell.data.vertices)} remain") + + +def flatten_rims(shell, lm): + """Pull every open rim onto its analytic line (post-offset, denim + practice): leg rims -> z_leg surface, scoop rims -> scoop plane, armhole + rims -> z_arm plane, strap side edges -> |x| snapped to the strap planes. + Only boundary verts move; weights/UVs ride along. + + The leg/top split plane sits MID-GAP between the thigh line (the leg + rims' analytic maximum) and the lowest top opening (armhole plane or + back scoop) — a dead zone with no legitimate boundary verts. A tight + margin (thigh_z + 2 cm) is NOT enough: the 12 mm outward offset runs + before flattening and drifts the high-cut side-apex verts upward, and on + thin_f two thigh-weighted apex verts crossed it, were classified as + armhole rim and teleported ~36 cm up to z_arm — QA showed them as sliver + spikes off the seat in deep Crouch_Fwd (worst vert-from-centroid 294 mm).""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + snap_eps = 0.006 * lm.scale + leg_z_max = 0.5 * (lm.thigh_z + min(lm.z_arm, lm.scoop_back)) + counts = {"leg": 0, "scoop": 0, "armhole": 0, "strap": 0} + boundary = set() + for e in bm.edges: + if len(e.link_faces) == 1: + boundary.update(v.index for v in e.verts) + for i in boundary: + v = bm.verts[i] + x, y, z = v.co.x, v.co.y, v.co.z + ax = abs(x) + if z < leg_z_max: + v.co.z = float(lm.z_leg(x, y)) + counts["leg"] += 1 + elif ax < lm.sx0 - snap_eps: + v.co.z = float(lm.scoop(y)) + counts["scoop"] += 1 + elif ax > lm.sx1 + snap_eps: + v.co.z = lm.z_arm + counts["armhole"] += 1 + else: + edge = lm.sx0 if abs(ax - lm.sx0) <= abs(ax - lm.sx1) else lm.sx1 + v.co.x = edge if x >= 0.0 else -edge + counts["strap"] += 1 + bm.to_mesh(me) + bm.free() + me.update() + log("flattened rims: " + " ".join(f"{k}={v}" for k, v in counts.items())) + + +# -------------------------------------------------------------------------- +# Region classification (per texel — interpolated position + normal) +# -------------------------------------------------------------------------- + +def classify_texels(pos, nrm, lm): + """Classify N texels. pos/nrm are (N,3) body-local arrays. Returns (N,) + uint8 label ids. Precedence: trim (straps + opening edges), block, body.""" + x, y, z = pos[:, 0], pos[:, 1], pos[:, 2] + ax = np.abs(x) + tw = lm.trim_w + scoop = lm.scoop(y) + zl = lm.z_leg(x, y) + + strap = (ax >= lm.sx0 - 0.5 * tw) & (ax <= lm.sx1 + 0.5 * tw) \ + & (z >= scoop - tw) + scoop_trim = (ax < lm.sx0) & (z >= scoop - tw) + arm_trim = (ax > lm.sx1) & (z >= lm.z_arm - tw) + leg_trim = z <= zl + tw + trim = strap | scoop_trim | arm_trim | leg_trim + + block = (np.abs(nrm[:, 0]) >= BLOCK_NX_MIN) & (z <= lm.z_arm - tw) + + lab = np.full(x.shape, LABEL_ID["body"], dtype=np.uint8) + lab[block] = LABEL_ID["block"] + lab[trim] = LABEL_ID["trim"] + return lab + + +def bake_mask_and_albedo(shell, lm, mask_path, albedo_name, seed): + """One pass over the faces (pre-solidify — exactly one face per texel): + bake the RGBA region mask AND the painted albedo (flat per-region + luminance + stitch lines on region boundaries + woven noise).""" + W = H = base.MASK_SIZE + mask = np.zeros((H, W, 4), dtype=np.float32) + mask[:, :, 1] = 1.0 # green background = body (bilinear-bleed safe) + albedo = np.zeros((H, W, 4), dtype=np.float32) + albedo[:, :, 0:3] = ALBEDO_LUMA["body"] + albedo[:, :, 3] = 1.0 + label_map = np.full((H, W), LABEL_ID["body"], dtype=np.uint8) + covered = np.zeros((H, W), dtype=bool) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + bm.normal_update() + uv_layer = bm.loops.layers.uv.active + if uv_layer is None: + raise RuntimeError("no active UV layer for bake") + + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv for loop in loops] + pos = [loop.vert.co for loop in loops] + nrm = [loop.vert.normal for loop in loops] + for i in range(1, len(loops) - 1): + tri = (0, i, i + 1) + ys, xs, w0, w1, w2 = cov._tri_texels( + uvs[tri[0]], uvs[tri[1]], uvs[tri[2]], W, H) + if ys.size == 0: + continue + p = np.empty((ys.size, 3), dtype=np.float32) + n = np.empty((ys.size, 3), dtype=np.float32) + for axis in range(3): + p[:, axis] = (w0 * pos[tri[0]][axis] + w1 * pos[tri[1]][axis] + + w2 * pos[tri[2]][axis]) + n[:, axis] = (w0 * nrm[tri[0]][axis] + w1 * nrm[tri[1]][axis] + + w2 * nrm[tri[2]][axis]) + n /= np.maximum(np.linalg.norm(n, axis=1, keepdims=True), 1e-9) + lab = classify_texels(p, n, lm) + label_map[ys, xs] = lab + covered[ys, xs] = True + mask[ys, xs] = LABEL_RGBA_ARR[lab] + albedo[ys, xs, 0:3] = LABEL_LUMA_ARR[lab][:, None] + bm.free() + total = max(int(covered.sum()), 1) + tex_counts = np.bincount(label_map[covered], minlength=len(LABELS)) + log("region texels: " + " ".join( + f"{LABELS[i]}={int(c)} ({100.0 * c / total:.1f}%)" + for i, c in enumerate(tex_counts) if c > 0)) + + # Woven-feel noise over the fills, before stitch lines (lines stay crisp). + rng = np.random.default_rng(seed) + noise = (rng.random((H, W, 1), dtype=np.float32) - 0.5) * 2.0 * ALBEDO_NOISE + albedo[:, :, 0:3] = np.clip(albedo[:, :, 0:3] + noise, 0.0, 1.0) + + # Stitch lines: label transitions where BOTH texels belong to rasterised + # geometry (skipping UV-island borders against background). + edge = np.zeros((H, W), dtype=bool) + dh = (label_map[:, 1:] != label_map[:, :-1]) & covered[:, 1:] & covered[:, :-1] + edge[:, 1:] |= dh + edge[:, :-1] |= dh + dv = (label_map[1:, :] != label_map[:-1, :]) & covered[1:, :] & covered[:-1, :] + edge[1:, :] |= dv + edge[:-1, :] |= dv + albedo[edge, 0:3] = STITCH_LUMA + log(f"stitch lines on {int(edge.sum())} boundary texels") + + # Alpha floor 2/255: keeps Godot's fix_alpha_border import pass a no-op + # (channel-packed region data, not transparency — coverall lesson). + mask[:, :, 3] = np.maximum(mask[:, :, 3], 2.0 / 255.0) + + img_mask = bpy.data.images.new(f"mask_{albedo_name}", W, H, alpha=True) + img_mask.alpha_mode = 'CHANNEL_PACKED' + img_mask.pixels.foreach_set(mask.reshape(-1)) + img_mask.update() + img_mask.filepath_raw = mask_path + img_mask.file_format = 'PNG' + img_mask.save() + log(f"baked region mask -> {mask_path}") + + img_albedo = bpy.data.images.new(f"albedo_{albedo_name}", W, H, alpha=False) + img_albedo.pixels.foreach_set(albedo.reshape(-1)) + img_albedo.update() + return img_albedo + + +# -------------------------------------------------------------------------- +# Parked logo UV2 (not logo-capable; the shader still samples UV2) +# -------------------------------------------------------------------------- + +def author_parked_uv2(shell): + me = shell.data + while len(me.uv_layers) > 1: + me.uv_layers.remove(me.uv_layers[-1]) + me.uv_layers.new(name="logo_uv") + me.uv_layers.active = me.uv_layers[0] + bm = bmesh.new() + bm.from_mesh(me) + uvl = bm.loops.layers.uv.get("logo_uv") + for face in bm.faces: + for loop in face.loops: + loop[uvl].uv = (2.0, 2.0) + bm.to_mesh(me) + bm.free() + me.update() + log("logo UV2 authored fully parked (not logo-capable)") + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def purge_strays(shell, armature): + """Drop authoring-scene leftovers before export (defensive hygiene). + + Investigated for T-1089 wave 2: the 'Icosphere' seen when re-importing + any garment GLB is NOT file content — the raw glTF JSON contains exactly + one mesh (the shell). It is a bone-display widget the Blender IMPORTER + fabricates (bone_heuristic), which also leaks one such object into the + authoring scene per body-segment import session (base.clear_scene()'s + bpy.ops select_all can miss it). Exports were never polluted + (use_selection holds), but removing every object that is not the shell + or its armature keeps each per-body authoring cycle hermetic. Bone + custom_shape references are cleared for the same reason (display-only, + no glTF effect).""" + cleared = 0 + for pb in armature.pose.bones: + if pb.custom_shape is not None: + pb.custom_shape = None + cleared += 1 + strays = [o for o in bpy.data.objects if o not in (shell, armature)] + for o in strays: + bpy.data.objects.remove(o, do_unlink=True) + if cleared or strays: + log(f"purged {len(strays)} stray objects, cleared {cleared} bone " + f"custom shapes before export") + + +def author_swimsuit(body_dir, out_dir, body, offset, seed): + crotch_z = probe_crotch(body_dir) + + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS # build_covered_mesh reads this + shell, armature = base.build_covered_mesh(body_dir) + weld_boundaries(shell) + + lm = SuitLandmarks(armature, crotch_z) + lm.apply_ring_measurements(*measure_rings(shell, lm)) + + suit_cuts(shell, lm) + base.offset_outward(shell, offset) + flatten_rims(shell, lm) + + # Bake + UV2 BEFORE solidify: the inner shell duplicates every face with + # the same atlas UVs but flipped normals — pre-solidify there is exactly + # one face per texel (coverall lesson; the block region is normal-gated). + author_parked_uv2(shell) + albedo_img = bake_mask_and_albedo( + shell, lm, os.path.join(out_dir, f"{body}_mask.png"), body, seed) + + base.solidify(shell, base.CLOTH_THICKNESS_M) + base.assign_fabric_material(shell, albedo_img) + + # Shared-name albedo before export so the GLB-embedded texture extracts + # to the _base_albedo.png convention (coverall lesson). + base.save_albedo_sidecar(albedo_img, os.path.join(out_dir, "base_albedo.png")) + purge_strays(shell, armature) + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + shutil.copy2(os.path.join(out_dir, "base_albedo.png"), + os.path.join(out_dir, f"{body}_base_albedo.png")) + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] [--offset M]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = base.PER_BODY_OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"swimsuit per-body mode: {len(bodies)} bodies, " + f"offset {offset * 1000:.0f} mm") + + results = [] + for i, body in enumerate(bodies): + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_swimsuit(body_dir, out_dir, body, offset, seed=NOISE_SEED + i) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + # Runtime fallback + shared sidecars mirror the reference body. + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_tank_top.py b/tooling/garment-fit/blender_author_tank_top.py new file mode 100644 index 000000000..cb2b50e3e --- /dev/null +++ b/tooling/garment-fit/blender_author_tank_top.py @@ -0,0 +1,565 @@ +""" +blender_author_tank_top.py (T-1089 wave 2, tank_top — per-body offset shell) + +Sleeveless tee authored per body via the offset-shell route. Imports +blender_author_offset_shell.py as the shared library (scene build, join, +offset, solidify, logo UV2, export), blender_author_denim_pants.py for the +required boundary-WELD practice, and blender_author_buttondown.py for the +per-pixel bake machinery (_gather_tris/_tri_cover/_interp_pos). What the tank +adds beyond the tee base, as reusable parameters: + + * coverage is torso + torso_upper ONLY — no arm segments. The armhole is a + real CUT at the shoulder: verts outboard of the strap (|x| > strap_out) + and above the underarm plane are deleted, leaving a shoulder strap between + the neck scoop and the armhole. + * scoop NECKLINE cut — front scoop lower than the back, blended smoothly + across the +-y transition, both guaranteed below the jagged natural neck + ring (the segment splitter's 4.5-7.6 cm teeth) so no natural boundary + survives except the hem. + * ring-swallowing thresholds — the natural neck ring and shoulder/arm rings + are MEASURED per body (open-boundary probe after weld) and the strap / + scoop / underarm cut planes are clamped so every jagged ring vert falls in + the deleted zone. Proportional defaults derive from bone landmarks exactly + like base.derive_thresholds. + * open-rim treatment (denim practice, adapted): the hem ring is FLATTENED to + a clean plane (the ring's deepest tooth, so the hem overlaps a pants + waistband); the scoop + armhole cut edges are Laplacian-SMOOTHED along the + boundary loops into fair curves (a plane can't represent a curved scoop). + * region mask is distance-to-opening based: texels within TRIM_W of the + neckline edge -> collar band (R), within TRIM_W of an armhole edge -> + armhole trim (B), everything else -> body (G). The painted albedo shares + the same distance fields (binding bands + stitch lines + hem stitch), so + mask and albedo always agree. + * logo-capable chest UV2 — base.author_logo_uv with the chest box rescaled + (LOGO_SCALE, aspect preserved) and dropped below the front scoop so the + decal never crosses the neckline. + +Region convention (spec): collar band=R (tint_0), body=G (tint_1), +armhole trim=B (tint_2). A unused. Bright default tints live in the manifest; +the albedo is a bright neutral so tints carry the colour (style pin: modern, +texture carries identity, flat toon-friendly). + +Run (per-body only — offset shells author per body, Q-060): + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_tank_top.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/tank_top \ + [--bodies average_m,child,...] [--offset 0.012] \ + [--front-scoop-frac 0.28] [--strap-out-frac 0.80] [--trim-w 0.020] + +Writes per body: /.glb + /_mask.png + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060 +(per-body authoring for offset shells). +""" + +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import blender_author_offset_shell as base # noqa: E402 +import blender_author_buttondown as bdn # noqa: E402 (per-pixel bake helpers) +import blender_author_denim_pants as denim # noqa: E402 (boundary weld) + +# -------------------------------------------------------------------------- +# Garment parameters (average_m metres; scaled per body by bone landmarks) +# -------------------------------------------------------------------------- + +COVERED_SEGMENTS = ["seg_torso", "seg_torso_upper"] + +OFFSET_M = 0.012 # snug per-body standoff (Q-060 ideal) +CLOTH_THICKNESS_M = 0.004 # jersey-weight cloth, same as the tee + +# Cut proportions. x fractions are of shoulder |x| (upperarm head); scoop +# drops are fractions of the spine_01->neck_01 span — the same landmark +# language as base.derive_thresholds. +STRAP_IN_FRAC = 0.54 # inner strap edge |x| +STRAP_OUT_FRAC = 0.80 # outer strap edge |x| (armhole starts here) +FRONT_SCOOP_DROP_FRAC = 0.28 # front neckline below the neck head +BACK_SCOOP_DROP_FRAC = 0.13 # back neckline below the neck head +MIN_STRAP_W_M = 0.024 # never squeeze the strap narrower than this +RING_MARGIN_M = 0.008 # clearance under/inside a measured jagged ring +SCOOP_BLEND_Y_M = 0.020 # front->back scoop height blend half-width + +# Open-rim treatment. +SMOOTH_ITERS = 12 # Laplacian passes on scoop/armhole boundary loops +SMOOTH_LAM = 0.5 +HEM_RING_SPAN_FRAC = 0.15 # boundary verts below spine_lo + f*span = hem ring + +# Painted trim (distances measured to the opening edges, post-offset). +TRIM_W_M = 0.020 # collar / armhole binding band width +STITCH_W_M = 0.0028 # stitch line half-width +HEM_STITCH_UP_M = 0.010 # hem stitch line height above the hem plane +LOGO_SCALE = 0.85 # chest box rescale (aspect preserved) +LOGO_TOP_GAP_M = 0.020 # logo box top below the front scoop + +MASK_SIZE = 512 +ALBEDO_SIZE = 1024 + +# Bright neutral jersey — the manifest default tints carry the actual colour. +FABRIC_RGB = (0.70, 0.71, 0.73) +FABRIC_NOISE = 0.025 +TRIM_MUL = 0.90 # binding bands read slightly denser than the body +STITCH_MUL = 0.55 # dark stitch lines +ALBEDO_SEED = 1094 # deterministic, distinct from other garments + + +def log(msg): + print(f"[tank-top] {msg}") + + +# -------------------------------------------------------------------------- +# Landmarks + ring probe + cut parameter derivation +# -------------------------------------------------------------------------- + +class TankLandmarks: + """Bone landmarks the proportional parameters scale from.""" + + def __init__(self, armature): + bones = armature.data.bones + ua_l = bones.get("upperarm_l") + ua_r = bones.get("upperarm_r") + neck = bones.get("neck_01") + spine01 = bones.get("spine_01") + if not all([ua_l, ua_r, neck, spine01]): + raise RuntimeError("landmark bones missing — not the 65-bone rig?") + self.shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 + self.neck_z = neck.head_local.z + self.spine_lo = spine01.head_local.z + self.span = self.neck_z - self.spine_lo + self.scale = self.shoulder_x / base._REF_SHOULDER_X + log(f"landmarks: shoulder_x={self.shoulder_x:.4f} neck_z={self.neck_z:.4f} " + f"spine_lo={self.spine_lo:.4f} span={self.span:.4f} scale={self.scale:.3f}") + + +def probe_rings(shell, lm): + """Measure the natural open-boundary rings of the welded torso shell. + + The segment splitter cuts along weight thresholds, so all three natural + boundaries (neck ring, arm rings, hem ring) are jagged teeth. The cut + thresholds below are clamped so the neck + arm rings fall entirely inside + the deleted zone; the hem ring is flattened to a plane instead. + """ + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bverts = set() + for e in bm.edges: + if len(e.link_faces) == 1: + bverts.update(v.index for v in e.verts) + hem_test_z = lm.spine_lo + HEM_RING_SPAN_FRAC * lm.span + hem, neck, arm = [], [], [] + for i in bverts: + co = bm.verts[i].co + if co.z < hem_test_z: + hem.append(co.copy()) + elif abs(co.x) < 0.5 * lm.shoulder_x: + neck.append(co.copy()) + else: + arm.append(co.copy()) + bm.free() + if not hem or not neck or not arm: + raise RuntimeError( + f"ring probe incomplete: hem={len(hem)} neck={len(neck)} arm={len(arm)}") + rings = { + "hem_min_z": min(c.z for c in hem), + "hem_max_z": max(c.z for c in hem), + "hem_test_z": hem_test_z, + "neck_max_ax": max(abs(c.x) for c in neck), + "neck_min_z": min(c.z for c in neck), + "arm_min_ax": min(abs(c.x) for c in arm), + "arm_min_z": min(c.z for c in arm), + } + log(f"rings: hem n={len(hem)} z {rings['hem_min_z']:.3f}..{rings['hem_max_z']:.3f}; " + f"neck n={len(neck)} |x|<={rings['neck_max_ax']:.3f} z>={rings['neck_min_z']:.3f}; " + f"arm n={len(arm)} |x|>={rings['arm_min_ax']:.3f} z>={rings['arm_min_z']:.3f}") + return rings + + +def derive_cut(lm, rings): + """Cut planes from proportional defaults, clamped to swallow the rings.""" + strap_out = min(STRAP_OUT_FRAC * lm.shoulder_x, + rings["arm_min_ax"] - RING_MARGIN_M) + strap_in = max(STRAP_IN_FRAC * lm.shoulder_x, + rings["neck_max_ax"] + RING_MARGIN_M) + min_w = MIN_STRAP_W_M * lm.scale + if strap_out - strap_in < min_w: + squeezed = strap_out - min_w + floor = rings["neck_max_ax"] + 0.004 + if squeezed < floor: + log(f"WARNING: strap squeezed against the neck ring " + f"(in={squeezed:.3f} floor={floor:.3f}) — using floor") + squeezed = floor + strap_in = squeezed + back_scoop = min(lm.neck_z - BACK_SCOOP_DROP_FRAC * lm.span, + rings["neck_min_z"] - RING_MARGIN_M) + front_scoop = min(lm.neck_z - FRONT_SCOOP_DROP_FRAC * lm.span, back_scoop) + armhole_z = rings["arm_min_z"] - RING_MARGIN_M + params = { + "strap_in": strap_in, + "strap_out": strap_out, + "strap_mid": 0.5 * (strap_in + strap_out), + "front_scoop": front_scoop, + "back_scoop": back_scoop, + "armhole_z": armhole_z, + "hem_plane": rings["hem_min_z"], + "hem_test_z": rings["hem_test_z"], + } + log(f"cut: strap |x| {strap_in:.3f}..{strap_out:.3f}, scoop front {front_scoop:.3f} " + f"back {back_scoop:.3f}, armhole z>{armhole_z:.3f}, hem plane {params['hem_plane']:.3f}") + return params + + +# -------------------------------------------------------------------------- +# Geometry: tank cut + hem flatten + boundary smoothing +# -------------------------------------------------------------------------- + +def _scoop_z(y, params): + """Neckline height at this y — front scoop blended into the back scoop.""" + t = (y * base.FRONT_Y_SIGN) / SCOOP_BLEND_Y_M * 0.5 + 0.5 + t = min(max(t, 0.0), 1.0) + return params["back_scoop"] + (params["front_scoop"] - params["back_scoop"]) * t + + +def tank_cut(shell, params): + """Delete the neck scoop and the armholes. + + Neck zone: |x| < strap_in AND z above the (front/back blended) scoop. + Armhole zone: |x| > strap_out AND z above the underarm plane. + Both thresholds were clamped so the jagged natural neck/arm rings fall + entirely inside the deleted zone — the only natural boundary that + survives is the hem ring. + """ + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + doomed = [] + for v in bm.verts: + x, y, z = v.co.x, v.co.y, v.co.z + ax = abs(x) + if ax < params["strap_in"] and z > _scoop_z(y, params): + doomed.append(v) + elif ax > params["strap_out"] and z > params["armhole_z"]: + doomed.append(v) + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"tank cut removed {len(doomed)} verts; {len(shell.data.vertices)} remain") + + +def flatten_hem(shell, params): + """Pull the hem ring's jagged teeth onto one clean plane (denim practice). + + The plane sits at the ring's DEEPEST tooth, so the straightened hem keeps + overlapping a pants waistband instead of retreating to the shallowest + notch (a tank tucks over the waist; extra length is correct here). + """ + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + hem_idx = set() + for e in bm.edges: + if len(e.link_faces) == 1: + for v in e.verts: + if v.co.z < params["hem_test_z"]: + hem_idx.add(v.index) + for i in hem_idx: + bm.verts[i].co.z = params["hem_plane"] + bm.to_mesh(me) + bm.free() + me.update() + log(f"flattened hem ring: {len(hem_idx)} verts -> z={params['hem_plane']:.3f}") + + +def smooth_open_rims(shell, params, iters=None, lam=SMOOTH_LAM): + """Laplacian-relax the scoop + armhole boundary loops into fair curves. + + Vertex-deletion cuts leave sawtooth edges at mesh resolution; a plane + flatten (denim) can't express a curved scoop, so each boundary vert is + repeatedly pulled toward the midpoint of its two loop neighbours. Hem + verts (already on their plane) are pinned; interior verts never move, so + weights/UVs are untouched. + """ + iters = SMOOTH_ITERS if iters is None else iters + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + nbr = {} + for e in bm.edges: + if len(e.link_faces) == 1: + a, b = e.verts + nbr.setdefault(a.index, []).append(b.index) + nbr.setdefault(b.index, []).append(a.index) + hem_lim = params["hem_plane"] + 0.001 + movable = [i for i, ns in nbr.items() + if len(ns) == 2 and bm.verts[i].co.z > hem_lim] + for _ in range(iters): + moved = {} + for i in movable: + n1, n2 = nbr[i] + mid = (bm.verts[n1].co + bm.verts[n2].co) * 0.5 + moved[i] = bm.verts[i].co.lerp(mid, lam) + for i, co in moved.items(): + bm.verts[i].co = co + bm.to_mesh(me) + bm.free() + me.update() + log(f"smoothed {len(movable)} scoop/armhole rim verts ({iters} passes)") + + +def collect_trim_edges(shell, params): + """Opening-edge vert positions (post-offset) for the distance-based trim. + + Returns (neck_pts, arm_pts) float32 arrays. Hem verts are excluded — the + hem gets a painted stitch line, not a tinted band (spec: 3 regions). + """ + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + neck, arm = [], [] + seen = set() + for e in bm.edges: + if len(e.link_faces) != 1: + continue + for v in e.verts: + if v.index in seen: + continue + seen.add(v.index) + if v.co.z < params["hem_test_z"]: + continue + if abs(v.co.x) < params["strap_mid"]: + neck.append(v.co[:]) + else: + arm.append(v.co[:]) + bm.free() + log(f"trim edges: neckline {len(neck)} verts, armholes {len(arm)} verts") + return (np.array(neck, dtype=np.float32), + np.array(arm, dtype=np.float32)) + + +# -------------------------------------------------------------------------- +# Per-pixel bake: region mask + painted albedo share the distance fields +# -------------------------------------------------------------------------- + +def _min_dist(pos, pts): + """Min euclidean distance from each row of pos (N,3) to the set pts (K,3).""" + if pts.size == 0: + return np.full(pos.shape[0], np.inf, dtype=np.float32) + d2 = ((pos[:, None, :] - pts[None, :, :]) ** 2).sum(axis=2) + return np.sqrt(d2.min(axis=1)) + + +def _classify_px(dn, da, trim_w): + """One-hot RGBA rows: collar band R / armhole trim B / body G.""" + n = dn.shape[0] + rgba = np.zeros((n, 4), dtype=np.float32) + is_r = (dn < trim_w) & (dn <= da) + is_b = (da < trim_w) & (da < dn) + rgba[:, 1] = 1.0 + rgba[is_r] = (1.0, 0.0, 0.0, 0.0) + rgba[is_b] = (0.0, 0.0, 1.0, 0.0) + return rgba + + +def _paint_px(pos, rows, dn, da, dims): + """Painted albedo: binding bands, band stitch lines, hem stitch (in-place).""" + mul = np.ones(pos.shape[0], dtype=np.float32) + d = np.minimum(dn, da) + mul[d < dims["trim_w"]] = TRIM_MUL + mul[np.abs(d - dims["trim_w"]) < dims["stitch_w"]] = STITCH_MUL + hem_line = np.abs(pos[:, 2] - dims["hem_stitch_z"]) < dims["stitch_w"] + mul[hem_line] = STITCH_MUL + out = rows * mul[:, None] + np.clip(out, 0.0, 1.0, out) + return out + + +def bake_tank_maps(shell, lm, params, neck_pts, arm_pts, mask_path, albedo_path): + """One pass over UV0: bake _mask.png + _base_albedo.png.""" + dims = { + "trim_w": TRIM_W_M * lm.scale, + "stitch_w": STITCH_W_M * lm.scale, + "hem_stitch_z": params["hem_plane"] + HEM_STITCH_UP_M * lm.scale, + } + tris = bdn._gather_tris(shell) + + mbuf = np.zeros((MASK_SIZE, MASK_SIZE, 4), dtype=np.float32) + mbuf[:, :, 1] = 1.0 # green background = body (bilinear-bleed safe) + + rng = np.random.default_rng(ALBEDO_SEED) + fabric = np.array(FABRIC_RGB, dtype=np.float32) + noise = (rng.random((ALBEDO_SIZE, ALBEDO_SIZE, 1), dtype=np.float32) + - 0.5) * 2.0 * FABRIC_NOISE + abuf = np.clip(fabric[None, None, :] + noise, 0.0, 1.0) + + for uv_a, uv_b, uv_c, co_a, co_b, co_c in tris: + cover = bdn._tri_cover(uv_a, uv_b, uv_c, MASK_SIZE, MASK_SIZE) + if cover is not None: + pos = bdn._interp_pos(cover, co_a, co_b, co_c) + dn = _min_dist(pos, neck_pts) + da = _min_dist(pos, arm_pts) + mbuf[cover[0], cover[1], :] = _classify_px(dn, da, dims["trim_w"]) + cover = bdn._tri_cover(uv_a, uv_b, uv_c, ALBEDO_SIZE, ALBEDO_SIZE) + if cover is not None: + pos = bdn._interp_pos(cover, co_a, co_b, co_c) + dn = _min_dist(pos, neck_pts) + da = _min_dist(pos, arm_pts) + abuf[cover[0], cover[1], :] = _paint_px( + pos, abuf[cover[0], cover[1], :], dn, da, dims) + + tot = MASK_SIZE * MASK_SIZE + log("mask texels: collar={:.1f}% body={:.1f}% armhole={:.1f}%".format( + 100.0 * float((mbuf[:, :, 0] > 0.5).sum()) / tot, + 100.0 * float((mbuf[:, :, 1] > 0.5).sum()) / tot, + 100.0 * float((mbuf[:, :, 2] > 0.5).sum()) / tot)) + + def _save(buf, name, path, alpha): + arr = buf + if not alpha: + arr = np.concatenate( + [buf, np.ones(buf.shape[:2] + (1,), dtype=np.float32)], axis=2) + img = bpy.data.images.new(name, buf.shape[1], buf.shape[0], alpha=alpha) + img.pixels.foreach_set(arr.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + _save(mbuf, "tank_region_mask", mask_path, alpha=True) + log(f"baked region mask -> {mask_path}") + albedo_img = _save(abuf, "tank_base_albedo", albedo_path, alpha=False) + log(f"saved painted albedo -> {albedo_path}") + return albedo_img + + +# -------------------------------------------------------------------------- +# Author one body +# -------------------------------------------------------------------------- + +def author_tank(body_dir, out_dir, body, offset): + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + lm = TankLandmarks(armature) + + denim.weld_boundaries(shell) # required practice: weld seam rings + rings = probe_rings(shell, lm) + params = derive_cut(lm, rings) + + tank_cut(shell, params) + flatten_hem(shell, params) + smooth_open_rims(shell, params) + + base.offset_outward(shell, offset) + neck_pts, arm_pts = collect_trim_edges(shell, params) + base.solidify(shell, CLOTH_THICKNESS_M) + + # Logo UV2: chest box rescaled (aspect preserved) and dropped below the + # front scoop so the decal sits fully on cloth. + thr = base.derive_thresholds(armature) + half_w = (thr["chest_x"][1] - thr["chest_x"][0]) * LOGO_SCALE / 2.0 + box_h = (thr["chest_z"][1] - thr["chest_z"][0]) * LOGO_SCALE + top = params["front_scoop"] - LOGO_TOP_GAP_M * lm.scale + thr_logo = dict(thr) + thr_logo["chest_x"] = (-half_w, half_w) + thr_logo["chest_z"] = (top - box_h, top) + base.author_logo_uv(shell, thr_logo) + + mask_path = os.path.join(out_dir, f"{body}_mask.png") + albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png") + albedo_img = bake_tank_maps(shell, lm, params, neck_pts, arm_pts, + mask_path, albedo_path) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +# -------------------------------------------------------------------------- +# Entry +# -------------------------------------------------------------------------- + +def main(): + global FRONT_SCOOP_DROP_FRAC, STRAP_OUT_FRAC, TRIM_W_M, FABRIC_RGB + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] [--offset M] " + "[--front-scoop-frac F] [--strap-out-frac F] [--trim-w M] " + "[--base-rgb r,g,b]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--front-scoop-frac" in argv: + FRONT_SCOOP_DROP_FRAC = float(argv[argv.index("--front-scoop-frac") + 1]) + if "--strap-out-frac" in argv: + STRAP_OUT_FRAC = float(argv[argv.index("--strap-out-frac") + 1]) + if "--trim-w" in argv: + TRIM_W_M = float(argv[argv.index("--trim-w") + 1]) + if "--base-rgb" in argv: + FABRIC_RGB = tuple( + float(v) for v in argv[argv.index("--base-rgb") + 1].split(",")) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"per-body mode: {len(bodies)} bodies, offset {offset * 1000:.1f} mm, " + f"front scoop {FRONT_SCOOP_DROP_FRAC}, strap out {STRAP_OUT_FRAC}") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_tank(body_dir, out_dir, body, offset) + results.append((body, "ok")) + except Exception as exc: # noqa: BLE001 — per-body isolation + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_track_jacket.py b/tooling/garment-fit/blender_author_track_jacket.py new file mode 100644 index 000000000..8bfe1ee14 --- /dev/null +++ b/tooling/garment-fit/blender_author_track_jacket.py @@ -0,0 +1,654 @@ +""" +blender_author_track_jacket.py (T-1089 wave 2 — track_jacket, sport family) + +Authors the TRACK JACKET as per-body offset shells, reusing +blender_author_offset_shell.py as a library (scene build, segment join, +bone-ratio thresholds, offset, solidify, logo UV2, skinned GLB export). +Sport-top sibling of blender_author_outerwear.py (jacket_modern); what track +kit needs beyond that script is delivered as reusable parameters, not hacks: + + * SLEEVE STRIPES AS THEIR OWN REGION (the recolorable team stripe): two + parallel stripes run along the TOP of each sleeve from the shoulder seam + to the cuff. Stripe placement uses a per-|x| arm-axis interpolation from + the upperarm/lowerarm bone landmarks (the denim script's per-z leg-axis + technique rotated onto the arms), with constant-metric arc widths, so the + stripes stay parallel on every body. + * Region layout per spec: collar=R, body=G, sleeve stripes=B, + cuffs + hem band=A (toon_garment.gdshader channel-blends 4 tints). + * TEXEL-level combined bake (denim best practice): every UV0 triangle is + rasterized once with barycentric-interpolated 3D positions and ONE + analytic feature-field evaluation drives BOTH the painted albedo and the + region mask, so paint and regions always agree. + * Painted albedo identity (luma-carried, survives any tint): full front + ZIP (bright teeth dashes + darker placket + edge stitching) running hem + -> through the stand collar; STAND COLLAR raised look via texture + shading (base seam ring + cast-shadow band under the collar + brighter + gradient toward the collar rim — geometry stays stock); ribbed knit cuff + + hem bands with border stitch lines; stripe edge piping. + * Required bottoms/footwear practices adopted for this top + (blender_author_denim_pants.py): boundary WELD of coincident + segment-seam rings before offsetting (elbow/shoulder/chest seams offset + as one surface, no gap rings) and open-rim FLATTENING — the segment + splitter leaves ~4.5-7.6 cm jagged teeth at the torso-bottom hem ring, + and the vert-threshold wrist cut leaves jagged sleeve ends. The hem ring + is pulled UP to its own valley (clean elastic hem, no invented + coverage) with a post-solidify residue clamp; the wrist rings are pulled + OUT to the nominal cut plane (clean cuff edge over real forearm skin). + * LEFT-BREAST logo patch (logo-capable): the base UV2 chest box shifts + off-centre — the zip owns the centre line — using the outerwear ratios. + +PER-BODY ONLY (Q-060: offset shells are authored per body, never SD-fit): + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_track_jacket.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/track_jacket \ + [--bodies average_m,child,...] [--offset 0.018] [--wrist-keep 0.90] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060. +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# -------------------------------------------------------------------------- +# Import the base offset-shell module (shared machinery) +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_spec = importlib.util.spec_from_file_location( + "offset_shell_base", os.path.join(_HERE, "blender_author_offset_shell.py")) +base = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(base) + + +def log(msg): + print(f"[track-jacket] {msg}") + + +# -------------------------------------------------------------------------- +# Parameters — track_jacket. A sport-top variant should only touch this block. +# -------------------------------------------------------------------------- + +GARMENT_ID = "track_jacket" + +SEGMENTS = [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", +] + +OFFSET_M = 0.018 # sport shell drape: between the 16 mm hoodie and the + # 22 mm casual jacket (per-body construction + # guarantees body clearance either way) +CLOTH_THICKNESS_M = 0.005 # tricot/poly shell weight +WRIST_KEEP_FRAC = 0.90 # fraction of the lowerarm kept (hands show) +CUFF_BAND_FRAC = 0.18 # last fraction of the KEPT forearm = knit cuff (A) +WELD_DIST = 5e-4 # boundary-ring weld tolerance (0.5 mm) +TEX_SIZE = 1024 # albedo + mask resolution (zip teeth need > 512) + +# Stand collar (outerwear-calibrated: taller + wider than the tee band). +COLLAR_DROP_NECK_FRAC = 0.55 # collar band starts this far below the neck head +COLLAR_X_MULT = 1.15 # slightly wider than the tee band + +# Sleeve stripes — metric widths on average_m, scaled by shoulder ratio. +STRIPE_GAP_HALF_M = 0.006 # half-gap between the two stripes (about the top line) +STRIPE_W_M = 0.018 # width of each stripe +STRIPE_EDGE_W_M = 0.0035 # painted piping line at each stripe border + +# Front zip (hem -> through the collar), slimmer than jacket_modern's placket. +ZIP_HALF_FRAC = 0.022 / base._REF_SHOULDER_X # placket half-width / shoulder |x| +TEETH_HALF_W = 0.006 # zip teeth strip half-width, metres +TEETH_PERIOD = 0.024 # dash period along Z, metres +TEETH_DUTY = 0.014 # bright dash length within a period, metres + +# Hem band height as fraction of the garment span (collar_z_min - hem plane). +HEM_BAND_FRAC = 0.07 +RIB_PERIOD_M = 0.009 # knit rib period on cuff/hem bands +LINE_W_M = 0.004 # seam / border stitch line half-width + +# Collar raised-look shading. +SHADOW_H_FRAC = 0.30 # under-collar cast-shadow band height, x neck_len +COLLAR_GRAD = 0.06 # extra luma toward the collar rim (raised read) +COLLAR_REACH_MULT = 1.35 # seam/shadow lateral reach, x collar_x_abs + +# Left-breast logo patch (outerwear ratios; centre line belongs to the zip). +CHEST_X_LO_FRAC = 0.035 / base._REF_SHOULDER_X +CHEST_X_HI_FRAC = 0.115 / base._REF_SHOULDER_X +CHEST_Z_LO_FRAC = (1.30 - base._REF_SPINE_LO_Z) / (base._REF_NECK_Z - base._REF_SPINE_LO_Z) +CHEST_Z_HI_FRAC = (1.42 - base._REF_SPINE_LO_Z) / (base._REF_NECK_Z - base._REF_SPINE_LO_Z) + +# Painted-albedo luma palette (toon_garment recolors by luma: tint*(luma*1.5+0.2)). +BASE_LUMA = 0.55 # tricot shell +STRIPE_LUMA = 0.62 # stripes slightly lighter (read under same-tint too) +STRIPE_EDGE_LUMA = 0.36 # stripe piping +COLLAR_LUMA = 0.57 # stand collar band +COLLAR_SEAM_LUMA = 0.30 # collar base seam ring +COLLAR_SHADOW_LUMA = 0.42 # cast shadow just under the collar base +PLACKET_LUMA = 0.48 # zip placket band +ZIP_EDGE_LUMA = 0.34 # placket edge stitching +TEETH_LUMA = 0.88 # bright zip teeth dashes +TEETH_GAP_LUMA = 0.34 # dark tape between dashes +BAND_LUMA = 0.46 # ribbed cuff + hem bands +BAND_LINE_LUMA = 0.32 # band border stitch line +RIB_DELTA = 0.035 # knit rib darkening within bands +ALBEDO_NOISE = 0.02 # woven jitter +FABRIC_HUE = np.array([0.97, 0.98, 1.03], dtype=np.float32) # faint cool cast +NOISE_SEED = 2094 + + +# -------------------------------------------------------------------------- +# Thresholds — base derivation + track-jacket extras from the same armature +# -------------------------------------------------------------------------- + +def track_thresholds(armature, wrist_keep): + thr = base.derive_thresholds(armature) + bones = armature.data.bones + neck = bones.get("neck_01") + ua_l = bones.get("upperarm_l") + ua_r = bones.get("upperarm_r") + la_l = bones.get("lowerarm_l") + spine01 = bones.get("spine_01") + + if ua_l and ua_r: + shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 + else: + shoulder_x = base._REF_SHOULDER_X + thr["sh"] = shoulder_x / base._REF_SHOULDER_X + + # Taller, wider stand collar. + if neck: + neck_len = neck.tail_local.z - neck.head_local.z + thr["collar_z_min"] = neck.head_local.z - COLLAR_DROP_NECK_FRAC * neck_len + thr["neck_len"] = neck_len + else: + thr["neck_len"] = base._REF_NECK_LEN + thr["collar_x_abs"] = thr["collar_x_abs"] * COLLAR_X_MULT + + # Front zip placket. + thr["zip_half_x"] = shoulder_x * ZIP_HALF_FRAC + + # Wrist cut planes + knit cuff start on the lowerarm bones. + cut_planes = [] + cuff_abs = [] + for bone_name, sign in [("lowerarm_l", +1), ("lowerarm_r", -1)]: + b = bones.get(bone_name) + if b is None: + log(f"WARNING: bone {bone_name} missing — sleeve left full-length") + continue + head_x = b.head_local.x + tail_x = b.tail_local.x + cut_x = head_x + wrist_keep * (tail_x - head_x) + band_x = head_x + (wrist_keep - CUFF_BAND_FRAC) * (tail_x - head_x) + cut_planes.append((sign, cut_x)) + cuff_abs.append(abs(band_x)) + log(f"wrist cut {bone_name}: keep |x| up to {cut_x:.3f} " + f"(elbow {head_x:.3f} -> wrist {tail_x:.3f}), cuff from {band_x:.3f}") + thr["wrist_cut_planes"] = cut_planes + thr["cuff_x_abs"] = min(cuff_abs) if cuff_abs else 1e9 + + # Per-|x| arm axis (shoulder -> elbow -> wrist) for stripe placement — + # the denim per-z leg-axis technique rotated onto the arms. The rig is + # x-mirrored, so the left-arm landmarks serve both sides via |x|. + if ua_l and la_l: + pts = [ua_l.head_local, la_l.head_local, la_l.tail_local] + order = np.argsort([abs(p.x) for p in pts]) + thr["arm_x"] = np.array([abs(pts[i].x) for i in order]) + thr["arm_y"] = np.array([pts[i].y for i in order]) + thr["arm_z"] = np.array([pts[i].z for i in order]) + else: + log("WARNING: arm landmark bones missing — stripes disabled") + thr["arm_x"] = None + + # Left-breast logo patch (replaces the base full-chest box). + thr["chest_x"] = (shoulder_x * CHEST_X_LO_FRAC, shoulder_x * CHEST_X_HI_FRAC) + if neck and spine01: + spine_lo = spine01.head_local.z + span = neck.head_local.z - spine_lo + thr["chest_z"] = (spine_lo + CHEST_Z_LO_FRAC * span, + spine_lo + CHEST_Z_HI_FRAC * span) + + log(f"track thresholds: collar z>={thr['collar_z_min']:.3f} " + f"|x|<{thr['collar_x_abs']:.3f} zip half {thr['zip_half_x']:.3f} " + f"cuff |x|>={thr['cuff_x_abs']:.3f} sh {thr['sh']:.3f} " + f"chest x=({thr['chest_x'][0]:.3f},{thr['chest_x'][1]:.3f}) " + f"z=({thr['chest_z'][0]:.3f},{thr['chest_z'][1]:.3f})") + return thr + + +# -------------------------------------------------------------------------- +# Geometry: weld + wrist cut + open-rim flattening (denim required practices) +# -------------------------------------------------------------------------- + +def weld_boundaries(shell): + """Merge coincident segment-boundary verts so the offset can't open cracks.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + before = len(bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=WELD_DIST) + merged = before - len(bm.verts) + bm.to_mesh(me) + bm.free() + me.update() + log(f"welded segment boundaries: {merged} verts merged " + f"({before} -> {len(me.vertices)})") + + +def wrist_cut(shell, thr): + """Delete forearm verts beyond the wrist plane on each side.""" + cut_planes = thr.get("wrist_cut_planes", []) + if not cut_planes: + log("WARNING: no wrist cut planes — sleeves stay full length") + return + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + to_delete = [] + for v in bm.verts: + for sign, thr_x in cut_planes: + if sign > 0 and v.co.x > thr_x: + to_delete.append(v) + break + if sign < 0 and v.co.x < thr_x: + to_delete.append(v) + break + bmesh.ops.delete(bm, geom=to_delete, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"wrist cut removed {len(to_delete)} verts; " + f"{len(shell.data.vertices)} remain") + + +def flatten_open_rims(shell, thr): + """Pull the open boundary rings onto clean planes (pre-offset). + + The segment splitter cuts along weight thresholds, so the torso-bottom + hem ring is a jagged ring of teeth (the denim script measured ~4.5-7.6 cm + across bodies), and the vert-threshold wrist cut leaves jagged sleeve + ends. Three rims are cleaned: + + * WRIST rings (|x| >= sleeve_x_abs, per side): pulled OUT to the + nominal bone-plane cut — a clean vertical cuff edge; the forearm + skin continues well past the plane, so no clip risk is invented. + * HEM ring (bottom of the remaining boundary): pulled UP to its own + valley (the shallowest notch) — a clean straight elastic hem without + inventing coverage below the authored shell. + * NECK ring (top): left natural — every wave-1 top ships the natural + neckline boundary and the collar band owns that edge visually. + + Only boundary verts move; weights and loop UVs ride along. Returns the + hem plane z (needed for the hem band paint + post-solidify clamp). + """ + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + boundary = set() + for e in bm.edges: + if len(e.link_faces) == 1: # open boundary + boundary.update(v.index for v in e.verts) + + sleeve_x = thr["sleeve_x_abs"] + cut_by_sign = {sign: cut_x for sign, cut_x in thr.get("wrist_cut_planes", [])} + wrist = [i for i in boundary if abs(bm.verts[i].co.x) >= sleeve_x] + torso = [i for i in boundary if abs(bm.verts[i].co.x) < sleeve_x] + zs = [bm.verts[i].co.z for i in torso] + z_mid = (min(zs) + max(zs)) / 2.0 + hem = [i for i in torso if bm.verts[i].co.z <= z_mid] + neck = [i for i in torso if bm.verts[i].co.z > z_mid] + + moved_wrist = 0 + for i in wrist: + v = bm.verts[i] + sign = 1 if v.co.x >= 0.0 else -1 + if sign in cut_by_sign: + v.co.x = cut_by_sign[sign] + moved_wrist += 1 + + hem_lo = min(bm.verts[i].co.z for i in hem) + hem_plane = max(bm.verts[i].co.z for i in hem) # the valley (shallowest notch) + for i in hem: + bm.verts[i].co.z = hem_plane + + bm.to_mesh(me) + bm.free() + me.update() + log(f"flattened open rims: {moved_wrist} wrist verts -> nominal cut planes; " + f"hem ring ({len(hem)} verts, teeth {hem_plane - hem_lo:.3f} m) -> " + f"z={hem_plane:.3f}; neck ring left natural ({len(neck)} verts)") + return hem_plane + + +def clamp_hem_residue(shell, hem_plane): + """Post-solidify safety clamp: interior tooth verts still below the hem + plane (multi-triangle teeth) get squashed onto it.""" + me = shell.data + n = 0 + for v in me.vertices: + if v.co.z < hem_plane: + v.co.z = hem_plane + n += 1 + me.update() + if n: + log(f"clamped {n} residual hem verts -> {hem_plane:.3f}") + + +# -------------------------------------------------------------------------- +# Track feature field (texel-level; drives albedo AND mask together) +# -------------------------------------------------------------------------- + +def _paint_texels(X, Y, Z, noise, thr, hem_plane, hem_top): + """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions. + + ONE feature evaluation drives both outputs (denim practice), so the + painted albedo and the region mask always agree. + Region priority: cuff/hem band (A) > sleeve stripes (B) > collar (R) > body (G). + """ + n = X.shape[0] + ax = np.abs(X) + front = Y * base.FRONT_Y_SIGN > 0.004 + sh = thr["sh"] + + on_sleeve = ax >= thr["sleeve_x_abs"] + in_cuff = ax >= thr["cuff_x_abs"] + in_hem = (~on_sleeve) & (Z <= hem_top) + czmin = thr["collar_z_min"] + in_collar = (~on_sleeve) & (Z >= czmin) & (ax < thr["collar_x_abs"]) + + # Sleeve stripes: arc distance from the top line of the per-|x| arm axis. + if thr.get("arm_x") is not None: + cy = np.interp(ax, thr["arm_x"], thr["arm_y"]) + cz = np.interp(ax, thr["arm_x"], thr["arm_z"]) + dy = Y - cy + dz = Z - cz + r = np.hypot(dy, dz) + 1e-9 + arc = np.arctan2(np.abs(dy), dz) * r # 0 on the sleeve top line + g0 = STRIPE_GAP_HALF_M * sh + w = STRIPE_W_M * sh + stripe_zone = on_sleeve & ~in_cuff + stripe = stripe_zone & (arc >= g0) & (arc <= g0 + w) + ew = STRIPE_EDGE_W_M * sh + stripe_edge = stripe_zone & ( + (np.abs(arc - g0) < ew) | (np.abs(arc - (g0 + w)) < ew)) + else: + arc = np.zeros(n, dtype=np.float32) + stripe = np.zeros(n, dtype=bool) + stripe_edge = np.zeros(n, dtype=bool) + + # --- region mask (collar=R, body=G, stripes=B, cuffs/hem=A) ------------- + mask = np.zeros((n, 4), dtype=np.float32) + is_a = in_cuff | in_hem + is_b = stripe & ~is_a + is_r = in_collar & ~is_a & ~is_b + is_g = ~(is_a | is_b | is_r) + mask[is_a, 3] = 1.0 + mask[is_b, 2] = 1.0 + mask[is_r, 0] = 1.0 + mask[is_g, 1] = 1.0 + + # --- painted luma detail ------------------------------------------------- + v = np.full(n, BASE_LUMA, dtype=np.float32) + lw = LINE_W_M * sh + + # Stripes + piping. + v[stripe] = STRIPE_LUMA + v[stripe_edge] = STRIPE_EDGE_LUMA + + # Stand collar raised look: band luma + brighter gradient toward the rim, + # cast-shadow band + seam ring at the collar base (texture-carried depth). + reach = thr["collar_x_abs"] * COLLAR_REACH_MULT + v[in_collar] = COLLAR_LUMA + grad = np.clip((Z - czmin) / max(thr["neck_len"], 1e-6), 0.0, 1.0) + v[in_collar] += COLLAR_GRAD * grad[in_collar] + shadow_h = SHADOW_H_FRAC * thr["neck_len"] + under = (~in_collar) & (~on_sleeve) & (ax < reach) \ + & (Z < czmin) & (Z >= czmin - shadow_h) + t = np.clip((czmin - Z) / max(shadow_h, 1e-6), 0.0, 1.0) + v[under] = COLLAR_SHADOW_LUMA \ + + (BASE_LUMA - COLLAR_SHADOW_LUMA) * t[under] + seam = (~on_sleeve) & (np.abs(Z - czmin) < lw) & (ax < reach) + v[seam] = COLLAR_SEAM_LUMA + + # Ribbed cuff + hem bands: band luma, knit ribs, border stitch lines. + band = in_cuff | in_hem + v[band] = BAND_LUMA + cuff_rib = in_cuff & (np.mod(arc, RIB_PERIOD_M) < RIB_PERIOD_M * 0.5) + hem_rib = in_hem & (np.mod(X, RIB_PERIOD_M) < RIB_PERIOD_M * 0.5) + v[cuff_rib | hem_rib] -= RIB_DELTA + v[np.abs(ax - thr["cuff_x_abs"]) < lw] = BAND_LINE_LUMA + v[(~on_sleeve) & (np.abs(Z - hem_top) < lw)] = BAND_LINE_LUMA + + # Front zip: placket band + edge stitching + dashed teeth, hem -> through + # the collar (paint overrides bands/collar luma; regions stay untouched). + zh = thr["zip_half_x"] + v[front & (ax < zh)] = PLACKET_LUMA + v[front & (np.abs(ax - zh) < lw * 0.7)] = ZIP_EDGE_LUMA + teeth = front & (ax < TEETH_HALF_W) + dash = np.mod(Z, TEETH_PERIOD) < TEETH_DUTY + v[teeth & dash] = TEETH_LUMA + v[teeth & ~dash] = TEETH_GAP_LUMA + + # --- albedo -------------------------------------------------------------- + lum = np.clip(v + noise, 0.0, 1.0) + alb = np.empty((n, 4), dtype=np.float32) + for c in range(3): + alb[:, c] = np.clip(lum * FABRIC_HUE[c], 0.0, 1.0) + alb[:, 3] = 1.0 + return alb, mask + + +def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, thr, + hem_plane, hem_top, W, H): + """Barycentric texel fill of one UV triangle: interpolate 3D positions, + evaluate the track feature field, write albedo + mask together.""" + a, b, c = uvs + A, B, C = cos + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + pxg = xs + 0.5 + pyg = ys + 0.5 + w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom + w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + w0i, w1i, w2i = w0[inside], w1[inside], w2[inside] + px3 = w0i * A.x + w1i * B.x + w2i * C.x + py3 = w0i * A.y + w1i * B.y + w2i * C.y + pz3 = w0i * A.z + w1i * B.z + w2i * C.z + ysin = ys[inside] + xsin = xs[inside] + alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], + thr, hem_plane, hem_top) + alb_buf[ysin, xsin] = alb + mask_buf[ysin, xsin] = mask + + +def paint_albedo_and_mask(shell, thr, hem_plane, hem_top, out_dir, body): + """Rasterize all UV0 triangles once, producing the painted albedo and the + region mask from one shared feature-field evaluation per texel.""" + W = H = TEX_SIZE + rng = np.random.default_rng(NOISE_SEED) + noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5) + * 2.0 * ALBEDO_NOISE) + + alb_buf = np.empty((H, W, 4), dtype=np.float32) + for c in range(3): + alb_buf[:, :, c] = np.clip( + (BASE_LUMA + noise_buf) * FABRIC_HUE[c], 0.0, 1.0) + alb_buf[:, :, 3] = 1.0 + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 # background = body green (bleed-safe) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + if not len(bm.loops.layers.uv): + raise RuntimeError("no UV layer for albedo/mask paint") + uv_layer = bm.loops.layers.uv[0] + + tri_count = 0 + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv.copy() for loop in loops] + cos = [loop.vert.co.copy() for loop in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_paint( + alb_buf, mask_buf, noise_buf, + (uvs[0], uvs[i], uvs[i + 1]), + (cos[0], cos[i], cos[i + 1]), + thr, hem_plane, hem_top, W, H) + tri_count += 1 + bm.free() + log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})") + + # Honest region tally over the whole mask (texel counts, background excl. + # impossible — background is body green by design). + tot = W * H + counts = { + "collar(R)": int((mask_buf[:, :, 0] > 0.5).sum()), + "body(G)": int((mask_buf[:, :, 1] > 0.5).sum()), + "stripes(B)": int((mask_buf[:, :, 2] > 0.5).sum()), + "cuff/hem(A)": int((mask_buf[:, :, 3] > 0.5).sum()), + } + log("mask texels: " + " ".join( + f"{k}={v} ({100.0 * v / tot:.1f}%)" for k, v in counts.items())) + + def _save(buf, name, path): + img = bpy.data.images.new(name, W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + _save(mask_buf, f"{GARMENT_ID}_mask_{body}", + os.path.join(out_dir, f"{body}_mask.png")) + albedo_img = _save(alb_buf, f"{GARMENT_ID}_albedo_{body}", + os.path.join(out_dir, f"{body}_base_albedo.png")) + log(f"saved mask -> {body}_mask.png") + log(f"saved albedo -> {body}_base_albedo.png") + # Repoint the image at the shared sidecar name before export: the glTF + # exporter names the embedded image after the file basename, and Godot + # extracts it as _.png -> _base_albedo.png (tshirt + # convention). The shared file is re-pointed to average_m's at the end. + albedo_img.filepath_raw = os.path.join(out_dir, "base_albedo.png") + albedo_img.save() + return albedo_img + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_track_jacket(body_dir, out_dir, body, offset, wrist_keep): + base.clear_scene() + base.COVERED_SEGMENTS = SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + thr = track_thresholds(armature, wrist_keep) + + weld_boundaries(shell) + wrist_cut(shell, thr) + hem_plane = flatten_open_rims(shell, thr) + base.offset_outward(shell, offset) + base.solidify(shell, CLOTH_THICKNESS_M) + clamp_hem_residue(shell, hem_plane) + + hem_top = hem_plane + HEM_BAND_FRAC * (thr["collar_z_min"] - hem_plane) + log(f"hem band: z {hem_plane:.3f}..{hem_top:.3f}") + + base.author_logo_uv(shell, thr) # UV2 before bake (bake reads UV0 directly) + albedo_img = paint_albedo_and_mask(shell, thr, hem_plane, hem_top, + out_dir, body) + base.assign_fabric_material(shell, albedo_img) + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--wrist-keep F]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = OFFSET_M + wrist_keep = WRIST_KEEP_FRAC + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--wrist-keep" in argv: + wrist_keep = float(argv[argv.index("--wrist-keep") + 1]) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"track-jacket per-body mode: {len(bodies)} bodies, " + f"offset {offset*1000:.0f} mm, wrist keep {wrist_keep:.2f}") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_track_jacket(body_dir, out_dir, body, offset, wrist_keep) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-qa/configs/boots_modern.json b/tooling/garment-qa/configs/boots_modern.json new file mode 100644 index 000000000..4f6a0d524 --- /dev/null +++ b/tooling/garment-qa/configs/boots_modern.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "boots_modern", "slot": "feet", "covers": ["leg_lower_l", "leg_lower_r"]} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/boots_modern" +} diff --git a/tooling/garment-qa/configs/cargo_pants.json b/tooling/garment-qa/configs/cargo_pants.json new file mode 100644 index 000000000..a21cb4011 --- /dev/null +++ b/tooling/garment-qa/configs/cargo_pants.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "cargo_pants", "slot": "legs"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/cargo_pants" +} diff --git a/tooling/garment-qa/configs/joggers_modern.json b/tooling/garment-qa/configs/joggers_modern.json new file mode 100644 index 000000000..1e940d6e9 --- /dev/null +++ b/tooling/garment-qa/configs/joggers_modern.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "joggers_modern", "slot": "legs"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/joggers_modern" +} diff --git a/tooling/garment-qa/configs/parka_thrds.json b/tooling/garment-qa/configs/parka_thrds.json new file mode 100644 index 000000000..d3817c7f1 --- /dev/null +++ b/tooling/garment-qa/configs/parka_thrds.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "parka_thrds", "slot": "torso"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/parka_thrds" +} diff --git a/tooling/garment-qa/configs/shoes_formal.json b/tooling/garment-qa/configs/shoes_formal.json new file mode 100644 index 000000000..5abe4ad94 --- /dev/null +++ b/tooling/garment-qa/configs/shoes_formal.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "shoes_formal", "slot": "feet", "covers": ["foot_l", "foot_r"]} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/shoes_formal" +} diff --git a/tooling/garment-qa/configs/slides.json b/tooling/garment-qa/configs/slides.json new file mode 100644 index 000000000..eaf83e0e8 --- /dev/null +++ b/tooling/garment-qa/configs/slides.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "slides", "slot": "feet", "covers": ["foot_l", "foot_r"]} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.012, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/slides" +} diff --git a/tooling/garment-qa/configs/sneakers_modern.json b/tooling/garment-qa/configs/sneakers_modern.json new file mode 100644 index 000000000..c9571bd6e --- /dev/null +++ b/tooling/garment-qa/configs/sneakers_modern.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "sneakers_modern", "slot": "feet"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/sneakers_modern" +} diff --git a/tooling/garment-qa/configs/sweater_modern.json b/tooling/garment-qa/configs/sweater_modern.json new file mode 100644 index 000000000..e2d5eb98b --- /dev/null +++ b/tooling/garment-qa/configs/sweater_modern.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "sweater_modern", "slot": "torso"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/sweater_modern" +} diff --git a/tooling/garment-qa/configs/swim_trunks.json b/tooling/garment-qa/configs/swim_trunks.json new file mode 100644 index 000000000..13b81c9ad --- /dev/null +++ b/tooling/garment-qa/configs/swim_trunks.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "swim_trunks", "slot": "legs"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/swim_trunks" +} diff --git a/tooling/garment-qa/configs/swimsuit_onepiece.json b/tooling/garment-qa/configs/swimsuit_onepiece.json new file mode 100644 index 000000000..70264e555 --- /dev/null +++ b/tooling/garment-qa/configs/swimsuit_onepiece.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "swimsuit_onepiece", "slot": "full_body"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/swimsuit_onepiece" +} diff --git a/tooling/garment-qa/configs/tank_top.json b/tooling/garment-qa/configs/tank_top.json new file mode 100644 index 000000000..22a974457 --- /dev/null +++ b/tooling/garment-qa/configs/tank_top.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "tank_top", "slot": "torso"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/tank_top" +} diff --git a/tooling/garment-qa/configs/track_jacket.json b/tooling/garment-qa/configs/track_jacket.json new file mode 100644 index 000000000..affddbfc7 --- /dev/null +++ b/tooling/garment-qa/configs/track_jacket.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "track_jacket", "slot": "torso"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/track_jacket" +}