|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import bpy, sys, os, math, argparse
|
|
|
from mathutils import Vector, Matrix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bpy.ops.object.armature_add(enter_editmode=True)
|
|
|
arm = bpy.context.object
|
|
|
arm.name = "Armature"
|
|
|
eb = arm.data.edit_bones
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
bpy.context.view_layer.objects.active = body
|
|
|
for o in others:
|
|
|
if o.type != "MESH":
|
|
|
continue
|
|
|
|
|
|
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:
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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():
|
|
|
|
|
|
pass
|
|
|
else:
|
|
|
add_placeholder_face_keys(body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|