jasongzy commited on
Commit
0e6cdcd
·
1 Parent(s): e9eca74

✨ feat: vroid2mixamo

Browse files
Files changed (3) hide show
  1. .gitattributes +1 -0
  2. utils.py +55 -0
  3. 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
+ )