fix(assets): lookbook findings — 44 missing extracted albedos, manifest nesting, lookbook tool (T-1089)
The lookbook render pass load-checked all 264 garment GLBs and found four garments (boots, cargo pants, jeans, tank top) shipped WITHOUT their per-body Godot-extracted albedo textures — 44 GLBs would fail to load after a clean import. Textures regenerated via forced reimport and committed. Also: swimsuit_onepiece manifest entry was double-nested (lead's merge bug — runtime unaffected, reads coverage.json); flattened. The parameterized lookbook_scene tool (drives the production compositor, auto-framing, job list) is committed for future catalogue renders. Grey head-template finding filed as T-1095. Lookbook itself delivered to the desktop, not committed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <config>. 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 <body>_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);
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||