""" blender_rebuild_forks.py Usage: tooling/blender --background --python tooling/blender_rebuild_forks.py -- [body_type ...] Rebuilds ONLY the fork body types (thin_m/f, heavy_m/f, child) through the same segmentation pipeline that produced the healthy six (blender_segment_body.py), using the T-1090 fix: the fork scale is applied to the mesh AND the embedded armature rest pose so each segment stays internally consistent with its own skeleton (required by the shared-skeleton compositor in character_visual.gd). Sources (Source-tier Godot - UE exports): Regular_Male_FullBody.gltf + scale (0.82, 1.0, 0.88) -> thin_m Regular_Female_FullBody.gltf + scale (0.82, 1.0, 0.88) -> thin_f Regular_Male_FullBody.gltf + scale (1.20, 1.08, 1.0) -> heavy_m Regular_Female_FullBody.gltf + scale (1.20, 1.08, 1.0) -> heavy_f Teen_Male_FullBody.gltf + scale (0.75, 0.72, 0.75) -> child [gender-neutral] The fork scale factors and source mapping are unchanged from blender_process_bodies.py — this driver reuses the same table so the healthy six are never touched. Output layout matches production: /{body_type}/seg_*.glb. """ import sys import os 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, source_label) — forks only. FORK_MANIFEST = [ ("Regular_Male_FullBody.gltf", "thin_m", (0.82, 1.0, 0.88), "Regular_Male_FullBody.gltf"), ("Regular_Female_FullBody.gltf", "thin_f", (0.82, 1.0, 0.88), "Regular_Female_FullBody.gltf"), ("Regular_Male_FullBody.gltf", "heavy_m", (1.20, 1.08, 1.0), "Regular_Male_FullBody.gltf"), ("Regular_Female_FullBody.gltf", "heavy_f", (1.20, 1.08, 1.0), "Regular_Female_FullBody.gltf"), ("Teen_Male_FullBody.gltf", "child", (0.75, 0.72, 0.75), "Teen_Male_FullBody.gltf"), ] FORK_README = """\ # Fork body type — auto-generated from mesh + armature scaling (T-1090) This directory contains **{body_type}** body segments, generated by applying a proportional scale to the source body ({source}) mesh AND its armature rest pose together, then segmenting: Scale: ({sx:.2f}, {sy:.2f}, {sz:.2f}) The scale is applied to the mesh vertices and the embedded armature rest pose with the SAME affine (T-1090 fix). This keeps each segment internally consistent with its own skeleton, which the shared-skeleton compositor (character_visual.gd) requires — a mesh-only scale detaches the head and explodes the limbs on relocation. Cross-sectional differentiation (thin = narrow, heavy = wide) survives the composite; global height normalises to the shared skeleton (see T-1090 report / Q-060 for the shared-skeleton scale-normalisation note). Status: auto-generated (rebuilt T-1090), 18 segments incl. seg_hips """ def write_fork_readme(output_dir, body_type, source, scale): sx, sy, sz = scale with open(os.path.join(output_dir, "README.md"), "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 args = argv[argv.index("--") + 1:] if "--" in argv else [] if len(args) < 2: print("Usage: -- [body_type ...]") sys.exit(1) source_dir = args[0] output_base_dir = args[1] only = set(args[2:]) # optional subset filter manifest = [m for m in FORK_MANIFEST if not only or m[1] in only] unique_sources = {gltf for (gltf, _, _, _) in 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 p in missing: print(f" {p}") sys.exit(1) results = [] for (gltf_filename, body_type, scale, source_label) in manifest: gltf_path = os.path.join(source_dir, gltf_filename) output_dir = os.path.join(output_base_dir, body_type) print(f"\n{'='*60}\n Fork: {body_type} scale={scale}\n Source: {gltf_filename}") exported, skipped = segment_body(gltf_path, output_dir, scale) write_fork_readme(output_dir, body_type, source_label, scale) results.append((body_type, exported, skipped)) print(f"\n{'='*60}\n=== Fork rebuild complete ===") for body_type, exported, skipped in results: print(f" {body_type}: {len(exported)} exported, {len(skipped)} skipped")