diff --git a/.gitignore b/.gitignore index 0e08a79..3fd846c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ pipeline/.genprops/ web/assets/models/ pipeline/.peds_orig/ pipeline/.props_orig/ +pipeline/.peds_r6/ diff --git a/docs/LANES/LANE_E_NOTES.md b/docs/LANES/LANE_E_NOTES.md index 62f4a65..c28ac7c 100644 --- a/docs/LANES/LANE_E_NOTES.md +++ b/docs/LANES/LANE_E_NOTES.md @@ -1,5 +1,23 @@ # LANE E — cross-lane notes +## Round 7 + +**→ Lane D (D1 — the flip gate): merged peds ready to validate.** `web/models/peds/*.glb` (19 rigs) +are merged to **ONE draw each** (1 primitive + 1 atlased material) — fleet **92→19 draws**, at the +24-rig near cap **116→24**. Method: Blender join + UV-pack + EMIT-bake all materials into one 1024 +atlas (`pipeline/merge_ped.py`). **Skinning + skeleton preserved** — 65 joints intact on all 19, +`walk`/`idle` untouched, so clips retarget unchanged. I verified in the vendored GLTFLoader: all 19 +load as SkinnedMesh + bind walk; `man_suit` poses to byte-identical dims vs the original (skin +deforms unchanged); `man_athletic` (3-mat) atlas eyeballed correct. Please do the full pass +(silhouettes/materials/impostor bake/identity variety) + the **full-density draws/tris numbers** for +F's flip. Pre-merge peds backed up (gitignored) at `pipeline/.peds_r6/` if you need a diff; per-ped +in `pipeline/_peds_merge.json`. **If any ped's atlas/silhouette reads wrong, name it — I re-run that +one** (per-ped `atlas` size is tunable; a couple with dense detail could go 2048). + +**E2 (GODVERSE stock pack) — building against C's schema** (their task 1). Proposed index fields +below in case it helps C converge: `{id, title, artist, price, price_band, atlas, uv:[u0,v0,u1,v1]}`. +Adjusting to C's LANE_C_NOTES contract when posted. + ## Round 6 **E1 done** — `publish.py` now merges into `_published.json` (no more R5 clobber); `--verify` + diff --git a/pipeline/AUDIT.md b/pipeline/AUDIT.md index 7e6dc47..bb51b6e 100644 --- a/pipeline/AUDIT.md +++ b/pipeline/AUDIT.md @@ -261,3 +261,21 @@ Both 3k tris, verified loading in the vendored GLTFLoader, published, `validate Provenance: MeshGod-generated (thriftgod_hero) — synthetic, web-ok. **Note:** also resolved Lane B's B3 ask — `novelty_record` (was 26.5k, spiking the busy view) is now 8k via the E1 bake. No speculative props added; further depot growth is available on request once C's dig / D's roster name concrete needs. + +## Round 7 — E1: ped sub-mesh merge to one draw (gates the roster flip) + +The 19 ped rigs carried **1–9 primitives / 1–3 materials each = 92 draws** for the fleet (≈116 at +the 24-rig near cap) — the optimisation D's memo gates full-density roster on. `pipeline/merge_ped.py` +(headless Blender) merges each ped to **ONE primitive + one atlased material**: join meshes → pack +every material's UV islands into a non-overlapping atlas UV → **EMIT-bake each source material's +base-colour into one 1024 atlas** (route base-colour→emission via the ORIGINAL uv so the atlas UV +doesn't hijack the lookup) → single material. **Skinning + skeleton fully preserved** (65 joints +intact on all 19, `JOINTS_0`/`WEIGHTS_0` kept; `walk`/`idle` untouched). + +Result: **fleet 92 → 19 draws (1/ped)**; at the 24-rig near cap **116 → 24 draws** (79% cut). Size +5M → 4M. Verified in the vendored `GLTFLoader`: all 19 load with `isSkinnedMesh` + bind the shared +`walk.glb` clip; `man_suit` (2-mat) poses to **byte-identical dimensions vs the original** +(109.038 × 62.5) proving the skin deforms unchanged; `man_athletic` (3-mat, most complex atlas) +eyeballed correct (skin/top/pants regions all right). Pre-merge decimated peds backed up (gitignored) +in `pipeline/.peds_r6/`; per-ped in `pipeline/_peds_merge.json`. → **Lane D validates (D1)** — +full silhouette/material/impostor/identity pass + full-density numbers — then **F flips the roster**. diff --git a/pipeline/_peds_merge.json b/pipeline/_peds_merge.json new file mode 100644 index 0000000..347d04d --- /dev/null +++ b/pipeline/_peds_merge.json @@ -0,0 +1,103 @@ +{ + "peds": [ + { + "file": "comical_boy_01.glb", + "size_before_kb": 121, + "size_after_kb": 125 + }, + { + "file": "comical_luchador_01.glb", + "size_before_kb": 159, + "size_after_kb": 163 + }, + { + "file": "man_athletic_01.glb", + "size_before_kb": 429, + "size_after_kb": 288 + }, + { + "file": "man_bouncer_01.glb", + "size_before_kb": 183, + "size_after_kb": 187 + }, + { + "file": "man_casual_01.glb", + "size_before_kb": 179, + "size_after_kb": 184 + }, + { + "file": "man_casual_02.glb", + "size_before_kb": 203, + "size_after_kb": 200 + }, + { + "file": "man_casual_03.glb", + "size_before_kb": 220, + "size_after_kb": 197 + }, + { + "file": "man_dj_streetwear_01.glb", + "size_before_kb": 291, + "size_after_kb": 239 + }, + { + "file": "man_elder_01.glb", + "size_before_kb": 200, + "size_after_kb": 203 + }, + { + "file": "man_grunge_01.glb", + "size_before_kb": 402, + "size_after_kb": 277 + }, + { + "file": "man_soldier_ww2_01.glb", + "size_before_kb": 326, + "size_after_kb": 244 + }, + { + "file": "man_suit_01.glb", + "size_before_kb": 436, + "size_after_kb": 303 + }, + { + "file": "man_worker_hivis_01.glb", + "size_before_kb": 463, + "size_after_kb": 279 + }, + { + "file": "woman_athleisure_01.glb", + "size_before_kb": 395, + "size_after_kb": 267 + }, + { + "file": "woman_business_01.glb", + "size_before_kb": 357, + "size_after_kb": 255 + }, + { + "file": "woman_casual_01.glb", + "size_before_kb": 351, + "size_after_kb": 396 + }, + { + "file": "woman_casual_02.glb", + "size_before_kb": 390, + "size_after_kb": 284 + }, + { + "file": "woman_chef_01.glb", + "size_before_kb": 336, + "size_after_kb": 339 + }, + { + "file": "woman_smart_01.glb", + "size_before_kb": 397, + "size_after_kb": 251 + } + ], + "fleet_draws_before": 92, + "fleet_draws_after": 19, + "near_cap_before": 116, + "near_cap_after": 24 +} \ No newline at end of file diff --git a/pipeline/batch_peds.json b/pipeline/batch_peds.json new file mode 100644 index 0000000..7ef8fc2 --- /dev/null +++ b/pipeline/batch_peds.json @@ -0,0 +1,97 @@ +[ + { + "src": "pipeline/.peds_r6/comical_boy_01.glb", + "name": "comical_boy_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/comical_luchador_01.glb", + "name": "comical_luchador_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_athletic_01.glb", + "name": "man_athletic_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_bouncer_01.glb", + "name": "man_bouncer_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_casual_01.glb", + "name": "man_casual_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_casual_02.glb", + "name": "man_casual_02", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_casual_03.glb", + "name": "man_casual_03", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_dj_streetwear_01.glb", + "name": "man_dj_streetwear_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_elder_01.glb", + "name": "man_elder_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_grunge_01.glb", + "name": "man_grunge_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_soldier_ww2_01.glb", + "name": "man_soldier_ww2_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_suit_01.glb", + "name": "man_suit_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/man_worker_hivis_01.glb", + "name": "man_worker_hivis_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/woman_athleisure_01.glb", + "name": "woman_athleisure_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/woman_business_01.glb", + "name": "woman_business_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/woman_casual_01.glb", + "name": "woman_casual_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/woman_casual_02.glb", + "name": "woman_casual_02", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/woman_chef_01.glb", + "name": "woman_chef_01", + "atlas": 1024 + }, + { + "src": "pipeline/.peds_r6/woman_smart_01.glb", + "name": "woman_smart_01", + "atlas": 1024 + } +] \ No newline at end of file diff --git a/pipeline/merge_ped.py b/pipeline/merge_ped.py new file mode 100644 index 0000000..c966935 --- /dev/null +++ b/pipeline/merge_ped.py @@ -0,0 +1,169 @@ +"""Merge a skinned ped rig's sub-meshes to ONE draw: join meshes → pack all materials into a single +atlas (bake) → one material → one primitive. Skinning (joints/weights) + skeleton are preserved, so +the shared walk.glb/idle.glb clips still retarget. Round-7 E1 (gates D's full-density roster flip). + + BL=/Applications/Blender.app/Contents/MacOS/Blender + "$BL" --background --python merge_ped.py -- BATCH.json OUT_DIR + +BATCH.json = list of {"src": "web/models/peds/x.glb", "name": "x", "atlas": 1024}. +Approach = the bake_lowpoly EMIT trick without the remesh: keep the mesh + armature, give it a +packed atlas UV, route each source material's base-colour→emission via the ORIGINAL UV, bake EMIT +into one atlas image, assign a single material. Falls back to material-merge-only (prims = #mats) +if a ped has no textures to bake. +""" +import bpy, sys, os, json + +ARGV = sys.argv[sys.argv.index("--") + 1:] +BATCH, OUT_DIR = ARGV[0], ARGV[1] +os.makedirs(OUT_DIR, exist_ok=True) + + +def wipe(): + for o in list(bpy.data.objects): + bpy.data.objects.remove(o, do_unlink=True) + for coll in (bpy.data.meshes, bpy.data.materials, bpy.data.images, bpy.data.armatures): + for d in list(coll): + if d.users == 0: + coll.remove(d) + + +def import_join(src): + bpy.ops.import_scene.gltf(filepath=src) + meshes = [o for o in bpy.data.objects if o.type == 'MESH'] + bpy.ops.object.select_all(action='DESELECT') + for m in meshes: + m.select_set(True) + bpy.context.view_layer.objects.active = meshes[0] + if len(meshes) > 1: + bpy.ops.object.join() # keeps vertex groups (weights) + armature modifier + return bpy.context.view_layer.objects.active + + +def route_basecolor_to_emission(obj, orig_uv): + """Each material: sample its base-colour texture via the ORIGINAL uv and feed Emission, so a + bake on the atlas UV captures albedo. Add an explicit UVMap node so the atlas UV (active during + bake) doesn't hijack the texture lookup.""" + for m in obj.data.materials: + if not m or not m.use_nodes: + continue + nt = m.node_tree + b = nt.nodes.get("Principled BSDF") + if not b: + continue + uvn = nt.nodes.new("ShaderNodeUVMap"); uvn.uv_map = orig_uv + for n in nt.nodes: + if n.type == 'TEX_IMAGE': + nt.links.new(uvn.outputs["UV"], n.inputs["Vector"]) + bc = b.inputs.get("Base Color") + ec = b.inputs.get("Emission Color") or b.inputs.get("Emission") + es = b.inputs.get("Emission Strength") + if ec is None: + continue + if bc and bc.is_linked: + nt.links.new(bc.links[0].from_socket, ec) + elif bc: + ec.default_value = bc.default_value + if es is not None: + es.default_value = 1.0 + + +def merge_one(spec): + wipe() + src = spec["src"] if os.path.isabs(spec["src"]) else os.path.join(os.getcwd(), spec["src"]) + obj = import_join(src) + prim_before = "n/a" + mats_before = len(obj.data.materials) + has_tex = any(m and m.use_nodes and any(n.type == 'TEX_IMAGE' for n in m.node_tree.nodes) + for m in obj.data.materials) + size = spec.get("atlas", 1024) + + if mats_before <= 1 or not has_tex: + # already single-material (or untextured) — nothing to atlas; export as-is (1 primitive) + out = _export(obj, spec) + return {"name": spec["name"], "file": os.path.basename(out), "materials": mats_before, + "atlased": False, "note": "already single-material" if mats_before <= 1 else "untextured"} + + orig_uv = obj.data.uv_layers.active.name + atlas_uv = obj.data.uv_layers.new(name="atlas").name + obj.data.uv_layers.active = obj.data.uv_layers[atlas_uv] + # pack every island of the atlas UV into non-overlapping 0-1 regions (separates the per-material + # islands so their baked pixels don't collide) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.select_all(action='SELECT') + try: + bpy.ops.uv.pack_islands(rotate=False, margin=0.003) + except TypeError: + bpy.ops.uv.pack_islands(margin=0.003) + bpy.ops.object.mode_set(mode='OBJECT') + + route_basecolor_to_emission(obj, orig_uv) + + img = bpy.data.images.new("ped_atlas", size, size) + for m in obj.data.materials: + if m and m.use_nodes: + tn = m.node_tree.nodes.new("ShaderNodeTexImage"); tn.image = img + m.node_tree.nodes.active = tn # bake target per material + + scn = bpy.context.scene + scn.render.engine = 'CYCLES' + try: + scn.cycles.device = 'GPU' + except Exception: + pass + scn.cycles.samples = 4 + scn.render.bake.margin = max(4, size // 128) + scn.render.bake.use_selected_to_active = False + obj.data.uv_layers.active = obj.data.uv_layers[atlas_uv] + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.bake(type='EMIT', use_clear=True) + + # single atlas material, atlas UV only + newmat = bpy.data.materials.new("ped_merged"); newmat.use_nodes = True + nt = newmat.node_tree + bsdf = nt.nodes.get("Principled BSDF") + tex = nt.nodes.new("ShaderNodeTexImage"); tex.image = img + nt.links.new(tex.outputs["Color"], bsdf.inputs["Base Color"]) + obj.data.materials.clear() + obj.data.materials.append(newmat) + for uv in list(obj.data.uv_layers): + if uv.name != atlas_uv: + obj.data.uv_layers.remove(uv) + + out = _export(obj, spec) + return {"name": spec["name"], "file": os.path.basename(out), "materials_before": mats_before, + "atlased": True, "atlas_px": size} + + +def _export(obj, spec): + out = os.path.join(OUT_DIR, os.path.basename(spec["src"])) + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + # select the armature too so the skin exports + for o in bpy.data.objects: + if o.type == 'ARMATURE': + o.select_set(True) + bpy.ops.export_scene.gltf(filepath=out, export_format='GLB', use_selection=True, + export_yup=True, export_skins=True, export_animations=False, + export_image_format='WEBP', export_image_quality=90) + return out + + +def main(): + specs = json.load(open(BATCH)) + results = [] + for spec in specs: + try: + r = merge_one(spec) + results.append(r) + print(f"OK {r['file']:32} atlased={r.get('atlased')} mats={r.get('materials_before', r.get('materials'))}") + except Exception as e: + print(f"ERR {spec.get('name')}: {e}") + results.append({"name": spec.get("name"), "error": str(e)}) + json.dump({"results": results}, open(os.path.join(OUT_DIR, "_merge_results.json"), "w"), indent=2) + print(f"MERGE_DONE ok={sum('error' not in r for r in results)}/{len(specs)}") + + +main() diff --git a/web/models/peds/comical_boy_01.glb b/web/models/peds/comical_boy_01.glb index 4be95ef..48360d9 100644 Binary files a/web/models/peds/comical_boy_01.glb and b/web/models/peds/comical_boy_01.glb differ diff --git a/web/models/peds/comical_luchador_01.glb b/web/models/peds/comical_luchador_01.glb index f8db204..f273fd0 100644 Binary files a/web/models/peds/comical_luchador_01.glb and b/web/models/peds/comical_luchador_01.glb differ diff --git a/web/models/peds/man_athletic_01.glb b/web/models/peds/man_athletic_01.glb index ca04c33..ffe7be7 100644 Binary files a/web/models/peds/man_athletic_01.glb and b/web/models/peds/man_athletic_01.glb differ diff --git a/web/models/peds/man_bouncer_01.glb b/web/models/peds/man_bouncer_01.glb index 7050449..d9a9c87 100644 Binary files a/web/models/peds/man_bouncer_01.glb and b/web/models/peds/man_bouncer_01.glb differ diff --git a/web/models/peds/man_casual_01.glb b/web/models/peds/man_casual_01.glb index 28250d9..731b672 100644 Binary files a/web/models/peds/man_casual_01.glb and b/web/models/peds/man_casual_01.glb differ diff --git a/web/models/peds/man_casual_02.glb b/web/models/peds/man_casual_02.glb index 892ae50..f37c5df 100644 Binary files a/web/models/peds/man_casual_02.glb and b/web/models/peds/man_casual_02.glb differ diff --git a/web/models/peds/man_casual_03.glb b/web/models/peds/man_casual_03.glb index fabbd5e..ffab16b 100644 Binary files a/web/models/peds/man_casual_03.glb and b/web/models/peds/man_casual_03.glb differ diff --git a/web/models/peds/man_dj_streetwear_01.glb b/web/models/peds/man_dj_streetwear_01.glb index bbd17f9..37da736 100644 Binary files a/web/models/peds/man_dj_streetwear_01.glb and b/web/models/peds/man_dj_streetwear_01.glb differ diff --git a/web/models/peds/man_elder_01.glb b/web/models/peds/man_elder_01.glb index 9cf6e58..be33b77 100644 Binary files a/web/models/peds/man_elder_01.glb and b/web/models/peds/man_elder_01.glb differ diff --git a/web/models/peds/man_grunge_01.glb b/web/models/peds/man_grunge_01.glb index a9514fd..d9b3058 100644 Binary files a/web/models/peds/man_grunge_01.glb and b/web/models/peds/man_grunge_01.glb differ diff --git a/web/models/peds/man_soldier_ww2_01.glb b/web/models/peds/man_soldier_ww2_01.glb index 6a40db3..b5d6c8f 100644 Binary files a/web/models/peds/man_soldier_ww2_01.glb and b/web/models/peds/man_soldier_ww2_01.glb differ diff --git a/web/models/peds/man_suit_01.glb b/web/models/peds/man_suit_01.glb index 9e1c9fc..e23a5ec 100644 Binary files a/web/models/peds/man_suit_01.glb and b/web/models/peds/man_suit_01.glb differ diff --git a/web/models/peds/man_worker_hivis_01.glb b/web/models/peds/man_worker_hivis_01.glb index 8e7bbd0..68f637b 100644 Binary files a/web/models/peds/man_worker_hivis_01.glb and b/web/models/peds/man_worker_hivis_01.glb differ diff --git a/web/models/peds/woman_athleisure_01.glb b/web/models/peds/woman_athleisure_01.glb index 41d0416..ef83f23 100644 Binary files a/web/models/peds/woman_athleisure_01.glb and b/web/models/peds/woman_athleisure_01.glb differ diff --git a/web/models/peds/woman_business_01.glb b/web/models/peds/woman_business_01.glb index 2e2cbb3..d84935c 100644 Binary files a/web/models/peds/woman_business_01.glb and b/web/models/peds/woman_business_01.glb differ diff --git a/web/models/peds/woman_casual_01.glb b/web/models/peds/woman_casual_01.glb index 54a1719..3f93791 100644 Binary files a/web/models/peds/woman_casual_01.glb and b/web/models/peds/woman_casual_01.glb differ diff --git a/web/models/peds/woman_casual_02.glb b/web/models/peds/woman_casual_02.glb index d4c5e6f..1ff6ff2 100644 Binary files a/web/models/peds/woman_casual_02.glb and b/web/models/peds/woman_casual_02.glb differ diff --git a/web/models/peds/woman_chef_01.glb b/web/models/peds/woman_chef_01.glb index c32295d..1b0eb0d 100644 Binary files a/web/models/peds/woman_chef_01.glb and b/web/models/peds/woman_chef_01.glb differ diff --git a/web/models/peds/woman_smart_01.glb b/web/models/peds/woman_smart_01.glb index f3b5733..1b51ed0 100644 Binary files a/web/models/peds/woman_smart_01.glb and b/web/models/peds/woman_smart_01.glb differ