- Fix stale 21→18 segment count in architecture doc, script docstrings, and disk budget table (critical — both reviewers) - Add average_m/README.md noting role as clothing reference body - Add __main__ guards to diagnostic scripts - Preflight-check all source paths in blender_process_bodies.py - Document solid-white hair masks as v0.2 placeholder for multi-region - Add fallback size comment for head mask generation - Add bald_mask.png (1×1 black) for sidecar convention consistency - Document child non-uniform scale rationale (Y=0.72 vs XZ=0.75) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
141 lines
5.8 KiB
Python
141 lines
5.8 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 (18 segs)
|
||
<source_dir>/Regular_Female_FullBody.gltf -> average_f (18 segs)
|
||
<source_dir>/Superhero_Male_FullBody.gltf -> muscular_m (18 segs)
|
||
<source_dir>/Superhero_Female_FullBody.gltf -> muscular_f (18 segs)
|
||
<source_dir>/Teen_Male_FullBody.gltf -> teen_m (18 segs)
|
||
<source_dir>/Teen_Female_FullBody.gltf -> teen_f (18 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 (18 files × 11 types = 198 GLBs)
|
||
|
||
Decisions: D-159 (11 body types), D-160 (18 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]
|
||
|
||
# Preflight: check ALL source files exist before starting any processing,
|
||
# so all missing files are reported at once rather than failing mid-loop.
|
||
unique_sources = {gltf_filename for (gltf_filename, _, _, _) in BODY_MANIFEST}
|
||
missing = [
|
||
os.path.join(source_dir, f)
|
||
for f in sorted(unique_sources)
|
||
if not os.path.exists(os.path.join(source_dir, f))
|
||
]
|
||
if missing:
|
||
print("\nERROR: Missing source GLTF files:")
|
||
for path in missing:
|
||
print(f" {path}")
|
||
sys.exit(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)
|
||
|
||
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}")
|