diff --git a/.pql/changelog/ticket_history/2026-07.sql b/.pql/changelog/ticket_history/2026-07.sql index bc1445e8d..b4407040e 100644 --- a/.pql/changelog/ticket_history/2026-07.sql +++ b/.pql/changelog/ticket_history/2026-07.sql @@ -975,3 +975,4 @@ ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/7 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; +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', 'status', 'backlog', 'review', NULL, '2026-07-07 09:23:38', '2026-07-07 09:23:38.268', '2026-07-07 09:23:38.268', NULL, 'dbfc4dd044d0fd988d9b5ff5e2514d7e', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/ticket_idmap/2026-07.sql b/.pql/changelog/ticket_idmap/2026-07.sql index a75f22422..c5f0b31ea 100644 --- a/.pql/changelog/ticket_idmap/2026-07.sql +++ b/.pql/changelog/ticket_idmap/2026-07.sql @@ -8,3 +8,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKH8SJRMXTP6XQNBXHRD4CQ0', 'T-1092', '2026-07-06 18:14:07.303', '2026-07-06 18:14:07.303', NULL, 'e34fe437eac3bdbd6a73f8d98bc871a9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKHFRCP5R0VBA9BKR97RRBJ0', 'T-1093', '2026-07-06 18:44:32.562', '2026-07-06 18:44:32.562', NULL, 'ce581695739eea8cd11433cb9f6c86d1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKJ25KG3ADM4M5XRXCVR2GQG', 'T-1094', '2026-07-06 20:04:59.393', '2026-07-06 20:04:59.393', NULL, 'e856fa3f89f621007a058db26aff2a05', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKR283RTWYXD2M005K65S52M', 'T-1095', '2026-07-07 10:04:11.591', '2026-07-07 10:04:11.591', NULL, 'e0a6ad2c4dce20a444a5af47d49b36c7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-07.sql b/.pql/changelog/tickets/2026-07.sql index 7b5d4bd1e..613e43244 100644 --- a/.pql/changelog/tickets/2026-07.sql +++ b/.pql/changelog/tickets/2026-07.sql @@ -791,3 +791,29 @@ ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/7 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); +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).', 'review', 'high', NULL, 'client', NULL, '2026-07-06 07:34:18.656', '2026-07-07 09:23:38.257', NULL, '15fb82c297b4afafa017d7247c7ba67e', 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 ('06FKR283RTWYXD2M005K65S52M', 'bug', '06FB0TNSRXX29H5X0EB0F0GBW4', 'head_001 template renders grey (untinted) on child/teen/muscular bodies', 'Found by the T-1089 lookbook pass (2026-07-07): overlay head templates (head_001) render grey faces on child/teen/muscular bodies — the template appears not to receive the body''s skin-tone tint, while each body''s own seg_head renders correctly. Workaround used in the lookbook: head_id empty -> body seg_head shows. If head templates are meant to be body-agnostic (D-161 head-as-BoneAttachment3D), the skin-tone shader application needs to cover template heads per body; investigate _apply skin tone path in character_visual.gd for the head template branch.', 'backlog', 'medium', NULL, 'client', NULL, '2026-07-07 10:04:11.590', '2026-07-07 10:04:11.590', NULL, 'fee4a7bdc5705284493e52c31dd588c1', 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_average_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/average_f_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_average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/average_m_average_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/average_m_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_average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/child_child_base_albedo.png b/client/assets/characters/clothing/boots_modern/child_child_base_albedo.png new file mode 100644 index 000000000..81c74c902 Binary files /dev/null and b/client/assets/characters/clothing/boots_modern/child_child_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/heavy_f_heavy_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/heavy_f_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_heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/heavy_m_heavy_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/heavy_m_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_heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/muscular_f_muscular_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/muscular_f_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_muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/muscular_m_muscular_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/muscular_m_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_muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/teen_f_teen_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/teen_f_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_teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/teen_m_teen_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/teen_m_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_teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/thin_f_thin_f_base_albedo.png b/client/assets/characters/clothing/boots_modern/thin_f_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_thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/boots_modern/thin_m_thin_m_base_albedo.png b/client/assets/characters/clothing/boots_modern/thin_m_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_thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/average_f_average_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/average_f_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_average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/average_m_average_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/average_m_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_average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/child_child_base_albedo.png b/client/assets/characters/clothing/cargo_pants/child_child_base_albedo.png new file mode 100644 index 000000000..6b84388a9 Binary files /dev/null and b/client/assets/characters/clothing/cargo_pants/child_child_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/heavy_f_heavy_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/heavy_f_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_heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/heavy_m_heavy_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/heavy_m_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_heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/muscular_f_muscular_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/muscular_f_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_muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/muscular_m_muscular_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/muscular_m_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_muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/teen_f_teen_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/teen_f_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_teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/teen_m_teen_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/teen_m_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_teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/thin_f_thin_f_base_albedo.png b/client/assets/characters/clothing/cargo_pants/thin_f_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_thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/cargo_pants/thin_m_thin_m_base_albedo.png b/client/assets/characters/clothing/cargo_pants/thin_m_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_thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/average_f_average_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/average_f_average_f_base_albedo.png new file mode 100644 index 000000000..5dd109790 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/average_f_average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/average_m_average_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/average_m_average_m_base_albedo.png new file mode 100644 index 000000000..3e79b15b8 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/average_m_average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/child_child_base_albedo.png b/client/assets/characters/clothing/jeans_modern/child_child_base_albedo.png new file mode 100644 index 000000000..66b6d5a4c Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/child_child_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/heavy_f_heavy_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/heavy_f_heavy_f_base_albedo.png new file mode 100644 index 000000000..36519b93b Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/heavy_f_heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/heavy_m_heavy_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/heavy_m_heavy_m_base_albedo.png new file mode 100644 index 000000000..f1b9e1f68 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/heavy_m_heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/muscular_f_muscular_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/muscular_f_muscular_f_base_albedo.png new file mode 100644 index 000000000..07d6aeb46 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/muscular_f_muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/muscular_m_muscular_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/muscular_m_muscular_m_base_albedo.png new file mode 100644 index 000000000..17f564e18 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/muscular_m_muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/teen_f_teen_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/teen_f_teen_f_base_albedo.png new file mode 100644 index 000000000..87566f6d6 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/teen_f_teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/teen_m_teen_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/teen_m_teen_m_base_albedo.png new file mode 100644 index 000000000..7b5d1c72c Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/teen_m_teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/thin_f_thin_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/thin_f_thin_f_base_albedo.png new file mode 100644 index 000000000..9b08389cc Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/thin_f_thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/thin_m_thin_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/thin_m_thin_m_base_albedo.png new file mode 100644 index 000000000..0bb6b7c05 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/thin_m_thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/average_f_average_f_base_albedo.png b/client/assets/characters/clothing/tank_top/average_f_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_average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/average_m_average_m_base_albedo.png b/client/assets/characters/clothing/tank_top/average_m_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_average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/child_child_base_albedo.png b/client/assets/characters/clothing/tank_top/child_child_base_albedo.png new file mode 100644 index 000000000..72dc7ede3 Binary files /dev/null and b/client/assets/characters/clothing/tank_top/child_child_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/heavy_f_heavy_f_base_albedo.png b/client/assets/characters/clothing/tank_top/heavy_f_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_heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/heavy_m_heavy_m_base_albedo.png b/client/assets/characters/clothing/tank_top/heavy_m_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_heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/muscular_f_muscular_f_base_albedo.png b/client/assets/characters/clothing/tank_top/muscular_f_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_muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/muscular_m_muscular_m_base_albedo.png b/client/assets/characters/clothing/tank_top/muscular_m_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_muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/teen_f_teen_f_base_albedo.png b/client/assets/characters/clothing/tank_top/teen_f_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_teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/teen_m_teen_m_base_albedo.png b/client/assets/characters/clothing/tank_top/teen_m_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_teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/thin_f_thin_f_base_albedo.png b/client/assets/characters/clothing/tank_top/thin_f_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_thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tank_top/thin_m_thin_m_base_albedo.png b/client/assets/characters/clothing/tank_top/thin_m_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_thin_m_base_albedo.png differ diff --git a/client/assets/characters/manifest.json b/client/assets/characters/manifest.json index 2469df06c..08f8e7004 100644 --- a/client/assets/characters/manifest.json +++ b/client/assets/characters/manifest.json @@ -204,22 +204,20 @@ "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" - ] - } + "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", diff --git a/client/tools/garment_preview/lookbook_scene.gd b/client/tools/garment_preview/lookbook_scene.gd new file mode 100644 index 000000000..9c4df1ffa --- /dev/null +++ b/client/tools/garment_preview/lookbook_scene.gd @@ -0,0 +1,317 @@ +extends Node3D +## Wardrobe LOOKBOOK capture (T-1089). +## +## Drives the PRODUCTION character compositor (CharacterVisual.load_descriptor) +## to render one catwalk-style full-body portrait per garment. Each garment is +## the HERO item, dressed with neutral complements so it reads (a top over +## jeans+sneakers, footwear over tee+shorts, full-body pieces on their own). +## Every shot uses production-identical data: the same multi-region toon_garment +## shader, region masks, per-region tints and thrds logo the game uses at +## runtime — nothing is hand-composited here. +## +## One Godot process renders the WHOLE lookbook: it reads a JSON job list from +## the LOOKBOOK_JOBS env var and loops through every entry, reusing a single +## CharacterVisual (load_descriptor clears + rebuilds each time). +## +## Job schema (array of objects): +## { +## "garment_id": "tshirt_modern", # for logging only +## "out": "/abs/path/x.png", # 768x1024 PNG written here +## "body": "average_m", # body-type key +## "skin_tone": 3, # 0..8 +## "head": "head_001", "hair": "buzzed", "eyebrow": "regular", +## "slots": {"torso": "tshirt_modern", "legs": "jeans_modern", ...}, +## "tints": {"tshirt_modern": [[r,g,b,a], ...], ...}, +## "logos": {"tshirt_modern": "thrds"}, +## "camera": "full" | "feet", +## "walk_phase": 0.08, # 0..1 fraction of the Walk clip +## "yaw": 25.0 # 3/4 turn, degrees +## } +## +## NOT a committed asset-pipeline generator — a marketing/QA capture tool. The +## .gd/.tscn pair may be committed; the rendered PNGs and job JSON are not. +## +## Run: +## LOOKBOOK_JOBS=/tmp/jobs.json godot4 --path client \ +## --rendering-driver opengl3 res://tools/garment_preview/lookbook_scene.tscn + +const VIEWPORT := Vector2i(768, 1024) +const WALK_CLIP := "ual1/Walk" + +const BODY_KEY_TO_ENUM := { + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "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, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +var _cam: Camera3D = null +var _cv: CharacterVisual = null +var _vp: SubViewport = null + + +func _ready() -> void: + # Render into a fixed-size offscreen SubViewport so the captured image is + # always 768x1024 regardless of the on-screen window / display resolution. + _vp = SubViewport.new() + _vp.size = VIEWPORT + _vp.own_world_3d = true + _vp.transparent_bg = false + _vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS + _vp.msaa_3d = Viewport.MSAA_4X + add_child(_vp) + _setup_stage() + _run_all.call_deferred() + + +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: + # Key + soft fill: a warm key from camera-left, a cool low-energy fill from + # camera-right so the toon regions keep gradient without a flat wash. + var key := DirectionalLight3D.new() + key.rotation_degrees = Vector3(-40, 28, 0) + key.light_energy = 1.25 + key.light_color = Color(1.0, 0.98, 0.94) + _vp.add_child(key) + var fill := DirectionalLight3D.new() + fill.rotation_degrees = Vector3(-18, -42, 0) + fill.light_energy = 0.45 + fill.light_color = Color(0.86, 0.90, 1.0) + _vp.add_child(fill) + + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.84, 0.85, 0.90) + env.ambient_light_energy = 0.85 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.20, 0.22, 0.26) + var we := WorldEnvironment.new() + we.environment = env + _vp.add_child(we) + + _build_backdrop() + + _cam = Camera3D.new() + _cam.fov = 42.0 + _cam.keep_aspect = Camera3D.KEEP_HEIGHT # fov is the VERTICAL angle + _cam.current = true + _vp.add_child(_cam) + + +## Studio infinity-cove: a vertical gradient sweep behind + a matching floor, +## toned so the floor meets the backdrop near-seamlessly (catwalk look). +func _build_backdrop() -> void: + var grad := Gradient.new() + grad.set_color(0, Color(0.16, 0.17, 0.21)) + grad.set_color(1, Color(0.30, 0.32, 0.37)) + var gtex := GradientTexture2D.new() + gtex.gradient = grad + gtex.fill_from = Vector2(0, 0) + gtex.fill_to = Vector2(0, 1) + gtex.width = 8 + gtex.height = 256 + + var back := MeshInstance3D.new() + var bmesh := QuadMesh.new() + bmesh.size = Vector2(12, 9) + back.mesh = bmesh + var bmat := StandardMaterial3D.new() + bmat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + bmat.albedo_texture = gtex + back.material_override = bmat + back.position = Vector3(0, 2.0, -2.2) + _vp.add_child(back) + + var floor := MeshInstance3D.new() + var fmesh := PlaneMesh.new() + fmesh.size = Vector2(12, 8) + floor.mesh = fmesh + var fmat := StandardMaterial3D.new() + fmat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + fmat.albedo_color = Color(0.235, 0.255, 0.295) + floor.material_override = fmat + floor.position = Vector3(0, 0.0, -0.5) + _vp.add_child(floor) + + +func _run_all() -> void: + var jobs_path := _env("LOOKBOOK_JOBS", "") + if jobs_path.is_empty(): + push_error("lookbook: LOOKBOOK_JOBS not set") + get_tree().quit(1) + return + var f := FileAccess.open(jobs_path, FileAccess.READ) + if f == null: + push_error("lookbook: cannot open %s" % jobs_path) + get_tree().quit(1) + return + var parsed: Variant = JSON.parse_string(f.get_as_text()) + f.close() + if not (parsed is Array): + push_error("lookbook: job file is not a JSON array") + get_tree().quit(1) + return + + _cv = CharacterVisual.new() + _vp.add_child(_cv) + + var jobs: Array = parsed + var ok := 0 + for i in jobs.size(): + var job: Dictionary = jobs[i] + if await _render_job(job, i, jobs.size()): + ok += 1 + + print("lookbook: DONE — %d/%d rendered" % [ok, jobs.size()]) + get_tree().quit(0) + + +func _render_job(job: Dictionary, idx: int, total: int) -> bool: + var gid := str(job.get("garment_id", "?")) + var body := str(job.get("body", "average_m")) + if not BODY_KEY_TO_ENUM.has(body): + push_error("lookbook: unknown body '%s' for %s" % [body, gid]) + return false + + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = str(job.get("head", "head_001")) + desc.hair_id = str(job.get("hair", "buzzed")) + desc.eyebrow_id = str(job.get("eyebrow", "regular")) + desc.skin_tone = int(job.get("skin_tone", 3)) + desc.clothing_slots = _to_str_dict(job.get("slots", {})) + desc.clothing_tints = _to_tint_map(job.get("tints", {})) + desc.clothing_logos = _to_str_dict(job.get("logos", {})) + _cv.load_descriptor(desc) + _cv.rotation_degrees.y = float(job.get("yaw", 25.0)) + + var skel := _cv.get_skeleton() + if skel == null: + push_error("lookbook: no skeleton for %s" % gid) + return false + + # Pose: freeze the Walk clip at a phase with clear leg separation. + var ap := _cv.get_animation_player() + if ap != null and ap.has_animation(WALK_CLIP): + _cv.play_animation(WALK_CLIP) + ap.pause() + var length := ap.get_animation(WALK_CLIP).length + ap.seek(length * float(job.get("walk_phase", 0.08)), true) + await get_tree().process_frame + else: + push_warning("lookbook: %s missing, using rest pose" % WALK_CLIP) + if idx == 0 and ap != null: + print(" available clips: ", _all_clip_names(ap)) + + _frame_camera(str(job.get("camera", "full"))) + + # Let the posed skeleton + framing settle, then capture. + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var out := str(job.get("out", "")) + if out.is_empty(): + push_error("lookbook: no out path for %s" % gid) + return false + DirAccess.make_dir_recursive_absolute(out.get_base_dir()) + var img := _vp.get_texture().get_image() + var err := img.save_png(out) + if err != OK: + push_error("lookbook: save failed (%d) for %s -> %s" % [err, gid, out]) + return false + print("lookbook [%d/%d]: %s (%s, %s) -> %s" % [idx + 1, total, gid, body, job.get("camera", "full"), out]) + return true + + +## Auto-frame from the true visible bounds so every body type + garment fills a +## consistent share of the portrait. Height is yaw-invariant, so the 3/4 turn +## does not change the framing. +func _frame_camera(mode: String) -> void: + var aabb := _visible_aabb(_cv) + var center := aabb.get_center() + var height: float = max(aabb.size.y, 0.2) + var fov_v := deg_to_rad(_cam.fov) + var fill := 0.86 + if mode == "feet": + fill = 0.84 + var visible_v := height / fill + var dist := (visible_v * 0.5) / tan(fov_v * 0.5) + + if mode == "feet": + # Mild high angle to bring the shoes forward, but AIM AT THE TRUE CENTRE + # (never a lowered target) so the head cannot rotate out of frame even on + # tall / puffy-silhouette garments. + var cam_pos := Vector3(center.x, center.y + height * 0.03, center.z + dist) + _cam.position = cam_pos + _cam.look_at_from_position(cam_pos, center, Vector3.UP) + else: + var cam_pos := Vector3(center.x, center.y, center.z + dist) + _cam.position = cam_pos + _cam.look_at_from_position(cam_pos, center, Vector3.UP) + + +func _visible_aabb(root: Node) -> AABB: + var acc := AABB() + var first := true + var stack: Array[Node] = [root] + while not stack.is_empty(): + var n: Node = stack.pop_back() + for c in n.get_children(): + stack.append(c) + if n is MeshInstance3D: + var mi := n as MeshInstance3D + if not mi.visible or mi.mesh == null: + continue + if str(mi.name).begins_with("_outline_"): + continue + var world := mi.global_transform * mi.mesh.get_aabb() + if first: + acc = world + first = false + else: + acc = acc.merge(world) + return acc + + +func _all_clip_names(ap: AnimationPlayer) -> Array: + var names: Array = [] + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + names.append("%s/%s" % [lib_name, a]) + return names + + +func _to_str_dict(v: Variant) -> Dictionary: + var out := {} + if v is Dictionary: + for k: String in v: + out[k] = str(v[k]) + return out + + +## Convert {"item": [[r,g,b,a], ...]} to {"item": Array[Color]}. +func _to_tint_map(v: Variant) -> Dictionary: + var out := {} + if not (v is Dictionary): + return out + for item: String in v: + var colors: Array[Color] = [] + var arr: Variant = v[item] + if arr is Array: + for c in arr: + if c is Array and (c as Array).size() >= 3: + var a: float = float(c[3]) if (c as Array).size() >= 4 else 1.0 + colors.append(Color(float(c[0]), float(c[1]), float(c[2]), a)) + out[item] = colors + return out diff --git a/client/tools/garment_preview/lookbook_scene.tscn b/client/tools/garment_preview/lookbook_scene.tscn new file mode 100644 index 000000000..08dd4ea26 --- /dev/null +++ b/client/tools/garment_preview/lookbook_scene.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://c8rtlookbk0garm"] + +[ext_resource type="Script" path="res://tools/garment_preview/lookbook_scene.gd" id="1_look"] + +[node name="Lookbook" type="Node3D"] +script = ExtResource("1_look")