"""WARDROBEGOD blender ops — headless Blender does the heavy lifting the browser can't. blender -b --python blender_ops.py -- ops: convert any format Blender reads → GLB (textures packed) scale uniform scale so bbox height = height_m, applied decimate Decimate modifier — PRESERVES vertex weights/rig fit [inflate_mm] weight-transfer the body's skinning onto the garment (nearest-face interpolated), parent to the body's armature. mode=merge → one GLB (dressed body); mode=garment → garment+skeleton only (a wardrobe item, reusable in assemble) assemble ... dressed combo: garments must have been fitted against the same skeleton (bone names match) House rules: never fit against an unrigged body (we abort); rigged meshes are never sent to the farm /finish (this file is the safe local path for them). """ import bpy, sys, os argv = sys.argv[sys.argv.index('--') + 1:] OP, ARGS = argv[0], argv[1:] def clean(): bpy.ops.wm.read_factory_settings(use_empty=True) def load(path): """Import path, return the set of objects it brought in.""" before = set(bpy.data.objects) ext = os.path.splitext(path)[1].lower() if ext == '.fbx': bpy.ops.import_scene.fbx(filepath=path) elif ext in ('.glb', '.gltf'): bpy.ops.import_scene.gltf(filepath=path) elif ext == '.obj': bpy.ops.wm.obj_import(filepath=path) else: raise SystemExit(f'unsupported format: {ext}') return list(set(bpy.data.objects) - before) def export(out): os.makedirs(os.path.dirname(out), exist_ok=True) bpy.ops.export_scene.gltf(filepath=out, export_format='GLB') print(f'WROTE {out}') def apply_mod(obj, name): with bpy.context.temp_override(object=obj, active_object=obj, selected_editable_objects=[obj]): bpy.ops.object.modifier_apply(modifier=name) def meshes(objs): return [o for o in objs if o.type == 'MESH'] def armatures(objs): return [o for o in objs if o.type == 'ARMATURE'] def total_tris(objs): return sum(sum(len(p.vertices) - 2 for p in o.data.polygons) for o in meshes(objs)) if OP == 'convert': clean(); load(ARGS[0]); export(ARGS[1]) elif OP == 'scale': clean() objs = load(ARGS[0]); target = float(ARGS[2]) import mathutils mn = mathutils.Vector((1e9,) * 3); mx = mathutils.Vector((-1e9,) * 3) for o in meshes(objs): for c in o.bound_box: w = o.matrix_world @ mathutils.Vector(c) mn = mathutils.Vector(map(min, mn, w)); mx = mathutils.Vector(map(max, mx, w)) h = mx.z - mn.z if h <= 0: raise SystemExit('flat object — no height to scale') s = target / h roots = [o for o in objs if not o.parent] for r in roots: r.scale = [c * s for c in r.scale] with bpy.context.temp_override(selected_editable_objects=roots + meshes(objs) + armatures(objs)): bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) print(f'scaled ×{s:.3f} → {target}m') export(ARGS[1]) elif OP == 'decimate': clean() objs = load(ARGS[0]); target = int(ARGS[2]) tris = total_tris(objs) ratio = min(1.0, target / max(tris, 1)) for o in meshes(objs): m = o.modifiers.new('dec', 'DECIMATE') m.ratio = ratio # keep the Armature modifier LAST so skinning still evaluates after the cut while o.modifiers[0].name != 'dec': with bpy.context.temp_override(object=o): bpy.ops.object.modifier_move_up(modifier='dec') apply_mod(o, 'dec') print(f'decimated {tris:,} → {total_tris(objs):,} tris (ratio {ratio:.3f}) — weights kept') export(ARGS[1]) elif OP == 'fit': clean() body_objs = load(ARGS[0]) arms = armatures(body_objs) if not arms: raise SystemExit('body has no armature — rig it first (MIRPAMO/Mixamo), then fit') arm = arms[0] body = max(meshes(body_objs), key=lambda o: len(o.vertex_groups), default=None) if body is None or not body.vertex_groups: raise SystemExit('body has no skin weights to copy') garm_objs = load(ARGS[1]) mode = ARGS[3] if len(ARGS) > 3 else 'merge' inflate = float(ARGS[4]) / 1000.0 if len(ARGS) > 4 else 0.0 for g in meshes(garm_objs): if inflate: # a few mm of air so the body doesn't poke through d = g.modifiers.new('puff', 'DISPLACE'); d.strength = inflate; d.mid_level = 0 apply_mod(g, 'puff') with bpy.context.temp_override(object=body, active_object=body, selected_editable_objects=[g, body], selected_objects=[g, body]): bpy.ops.object.data_transfer(data_type='VGROUP_WEIGHTS', use_create=True, vert_mapping='POLYINTERP_NEAREST', layers_select_src='ALL', layers_select_dst='NAME') g.parent = arm am = g.modifiers.new('arm', 'ARMATURE'); am.object = arm # garment armatures that came along with the import are dupes — drop them for a in armatures(garm_objs): bpy.data.objects.remove(a, do_unlink=True) if mode == 'garment': bpy.data.objects.remove(body, do_unlink=True) for o in meshes(body_objs): if o.name in bpy.data.objects: bpy.data.objects.remove(o, do_unlink=True) print(f'fitted {len(meshes(garm_objs))} garment mesh(es), mode={mode}, inflate={inflate * 1000:.0f}mm') export(ARGS[2]) elif OP == 'assemble': clean() body_objs = load(ARGS[0]) arms = armatures(body_objs) if not arms: raise SystemExit('body has no armature') arm = arms[0] for gpath in ARGS[2:]: gobjs = load(gpath) for g in meshes(gobjs): g.parent = arm for m in g.modifiers: if m.type == 'ARMATURE': m.object = arm if not any(m.type == 'ARMATURE' for m in g.modifiers): am = g.modifiers.new('arm', 'ARMATURE'); am.object = arm for a in armatures(gobjs): # bone names match by contract — drop the dupe bpy.data.objects.remove(a, do_unlink=True) print(f'assembled body + {len(ARGS) - 2} garment file(s)') export(ARGS[1]) else: raise SystemExit(f'unknown op {OP}')