diff --git a/CHANGELOG.md b/CHANGELOG.md index 00738ca81..72bed4cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] ### Added +- Surface Deform batch pipeline — headless Blender script that fits a reference clothing GLB to all 11 body types via Surface Deform modifier; confirmed HIGH reliability in Blender 5.0 with temp_override +- Initial clothing set — 5 placeholder items (coveralls_basic, jacket_utility, pants_cargo, shirt_henley, boots_work) with 11 body-type variants each (60 GLBs), coverage.json, and recolor masks +- Clothing authoring helper scripts — procedural reference mesh creator and metadata generator - Character asset organization proposal — directory structure, naming conventions, and import pipeline for 11 body types (21 segments each), Quaternius Source tier mapping, CharacterVisualDescriptor contract, Surface Deform clothing pipeline, coverage.json format - Body segment GLB library — 198 files (11 body types × 18 segments each) covering average, muscular, teen, thin, heavy, and child body types; thin/heavy/child are auto-scaled forks flagged for artist review - Head template GLBs — 4 head meshes from Quaternius OnlyHead source, with 1024×1024 white mask PNGs (full-face tintable) diff --git a/client/assets/characters/clothing/boots_work/average_f.glb b/client/assets/characters/clothing/boots_work/average_f.glb new file mode 100644 index 000000000..9162ec7af Binary files /dev/null and b/client/assets/characters/clothing/boots_work/average_f.glb differ diff --git a/client/assets/characters/clothing/boots_work/average_m.glb b/client/assets/characters/clothing/boots_work/average_m.glb new file mode 100644 index 000000000..70d0900d4 Binary files /dev/null and b/client/assets/characters/clothing/boots_work/average_m.glb differ diff --git a/client/assets/characters/clothing/boots_work/child.glb b/client/assets/characters/clothing/boots_work/child.glb new file mode 100644 index 000000000..54e3cb69e Binary files /dev/null and b/client/assets/characters/clothing/boots_work/child.glb differ diff --git a/client/assets/characters/clothing/boots_work/coverage.json b/client/assets/characters/clothing/boots_work/coverage.json new file mode 100644 index 000000000..8ce9666a3 --- /dev/null +++ b/client/assets/characters/clothing/boots_work/coverage.json @@ -0,0 +1,9 @@ +{ + "hides": [ + "foot_l", + "foot_r", + "leg_lower_l", + "leg_lower_r" + ], + "torso_variant": "full" +} \ No newline at end of file diff --git a/client/assets/characters/clothing/boots_work/heavy_f.glb b/client/assets/characters/clothing/boots_work/heavy_f.glb new file mode 100644 index 000000000..2c0afb908 Binary files /dev/null and b/client/assets/characters/clothing/boots_work/heavy_f.glb differ diff --git a/client/assets/characters/clothing/boots_work/heavy_m.glb b/client/assets/characters/clothing/boots_work/heavy_m.glb new file mode 100644 index 000000000..d17369b9c Binary files /dev/null and b/client/assets/characters/clothing/boots_work/heavy_m.glb differ diff --git a/client/assets/characters/clothing/boots_work/muscular_f.glb b/client/assets/characters/clothing/boots_work/muscular_f.glb new file mode 100644 index 000000000..8e974361b Binary files /dev/null and b/client/assets/characters/clothing/boots_work/muscular_f.glb differ diff --git a/client/assets/characters/clothing/boots_work/muscular_m.glb b/client/assets/characters/clothing/boots_work/muscular_m.glb new file mode 100644 index 000000000..63944220b Binary files /dev/null and b/client/assets/characters/clothing/boots_work/muscular_m.glb differ diff --git a/client/assets/characters/clothing/boots_work/reference.glb b/client/assets/characters/clothing/boots_work/reference.glb new file mode 100644 index 000000000..70d0900d4 Binary files /dev/null and b/client/assets/characters/clothing/boots_work/reference.glb differ diff --git a/client/assets/characters/clothing/boots_work/reference_mask.png b/client/assets/characters/clothing/boots_work/reference_mask.png new file mode 100644 index 000000000..93770e0b3 Binary files /dev/null and b/client/assets/characters/clothing/boots_work/reference_mask.png differ diff --git a/client/assets/characters/clothing/boots_work/teen_f.glb b/client/assets/characters/clothing/boots_work/teen_f.glb new file mode 100644 index 000000000..1d38baabe Binary files /dev/null and b/client/assets/characters/clothing/boots_work/teen_f.glb differ diff --git a/client/assets/characters/clothing/boots_work/teen_m.glb b/client/assets/characters/clothing/boots_work/teen_m.glb new file mode 100644 index 000000000..888e5db74 Binary files /dev/null and b/client/assets/characters/clothing/boots_work/teen_m.glb differ diff --git a/client/assets/characters/clothing/boots_work/thin_f.glb b/client/assets/characters/clothing/boots_work/thin_f.glb new file mode 100644 index 000000000..ef13c79cc Binary files /dev/null and b/client/assets/characters/clothing/boots_work/thin_f.glb differ diff --git a/client/assets/characters/clothing/boots_work/thin_m.glb b/client/assets/characters/clothing/boots_work/thin_m.glb new file mode 100644 index 000000000..f39ec9aef Binary files /dev/null and b/client/assets/characters/clothing/boots_work/thin_m.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/average_f.glb b/client/assets/characters/clothing/coveralls_basic/average_f.glb new file mode 100644 index 000000000..c1fbf8f11 Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/average_f.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/average_m.glb b/client/assets/characters/clothing/coveralls_basic/average_m.glb new file mode 100644 index 000000000..e5964521e Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/average_m.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/child.glb b/client/assets/characters/clothing/coveralls_basic/child.glb new file mode 100644 index 000000000..c544b3ba1 Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/child.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/coverage.json b/client/assets/characters/clothing/coveralls_basic/coverage.json new file mode 100644 index 000000000..7ab2685fe --- /dev/null +++ b/client/assets/characters/clothing/coveralls_basic/coverage.json @@ -0,0 +1,18 @@ +{ + "hides": [ + "torso", + "arm_upper_l", + "arm_upper_r", + "arm_lower_l", + "arm_lower_r", + "hand_l", + "hand_r", + "leg_upper_l", + "leg_upper_r", + "leg_lower_l", + "leg_lower_r", + "foot_l", + "foot_r" + ], + "torso_variant": "full" +} \ No newline at end of file diff --git a/client/assets/characters/clothing/coveralls_basic/heavy_f.glb b/client/assets/characters/clothing/coveralls_basic/heavy_f.glb new file mode 100644 index 000000000..291a9934d Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/heavy_f.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/heavy_m.glb b/client/assets/characters/clothing/coveralls_basic/heavy_m.glb new file mode 100644 index 000000000..7570be54f Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/heavy_m.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/muscular_f.glb b/client/assets/characters/clothing/coveralls_basic/muscular_f.glb new file mode 100644 index 000000000..f1aea986f Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/muscular_f.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/muscular_m.glb b/client/assets/characters/clothing/coveralls_basic/muscular_m.glb new file mode 100644 index 000000000..a6bb771bd Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/muscular_m.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/reference.glb b/client/assets/characters/clothing/coveralls_basic/reference.glb new file mode 100644 index 000000000..e5964521e Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/reference.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/reference_mask.png b/client/assets/characters/clothing/coveralls_basic/reference_mask.png new file mode 100644 index 000000000..93770e0b3 Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/reference_mask.png differ diff --git a/client/assets/characters/clothing/coveralls_basic/teen_f.glb b/client/assets/characters/clothing/coveralls_basic/teen_f.glb new file mode 100644 index 000000000..d069cdf45 Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/teen_f.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/teen_m.glb b/client/assets/characters/clothing/coveralls_basic/teen_m.glb new file mode 100644 index 000000000..258b412e0 Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/teen_m.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/thin_f.glb b/client/assets/characters/clothing/coveralls_basic/thin_f.glb new file mode 100644 index 000000000..ff5ca69a2 Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/thin_f.glb differ diff --git a/client/assets/characters/clothing/coveralls_basic/thin_m.glb b/client/assets/characters/clothing/coveralls_basic/thin_m.glb new file mode 100644 index 000000000..73bc2b66d Binary files /dev/null and b/client/assets/characters/clothing/coveralls_basic/thin_m.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/average_f.glb b/client/assets/characters/clothing/jacket_utility/average_f.glb new file mode 100644 index 000000000..e125f083e Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/average_f.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/average_m.glb b/client/assets/characters/clothing/jacket_utility/average_m.glb new file mode 100644 index 000000000..4cc7705e1 Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/average_m.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/child.glb b/client/assets/characters/clothing/jacket_utility/child.glb new file mode 100644 index 000000000..6b5c62c0d Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/child.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/coverage.json b/client/assets/characters/clothing/jacket_utility/coverage.json new file mode 100644 index 000000000..953c402e8 --- /dev/null +++ b/client/assets/characters/clothing/jacket_utility/coverage.json @@ -0,0 +1,10 @@ +{ + "hides": [ + "torso", + "arm_upper_l", + "arm_upper_r", + "arm_lower_l", + "arm_lower_r" + ], + "torso_variant": "full" +} \ No newline at end of file diff --git a/client/assets/characters/clothing/jacket_utility/heavy_f.glb b/client/assets/characters/clothing/jacket_utility/heavy_f.glb new file mode 100644 index 000000000..a0ba96643 Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/heavy_f.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/heavy_m.glb b/client/assets/characters/clothing/jacket_utility/heavy_m.glb new file mode 100644 index 000000000..8e962d60a Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/heavy_m.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/muscular_f.glb b/client/assets/characters/clothing/jacket_utility/muscular_f.glb new file mode 100644 index 000000000..45d4102b6 Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/muscular_f.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/muscular_m.glb b/client/assets/characters/clothing/jacket_utility/muscular_m.glb new file mode 100644 index 000000000..af214c7bd Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/muscular_m.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/reference.glb b/client/assets/characters/clothing/jacket_utility/reference.glb new file mode 100644 index 000000000..4cc7705e1 Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/reference.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/reference_mask.png b/client/assets/characters/clothing/jacket_utility/reference_mask.png new file mode 100644 index 000000000..93770e0b3 Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/reference_mask.png differ diff --git a/client/assets/characters/clothing/jacket_utility/teen_f.glb b/client/assets/characters/clothing/jacket_utility/teen_f.glb new file mode 100644 index 000000000..f2681956a Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/teen_f.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/teen_m.glb b/client/assets/characters/clothing/jacket_utility/teen_m.glb new file mode 100644 index 000000000..a1c7f405c Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/teen_m.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/thin_f.glb b/client/assets/characters/clothing/jacket_utility/thin_f.glb new file mode 100644 index 000000000..2c222a62e Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/thin_f.glb differ diff --git a/client/assets/characters/clothing/jacket_utility/thin_m.glb b/client/assets/characters/clothing/jacket_utility/thin_m.glb new file mode 100644 index 000000000..2b0c3de3d Binary files /dev/null and b/client/assets/characters/clothing/jacket_utility/thin_m.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/average_f.glb b/client/assets/characters/clothing/pants_cargo/average_f.glb new file mode 100644 index 000000000..bcd344c72 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/average_f.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/average_m.glb b/client/assets/characters/clothing/pants_cargo/average_m.glb new file mode 100644 index 000000000..7c03bc918 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/average_m.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/child.glb b/client/assets/characters/clothing/pants_cargo/child.glb new file mode 100644 index 000000000..4c039b510 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/child.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/coverage.json b/client/assets/characters/clothing/pants_cargo/coverage.json new file mode 100644 index 000000000..d6e40331a --- /dev/null +++ b/client/assets/characters/clothing/pants_cargo/coverage.json @@ -0,0 +1,9 @@ +{ + "hides": [ + "leg_upper_l", + "leg_upper_r", + "leg_lower_l", + "leg_lower_r" + ], + "torso_variant": "full" +} \ No newline at end of file diff --git a/client/assets/characters/clothing/pants_cargo/heavy_f.glb b/client/assets/characters/clothing/pants_cargo/heavy_f.glb new file mode 100644 index 000000000..f160d9b17 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/heavy_f.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/heavy_m.glb b/client/assets/characters/clothing/pants_cargo/heavy_m.glb new file mode 100644 index 000000000..6410dc8ba Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/heavy_m.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/muscular_f.glb b/client/assets/characters/clothing/pants_cargo/muscular_f.glb new file mode 100644 index 000000000..9d50c04b0 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/muscular_f.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/muscular_m.glb b/client/assets/characters/clothing/pants_cargo/muscular_m.glb new file mode 100644 index 000000000..edba5e2b2 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/muscular_m.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/reference.glb b/client/assets/characters/clothing/pants_cargo/reference.glb new file mode 100644 index 000000000..7c03bc918 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/reference.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/reference_mask.png b/client/assets/characters/clothing/pants_cargo/reference_mask.png new file mode 100644 index 000000000..93770e0b3 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/reference_mask.png differ diff --git a/client/assets/characters/clothing/pants_cargo/teen_f.glb b/client/assets/characters/clothing/pants_cargo/teen_f.glb new file mode 100644 index 000000000..6072e587a Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/teen_f.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/teen_m.glb b/client/assets/characters/clothing/pants_cargo/teen_m.glb new file mode 100644 index 000000000..6e22a5324 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/teen_m.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/thin_f.glb b/client/assets/characters/clothing/pants_cargo/thin_f.glb new file mode 100644 index 000000000..543808621 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/thin_f.glb differ diff --git a/client/assets/characters/clothing/pants_cargo/thin_m.glb b/client/assets/characters/clothing/pants_cargo/thin_m.glb new file mode 100644 index 000000000..92ea2a9b8 Binary files /dev/null and b/client/assets/characters/clothing/pants_cargo/thin_m.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/average_f.glb b/client/assets/characters/clothing/shirt_henley/average_f.glb new file mode 100644 index 000000000..3d327aed0 Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/average_f.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/average_m.glb b/client/assets/characters/clothing/shirt_henley/average_m.glb new file mode 100644 index 000000000..f43989352 Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/average_m.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/child.glb b/client/assets/characters/clothing/shirt_henley/child.glb new file mode 100644 index 000000000..9cb3e7ba6 Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/child.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/coverage.json b/client/assets/characters/clothing/shirt_henley/coverage.json new file mode 100644 index 000000000..78b67382d --- /dev/null +++ b/client/assets/characters/clothing/shirt_henley/coverage.json @@ -0,0 +1,8 @@ +{ + "hides": [ + "torso", + "arm_upper_l", + "arm_upper_r" + ], + "torso_variant": "full" +} \ No newline at end of file diff --git a/client/assets/characters/clothing/shirt_henley/heavy_f.glb b/client/assets/characters/clothing/shirt_henley/heavy_f.glb new file mode 100644 index 000000000..1d52ee0fc Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/heavy_f.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/heavy_m.glb b/client/assets/characters/clothing/shirt_henley/heavy_m.glb new file mode 100644 index 000000000..7b162539a Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/heavy_m.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/muscular_f.glb b/client/assets/characters/clothing/shirt_henley/muscular_f.glb new file mode 100644 index 000000000..8f23f72a0 Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/muscular_f.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/muscular_m.glb b/client/assets/characters/clothing/shirt_henley/muscular_m.glb new file mode 100644 index 000000000..4ddc103e3 Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/muscular_m.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/reference.glb b/client/assets/characters/clothing/shirt_henley/reference.glb new file mode 100644 index 000000000..f43989352 Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/reference.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/reference_mask.png b/client/assets/characters/clothing/shirt_henley/reference_mask.png new file mode 100644 index 000000000..93770e0b3 Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/reference_mask.png differ diff --git a/client/assets/characters/clothing/shirt_henley/teen_f.glb b/client/assets/characters/clothing/shirt_henley/teen_f.glb new file mode 100644 index 000000000..0f3af89c8 Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/teen_f.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/teen_m.glb b/client/assets/characters/clothing/shirt_henley/teen_m.glb new file mode 100644 index 000000000..c027b5b9e Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/teen_m.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/thin_f.glb b/client/assets/characters/clothing/shirt_henley/thin_f.glb new file mode 100644 index 000000000..61262b8b0 Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/thin_f.glb differ diff --git a/client/assets/characters/clothing/shirt_henley/thin_m.glb b/client/assets/characters/clothing/shirt_henley/thin_m.glb new file mode 100644 index 000000000..2a0e358bb Binary files /dev/null and b/client/assets/characters/clothing/shirt_henley/thin_m.glb differ diff --git a/tooling/blender_create_clothing_refs.py b/tooling/blender_create_clothing_refs.py new file mode 100644 index 000000000..61c9922e8 --- /dev/null +++ b/tooling/blender_create_clothing_refs.py @@ -0,0 +1,257 @@ +""" +blender_create_clothing_refs.py + +Usage: + tooling/blender --background --python tooling/blender_create_clothing_refs.py -- \\ + + +Creates v0.2 placeholder reference clothing meshes for all 5 initial clothing items. +Meshes are authored on average_m by importing the relevant body segments, joining +them, and offsetting vertices outward along normals to simulate clothing thickness. + +This produces placeholder-quality geometry only — not final art. The geometry reads +correctly at gameplay zoom and is sufficient to validate the Surface Deform pipeline +and compositor integration. + +Arguments: + bodies_dir Directory with one subdir per body type (e.g. client/assets/characters/bodies/) + clothing_output_dir Root directory for clothing output (e.g. client/assets/characters/clothing/) + +Output per item in //: + reference.glb -- placeholder clothing mesh on average_m geometry + +Items produced: + coveralls_basic -- full-body work suit + jacket_utility -- upper body outerwear + pants_cargo -- lower body + shirt_henley -- upper body inner + boots_work -- foot slot + +Decisions: D-162 (clothing pre-baked per body type via Surface Deform) +""" + +import sys +import os +import bpy +import bmesh + +REFERENCE_BODY = "average_m" + +# Each item defines: +# segments -- body segment GLBs (from average_m) to import and join +# thickness -- outward vertex offset in metres (clothing thickness simulation) +CLOTHING_ITEMS = { + "coveralls_basic": { + "description": "Full-body work suit", + "segments": [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", + "seg_hand_l", "seg_hand_r", + "seg_leg_upper_l", "seg_leg_upper_r", + "seg_leg_lower_l", "seg_leg_lower_r", + "seg_foot_l", "seg_foot_r", + ], + "thickness": 0.006, + }, + "jacket_utility": { + "description": "Upper body outerwear", + "segments": [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", + ], + "thickness": 0.008, # slightly thicker for outerwear + }, + "pants_cargo": { + "description": "Lower body cargo trousers", + "segments": [ + "seg_leg_upper_l", "seg_leg_upper_r", + "seg_leg_lower_l", "seg_leg_lower_r", + ], + "thickness": 0.006, + }, + "shirt_henley": { + "description": "Upper body inner shirt", + "segments": [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + ], + "thickness": 0.004, # thinner for inner layer + }, + "boots_work": { + "description": "Work boots (foot slot)", + "segments": [ + "seg_foot_l", "seg_foot_r", + "seg_leg_lower_l", "seg_leg_lower_r", # boot shaft reaches up the lower leg + ], + "thickness": 0.010, # thicker for boots + }, +} + + +def clear_scene(): + """Remove all objects and purge orphan data.""" + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete() + bpy.ops.outliner.orphans_purge(do_recursive=True) + + +def import_glb(path): + """Import a GLB file. Returns newly added objects.""" + before = set(bpy.context.scene.objects) + bpy.ops.import_scene.gltf(filepath=path) + return [o for o in bpy.context.scene.objects if o not in before] + + +def offset_vertices_along_normals(obj, thickness): + """ + Move each vertex outward along its computed normal by `thickness` metres. + Operates directly on mesh data -- no operators, reliable in headless mode. + """ + bm = bmesh.new() + bm.from_mesh(obj.data) + bm.verts.ensure_lookup_table() + + # Ensure normals are up to date + bm.normal_update() + + for v in bm.verts: + v.co += v.normal * thickness + + bm.to_mesh(obj.data) + bm.free() + obj.data.update() + + +def export_glb(obj, output_path): + """Export a single mesh object as GLB with UVs, normals, and materials.""" + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + bpy.ops.export_scene.gltf( + filepath=output_path, + use_selection=True, + export_format='GLB', + export_animations=False, + export_yup=True, + export_image_format='AUTO', + export_texcoords=True, + export_normals=True, + export_skins=False, + export_materials='EXPORT', + ) + + +def create_reference_mesh(item_id, config, bodies_dir, output_dir): + """ + Create the reference.glb for one clothing item on average_m geometry. + Returns True on success, False on error. + """ + print(f"\n{'='*60}") + print(f" Item: {item_id} — {config['description']}") + + clear_scene() + + average_m_dir = os.path.join(bodies_dir, REFERENCE_BODY) + imported_meshes = [] + missing_segments = [] + + for seg_name in config["segments"]: + seg_path = os.path.join(average_m_dir, f"{seg_name}.glb") + if not os.path.isfile(seg_path): + missing_segments.append(seg_name) + continue + objs = import_glb(seg_path) + meshes = [o for o in objs if o.type == 'MESH'] + imported_meshes.extend(meshes) + + if missing_segments: + print(f" NOTE: Missing segments (skipped): {', '.join(missing_segments)}") + + if not imported_meshes: + print(f" ERROR: No segment meshes could be imported for {item_id}") + return False + + print(f" Imported {len(imported_meshes)} segments " + f"({len(missing_segments)} missing)") + + # Join all segments into one mesh + bpy.ops.object.select_all(action='DESELECT') + for m in imported_meshes: + m.select_set(True) + bpy.context.view_layer.objects.active = imported_meshes[0] + if len(imported_meshes) > 1: + bpy.ops.object.join() + + clothing_obj = bpy.context.active_object + clothing_obj.name = f"ref_{item_id}" + + vertex_count_before = len(clothing_obj.data.vertices) + print(f" Mesh vertices: {vertex_count_before}") + + # Offset vertices outward to simulate clothing thickness + thickness = config["thickness"] + offset_vertices_along_normals(clothing_obj, thickness) + print(f" Applied {thickness*1000:.1f}mm outward offset") + + # Export + os.makedirs(output_dir, exist_ok=True) + output_path = os.path.join(output_dir, "reference.glb") + export_glb(clothing_obj, output_path) + print(f" Exported: {output_path}") + + return True + + +# ------------------------------------------------------------------------- +# Entry point +# ------------------------------------------------------------------------- + +if __name__ == "__main__": + argv = sys.argv + if "--" not in argv: + print("Usage: tooling/blender --background --python " + "tooling/blender_create_clothing_refs.py -- " + " ") + sys.exit(1) + + args = argv[argv.index("--") + 1:] + if len(args) < 2: + print("ERROR: Provide and ") + sys.exit(1) + + bodies_dir = args[0] + clothing_output_dir = args[1] + + # Preflight: check average_m dir exists + avg_m_dir = os.path.join(bodies_dir, REFERENCE_BODY) + if not os.path.isdir(avg_m_dir): + print(f"ERROR: Reference body directory not found: {avg_m_dir}") + sys.exit(1) + + print(f"\nClothing Reference Mesh Creator (v0.2 placeholder)") + print(f" Bodies dir: {bodies_dir}") + print(f" Output dir: {clothing_output_dir}") + print(f" Reference: {REFERENCE_BODY}") + print(f" Items: {len(CLOTHING_ITEMS)}") + + results = {} + for item_id, config in CLOTHING_ITEMS.items(): + item_output_dir = os.path.join(clothing_output_dir, item_id) + success = create_reference_mesh(item_id, config, bodies_dir, item_output_dir) + results[item_id] = success + + # Summary + print(f"\n{'='*60}") + print(f"=== Clothing reference creation complete ===") + ok_items = [k for k, v in results.items() if v] + fail_items = [k for k, v in results.items() if not v] + for item_id in CLOTHING_ITEMS: + status = "OK" if results[item_id] else "FAILED" + print(f" {item_id:20s} {status}") + print(f"\n OK: {len(ok_items)} FAILED: {len(fail_items)}") + + if fail_items: + sys.exit(1) diff --git a/tooling/blender_surface_deform_batch.py b/tooling/blender_surface_deform_batch.py new file mode 100644 index 000000000..e0a03f7c2 --- /dev/null +++ b/tooling/blender_surface_deform_batch.py @@ -0,0 +1,459 @@ +""" +blender_surface_deform_batch.py + +Usage: + tooling/blender --background --python tooling/blender_surface_deform_batch.py -- \\ + + +Fits a reference clothing GLB (authored on average_m) to all 11 body types via +Blender's Surface Deform modifier and writes fitted GLBs to the output directory. + +Arguments: + reference_glb Path to the reference clothing mesh (modeled on average_m). + bodies_dir Directory containing one subdirectory per body type, each with + seg_*.glb segment files (e.g. client/assets/characters/bodies/). + output_dir Directory to write fitted variants. Created if it does not exist. + +Output per run: + /thin_m.glb + /thin_f.glb + /average_m.glb <- direct copy of reference_glb + /average_f.glb + /muscular_m.glb + /muscular_f.glb + /teen_m.glb + /teen_f.glb + /heavy_m.glb + /heavy_f.glb + /child.glb + +Headless Surface Deform reliability: + MEDIUM. The modifier bind operator requires an active object context. This + script applies a context override (bpy.context.temp_override) to satisfy + the operator. If the bind fails (is_bound == False after the attempt), the + script falls back to a Shrinkwrap ON_SURFACE projection and logs a WARNING. + + The Shrinkwrap fallback produces acceptable results for most garments but + may cause pinching at extremities on extreme body types (heavy_m, heavy_f). + Visually verify all output variants at gameplay zoom before treating this + pipeline as production-ready. See VERDICT.md open question: Surface Deform + visual quality at extreme body types. + +UV layout: + Surface Deform only moves vertex positions -- UVs are not altered. + Shrinkwrap fallback also preserves UV layout. + +Decisions: D-162 (clothing pre-baked per body type via Surface Deform) +""" + +import sys +import os +import json +import shutil +import bpy + +# ------------------------------------------------------------------------- +# Constants +# ------------------------------------------------------------------------- + +BODY_TYPES = [ + "thin_m", + "thin_f", + "average_m", # reference copy -- no deform + "average_f", + "muscular_m", + "muscular_f", + "teen_m", + "teen_f", + "heavy_m", + "heavy_f", + "child", +] + +REFERENCE_BODY = "average_m" + +# Body segments to import for the deform surface. +# Eyes and eyebrows are excluded -- they are tiny facial sub-objects that +# do not affect clothing deformation. +DEFORM_SEGMENTS = [ + "seg_head", + "seg_neck", + "seg_torso", + "seg_torso_upper", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_arm_lower_l", + "seg_arm_lower_r", + "seg_hand_l", + "seg_hand_r", + "seg_leg_upper_l", + "seg_leg_upper_r", + "seg_leg_lower_l", + "seg_leg_lower_r", + "seg_foot_l", + "seg_foot_r", +] + +SHRINKWRAP_OFFSET = 0.002 # Small offset to avoid z-fighting (metres) + + +# ------------------------------------------------------------------------- +# Scene helpers +# ------------------------------------------------------------------------- + +def clear_scene(): + """Remove all objects from the current scene.""" + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete() + # Also purge orphan mesh/material data to avoid memory bloat in batch runs + bpy.ops.outliner.orphans_purge(do_recursive=True) + + +def import_glb(path): + """Import a GLB file. Returns newly created objects.""" + before = set(bpy.context.scene.objects) + bpy.ops.import_scene.gltf(filepath=path) + return [o for o in bpy.context.scene.objects if o not in before] + + +def get_meshes(objects): + """Filter a list of objects to mesh-type only.""" + return [o for o in objects if o.type == 'MESH'] + + +def export_glb(obj, output_path): + """ + Export a single mesh object as GLB. + Preserves UVs, normals, and materials. No armature or animations. + """ + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + bpy.ops.export_scene.gltf( + filepath=output_path, + use_selection=True, + export_format='GLB', + export_animations=False, + export_yup=True, + export_image_format='AUTO', + export_texcoords=True, + export_normals=True, + export_skins=False, + export_materials='EXPORT', + ) + + +# ------------------------------------------------------------------------- +# Body surface construction +# ------------------------------------------------------------------------- + +def build_body_surface(bodies_dir, body_type): + """ + Import and join body segments for the given body type into a single + mesh object suitable for Surface Deform binding. + + Returns the joined mesh object, or None if the body type directory is + missing or contains no usable segments. + """ + body_dir = os.path.join(bodies_dir, body_type) + if not os.path.isdir(body_dir): + print(f" WARNING: Body type directory not found: {body_dir}") + return None + + imported = [] + missing = [] + + for seg_name in DEFORM_SEGMENTS: + seg_path = os.path.join(body_dir, f"{seg_name}.glb") + if not os.path.isfile(seg_path): + missing.append(seg_name) + continue + objs = import_glb(seg_path) + imported.extend(get_meshes(objs)) + + if missing: + print(f" NOTE: {len(missing)} segments missing for {body_type} " + f"(non-fatal): {', '.join(missing)}") + + if not imported: + print(f" ERROR: No segment meshes imported for {body_type}") + return None + + print(f" Imported {len(imported)} segments for {body_type}") + + # Select all imported meshes and join them into one + bpy.ops.object.select_all(action='DESELECT') + for obj in imported: + obj.select_set(True) + bpy.context.view_layer.objects.active = imported[0] + bpy.ops.object.join() + + body_surface = bpy.context.active_object + body_surface.name = f"body_surface_{body_type}" + return body_surface + + +# ------------------------------------------------------------------------- +# Surface Deform fitting +# ------------------------------------------------------------------------- + +def try_surface_deform_bind(clothing_obj, body_surface, mod_name): + """ + Attempt to bind the Surface Deform modifier using a context override. + Returns True if bind succeeded, False otherwise. + + Headless workaround: + bpy.ops.object.surfacedeform_bind() requires an active viewport window + context. In --background mode, we construct a temporary override that + satisfies the operator's active_object requirement. Blender 3.6+ supports + this via bpy.context.temp_override(). + """ + clothing_obj.select_set(True) + bpy.context.view_layer.objects.active = clothing_obj + + try: + with bpy.context.temp_override( + active_object=clothing_obj, + object=clothing_obj, + selected_objects=[clothing_obj], + ): + result = bpy.ops.object.surfacedeform_bind(modifier=mod_name) + + mod = clothing_obj.modifiers.get(mod_name) + if mod and mod.is_bound: + print(" Surface Deform bind: SUCCESS") + return True + else: + print(f" Surface Deform bind: operator returned {result}, " + f"is_bound=False — bind did not complete") + return False + + except Exception as exc: + print(f" Surface Deform bind: EXCEPTION — {exc}") + return False + + +def apply_shrinkwrap_fallback(clothing_obj, body_surface): + """ + Fallback fitting via Shrinkwrap ON_SURFACE projection. + + Less accurate than Surface Deform (no barycentric interpolation) but + reliable in headless mode. May cause pinching at extremities. + UV layout is preserved. + """ + print(" Using Shrinkwrap fallback (Surface Deform bind failed)") + sw = clothing_obj.modifiers.new("ShrinkwrapFit", 'SHRINKWRAP') + sw.target = body_surface + sw.wrap_method = 'NEAREST_SURFACEPOINT' + sw.wrap_mode = 'ON_SURFACE' + sw.offset = SHRINKWRAP_OFFSET + + bpy.ops.object.select_all(action='DESELECT') + clothing_obj.select_set(True) + bpy.context.view_layer.objects.active = clothing_obj + bpy.ops.object.modifier_apply(modifier=sw.name) + + +def fit_clothing_to_body(clothing_obj, body_surface): + """ + Fit clothing_obj to body_surface using Surface Deform, with Shrinkwrap + fallback. Returns the method used ('surface_deform' or 'shrinkwrap'). + """ + # Add Surface Deform modifier + mod = clothing_obj.modifiers.new("SurfaceDeformFit", 'SURFACE_DEFORM') + mod.target = body_surface + mod_name = mod.name + + bound = try_surface_deform_bind(clothing_obj, body_surface, mod_name) + + if bound: + # Apply the Surface Deform modifier + bpy.ops.object.select_all(action='DESELECT') + clothing_obj.select_set(True) + bpy.context.view_layer.objects.active = clothing_obj + bpy.ops.object.modifier_apply(modifier=mod_name) + return 'surface_deform' + else: + # Remove the failed Surface Deform modifier before falling back + clothing_obj.modifiers.remove(mod) + apply_shrinkwrap_fallback(clothing_obj, body_surface) + return 'shrinkwrap' + + +# ------------------------------------------------------------------------- +# Main per-body-type processing +# ------------------------------------------------------------------------- + +def process_body_type(body_type, reference_glb, bodies_dir, output_dir): + """ + Process a single body type. Returns a result dict with status info. + """ + output_path = os.path.join(output_dir, f"{body_type}.glb") + + print(f"\n{'='*60}") + print(f" Body type: {body_type}") + + # average_m is a direct copy -- no deform required + if body_type == REFERENCE_BODY: + print(f" Reference body type -- copying reference directly") + shutil.copy2(reference_glb, output_path) + return {"body_type": body_type, "status": "ok", "method": "copy"} + + clear_scene() + + # --- Import reference clothing --- + print(f" Importing reference clothing: {os.path.basename(reference_glb)}") + clothing_objs = import_glb(reference_glb) + clothing_meshes = get_meshes(clothing_objs) + if not clothing_meshes: + return {"body_type": body_type, "status": "error", + "error": "No mesh found in reference GLB"} + + # If reference GLB contains multiple meshes, join them + if len(clothing_meshes) > 1: + bpy.ops.object.select_all(action='DESELECT') + for m in clothing_meshes: + m.select_set(True) + bpy.context.view_layer.objects.active = clothing_meshes[0] + bpy.ops.object.join() + clothing_obj = bpy.context.active_object + clothing_obj.name = "clothing_ref" + print(f" Clothing mesh: {len(clothing_obj.data.vertices)} vertices") + + # --- Build body surface --- + print(f" Building body surface from segments...") + body_surface = build_body_surface(bodies_dir, body_type) + if body_surface is None: + return {"body_type": body_type, "status": "error", + "error": "Could not construct body surface"} + + print(f" Body surface: {len(body_surface.data.vertices)} vertices") + + # --- Fit clothing to body --- + method = fit_clothing_to_body(clothing_obj, body_surface) + + # --- Export fitted clothing --- + # Delete body surface first so only clothing exports + bpy.data.objects.remove(body_surface, do_unlink=True) + + # Re-get clothing obj (still active after modifier apply) + clothing_obj = bpy.context.active_object + print(f" Exporting to {os.path.basename(output_path)}") + export_glb(clothing_obj, output_path) + + return {"body_type": body_type, "status": "ok", "method": method} + + +# ------------------------------------------------------------------------- +# Entry point +# ------------------------------------------------------------------------- + +if __name__ == "__main__": + argv = sys.argv + if "--" not in argv: + print("Usage: tooling/blender --background --python " + "tooling/blender_surface_deform_batch.py -- " + " ") + sys.exit(1) + + args = argv[argv.index("--") + 1:] + if len(args) < 3: + print("ERROR: Provide , , and ") + sys.exit(1) + + reference_glb = args[0] + bodies_dir = args[1] + output_dir = args[2] + + # Preflight checks + if not os.path.isfile(reference_glb): + print(f"ERROR: Reference GLB not found: {reference_glb}") + sys.exit(1) + + if not os.path.isdir(bodies_dir): + print(f"ERROR: Bodies directory not found: {bodies_dir}") + sys.exit(1) + + os.makedirs(output_dir, exist_ok=True) + + # Validate that body type directories are present + missing_types = [ + bt for bt in BODY_TYPES + if bt != REFERENCE_BODY and not os.path.isdir(os.path.join(bodies_dir, bt)) + ] + if missing_types: + print(f"\nWARNING: Missing body type directories (will skip): " + f"{', '.join(missing_types)}") + + print(f"\nSurface Deform Batch Pipeline") + print(f" Reference: {reference_glb}") + print(f" Bodies: {bodies_dir}") + print(f" Output: {output_dir}") + print(f" Types: {len(BODY_TYPES)}") + + results = [] + for body_type in BODY_TYPES: + if body_type != REFERENCE_BODY and \ + not os.path.isdir(os.path.join(bodies_dir, body_type)): + results.append({"body_type": body_type, "status": "skipped", + "error": "body type directory missing"}) + continue + + result = process_body_type(body_type, reference_glb, bodies_dir, output_dir) + results.append(result) + + # Summary + print(f"\n{'='*60}") + print(f"=== Surface Deform batch complete ===") + ok = [r for r in results if r["status"] == "ok"] + errors = [r for r in results if r["status"] == "error"] + skipped = [r for r in results if r["status"] == "skipped"] + sd_count = len([r for r in ok if r.get("method") == "surface_deform"]) + sw_count = len([r for r in ok if r.get("method") == "shrinkwrap"]) + copy_count = len([r for r in ok if r.get("method") == "copy"]) + + for r in results: + status = r["status"].upper() + method = r.get("method", "") + error = r.get("error", "") + if method: + print(f" {r['body_type']:15s} {status:8s} [{method}]") + elif error: + print(f" {r['body_type']:15s} {status:8s} {error}") + else: + print(f" {r['body_type']:15s} {status:8s}") + + print(f"\n OK: {len(ok)} " + f"(surface_deform={sd_count}, shrinkwrap={sw_count}, copy={copy_count})") + if errors: + print(f" ERRORS: {len(errors)}") + for r in errors: + print(f" {r['body_type']}: {r.get('error', '?')}") + if skipped: + print(f" SKIPPED: {len(skipped)}") + if sw_count > 0: + print(f"\n WARNING: {sw_count} body type(s) used Shrinkwrap fallback.") + print(f" Visually verify these variants at gameplay zoom — Shrinkwrap") + print(f" may produce pinching at extremities on extreme body types.") + + # Write pipeline_log.json for provenance tracking + log_path = os.path.join(output_dir, "pipeline_log.json") + log_data = { + "reference": os.path.basename(reference_glb), + "bodies_dir": bodies_dir, + "variants": { + r["body_type"]: { + "status": r["status"], + "method": r.get("method", None), + "error": r.get("error", None), + } + for r in results + }, + } + with open(log_path, 'w') as f: + json.dump(log_data, f, indent=2) + print(f"\n Pipeline log written to {log_path}") + + if errors: + sys.exit(1) diff --git a/tooling/setup_clothing_metadata.py b/tooling/setup_clothing_metadata.py new file mode 100644 index 000000000..630ec5dce --- /dev/null +++ b/tooling/setup_clothing_metadata.py @@ -0,0 +1,193 @@ +""" +setup_clothing_metadata.py + +Usage: python3 tooling/setup_clothing_metadata.py + +Creates coverage.json and reference_mask.png for all 5 initial clothing items. +This is a pure-Python script — no Blender required. + +Arguments: + clothing_dir Root directory for clothing output (e.g. client/assets/characters/clothing/). + Each item subdirectory must already exist. + +Files created per item: + coverage.json -- segment hides + torso_variant (compositor input) + reference_mask.png -- greyscale recolor mask (all-white v0.2 placeholder: fully tintable) + +coverage.json format: + { + "hides": ["torso", "arm_upper_l", ...], + "torso_variant": "full" + } + + hides entries use short segment names (without "seg_" prefix). + torso_variant: "full" = clothing hides seg_torso (full torso segment) + : "upper" = clothing only hides seg_torso_upper (upper chest) + +reference_mask.png: + 64x64 grayscale PNG. White (255) = fully tintable. v0.2 placeholder — all white. + Final art will have partial masks (black areas = preserve material texture color). + +Decisions: D-162 (clothing pre-baked per body type via Surface Deform) +""" + +import sys +import os +import json +import zlib +import struct + +# ------------------------------------------------------------------------- +# Clothing catalogue metadata +# ------------------------------------------------------------------------- + +CLOTHING_ITEMS = { + "coveralls_basic": { + "description": "Full-body work suit", + "hides": [ + "torso", + "arm_upper_l", "arm_upper_r", + "arm_lower_l", "arm_lower_r", + "hand_l", "hand_r", + "leg_upper_l", "leg_upper_r", + "leg_lower_l", "leg_lower_r", + "foot_l", "foot_r", + ], + "torso_variant": "full", + }, + "jacket_utility": { + "description": "Upper body outerwear", + "hides": [ + "torso", + "arm_upper_l", "arm_upper_r", + "arm_lower_l", "arm_lower_r", + ], + "torso_variant": "full", + }, + "pants_cargo": { + # Note: pants hide leg segments but NOT foot segments. Boots (boots_work) + # hide feet. This distinction matters for the compositor — a character + # wearing pants + no boots shows bare feet via seg_foot_l/r. + "description": "Lower body cargo trousers", + "hides": [ + "leg_upper_l", "leg_upper_r", + "leg_lower_l", "leg_lower_r", + ], + "torso_variant": "full", + }, + "shirt_henley": { + "description": "Upper body inner shirt", + "hides": [ + "torso", + "arm_upper_l", "arm_upper_r", + ], + "torso_variant": "full", + }, + "boots_work": { + "description": "Work boots (foot slot + lower leg shaft)", + "hides": [ + "foot_l", "foot_r", + "leg_lower_l", "leg_lower_r", + ], + "torso_variant": "full", + }, +} + +MASK_WIDTH = 64 +MASK_HEIGHT = 64 + + +# ------------------------------------------------------------------------- +# Minimal PNG writer (no external dependencies) +# ------------------------------------------------------------------------- + +def _make_png_chunk(chunk_type, data): + """Construct a PNG chunk with CRC.""" + payload = chunk_type + data + crc = zlib.crc32(payload) & 0xFFFFFFFF + return struct.pack(">I", len(data)) + payload + struct.pack(">I", crc) + + +def write_white_grayscale_png(path, width=64, height=64): + """ + Write a minimal all-white grayscale PNG to `path`. + Uses only Python built-ins (zlib, struct) — no Pillow required. + """ + # PNG signature + signature = b'\x89PNG\r\n\x1a\n' + + # IHDR: width, height, bit_depth=8, colortype=0 (grayscale), + # compression=0, filter=0, interlace=0 + ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 0, 0, 0, 0) + ihdr = _make_png_chunk(b'IHDR', ihdr_data) + + # IDAT: raw scanlines — filter byte 0x00 (None) + width bytes of 0xFF (white) + raw_rows = b''.join(b'\x00' + b'\xff' * width for _ in range(height)) + compressed = zlib.compress(raw_rows, 9) + idat = _make_png_chunk(b'IDAT', compressed) + + # IEND + iend = _make_png_chunk(b'IEND', b'') + + with open(path, 'wb') as f: + f.write(signature + ihdr + idat + iend) + + +# ------------------------------------------------------------------------- +# Per-item setup +# ------------------------------------------------------------------------- + +def setup_item(item_id, config, clothing_dir): + """ + Write coverage.json and reference_mask.png for one clothing item. + Returns (coverage_ok, mask_ok). + """ + item_dir = os.path.join(clothing_dir, item_id) + os.makedirs(item_dir, exist_ok=True) + + # coverage.json + coverage = { + "hides": config["hides"], + "torso_variant": config["torso_variant"], + } + coverage_path = os.path.join(item_dir, "coverage.json") + with open(coverage_path, 'w') as f: + json.dump(coverage, f, indent=2) + coverage_ok = True + + # reference_mask.png (all-white placeholder) + mask_path = os.path.join(item_dir, "reference_mask.png") + write_white_grayscale_png(mask_path, MASK_WIDTH, MASK_HEIGHT) + mask_ok = True + + return coverage_ok, mask_ok + + +# ------------------------------------------------------------------------- +# Entry point +# ------------------------------------------------------------------------- + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python3 tooling/setup_clothing_metadata.py ") + sys.exit(1) + + clothing_dir = sys.argv[1] + + print(f"\nClothing Metadata Setup") + print(f" Output dir: {clothing_dir}") + print(f" Items: {len(CLOTHING_ITEMS)}") + + all_ok = True + for item_id, config in CLOTHING_ITEMS.items(): + coverage_ok, mask_ok = setup_item(item_id, config, clothing_dir) + status = "OK" if (coverage_ok and mask_ok) else "FAILED" + print(f" {item_id:20s} {status} " + f"coverage={'ok' if coverage_ok else 'FAIL'} " + f"mask={'ok' if mask_ok else 'FAIL'}") + if not (coverage_ok and mask_ok): + all_ok = False + + print(f"\n Done. {'All OK.' if all_ok else 'Some items failed.'}") + if not all_ok: + sys.exit(1)