Mixamo2 / vroid2mixamo.py
jasongzy's picture
🦄 refactor: misc
60e2a4a
import math
import os
from dataclasses import dataclass
from functools import cached_property
from glob import glob
from typing import Iterator
try:
from typing import Self
except ImportError:
from typing_extensions import Self
from tqdm import tqdm
from utils import (
Armature,
HiddenPrints,
Mode,
bpy,
get_all_mesh_obj,
get_armature_obj,
load_file,
mathutils,
remove_all,
remove_collection,
remove_empty_vgroups,
reset,
select_objs,
transfer_weights,
update,
)
MIXAMO_PREFIX = "mixamorig:"
VROID_JOINTS_MAP = {
"J_Bip_C_Hips": f"{MIXAMO_PREFIX}Hips",
"J_Bip_C_Spine": f"{MIXAMO_PREFIX}Spine",
"J_Bip_C_Chest": f"{MIXAMO_PREFIX}Spine1",
"J_Bip_C_UpperChest": f"{MIXAMO_PREFIX}Spine2",
"J_Bip_C_Neck": f"{MIXAMO_PREFIX}Neck",
"J_Bip_C_Head": f"{MIXAMO_PREFIX}Head",
"J_Bip_L_Shoulder": f"{MIXAMO_PREFIX}LeftShoulder",
"J_Bip_L_UpperArm": f"{MIXAMO_PREFIX}LeftArm",
"J_Bip_L_LowerArm": f"{MIXAMO_PREFIX}LeftForeArm",
"J_Bip_L_Hand": f"{MIXAMO_PREFIX}LeftHand",
"J_Bip_L_Index1": f"{MIXAMO_PREFIX}LeftHandIndex1",
"J_Bip_L_Index2": f"{MIXAMO_PREFIX}LeftHandIndex2",
"J_Bip_L_Index3": f"{MIXAMO_PREFIX}LeftHandIndex3",
"J_Bip_L_Little1": f"{MIXAMO_PREFIX}LeftHandPinky1",
"J_Bip_L_Little2": f"{MIXAMO_PREFIX}LeftHandPinky2",
"J_Bip_L_Little3": f"{MIXAMO_PREFIX}LeftHandPinky3",
"J_Bip_L_Middle1": f"{MIXAMO_PREFIX}LeftHandMiddle1",
"J_Bip_L_Middle2": f"{MIXAMO_PREFIX}LeftHandMiddle2",
"J_Bip_L_Middle3": f"{MIXAMO_PREFIX}LeftHandMiddle3",
"J_Bip_L_Ring1": f"{MIXAMO_PREFIX}LeftHandRing1",
"J_Bip_L_Ring2": f"{MIXAMO_PREFIX}LeftHandRing2",
"J_Bip_L_Ring3": f"{MIXAMO_PREFIX}LeftHandRing3",
"J_Bip_L_Thumb1": f"{MIXAMO_PREFIX}LeftHandThumb1",
"J_Bip_L_Thumb2": f"{MIXAMO_PREFIX}LeftHandThumb2",
"J_Bip_L_Thumb3": f"{MIXAMO_PREFIX}LeftHandThumb3",
"J_Bip_R_Shoulder": f"{MIXAMO_PREFIX}RightShoulder",
"J_Bip_R_UpperArm": f"{MIXAMO_PREFIX}RightArm",
"J_Bip_R_LowerArm": f"{MIXAMO_PREFIX}RightForeArm",
"J_Bip_R_Hand": f"{MIXAMO_PREFIX}RightHand",
"J_Bip_R_Index1": f"{MIXAMO_PREFIX}RightHandIndex1",
"J_Bip_R_Index2": f"{MIXAMO_PREFIX}RightHandIndex2",
"J_Bip_R_Index3": f"{MIXAMO_PREFIX}RightHandIndex3",
"J_Bip_R_Little1": f"{MIXAMO_PREFIX}RightHandPinky1",
"J_Bip_R_Little2": f"{MIXAMO_PREFIX}RightHandPinky2",
"J_Bip_R_Little3": f"{MIXAMO_PREFIX}RightHandPinky3",
"J_Bip_R_Middle1": f"{MIXAMO_PREFIX}RightHandMiddle1",
"J_Bip_R_Middle2": f"{MIXAMO_PREFIX}RightHandMiddle2",
"J_Bip_R_Middle3": f"{MIXAMO_PREFIX}RightHandMiddle3",
"J_Bip_R_Ring1": f"{MIXAMO_PREFIX}RightHandRing1",
"J_Bip_R_Ring2": f"{MIXAMO_PREFIX}RightHandRing2",
"J_Bip_R_Ring3": f"{MIXAMO_PREFIX}RightHandRing3",
"J_Bip_R_Thumb1": f"{MIXAMO_PREFIX}RightHandThumb1",
"J_Bip_R_Thumb2": f"{MIXAMO_PREFIX}RightHandThumb2",
"J_Bip_R_Thumb3": f"{MIXAMO_PREFIX}RightHandThumb3",
"J_Bip_L_UpperLeg": f"{MIXAMO_PREFIX}LeftUpLeg",
"J_Bip_L_LowerLeg": f"{MIXAMO_PREFIX}LeftLeg",
"J_Bip_L_Foot": f"{MIXAMO_PREFIX}LeftFoot",
"J_Bip_L_ToeBase": f"{MIXAMO_PREFIX}LeftToeBase",
"J_Bip_R_UpperLeg": f"{MIXAMO_PREFIX}RightUpLeg",
"J_Bip_R_LowerLeg": f"{MIXAMO_PREFIX}RightLeg",
"J_Bip_R_Foot": f"{MIXAMO_PREFIX}RightFoot",
"J_Bip_R_ToeBase": f"{MIXAMO_PREFIX}RightToeBase",
#
# "J_Opt_L_RabbitEar1_01": f"{MIXAMO_PREFIX}LRabbitEar1",
"J_Opt_L_RabbitEar2_01": f"{MIXAMO_PREFIX}LRabbitEar2",
# "J_Opt_R_RabbitEar1_01": f"{MIXAMO_PREFIX}RRabbitEar1",
"J_Opt_R_RabbitEar2_01": f"{MIXAMO_PREFIX}RRabbitEar2",
"J_Opt_C_FoxTail1_01": f"{MIXAMO_PREFIX}FoxTail1",
"J_Opt_C_FoxTail2_01": f"{MIXAMO_PREFIX}FoxTail2",
"J_Opt_C_FoxTail3_01": f"{MIXAMO_PREFIX}FoxTail3",
"J_Opt_C_FoxTail4_01": f"{MIXAMO_PREFIX}FoxTail4",
"J_Opt_C_FoxTail5_01": f"{MIXAMO_PREFIX}FoxTail5",
}
VROID_JOINTS = set(VROID_JOINTS_MAP.values())
def enable_vrm(addon_path="VRM_Addon_for_Blender-Extension-2_20_88.zip"):
"""https://github.com/saturday06/VRM-Addon-for-Blender"""
assert os.path.isfile(addon_path), f"Addon file not found: {addon_path}"
import shutil
# bpy.ops.preferences.addon_install(filepath=os.path.abspath(addon_path))
repo = "user_default"
shutil.rmtree(bpy.utils.user_resource("EXTENSIONS", path=repo))
bpy.ops.extensions.package_install_files(filepath=os.path.abspath(addon_path), repo=repo)
bpy.ops.preferences.addon_enable(module=f"bl_ext.{repo}.vrm")
def load_vrm(filepath: str):
old_objs = set(bpy.context.scene.objects)
bpy.ops.import_scene.vrm(
filepath=filepath,
use_addon_preferences=True,
extract_textures_into_folder=False,
make_new_texture_folder=True,
set_shading_type_to_material_on_import=False,
set_view_transform_to_standard_on_import=True,
set_armature_display_to_wire=False,
set_armature_display_to_show_in_front=False,
set_armature_bone_shape_to_default=True,
)
remove_collection("glTF_not_exported")
remove_collection("Colliders")
imported_objs = set(bpy.context.scene.objects) - old_objs
imported_objs = sorted(imported_objs, key=lambda x: x.name)
print("Imported:", imported_objs)
return imported_objs
@dataclass(frozen=True)
class Joint:
name: str
index: int
parent: Self | None
children: list[Self]
def __repr__(self):
return f"{self.__class__.__name__}({self.name})"
def __iter__(self) -> Iterator[Self]:
yield self
for child in self.children:
yield from child
@cached_property
def children_recursive(self) -> list[Self]:
# return [child for child in self if child is not self]
children_list = []
if not self.children:
return children_list
for child in self.children:
children_list.append(child)
children_list.extend(child.children_recursive)
return children_list
def __len__(self):
return len(self.children_recursive) + 1
def __contains__(self, item: Self | str):
if isinstance(item, str):
return item == self.name or item in (child.name for child in self.children_recursive)
elif isinstance(item, Joint):
return item is Self or item in self.children_recursive
else:
raise TypeError(f"Item must be {self.__class__.__name__} or str, not {type(item)}")
@cached_property
def children_recursive_dict(self) -> dict[str, Self]:
return {child.name: child for child in self.children_recursive}
def __getitem__(self, index: int | str) -> Self:
if index in (0, self.name):
return self
if isinstance(index, int):
index -= 1
return self.children_recursive[index]
elif isinstance(index, str):
return self.children_recursive_dict[index]
else:
raise TypeError(f"Index must be int or str, not {type(index)}")
@cached_property
def parent_recursive(self) -> list[Self]:
parent_list = []
if self.parent is None:
return parent_list
parent_list.append(self.parent)
parent_list.extend(self.parent.parent_recursive)
return parent_list
def get_first_valid_parent(self, valid_names: list[str]) -> Self | None:
return next((parent for parent in self.parent_recursive if parent.name in valid_names), None)
def build_skeleton(armature_obj, bones_idx_dict: dict[str, int] = None):
def get_children(bone, parent=None):
joint = Joint(
bone.name, index=bones_idx_dict[bone.name] if bones_idx_dict else None, parent=parent, children=[]
)
children = [b for b in bone.children if not bones_idx_dict or b.name in bones_idx_dict]
if not children:
return joint
for child in bone.children:
joint.children.append(get_children(child, parent=joint))
return joint
hips_bone = armature_obj.data.bones[f"{MIXAMO_PREFIX}Hips"]
hips = get_children(hips_bone)
return hips
if __name__ == "__main__":
input_dir = "./character_vroid"
output_dir = "./character_vroid_refined"
keep_texture = False
os.makedirs(output_dir, exist_ok=True)
with HiddenPrints():
reset()
enable_vrm()
for vrm_path in tqdm(sorted(glob(os.path.join(input_dir, "*.vrm"))), dynamic_ncols=True):
with HiddenPrints(suppress_err=True):
remove_all()
objs = load_vrm(vrm_path)
armature_obj = get_armature_obj(objs)
armature_data: Armature = armature_obj.data
mesh_objs = get_all_mesh_obj(objs)
# Correct global scaling and rotation
armature_obj.scale = (100, 100, 100)
armature_obj.rotation_mode = "XYZ"
armature_obj.rotation_euler[0] = -math.pi / 2
bpy.context.view_layer.objects.active = armature_obj
armature_obj.select_set(state=True)
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
armature_obj.scale = (0.01, 0.01, 0.01)
armature_obj.rotation_euler[0] = math.pi / 2
update()
for bone in armature_data.bones:
if bone.name in VROID_JOINTS_MAP:
bone.name = VROID_JOINTS_MAP[bone.name]
with Mode("EDIT", armature_obj):
armature_data.edit_bones.remove(armature_data.edit_bones["Root"])
kinematic_tree = build_skeleton(armature_obj)
for bone_name in VROID_JOINTS:
assert bone_name in kinematic_tree, f"{bone_name} not found in {vrm_path}"
for bone in armature_data.bones:
if bone.name not in VROID_JOINTS:
valid_parent = kinematic_tree[bone.name].get_first_valid_parent(VROID_JOINTS)
if valid_parent is not None:
transfer_weights(bone.name, valid_parent.name, mesh_objs)
# remove_empty_vgroups(mesh_objs)
update()
with Mode("EDIT", armature_obj):
for bone in armature_data.edit_bones:
if bone.name not in VROID_JOINTS:
armature_data.edit_bones.remove(bone)
# Attach parent.tail to child.head (instead of inverse by setting bone.use_connect=True)
for parent, child in (
(f"{MIXAMO_PREFIX}Hips", f"{MIXAMO_PREFIX}Spine"),
(f"{MIXAMO_PREFIX}Spine", f"{MIXAMO_PREFIX}Spine1"),
(f"{MIXAMO_PREFIX}Spine1", f"{MIXAMO_PREFIX}Spine2"),
(f"{MIXAMO_PREFIX}LeftFoot", f"{MIXAMO_PREFIX}LeftToeBase"),
(f"{MIXAMO_PREFIX}RightFoot", f"{MIXAMO_PREFIX}RightToeBase"),
):
bone_parent = armature_data.edit_bones[parent]
bone_child = armature_data.edit_bones[child]
bone_roll = bone_parent.matrix.to_3x3().copy() @ mathutils.Vector((0.0, 0.0, 1.0))
bone_parent.tail = bone_child.head
bone_parent.align_roll(bone_roll)
# Correct roll
template = load_file(os.path.join(output_dir, "../bones.fbx"))
template_armature = get_armature_obj(template)
roll_dict = {}
with Mode("EDIT", template_armature):
for bone in template_armature.data.edit_bones:
roll_dict[bone.name] = bone.roll
for obj in template:
bpy.data.objects.remove(obj, do_unlink=True)
with Mode("EDIT", armature_obj):
for bone in armature_data.edit_bones:
if bone.name in roll_dict:
bone.roll = roll_dict[bone.name]
update()
select_objs(objs, deselect_first=True)
# Warning: exporting to fbx will change tail locations of some bones
# Use bpy.ops.wm.save_as_mainfile to reproduce this bug
bpy.ops.export_scene.fbx(
filepath=os.path.join(output_dir, os.path.basename(vrm_path).replace(".vrm", ".fbx")),
check_existing=False,
use_selection=True,
use_triangles=True,
add_leaf_bones=False,
bake_anim=False,
path_mode="COPY",
embed_textures=keep_texture,
)
bones_path = os.path.join(output_dir, "../bones_vroid.fbx")
if not os.path.isfile(bones_path):
select_objs([get_armature_obj(objs)], deselect_first=True)
bpy.ops.export_scene.fbx(
filepath=bones_path,
check_existing=False,
use_selection=True,
add_leaf_bones=False,
bake_anim=False,
)