genshin / blender /autorig_pipeline.py
aptol's picture
Upload 8 files
ccf0740 verified
# 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)