Assemble any of the 53 kit rigs parts cross-rig from the UI; genitals slot same-rig additive only. LODs land in library/garments/lod/ (subdir keeps scan() from double-listing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.7 KiB
Python
95 lines
3.7 KiB
Python
"""Assemble a character from socket-kit parts chosen in the wardrobegod bench.
|
|
|
|
Blender -b --python kit_assemble.py -- <spec.json> <out.glb> <render.png>
|
|
|
|
spec.json:
|
|
{"target_tag": "kachujin", # torso's rig defines the character
|
|
"parts": {"torso": {"blend": "/abs/path.blend", "object": "kachujin_torso", "source_rig": "kachujin"},
|
|
"head": {...}, "hand_L": {...}, ... ,
|
|
"genitals": {...}}} # additive slot, same-rig only
|
|
|
|
Cross-rig parts are auto-fitted by assemble.py fit_part (cut-bone length
|
|
ratio). The genitals slot has no cut bone — it is attach_part only, and the
|
|
server refuses cross-rig genitals before we ever get here.
|
|
"""
|
|
import bpy, sys, os, json, math
|
|
from mathutils import Vector
|
|
|
|
KIT = os.path.expanduser("~/Documents/character_kit/modular")
|
|
|
|
argv = sys.argv[sys.argv.index("--") + 1:]
|
|
spec = json.load(open(argv[0]))
|
|
out_glb, out_png = argv[1], argv[2]
|
|
target_tag = spec["target_tag"]
|
|
parts = spec["parts"]
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
# group appends per blend file: each part needs its object + its rig (for fit)
|
|
by_blend = {}
|
|
for slot, p in parts.items():
|
|
by_blend.setdefault(p["blend"], set()).add(p["object"])
|
|
by_blend[p["blend"]].add(p["source_rig"] + "_rig")
|
|
|
|
for blend, names in by_blend.items():
|
|
with bpy.data.libraries.load(blend) as (src, dst):
|
|
dst.objects = [n for n in src.objects if n in names]
|
|
for o in dst.objects:
|
|
if o is not None and o.name not in bpy.context.scene.collection.objects:
|
|
bpy.context.scene.collection.objects.link(o)
|
|
|
|
for a in [o for o in bpy.data.objects if o.type == 'ARMATURE']:
|
|
a.data.pose_position = 'REST'
|
|
|
|
ns = {}
|
|
exec(open(os.path.join(KIT, "scripts", "assemble.py")).read(), ns)
|
|
|
|
std = {s: p["object"] for s, p in parts.items() if s != "genitals"}
|
|
source_arms = {p["object"]: p["source_rig"] + "_rig"
|
|
for s, p in parts.items()
|
|
if s != "genitals" and p["source_rig"] != target_tag}
|
|
ns["assemble"](f"{target_tag}_rig", std, source_arms=source_arms)
|
|
|
|
gen = parts.get("genitals")
|
|
if gen:
|
|
if gen["source_rig"] != target_tag:
|
|
raise SystemExit("genitals slot is same-rig only (additive part)")
|
|
ns["attach_part"](bpy.data.objects[gen["object"]],
|
|
bpy.data.objects[f"{target_tag}_rig"])
|
|
|
|
# drop donor rigs — cross-rig parts are baked into the target's space by fit_part
|
|
for o in list(bpy.data.objects):
|
|
if o.type == 'ARMATURE' and o.name != f"{target_tag}_rig":
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
|
|
|
bpy.ops.export_scene.gltf(filepath=out_glb, export_apply=False)
|
|
|
|
|
|
def bbox(o):
|
|
pts = [o.matrix_world @ Vector(c) for c in o.bound_box]
|
|
return (Vector((min(p[i] for p in pts) for i in range(3))),
|
|
Vector((max(p[i] for p in pts) for i in range(3))))
|
|
|
|
|
|
sun = bpy.data.objects.new("sun", bpy.data.lights.new("sun", 'SUN'))
|
|
sun.data.energy = 3.5
|
|
sun.rotation_euler = (math.radians(52), 0, math.radians(35))
|
|
bpy.context.scene.collection.objects.link(sun)
|
|
cam = bpy.data.objects.new("cam", bpy.data.cameras.new("cam"))
|
|
bpy.context.scene.collection.objects.link(cam)
|
|
sc = bpy.context.scene
|
|
sc.camera = cam
|
|
sc.render.engine = 'BLENDER_EEVEE'
|
|
sc.render.resolution_x = sc.render.resolution_y = 800
|
|
sc.world = bpy.data.worlds.new("w")
|
|
sc.world.color = (0.15, 0.15, 0.17)
|
|
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
|
|
lo = Vector((min(bbox(o)[0][i] for o in meshes) for i in range(3)))
|
|
hi = Vector((max(bbox(o)[1][i] for o in meshes) for i in range(3)))
|
|
c, h = (lo + hi) / 2, (hi - lo).length
|
|
cam.location = c + Vector((0, -h * 1.05, h * 0.05))
|
|
cam.rotation_euler = (c - cam.location).to_track_quat('-Z', 'Y').to_euler()
|
|
sc.render.filepath = out_png
|
|
bpy.ops.render.render(write_still=True)
|
|
print("KIT_ASSEMBLE_OK", out_glb)
|