Files
settled-reach/tooling/blender_process_bodies.py
T
jpmschweitzerandClaude Opus 4.6 a60493c2f7 chore(assets): add Blender headless pipeline scripts for character assets
Adds 8 scripts to tooling/ for the Sprint 28 character asset pipeline:

- blender_segment_body.py: segments a FullBody GLTF into 18 body-part
  GLBs using BMesh vertex-weight selection + 1-ring boundary expansion
- blender_process_bodies.py: batch driver for all 11 body types (6 direct
  + 5 auto-scaled forks: thin, heavy, child)
- blender_process_heads.py: processes 4 OnlyHead .blend files into GLBs
  + 1024x1024 solid-white mask PNGs (D-159, D-160)
- blender_process_hair.py: converts 20 GLTF hairstyle/eyebrow exports
  to accessory GLBs + mask PNGs, creates bald.glb placeholder
- blender_inspect_body.py: diagnostic — lists mesh objects + vertex counts
- blender_inspect_vgroups.py: diagnostic — lists vertex groups on skinned mesh
- glb_strip_utility_nodes.py: post-process GLBs to strip utility nodes
  (Icosphere, WGT-*, DEF-*, ORG-* meshes) from the GLTF scene graph
- check_icosphere.py: diagnostic — reads GLB JSON chunk to verify node list

All Blender scripts require flatpak Blender via tooling/blender wrapper.
Scripts must live in project directories (flatpak sandbox blocks /tmp/).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 15:07:55 +01:00

131 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
blender_process_bodies.py
Usage: tooling/blender --background --python tooling/blender_process_bodies.py -- <source_dir> <output_base_dir>
Drives the full body segmentation pipeline for all 11 body types.
Calls blender_segment_body.py logic inline (same Blender session, one pass per body type).
Source GLTF files (pre-exported from Source tier, Godot - UE format):
<source_dir>/Regular_Male_FullBody.gltf -> average_m (21 segs)
<source_dir>/Regular_Female_FullBody.gltf -> average_f (21 segs)
<source_dir>/Superhero_Male_FullBody.gltf -> muscular_m (21 segs)
<source_dir>/Superhero_Female_FullBody.gltf -> muscular_f (21 segs)
<source_dir>/Teen_Male_FullBody.gltf -> teen_m (21 segs)
<source_dir>/Teen_Female_FullBody.gltf -> teen_f (21 segs)
Fork body types (vertex-level scale applied before segmentation):
Regular_Male + scale (0.82, 1.0, 0.88) -> thin_m
Regular_Female + scale (0.82, 1.0, 0.88) -> thin_f
Regular_Male + scale (1.20, 1.08, 1.0) -> heavy_m
Regular_Female + scale (1.20, 1.08, 1.0) -> heavy_f
Teen_Male + scale (0.75, 0.72, 0.75) -> child [gender-neutral]
Fork body types are approximate (auto-generated from mesh scaling). They produce
visually distinct silhouettes but may need artist refinement for final quality.
A README is placed in each fork directory marking this status.
Output: <output_base_dir>/{body_type}/seg_{name}.glb (21 files × 11 types = 231 GLBs)
Decisions: D-159 (11 body types), D-160 (21 segments), D-164 (Source .blends as starting point)
"""
import sys
import os
# We import the segmentation logic from blender_segment_body.py which must be
# in the same directory as this script.
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, script_dir)
from blender_segment_body import segment_body
# (gltf_filename, body_type_key, scale, is_fork)
BODY_MANIFEST = [
# --- Direct body types (segment as-is) ---
("Regular_Male_FullBody.gltf", "average_m", (1.0, 1.0, 1.0), False),
("Regular_Female_FullBody.gltf", "average_f", (1.0, 1.0, 1.0), False),
("Superhero_Male_FullBody.gltf", "muscular_m", (1.0, 1.0, 1.0), False),
("Superhero_Female_FullBody.gltf","muscular_f", (1.0, 1.0, 1.0), False),
("Teen_Male_FullBody.gltf", "teen_m", (1.0, 1.0, 1.0), False),
("Teen_Female_FullBody.gltf", "teen_f", (1.0, 1.0, 1.0), False),
# --- Fork body types (auto-scaled from source) ---
# thin: narrow/ectomorph — narrower X, slightly shorter Z
("Regular_Male_FullBody.gltf", "thin_m", (0.82, 1.0, 0.88), True),
("Regular_Female_FullBody.gltf", "thin_f", (0.82, 1.0, 0.88), True),
# heavy: wide/endomorph — wider X, slightly taller Y
("Regular_Male_FullBody.gltf", "heavy_m", (1.20, 1.08, 1.0), True),
("Regular_Female_FullBody.gltf", "heavy_f", (1.20, 1.08, 1.0), True),
# child: gender-neutral, forked from Teen Male, scaled down
("Teen_Male_FullBody.gltf", "child", (0.75, 0.72, 0.75), True),
]
FORK_README = """\
# Fork body type — auto-generated from mesh scaling
This directory contains **{body_type}** body segments, generated by applying
a proportional mesh scale to the source body ({source}):
Scale: ({sx:.2f}, {sy:.2f}, {sz:.2f})
These segments are APPROXIMATE. They produce a visually distinct silhouette
but are not artist-authored from scratch. Review the proportions and refine
the base mesh manually if the auto-scale result is not satisfactory.
Status: auto-generated, needs artist review
Sprint 28 — visual team
"""
def write_fork_readme(output_dir, body_type, source, scale):
sx, sy, sz = scale
readme_path = os.path.join(output_dir, "README.md")
with open(readme_path, 'w') as f:
f.write(FORK_README.format(body_type=body_type, source=source, sx=sx, sy=sy, sz=sz))
if __name__ == "__main__":
argv = sys.argv
if "--" not in argv:
print("Usage: tooling/blender --background --python tooling/blender_process_bodies.py -- <source_dir> <output_base_dir>")
sys.exit(1)
args = argv[argv.index("--") + 1:]
if len(args) < 2:
print("ERROR: Provide <source_dir> and <output_base_dir>")
sys.exit(1)
source_dir = args[0]
output_base_dir = args[1]
results = []
for (gltf_filename, body_type, scale, is_fork) in BODY_MANIFEST:
gltf_path = os.path.join(source_dir, gltf_filename)
output_dir = os.path.join(output_base_dir, body_type)
if not os.path.exists(gltf_path):
print(f"\nERROR: GLTF not found: {gltf_path}")
sys.exit(1)
print(f"\n{'='*60}")
print(f" Body type: {body_type} {'[FORK]' if is_fork else '[DIRECT]'}")
print(f" Source: {gltf_filename}")
if is_fork:
print(f" Scale: {scale}")
exported, skipped = segment_body(gltf_path, output_dir, scale)
if is_fork:
write_fork_readme(output_dir, body_type, gltf_filename, scale)
results.append((body_type, exported, skipped, is_fork))
# Summary
print(f"\n{'='*60}")
print(f"=== Body type segmentation complete ===")
total_exported = 0
for body_type, exported, skipped, is_fork in results:
flag = " [FORK]" if is_fork else ""
print(f" {body_type}{flag}: {len(exported)} exported, {len(skipped)} skipped")
total_exported += len(exported)
print(f" Total GLBs: {total_exported}")