feat(assets): face-based segmentation pipeline + solidify for hair/clothing
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>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""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}")
|
||||
Reference in New Issue
Block a user