✨ feat: vroid2mixamo
Browse files- .gitattributes +1 -0
- utils.py +55 -0
- vroid2mixamo.py +316 -0
.gitattributes
CHANGED
|
@@ -61,3 +61,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 61 |
*.glb filter=lfs diff=lfs merge=lfs -text
|
| 62 |
*.ply filter=lfs diff=lfs merge=lfs -text
|
| 63 |
*.obj filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 61 |
*.glb filter=lfs diff=lfs merge=lfs -text
|
| 62 |
*.ply filter=lfs diff=lfs merge=lfs -text
|
| 63 |
*.obj filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
*.vrm filter=lfs diff=lfs merge=lfs -text
|
utils.py
CHANGED
|
@@ -62,6 +62,14 @@ def reset():
|
|
| 62 |
bpy.ops.wm.read_factory_settings(use_empty=True)
|
| 63 |
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
def remove_all(delete_actions=True):
|
| 66 |
for obj in bpy.data.objects.values():
|
| 67 |
bpy.data.objects.remove(obj, do_unlink=True)
|
|
@@ -76,6 +84,15 @@ def remove_empty():
|
|
| 76 |
bpy.data.batch_remove(childless_empties)
|
| 77 |
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
def load_file(filepath: str, *args, **kwargs) -> "list[Object]":
|
| 80 |
old_objs = set(bpy.context.scene.objects)
|
| 81 |
if filepath.endswith(".glb"):
|
|
@@ -187,6 +204,44 @@ def get_rest_bones(armature_obj: Object):
|
|
| 187 |
return rest_bones, rest_bones_tail, bones_idx_dict
|
| 188 |
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
def mesh_quads2tris(obj_list: "list[Object]" = None):
|
| 191 |
if not obj_list:
|
| 192 |
obj_list = bpy.context.scene.objects
|
|
|
|
| 62 |
bpy.ops.wm.read_factory_settings(use_empty=True)
|
| 63 |
|
| 64 |
|
| 65 |
+
def update():
|
| 66 |
+
bpy.context.view_layer.update()
|
| 67 |
+
bpy.context.scene.update_tag()
|
| 68 |
+
for obj in bpy.context.scene.objects:
|
| 69 |
+
# obj.hide_render = obj.hide_render
|
| 70 |
+
obj.update_tag()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
def remove_all(delete_actions=True):
|
| 74 |
for obj in bpy.data.objects.values():
|
| 75 |
bpy.data.objects.remove(obj, do_unlink=True)
|
|
|
|
| 84 |
bpy.data.batch_remove(childless_empties)
|
| 85 |
|
| 86 |
|
| 87 |
+
def remove_collection(coll_name: str):
|
| 88 |
+
if coll_name not in bpy.data.collections:
|
| 89 |
+
return
|
| 90 |
+
coll = bpy.data.collections[coll_name]
|
| 91 |
+
for c in coll.children:
|
| 92 |
+
remove_collection(c)
|
| 93 |
+
bpy.data.collections.remove(coll, do_unlink=True)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
def load_file(filepath: str, *args, **kwargs) -> "list[Object]":
|
| 97 |
old_objs = set(bpy.context.scene.objects)
|
| 98 |
if filepath.endswith(".glb"):
|
|
|
|
| 204 |
return rest_bones, rest_bones_tail, bones_idx_dict
|
| 205 |
|
| 206 |
|
| 207 |
+
def transfer_weights(source_bone_name: str, target_bone_name: str, mesh_obj_list: "list[Object]"):
|
| 208 |
+
if isinstance(mesh_obj_list, Object):
|
| 209 |
+
mesh_obj_list = [mesh_obj_list]
|
| 210 |
+
for obj in mesh_obj_list:
|
| 211 |
+
source_group = obj.vertex_groups.get(source_bone_name)
|
| 212 |
+
if source_group is None:
|
| 213 |
+
return
|
| 214 |
+
source_i = source_group.index
|
| 215 |
+
target_group = obj.vertex_groups.get(target_bone_name)
|
| 216 |
+
if target_group is None:
|
| 217 |
+
target_group = obj.vertex_groups.new(name=target_bone_name)
|
| 218 |
+
|
| 219 |
+
for v in obj.data.vertices:
|
| 220 |
+
for g in v.groups:
|
| 221 |
+
if g.group == source_i:
|
| 222 |
+
target_group.add((v.index,), g.weight, "ADD")
|
| 223 |
+
obj.vertex_groups.remove(source_group)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def remove_empty_vgroups(mesh_obj_list: "list[Object]"):
|
| 227 |
+
if isinstance(mesh_obj_list, Object):
|
| 228 |
+
mesh_obj_list = [mesh_obj_list]
|
| 229 |
+
for obj in mesh_obj_list:
|
| 230 |
+
vertex_groups = obj.vertex_groups
|
| 231 |
+
groups = {r: None for r in range(len(vertex_groups))}
|
| 232 |
+
|
| 233 |
+
for vert in obj.data.vertices:
|
| 234 |
+
for vg in vert.groups:
|
| 235 |
+
i = vg.group
|
| 236 |
+
if i in groups:
|
| 237 |
+
del groups[i]
|
| 238 |
+
|
| 239 |
+
lis = list(groups)
|
| 240 |
+
lis.sort(reverse=True)
|
| 241 |
+
for i in lis:
|
| 242 |
+
vertex_groups.remove(vertex_groups[i])
|
| 243 |
+
|
| 244 |
+
|
| 245 |
def mesh_quads2tris(obj_list: "list[Object]" = None):
|
| 246 |
if not obj_list:
|
| 247 |
obj_list = bpy.context.scene.objects
|
vroid2mixamo.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import os
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from functools import cached_property
|
| 5 |
+
from glob import glob
|
| 6 |
+
from typing import Iterator
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
from typing import Self
|
| 10 |
+
except ImportError:
|
| 11 |
+
from typing_extensions import Self
|
| 12 |
+
|
| 13 |
+
from tqdm import tqdm
|
| 14 |
+
|
| 15 |
+
from utils import (
|
| 16 |
+
Armature,
|
| 17 |
+
HiddenPrints,
|
| 18 |
+
Mode,
|
| 19 |
+
bpy,
|
| 20 |
+
get_all_mesh_obj,
|
| 21 |
+
get_armature_obj,
|
| 22 |
+
load_file,
|
| 23 |
+
mathutils,
|
| 24 |
+
remove_all,
|
| 25 |
+
remove_collection,
|
| 26 |
+
remove_empty_vgroups,
|
| 27 |
+
reset,
|
| 28 |
+
select_objs,
|
| 29 |
+
transfer_weights,
|
| 30 |
+
update,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
MIXAMO_PREFIX = "mixamorig:"
|
| 34 |
+
VROID_JOINTS_MAP = {
|
| 35 |
+
"J_Bip_C_Hips": f"{MIXAMO_PREFIX}Hips",
|
| 36 |
+
"J_Bip_C_Spine": f"{MIXAMO_PREFIX}Spine",
|
| 37 |
+
"J_Bip_C_Chest": f"{MIXAMO_PREFIX}Spine1",
|
| 38 |
+
"J_Bip_C_UpperChest": f"{MIXAMO_PREFIX}Spine2",
|
| 39 |
+
"J_Bip_C_Neck": f"{MIXAMO_PREFIX}Neck",
|
| 40 |
+
"J_Bip_C_Head": f"{MIXAMO_PREFIX}Head",
|
| 41 |
+
"J_Bip_L_Shoulder": f"{MIXAMO_PREFIX}LeftShoulder",
|
| 42 |
+
"J_Bip_L_UpperArm": f"{MIXAMO_PREFIX}LeftArm",
|
| 43 |
+
"J_Bip_L_LowerArm": f"{MIXAMO_PREFIX}LeftForeArm",
|
| 44 |
+
"J_Bip_L_Hand": f"{MIXAMO_PREFIX}LeftHand",
|
| 45 |
+
"J_Bip_L_Index1": f"{MIXAMO_PREFIX}LeftHandIndex1",
|
| 46 |
+
"J_Bip_L_Index2": f"{MIXAMO_PREFIX}LeftHandIndex2",
|
| 47 |
+
"J_Bip_L_Index3": f"{MIXAMO_PREFIX}LeftHandIndex3",
|
| 48 |
+
"J_Bip_L_Little1": f"{MIXAMO_PREFIX}LeftHandPinky1",
|
| 49 |
+
"J_Bip_L_Little2": f"{MIXAMO_PREFIX}LeftHandPinky2",
|
| 50 |
+
"J_Bip_L_Little3": f"{MIXAMO_PREFIX}LeftHandPinky3",
|
| 51 |
+
"J_Bip_L_Middle1": f"{MIXAMO_PREFIX}LeftHandMiddle1",
|
| 52 |
+
"J_Bip_L_Middle2": f"{MIXAMO_PREFIX}LeftHandMiddle2",
|
| 53 |
+
"J_Bip_L_Middle3": f"{MIXAMO_PREFIX}LeftHandMiddle3",
|
| 54 |
+
"J_Bip_L_Ring1": f"{MIXAMO_PREFIX}LeftHandRing1",
|
| 55 |
+
"J_Bip_L_Ring2": f"{MIXAMO_PREFIX}LeftHandRing2",
|
| 56 |
+
"J_Bip_L_Ring3": f"{MIXAMO_PREFIX}LeftHandRing3",
|
| 57 |
+
"J_Bip_L_Thumb1": f"{MIXAMO_PREFIX}LeftHandThumb1",
|
| 58 |
+
"J_Bip_L_Thumb2": f"{MIXAMO_PREFIX}LeftHandThumb2",
|
| 59 |
+
"J_Bip_L_Thumb3": f"{MIXAMO_PREFIX}LeftHandThumb3",
|
| 60 |
+
"J_Bip_R_Shoulder": f"{MIXAMO_PREFIX}RightShoulder",
|
| 61 |
+
"J_Bip_R_UpperArm": f"{MIXAMO_PREFIX}RightArm",
|
| 62 |
+
"J_Bip_R_LowerArm": f"{MIXAMO_PREFIX}RightForeArm",
|
| 63 |
+
"J_Bip_R_Hand": f"{MIXAMO_PREFIX}RightHand",
|
| 64 |
+
"J_Bip_R_Index1": f"{MIXAMO_PREFIX}RightHandIndex1",
|
| 65 |
+
"J_Bip_R_Index2": f"{MIXAMO_PREFIX}RightHandIndex2",
|
| 66 |
+
"J_Bip_R_Index3": f"{MIXAMO_PREFIX}RightHandIndex3",
|
| 67 |
+
"J_Bip_R_Little1": f"{MIXAMO_PREFIX}RightHandPinky1",
|
| 68 |
+
"J_Bip_R_Little2": f"{MIXAMO_PREFIX}RightHandPinky2",
|
| 69 |
+
"J_Bip_R_Little3": f"{MIXAMO_PREFIX}RightHandPinky3",
|
| 70 |
+
"J_Bip_R_Middle1": f"{MIXAMO_PREFIX}RightHandMiddle1",
|
| 71 |
+
"J_Bip_R_Middle2": f"{MIXAMO_PREFIX}RightHandMiddle2",
|
| 72 |
+
"J_Bip_R_Middle3": f"{MIXAMO_PREFIX}RightHandMiddle3",
|
| 73 |
+
"J_Bip_R_Ring1": f"{MIXAMO_PREFIX}RightHandRing1",
|
| 74 |
+
"J_Bip_R_Ring2": f"{MIXAMO_PREFIX}RightHandRing2",
|
| 75 |
+
"J_Bip_R_Ring3": f"{MIXAMO_PREFIX}RightHandRing3",
|
| 76 |
+
"J_Bip_R_Thumb1": f"{MIXAMO_PREFIX}RightHandThumb1",
|
| 77 |
+
"J_Bip_R_Thumb2": f"{MIXAMO_PREFIX}RightHandThumb2",
|
| 78 |
+
"J_Bip_R_Thumb3": f"{MIXAMO_PREFIX}RightHandThumb3",
|
| 79 |
+
"J_Bip_L_UpperLeg": f"{MIXAMO_PREFIX}LeftUpLeg",
|
| 80 |
+
"J_Bip_L_LowerLeg": f"{MIXAMO_PREFIX}LeftLeg",
|
| 81 |
+
"J_Bip_L_Foot": f"{MIXAMO_PREFIX}LeftFoot",
|
| 82 |
+
"J_Bip_L_ToeBase": f"{MIXAMO_PREFIX}LeftToeBase",
|
| 83 |
+
"J_Bip_R_UpperLeg": f"{MIXAMO_PREFIX}RightUpLeg",
|
| 84 |
+
"J_Bip_R_LowerLeg": f"{MIXAMO_PREFIX}RightLeg",
|
| 85 |
+
"J_Bip_R_Foot": f"{MIXAMO_PREFIX}RightFoot",
|
| 86 |
+
"J_Bip_R_ToeBase": f"{MIXAMO_PREFIX}RightToeBase",
|
| 87 |
+
#
|
| 88 |
+
# "J_Opt_L_RabbitEar1_01": f"{MIXAMO_PREFIX}LRabbitEar1",
|
| 89 |
+
"J_Opt_L_RabbitEar2_01": f"{MIXAMO_PREFIX}LRabbitEar2",
|
| 90 |
+
# "J_Opt_R_RabbitEar1_01": f"{MIXAMO_PREFIX}RRabbitEar1",
|
| 91 |
+
"J_Opt_R_RabbitEar2_01": f"{MIXAMO_PREFIX}RRabbitEar2",
|
| 92 |
+
"J_Opt_C_FoxTail1_01": f"{MIXAMO_PREFIX}FoxTail1",
|
| 93 |
+
"J_Opt_C_FoxTail2_01": f"{MIXAMO_PREFIX}FoxTail2",
|
| 94 |
+
"J_Opt_C_FoxTail3_01": f"{MIXAMO_PREFIX}FoxTail3",
|
| 95 |
+
"J_Opt_C_FoxTail4_01": f"{MIXAMO_PREFIX}FoxTail4",
|
| 96 |
+
"J_Opt_C_FoxTail5_01": f"{MIXAMO_PREFIX}FoxTail5",
|
| 97 |
+
}
|
| 98 |
+
VROID_JOINTS = set(VROID_JOINTS_MAP.values())
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def enable_vrm(addon_path="VRM_Addon_for_Blender-Extension-2_20_88.zip"):
|
| 102 |
+
"""https://github.com/saturday06/VRM-Addon-for-Blender"""
|
| 103 |
+
assert os.path.isfile(addon_path), f"Addon file not found: {addon_path}"
|
| 104 |
+
import shutil
|
| 105 |
+
|
| 106 |
+
# bpy.ops.preferences.addon_install(filepath=os.path.abspath(addon_path))
|
| 107 |
+
repo = "user_default"
|
| 108 |
+
shutil.rmtree(bpy.utils.user_resource("EXTENSIONS", path=repo))
|
| 109 |
+
bpy.ops.extensions.package_install_files(filepath=os.path.abspath(addon_path), repo=repo)
|
| 110 |
+
bpy.ops.preferences.addon_enable(module=f"bl_ext.{repo}.vrm")
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def load_vrm(filepath: str):
|
| 114 |
+
old_objs = set(bpy.context.scene.objects)
|
| 115 |
+
bpy.ops.import_scene.vrm(
|
| 116 |
+
filepath=filepath,
|
| 117 |
+
use_addon_preferences=True,
|
| 118 |
+
extract_textures_into_folder=False,
|
| 119 |
+
make_new_texture_folder=True,
|
| 120 |
+
set_shading_type_to_material_on_import=False,
|
| 121 |
+
set_view_transform_to_standard_on_import=True,
|
| 122 |
+
set_armature_display_to_wire=False,
|
| 123 |
+
set_armature_display_to_show_in_front=False,
|
| 124 |
+
set_armature_bone_shape_to_default=True,
|
| 125 |
+
)
|
| 126 |
+
remove_collection("glTF_not_exported")
|
| 127 |
+
remove_collection("Colliders")
|
| 128 |
+
imported_objs = set(bpy.context.scene.objects) - old_objs
|
| 129 |
+
imported_objs = sorted(imported_objs, key=lambda x: x.name)
|
| 130 |
+
print("Imported:", imported_objs)
|
| 131 |
+
return imported_objs
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
@dataclass(frozen=True)
|
| 135 |
+
class Joint:
|
| 136 |
+
name: str
|
| 137 |
+
index: int
|
| 138 |
+
parent: Self | None
|
| 139 |
+
children: list[Self]
|
| 140 |
+
|
| 141 |
+
def __repr__(self):
|
| 142 |
+
return f"{self.__class__.__name__}({self.name})"
|
| 143 |
+
|
| 144 |
+
def __iter__(self) -> Iterator[Self]:
|
| 145 |
+
yield self
|
| 146 |
+
for child in self.children:
|
| 147 |
+
yield from child
|
| 148 |
+
|
| 149 |
+
@cached_property
|
| 150 |
+
def children_recursive(self) -> list[Self]:
|
| 151 |
+
# return [child for child in self if child is not self]
|
| 152 |
+
children_list = []
|
| 153 |
+
if not self.children:
|
| 154 |
+
return children_list
|
| 155 |
+
for child in self.children:
|
| 156 |
+
children_list.append(child)
|
| 157 |
+
children_list.extend(child.children_recursive)
|
| 158 |
+
return children_list
|
| 159 |
+
|
| 160 |
+
def __len__(self):
|
| 161 |
+
return len(self.children_recursive) + 1
|
| 162 |
+
|
| 163 |
+
def __contains__(self, item: Self | str):
|
| 164 |
+
if isinstance(item, str):
|
| 165 |
+
return item == self.name or item in (child.name for child in self.children_recursive)
|
| 166 |
+
elif isinstance(item, Joint):
|
| 167 |
+
return item is Self or item in self.children_recursive
|
| 168 |
+
else:
|
| 169 |
+
raise TypeError(f"Item must be {self.__class__.__name__} or str, not {type(item)}")
|
| 170 |
+
|
| 171 |
+
@cached_property
|
| 172 |
+
def children_recursive_dict(self) -> dict[str, Self]:
|
| 173 |
+
return {child.name: child for child in self.children_recursive}
|
| 174 |
+
|
| 175 |
+
def __getitem__(self, index: int | str) -> Self:
|
| 176 |
+
if index in (0, self.name):
|
| 177 |
+
return self
|
| 178 |
+
if isinstance(index, int):
|
| 179 |
+
index -= 1
|
| 180 |
+
return self.children_recursive[index]
|
| 181 |
+
elif isinstance(index, str):
|
| 182 |
+
return self.children_recursive_dict[index]
|
| 183 |
+
else:
|
| 184 |
+
raise TypeError(f"Index must be int or str, not {type(index)}")
|
| 185 |
+
|
| 186 |
+
@cached_property
|
| 187 |
+
def parent_recursive(self) -> list[Self]:
|
| 188 |
+
parent_list = []
|
| 189 |
+
if self.parent is None:
|
| 190 |
+
return parent_list
|
| 191 |
+
parent_list.append(self.parent)
|
| 192 |
+
parent_list.extend(self.parent.parent_recursive)
|
| 193 |
+
return parent_list
|
| 194 |
+
|
| 195 |
+
def get_first_valid_parent(self, valid_names: list[str]) -> Self | None:
|
| 196 |
+
return next((parent for parent in self.parent_recursive if parent.name in valid_names), None)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def build_skeleton(armature_obj, bones_idx_dict: dict[str, int] = None):
|
| 200 |
+
def get_children(bone, parent=None):
|
| 201 |
+
joint = Joint(
|
| 202 |
+
bone.name, index=bones_idx_dict[bone.name] if bones_idx_dict else None, parent=parent, children=[]
|
| 203 |
+
)
|
| 204 |
+
children = [b for b in bone.children if not bones_idx_dict or b.name in bones_idx_dict]
|
| 205 |
+
if not children:
|
| 206 |
+
return joint
|
| 207 |
+
for child in bone.children:
|
| 208 |
+
joint.children.append(get_children(child, parent=joint))
|
| 209 |
+
return joint
|
| 210 |
+
|
| 211 |
+
hips_bone = armature_obj.data.bones[f"{MIXAMO_PREFIX}Hips"]
|
| 212 |
+
hips = get_children(hips_bone)
|
| 213 |
+
return hips
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
if __name__ == "__main__":
|
| 217 |
+
input_dir = "./character_vroid"
|
| 218 |
+
output_dir = "./character_vroid_refined"
|
| 219 |
+
keep_texture = False
|
| 220 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 221 |
+
|
| 222 |
+
with HiddenPrints():
|
| 223 |
+
reset()
|
| 224 |
+
enable_vrm()
|
| 225 |
+
|
| 226 |
+
for vrm_path in tqdm(sorted(glob(os.path.join(input_dir, "*.vrm"))), dynamic_ncols=True):
|
| 227 |
+
with HiddenPrints(suppress_err=True):
|
| 228 |
+
remove_all()
|
| 229 |
+
objs = load_vrm(vrm_path)
|
| 230 |
+
armature_obj = get_armature_obj(objs)
|
| 231 |
+
armature_data: Armature = armature_obj.data
|
| 232 |
+
mesh_objs = get_all_mesh_obj(objs)
|
| 233 |
+
|
| 234 |
+
# Correct global scaling and rotation
|
| 235 |
+
armature_obj.scale = (100, 100, 100)
|
| 236 |
+
armature_obj.rotation_mode = "XYZ"
|
| 237 |
+
armature_obj.rotation_euler[0] = -math.pi / 2
|
| 238 |
+
bpy.context.view_layer.objects.active = armature_obj
|
| 239 |
+
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
|
| 240 |
+
armature_obj.scale = (0.01, 0.01, 0.01)
|
| 241 |
+
armature_obj.rotation_euler[0] = math.pi / 2
|
| 242 |
+
update()
|
| 243 |
+
|
| 244 |
+
for bone in armature_data.bones:
|
| 245 |
+
if bone.name in VROID_JOINTS_MAP:
|
| 246 |
+
bone.name = VROID_JOINTS_MAP[bone.name]
|
| 247 |
+
with Mode("EDIT", armature_obj):
|
| 248 |
+
armature_data.edit_bones.remove(armature_data.edit_bones["Root"])
|
| 249 |
+
|
| 250 |
+
kinematic_tree = build_skeleton(armature_obj)
|
| 251 |
+
for bone_name in VROID_JOINTS:
|
| 252 |
+
assert bone_name in kinematic_tree, f"{bone_name} not found in {vrm_path}"
|
| 253 |
+
for bone in armature_data.bones:
|
| 254 |
+
if bone.name not in VROID_JOINTS:
|
| 255 |
+
valid_parent = kinematic_tree[bone.name].get_first_valid_parent(VROID_JOINTS)
|
| 256 |
+
if valid_parent is not None:
|
| 257 |
+
transfer_weights(bone.name, valid_parent.name, mesh_objs)
|
| 258 |
+
# remove_empty_vgroups(mesh_objs)
|
| 259 |
+
update()
|
| 260 |
+
|
| 261 |
+
with Mode("EDIT", armature_obj):
|
| 262 |
+
for bone in armature_data.edit_bones:
|
| 263 |
+
if bone.name not in VROID_JOINTS:
|
| 264 |
+
armature_data.edit_bones.remove(bone)
|
| 265 |
+
# Attach parent.tail to child.head (instead of inverse by setting bone.use_connect=True)
|
| 266 |
+
for parent, child in (
|
| 267 |
+
(f"{MIXAMO_PREFIX}Hips", f"{MIXAMO_PREFIX}Spine"),
|
| 268 |
+
(f"{MIXAMO_PREFIX}Spine", f"{MIXAMO_PREFIX}Spine1"),
|
| 269 |
+
(f"{MIXAMO_PREFIX}Spine1", f"{MIXAMO_PREFIX}Spine2"),
|
| 270 |
+
(f"{MIXAMO_PREFIX}LeftFoot", f"{MIXAMO_PREFIX}LeftToeBase"),
|
| 271 |
+
(f"{MIXAMO_PREFIX}RightFoot", f"{MIXAMO_PREFIX}RightToeBase"),
|
| 272 |
+
):
|
| 273 |
+
bone_parent = armature_data.edit_bones[parent]
|
| 274 |
+
bone_child = armature_data.edit_bones[child]
|
| 275 |
+
bone_roll = bone_parent.matrix.to_3x3().copy() @ mathutils.Vector((0.0, 0.0, 1.0))
|
| 276 |
+
bone_parent.tail = bone_child.head
|
| 277 |
+
bone_parent.align_roll(bone_roll)
|
| 278 |
+
|
| 279 |
+
# Correct roll
|
| 280 |
+
template = load_file(os.path.join(output_dir, "../bones.fbx"))
|
| 281 |
+
template_armature = get_armature_obj(template)
|
| 282 |
+
roll_dict = {}
|
| 283 |
+
with Mode("EDIT", template_armature):
|
| 284 |
+
for bone in template_armature.data.edit_bones:
|
| 285 |
+
roll_dict[bone.name] = bone.roll
|
| 286 |
+
for obj in template:
|
| 287 |
+
bpy.data.objects.remove(obj, do_unlink=True)
|
| 288 |
+
with Mode("EDIT", armature_obj):
|
| 289 |
+
for bone in armature_data.edit_bones:
|
| 290 |
+
if bone.name in roll_dict:
|
| 291 |
+
bone.roll = roll_dict[bone.name]
|
| 292 |
+
|
| 293 |
+
update()
|
| 294 |
+
select_objs(objs, deselect_first=True)
|
| 295 |
+
# Warning: exporting to fbx will change tail locations of some bones
|
| 296 |
+
# Use bpy.ops.wm.save_as_mainfile to reproduce this bug
|
| 297 |
+
bpy.ops.export_scene.fbx(
|
| 298 |
+
filepath=os.path.join(output_dir, os.path.basename(vrm_path).replace(".vrm", ".fbx")),
|
| 299 |
+
check_existing=False,
|
| 300 |
+
use_selection=True,
|
| 301 |
+
use_triangles=True,
|
| 302 |
+
add_leaf_bones=False,
|
| 303 |
+
bake_anim=False,
|
| 304 |
+
path_mode="COPY",
|
| 305 |
+
embed_textures=keep_texture,
|
| 306 |
+
)
|
| 307 |
+
bones_path = os.path.join(output_dir, "../bones_vroid.fbx")
|
| 308 |
+
if not os.path.isfile(bones_path):
|
| 309 |
+
select_objs([get_armature_obj(objs)], deselect_first=True)
|
| 310 |
+
bpy.ops.export_scene.fbx(
|
| 311 |
+
filepath=bones_path,
|
| 312 |
+
check_existing=False,
|
| 313 |
+
use_selection=True,
|
| 314 |
+
add_leaf_bones=False,
|
| 315 |
+
bake_anim=False,
|
| 316 |
+
)
|