|
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, |
|
) |
|
|