"""Assemble a character from socket-kit parts chosen in the wardrobegod bench. Blender -b --python kit_assemble.py -- 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)