Segmentation: - Face-based exclusive assignment (no overlap, no caps needed) - Hips split from torso (pelvis as own segment) - Torso_upper independent (spine_03 + clavicle, not a variant) - Clavicle moved from arm_upper to torso_upper Hair pipeline: - Solidify modifier (20mm) for scalp hair volume - Normal recalculation before solidify for consistent direction - Eyebrows/facial hair NOT solidified - Fixed mask generation (was outputting black, now white) Clothing pipeline: - Solidify modifier (25mm) for clothing thickness - Peasant shoes added from Fantasy pack Tooling: - convert_outfit.py with solidify support - inspect_glb.py, check_hair_symmetry.py for debugging - Segment reference distribution locked - Q-063 footsteps VFX filed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
76 lines
2.6 KiB
Python
76 lines
2.6 KiB
Python
"""Render the raw Quaternius body + hair from multiple angles to check for clipping."""
|
|
import sys
|
|
import os
|
|
import math
|
|
import bpy
|
|
|
|
if __name__ == "__main__":
|
|
argv = sys.argv
|
|
if "--" not in argv:
|
|
sys.exit(1)
|
|
out_dir = argv[argv.index("--") + 1]
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
# Load body
|
|
body_path = "/var/mnt/data/projects/settled-reach/main/docs/assets/downloads/Universal Base Characters[Source]/Base Characters/Exports/Godot - UE/Superhero_Female_FullBody.gltf"
|
|
bpy.ops.import_scene.gltf(filepath=body_path)
|
|
|
|
# Load bob hair
|
|
hair_path = "/var/mnt/data/projects/settled-reach/main/docs/assets/downloads/Universal Base Characters[Source]/Hairstyles/Rigged to Head Bone/glTF (Godot -Unreal)/Female/Hair_Bob.gltf"
|
|
bpy.ops.import_scene.gltf(filepath=hair_path)
|
|
|
|
# Remove icospheres
|
|
for obj in list(bpy.context.scene.objects):
|
|
if "Icosphere" in obj.name:
|
|
bpy.data.objects.remove(obj, do_unlink=True)
|
|
|
|
# Setup camera
|
|
cam_data = bpy.data.cameras.new("TestCam")
|
|
cam_data.lens = 85
|
|
cam = bpy.data.objects.new("TestCam", cam_data)
|
|
bpy.context.scene.collection.objects.link(cam)
|
|
bpy.context.scene.camera = cam
|
|
|
|
# Light
|
|
light_data = bpy.data.lights.new("TestLight", type='SUN')
|
|
light_data.energy = 3.0
|
|
light = bpy.data.objects.new("TestLight", light_data)
|
|
light.rotation_euler = (math.radians(45), 0, math.radians(30))
|
|
bpy.context.scene.collection.objects.link(light)
|
|
|
|
# Render settings
|
|
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
|
|
bpy.context.scene.render.resolution_x = 800
|
|
bpy.context.scene.render.resolution_y = 600
|
|
bpy.context.scene.render.film_transparent = True
|
|
|
|
# Head center (approx)
|
|
target = (0.0, 0.0, 1.65)
|
|
dist = 0.5
|
|
|
|
angles = {
|
|
"front": 0,
|
|
"right": 90,
|
|
"back": 180,
|
|
"left": 270,
|
|
}
|
|
|
|
for name, deg in angles.items():
|
|
rad = math.radians(deg)
|
|
cx = target[0] + dist * math.sin(rad)
|
|
cy = target[1] - dist * math.cos(rad)
|
|
cz = target[2]
|
|
cam.location = (cx, cy, cz)
|
|
# Point at target
|
|
direction = (target[0] - cx, target[1] - cy, target[2] - cz)
|
|
rot_z = math.atan2(direction[0], -direction[1])
|
|
rot_x = math.atan2(math.sqrt(direction[0]**2 + direction[1]**2), direction[2])
|
|
cam.rotation_euler = (rot_x, 0, rot_z)
|
|
|
|
filepath = os.path.join(out_dir, f"raw_bob_{name}.png")
|
|
bpy.context.scene.render.filepath = filepath
|
|
bpy.ops.render.render(write_still=True)
|
|
print(f"Rendered: {filepath}")
|