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>
131 lines
5.4 KiB
Python
131 lines
5.4 KiB
Python
"""
|
||
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}")
|