| 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_RabbitEar2_01": f"{MIXAMO_PREFIX}LRabbitEar2", |
| |
| "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 |
|
|
| |
| 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]: |
| |
| 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) |
|
|
| |
| 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) |
| |
| 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) |
| |
| 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) |
|
|
| |
| 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) |
| |
| |
| 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, |
| ) |
|
|