Complete character pipeline spike validating the Quaternius rig as foundation for The Settled Reach's 3D character system. Validated: - 65-bone skeleton + Universal Animation Library as rig foundation - Body segmentation into 15 bone-group regions with 1-ring vertex overlap - Trellis-generated heads via BoneAttachment3D - Skin tone texture generation pipeline (9 variants from source) - Toon shader + inverted hull outline at gameplay zoom - CharacterVisual class as compositor prototype Failed (documented): - Trellis clothing auto-rigging (sculptures, not garments) - Bone scaling for body type variants (catastrophic joint deformation) - Runtime per-segment clothing scaling (same failure as Blender-side) Includes: Blender pipeline scripts (segmentation, auto-rigging, Surface Deform fitting), Godot showcase with interactive controls, automated screenshot cycle, smoke tests, team reviews, and VERDICT.md. D-158 through D-164 locked. Q-060 through Q-062 opened. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
3.6 KiB
Python
116 lines
3.6 KiB
Python
"""
|
|
OBSOLETE: Placeholder body type GLBs (workaround for failed bone-scaling approach).
|
|
|
|
This script was a stopgap to produce placeholder body type files with blue
|
|
material tinting after create_body_types.py produced broken meshes. No longer
|
|
needed -- the body type pipeline will use hand-authored meshes instead.
|
|
|
|
Preserved as documentation only.
|
|
|
|
Run via:
|
|
tooling/blender --background --python \\
|
|
spikes/quaternius-aesthetic/scripts/blender/create_placeholder_bodies.py \\
|
|
-- <base_characters_dir/> <output_dir/>
|
|
"""
|
|
|
|
import bpy
|
|
import sys
|
|
import os
|
|
|
|
argv = sys.argv
|
|
argv = argv[argv.index("--") + 1:] if "--" in argv else []
|
|
|
|
if len(argv) < 2:
|
|
print("Usage: -- <base_characters_dir/> <output_dir/>")
|
|
sys.exit(1)
|
|
|
|
INPUT_DIR = argv[0]
|
|
OUTPUT_DIR = argv[1]
|
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
|
|
MALE_GLTF = os.path.join(INPUT_DIR, "Superhero_Male_FullBody.gltf")
|
|
FEMALE_GLTF = os.path.join(INPUT_DIR, "Superhero_Female_FullBody.gltf")
|
|
|
|
PLACEHOLDER_COLOR = (0.2, 0.35, 0.65, 1.0) # muted blue
|
|
|
|
# muscular keeps original textures (it IS the Superhero base)
|
|
# all others get blue placeholder material
|
|
VARIANTS = [
|
|
("male_thin", MALE_GLTF, True),
|
|
("male_average", MALE_GLTF, True),
|
|
("male_muscular", MALE_GLTF, False), # keep original
|
|
("male_heavy", MALE_GLTF, True),
|
|
("female_thin", FEMALE_GLTF, True),
|
|
("female_average", FEMALE_GLTF, True),
|
|
("female_muscular", FEMALE_GLTF, False), # keep original
|
|
("female_heavy", FEMALE_GLTF, True),
|
|
("child", FEMALE_GLTF, True),
|
|
]
|
|
|
|
print("=== Create placeholder body types ===")
|
|
|
|
|
|
def clear_scene():
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
bpy.ops.object.delete()
|
|
for c in list(bpy.data.collections):
|
|
bpy.data.collections.remove(c)
|
|
|
|
|
|
def apply_placeholder_material(obj):
|
|
"""Replace all materials with solid blue placeholder."""
|
|
if obj.type != 'MESH':
|
|
return
|
|
# Create placeholder material
|
|
mat = bpy.data.materials.new(name="placeholder_blue")
|
|
mat.use_nodes = True
|
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
|
if bsdf:
|
|
bsdf.inputs["Base Color"].default_value = PLACEHOLDER_COLOR
|
|
bsdf.inputs["Roughness"].default_value = 1.0
|
|
bsdf.inputs["Metallic"].default_value = 0.0
|
|
# Clear existing materials and assign placeholder
|
|
obj.data.materials.clear()
|
|
obj.data.materials.append(mat)
|
|
|
|
|
|
for variant_name, source_gltf, is_placeholder in VARIANTS:
|
|
print(f"\n {variant_name}...")
|
|
clear_scene()
|
|
|
|
if not os.path.exists(source_gltf):
|
|
print(f" SKIP: source not found: {source_gltf}")
|
|
continue
|
|
|
|
bpy.ops.import_scene.gltf(filepath=source_gltf)
|
|
objects = list(bpy.data.objects)
|
|
|
|
# Apply placeholder material to placeholder variants only
|
|
if is_placeholder:
|
|
for obj in objects:
|
|
if obj.type == 'MESH':
|
|
apply_placeholder_material(obj)
|
|
|
|
# Select all for export
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
if objects:
|
|
bpy.context.view_layer.objects.active = objects[0]
|
|
|
|
output_path = os.path.join(OUTPUT_DIR, f"{variant_name}.glb")
|
|
bpy.ops.export_scene.gltf(
|
|
filepath=output_path,
|
|
export_format='GLB',
|
|
use_selection=True,
|
|
export_apply=False,
|
|
export_animations=False,
|
|
export_skins=True,
|
|
)
|
|
size_kb = os.path.getsize(output_path) // 1024
|
|
print(f" Exported: {variant_name}.glb ({size_kb} KB)")
|
|
|
|
print("\n=== Done ===")
|
|
for name, _, _ in VARIANTS:
|
|
path = os.path.join(OUTPUT_DIR, f"{name}.glb")
|
|
status = "[OK]" if os.path.exists(path) else "[MISSING]"
|
|
print(f" {status} {name}.glb")
|