# blender/autorig_pipeline.py # 헤드리스(배치) 사용 예: # blender -b --python blender/autorig_pipeline.py -- \ # --in model.glb --out rigged_toon.fbx --export fbx \ # --outline --toon --surface_deform --limit_cloth_bones --try_faceit # # 기능 요약: # 1) GLB/GLTF/FBX/OBJ 불러오기 # 2) Body/의복/헤어/소품 대략 분류 # 3) 휴머노이드 Armature(T-포즈) 자동 생성 + Body 자동 가중치 # 4) 의복: Surface Deform 또는 본 영향 축소 # 5) Toon 머티리얼 + 인버티드 헐(Outline) 적용 # 6) 표정: Faceit 감지 시 안내, 없으면 기본 Shape Key 플레이스홀더 생성 # 7) FBX 또는 GLB 내보내기 import bpy, sys, os, math, argparse from mathutils import Vector, Matrix # ----------------------- # CLI args # ----------------------- def parse_args(): argv = sys.argv if "--" in argv: argv = argv[argv.index("--") + 1:] else: argv = [] p = argparse.ArgumentParser() p.add_argument("--in", dest="inp", required=True, help="입력 경로 (GLB/GLTF/FBX/OBJ)") p.add_argument("--out", dest="out", required=True, help="출력 파일 경로") p.add_argument("--export", choices=["fbx", "glb"], default="fbx", help="내보내기 포맷") p.add_argument("--outline", action="store_true", help="인버티드 헐 외곽선 생성") p.add_argument("--toon", action="store_true", help="Toon 셰이딩 적용") p.add_argument("--surface_deform", action="store_true", help="의복 Surface Deform 바인딩") p.add_argument("--limit_cloth_bones", action="store_true", help="의복 Armature 본 영향 축소(간이)") p.add_argument("--try_faceit", action="store_true", help="설치 시 Faceit 활용 안내") p.add_argument("--scale", type=float, default=1.0, help="전체 스케일") return p.parse_args() args = parse_args() inp_path = bpy.path.abspath(args.inp) out_path = bpy.path.abspath(args.out) export_fmt = args.export # ----------------------- # 초기화 # ----------------------- for obj in list(bpy.data.objects): obj.select_set(False) # ----------------------- # Import model # ----------------------- ext = os.path.splitext(inp_path)[1].lower() if ext in [".glb", ".gltf"]: bpy.ops.import_scene.gltf(filepath=inp_path) elif ext in [".fbx"]: bpy.ops.import_scene.fbx(filepath=inp_path) elif ext in [".obj"]: bpy.ops.import_scene.obj(filepath=inp_path) else: raise RuntimeError("지원하지 않는 포맷: " + ext) # 모든 Mesh 수집 meshes = [o for o in bpy.context.scene.objects if o.type == "MESH"] if not meshes: raise RuntimeError("MESH 오브젝트가 없습니다.") # 컬렉션 정리 coll = bpy.data.collections.new("CHARACTER") bpy.context.scene.collection.children.link(coll) for o in meshes: # 기존 컬렉션에서 제거 후 새 컬렉션으로 이동 if o.users_collection: for c in o.users_collection: try: c.objects.unlink(o) except Exception: pass coll.objects.link(o) # 스케일 정리 for o in meshes: o.select_set(True) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) for o in meshes: o.select_set(False) # ----------------------- # Body 추정: 바운딩박스 볼륨 기준 # ----------------------- def mesh_bbox_volume(obj): m = obj.matrix_world bb = [m @ Vector(corner) for corner in obj.bound_box] minv = Vector((min(v.x for v in bb), min(v.y for v in bb), min(v.z for v in bb))) maxv = Vector((max(v.x for v in bb), max(v.y for v in bb), max(v.z for v in bb))) sz = maxv - minv return abs(sz.x * sz.y * sz.z) volumes = sorted([(mesh_bbox_volume(o), o) for o in meshes], key=lambda x: x[0], reverse=True) body = volumes[0][1] others = [o for _, o in volumes[1:]] body.name = "Body" # 의복/헤어/소품 키워드 분류(간이) CLOTH_KEYS = ["cloth","skirt","dress","coat","cape","jacket","shirt","pants","sleeve","kimono","robe"] HAIR_KEYS = ["hair","bang","ponytail","fringe","bun","pigtail"] def classify(o): name = (o.name + " " + " ".join([m.name for m in o.data.materials if m])).lower() if any(k in name for k in CLOTH_KEYS): return "Cloth" if any(k in name for k in HAIR_KEYS): return "Hair" return "Prop" for o in others: try: o["part_type"] = classify(o) except Exception: pass # ----------------------- # Armature 생성 (간이 휴머노이드 T-포즈) # ----------------------- bpy.ops.object.armature_add(enter_editmode=True) arm = bpy.context.object arm.name = "Armature" eb = arm.data.edit_bones # Body AABB 기반 중심/크기 bb = [body.matrix_world @ Vector(c) for c in body.bound_box] minv = Vector((min(v.x for v in bb), min(v.y for v in bb), min(v.z for v in bb))) maxv = Vector((max(v.x for v in bb), max(v.y for v in bb), max(v.z for v in bb))) cent = (minv + maxv) / 2 height = (maxv.z - minv.z) width = (maxv.x - minv.x) def add_bone(name, head, tail, parent=None, roll=0.0): b = eb.new(name) b.head = head; b.tail = tail; b.roll = roll if parent: b.parent = parent return b # 코어 스파인 hips = add_bone("Hips", Vector((cent.x, cent.y, minv.z + height*0.20)), Vector((cent.x, cent.y, minv.z + height*0.30))) spine = add_bone("Spine", hips.tail, Vector((cent.x, cent.y, minv.z + height*0.45)), parent=hips) chest = add_bone("Chest", spine.tail, Vector((cent.x, cent.y, minv.z + height*0.60)), parent=spine) neck = add_bone("Neck", Vector((cent.x, cent.y, minv.z + height*0.68)), Vector((cent.x, cent.y, minv.z + height*0.74)), parent=chest) headb = add_bone("Head", neck.tail, Vector((cent.x, cent.y, minv.z + height*0.86)), parent=neck) # 팔(T-포즈) arm_len = max(width*0.35, 0.05) ua_L = add_bone("UpperArm.L", Vector((cent.x+0.05*width, cent.y, chest.tail.z-0.02*height)), Vector((cent.x+0.05*width+arm_len*0.5, cent.y, chest.tail.z-0.02*height)), parent=chest) fa_L = add_bone("LowerArm.L", ua_L.tail, ua_L.tail + Vector((arm_len*0.5, 0, 0)), parent=ua_L) handL= add_bone("Hand.L", fa_L.tail, fa_L.tail + Vector((arm_len*0.2, 0, 0)), parent=fa_L) ua_R = add_bone("UpperArm.R", Vector((cent.x-0.05*width, cent.y, chest.tail.z-0.02*height)), Vector((cent.x-0.05*width-arm_len*0.5, cent.y, chest.tail.z-0.02*height)), parent=chest) fa_R = add_bone("LowerArm.R", ua_R.tail, ua_R.tail - Vector((arm_len*0.5, 0, 0)), parent=ua_R) handR= add_bone("Hand.R", fa_R.tail, fa_R.tail - Vector((arm_len*0.2, 0, 0)), parent=fa_R) # 다리 leg_off = max(width*0.12, 0.02) thighL = add_bone("Thigh.L", Vector((cent.x+leg_off, cent.y, hips.head.z)), Vector((cent.x+leg_off, cent.y, minv.z + height*0.05)), parent=hips) shinL = add_bone("Shin.L", thighL.tail, Vector((cent.x+leg_off, cent.y, minv.z + 0.01*height)), parent=thighL) footL = add_bone("Foot.L", shinL.tail, shinL.tail + Vector((0.0, 0.05*height, -0.02*height)), parent=shinL) thighR = add_bone("Thigh.R", Vector((cent.x-leg_off, cent.y, hips.head.z)), Vector((cent.x-leg_off, cent.y, minv.z + height*0.05)), parent=hips) shinR = add_bone("Shin.R", thighR.tail, Vector((cent.x-leg_off, cent.y, minv.z + 0.01*height)), parent=thighR) footR = add_bone("Foot.R", shinR.tail, shinR.tail + Vector((0.0, 0.05*height, -0.02*height)), parent=shinR) bpy.ops.object.mode_set(mode='OBJECT') # ----------------------- # Body → Armature 자동 바인딩 # ----------------------- for o in meshes: o.select_set(False) body.select_set(True) arm.select_set(True) bpy.context.view_layer.objects.active = arm bpy.ops.object.parent_set(type='ARMATURE_AUTO') # ----------------------- # 의복 처리 # ----------------------- if args.surface_deform: # Body 기준 Surface Deform (본가중치 최소화) bpy.context.view_layer.objects.active = body for o in others: if o.type != "MESH": continue # 기존 Armature modifier 제거 for m in list(o.modifiers): if m.type == "ARMATURE": try: o.modifiers.remove(m) except Exception: pass sdef = o.modifiers.new("SurfaceDeform", "SURFACE_DEFORM") sdef.target = body try: bpy.ops.object.surfacedeform_bind(modifier=sdef.name) except Exception as e: print("[WARN] SurfaceDeform bind 실패:", e) elif args.limit_cloth_bones: # Armature 유지하되 본 영향 축소(간이 훅) for o in others: if o.type != "MESH": continue has_arm = any(m.type == "ARMATURE" for m in o.modifiers) if not has_arm: m = o.modifiers.new("Armature", "ARMATURE") m.object = arm # 세부 가중치 조정은 모델마다 달라 완전 자동화 난이도 높음 → 후편집 전제 # ----------------------- # Toon Shader & Outline # ----------------------- def make_toon_material(name="ToonMat", base_color=(0.8, 0.8, 0.8, 1.0)): mat = bpy.data.materials.new(name) mat.use_nodes = True nt = mat.node_tree nodes = nt.nodes; links = nt.links # 초기화 for n in list(nodes): nodes.remove(n) out = nodes.new("ShaderNodeOutputMaterial"); out.location = (400, 0) toon = nodes.new("ShaderNodeBsdfToon"); toon.location = (0, 0) toon.inputs["Color"].default_value = base_color toon.inputs["Size"].default_value = 0.5 toon.inputs["Smooth"].default_value = 0.0 links.new(toon.outputs["BSDF"], out.inputs["Surface"]) return mat def apply_toon(obj): if obj.type != "MESH": return if not obj.data.materials: obj.data.materials.append(make_toon_material()) else: for i, _ in enumerate(obj.data.materials): obj.data.materials[i] = make_toon_material(f"Toon_{obj.name}_{i}") def add_outline(obj, thickness=0.003): if obj.type != "MESH": return outline = obj.copy() outline.data = obj.data.copy() outline.name = obj.name + "_Outline" # 컬렉션 링크 for c in outline.users_collection: try: c.objects.unlink(outline) except Exception: pass bpy.context.scene.collection.objects.link(outline) # 검정 Emission 머티리얼 mat = bpy.data.materials.new("OutlineBlack") mat.use_nodes = True nodes = mat.node_tree.nodes; links = mat.node_tree.links for n in list(nodes): nodes.remove(n) out = nodes.new("ShaderNodeOutputMaterial"); out.location = (200, 0) em = nodes.new("ShaderNodeEmission"); em.location = (0, 0) em.inputs["Color"].default_value = (0, 0, 0, 1) em.inputs["Strength"].default_value = 1.0 links.new(em.outputs["Emission"], out.inputs["Surface"]) outline.data.materials.clear() outline.data.materials.append(mat) # Solidify로 외곽선(노멀 반전) bpy.context.view_layer.objects.active = outline solid = outline.modifiers.new("OutlineSolidify", "SOLIDIFY") solid.thickness = -abs(thickness) solid.offset = 1.0 solid.use_flip_normals = True solid.material_offset = 0 outline.show_in_front = True if args.toon: apply_toon(body) for o in others: apply_toon(o) if args.outline: add_outline(body) for o in others: add_outline(o, thickness=0.003) # ----------------------- # 표정(Shape Keys) # ----------------------- def ensure_basis_key(obj): if obj.data.shape_keys is None: obj.shape_key_add(name="Basis") return obj.data.shape_keys.key_blocks["Basis"] def add_placeholder_face_keys(obj): ensure_basis_key(obj) keys = [ "EyeBlink_L","EyeBlink_R","BrowDown_L","BrowDown_R", "BrowUp","JawOpen","MouthSmile_L","MouthSmile_R", "MouthFrown","MouthPucker","MouthLeft","MouthRight" ] for k in keys: try: obj.shape_key_add(name=k) except Exception: pass def try_faceit_generate(): # 애드온 설치 감지(자동 실행은 버전 의존으로 생략) if "faceit" in bpy.context.preferences.addons: print("[INFO] Faceit addon detected. Use GUI to auto-generate blendshapes if needed.") return True for k in bpy.context.preferences.addons.keys(): if "face" in k and "it" in k: print(f"[INFO] Possibly Faceit-like addon detected: {k}") return True return False if args.try_faceit and try_faceit_generate(): # 사용자 GUI에서 Faceit 워크플로 진행 권장 pass else: add_placeholder_face_keys(body) # ----------------------- # Export # ----------------------- # 선택 정리 for o in bpy.context.scene.objects: o.select_set(False) arm.select_set(True) body.select_set(True) for o in others: if o.type == "MESH": o.select_set(True) bpy.context.view_layer.objects.active = arm if export_fmt == "fbx": bpy.ops.export_scene.fbx( filepath=out_path, use_selection=True, apply_unit_scale=True, bake_space_transform=True, add_leaf_bones=False, mesh_smooth_type='FACE' ) else: bpy.ops.export_scene.gltf( filepath=out_path, export_format='GLB', use_selection=True, export_apply=True ) print("[DONE] Exported:", out_path)