| import math |
| import os |
| import sys |
|
|
| import bpy |
| import numpy as np |
| from bpy.types import Action, Armature, Mesh, Object |
|
|
| |
| import bmesh |
| import mathutils |
|
|
|
|
| class HiddenPrints: |
| def __init__(self, enable=True, suppress_err=False): |
| self.enable = enable |
| self.suppress_err = suppress_err |
|
|
| def __enter__(self): |
| if not self.enable: |
| return |
| self._original_stdout = sys.stdout |
| sys.stdout = open(os.devnull, "w") |
| if self.suppress_err: |
| self._original_stderr = sys.stderr |
| sys.stderr = open(os.devnull, "w") |
|
|
| def __exit__(self, exc_type, exc_val, exc_tb): |
| if not self.enable: |
| return |
| sys.stdout.close() |
| sys.stdout = self._original_stdout |
| if self.suppress_err: |
| sys.stderr.close() |
| sys.stderr = self._original_stderr |
|
|
|
|
| USE_WORLD_COORDINATES = False |
|
|
|
|
| class Mode: |
| def __init__(self, mode_name="EDIT", active_obj: Object = None): |
| self.mode = mode_name |
| self.active = active_obj |
| self.pre_active = None |
| self.pre_mode = "OBJECT" |
|
|
| def __enter__(self): |
| self.pre_active = bpy.context.view_layer.objects.active |
| if self.pre_active is not None: |
| self.pre_mode = bpy.context.object.mode |
| bpy.context.view_layer.objects.active = self.active |
| bpy.ops.object.mode_set(mode=self.mode) |
| return self.active |
|
|
| def __exit__(self, exc_type, exc_val, exc_tb): |
| bpy.ops.object.mode_set(mode=self.pre_mode) |
| bpy.context.view_layer.objects.active = self.pre_active |
|
|
|
|
| def reset(): |
| bpy.ops.wm.read_factory_settings(use_empty=True) |
|
|
|
|
| def update(): |
| bpy.context.view_layer.update() |
| bpy.context.scene.update_tag() |
| for obj in bpy.context.scene.objects: |
| |
| obj.update_tag() |
|
|
|
|
| def remove_all(delete_actions=True): |
| for obj in bpy.data.objects.values(): |
| bpy.data.objects.remove(obj, do_unlink=True) |
| bpy.ops.outliner.orphans_purge(do_recursive=True) |
| if delete_actions: |
| for action in bpy.data.actions: |
| bpy.data.actions.remove(action, do_unlink=True) |
|
|
|
|
| def remove_empty(): |
| childless_empties = [e for e in bpy.data.objects if e.type.startswith("EMPTY") and not e.children] |
| bpy.data.batch_remove(childless_empties) |
|
|
|
|
| def remove_collection(coll_name: str): |
| if coll_name not in bpy.data.collections: |
| return |
| coll = bpy.data.collections[coll_name] |
| for c in coll.children: |
| remove_collection(c) |
| bpy.data.collections.remove(coll, do_unlink=True) |
|
|
|
|
| def load_file(filepath: str, *args, **kwargs) -> "list[Object]": |
| old_objs = set(bpy.context.scene.objects) |
| if filepath.endswith(".glb"): |
| bpy.ops.import_scene.gltf(filepath=filepath, *args, **kwargs) |
| elif filepath.endswith(".fbx"): |
| bpy.ops.import_scene.fbx(filepath=filepath, *args, **kwargs) |
| elif filepath.endswith(".obj"): |
| bpy.ops.wm.obj_import(filepath=filepath, *args, **kwargs) |
| elif filepath.endswith(".ply"): |
| bpy.ops.wm.ply_import(filepath=filepath, *args, **kwargs) |
| else: |
| raise RuntimeError(f"Invalid input file: {filepath}") |
| 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 |
|
|
|
|
| def select_all(): |
| bpy.ops.object.select_all(action="SELECT") |
|
|
|
|
| def deselect(): |
| bpy.ops.object.select_all(action="DESELECT") |
|
|
|
|
| def select_objs(obj_list: "list[Object]" = None, deselect_first=False): |
| if not obj_list: |
| obj_list = bpy.context.scene.objects |
| if deselect_first: |
| deselect() |
| for obj in obj_list: |
| obj.select_set(True) |
|
|
|
|
| def select_mesh(obj_list: "list[Object]" = None, all=True, deselect_first=False): |
| if not obj_list: |
| obj_list = bpy.context.scene.objects |
| if deselect_first: |
| deselect() |
| for obj in obj_list: |
| if obj.type == "MESH": |
| if all: |
| obj.select_set(True) |
| else: |
| break |
|
|
|
|
| class Select: |
| """ |
| Deselecting before and after selecting the specified objects. |
| """ |
|
|
| def __init__(self, objs: "Object | list[Object]" = None): |
| self.objs = (objs,) if isinstance(objs, Object) else objs |
| self.objs: "tuple[Object]" = tuple(self.objs) |
|
|
| def __enter__(self): |
| select_objs(self.objs, deselect_first=True) |
| return self.objs |
|
|
| def __exit__(self, exc_type, exc_val, exc_tb): |
| deselect() |
|
|
|
|
| def get_type_objs(obj_list: "list[Object]" = None, type="MESH", sort=True) -> "list[Object]": |
| if not obj_list: |
| obj_list = bpy.context.scene.objects |
| type_obj_list = [obj for obj in obj_list if obj.type == type] |
| if sort: |
| type_obj_list = sorted(type_obj_list, key=lambda x: x.name) |
| return type_obj_list |
|
|
|
|
| def get_all_mesh_obj(obj_list: "list[Object]" = None): |
| return get_type_objs(obj_list, "MESH") |
|
|
|
|
| def get_all_armature_obj(obj_list: "list[Object]" = None): |
| return get_type_objs(obj_list, "ARMATURE") |
|
|
|
|
| def get_armature_obj(obj_list: "list[Object]" = None) -> Object: |
| if not obj_list: |
| obj_list = bpy.context.scene.objects |
| for obj in obj_list: |
| if obj.type == "ARMATURE": |
| return obj |
|
|
|
|
| def get_rest_bones(armature_obj: Object): |
| if armature_obj is None: |
| return None, None, None |
| rest_bones = [] |
| rest_bones_tail = [] |
| bones_idx_dict: "dict[str, int]" = {} |
| armature_data: Armature = armature_obj.data |
| for i, bone in enumerate(armature_data.bones): |
| pos = bone.head_local |
| pos_tail = bone.tail_local |
| if USE_WORLD_COORDINATES: |
| pos = armature_obj.matrix_world @ pos |
| pos_tail = armature_obj.matrix_world @ pos_tail |
| rest_bones.append(pos) |
| rest_bones_tail.append(pos_tail) |
| bones_idx_dict[bone.name] = i |
| rest_bones = np.stack(rest_bones, axis=0) |
| rest_bones_tail = np.stack(rest_bones_tail, axis=0) |
| return rest_bones, rest_bones_tail, bones_idx_dict |
|
|
|
|
| def transfer_weights(source_bone_name: str, target_bone_name: str, mesh_obj_list: "list[Object]"): |
| if isinstance(mesh_obj_list, Object): |
| mesh_obj_list = [mesh_obj_list] |
| for obj in mesh_obj_list: |
| source_group = obj.vertex_groups.get(source_bone_name) |
| if source_group is None: |
| return |
| source_i = source_group.index |
| target_group = obj.vertex_groups.get(target_bone_name) |
| if target_group is None: |
| target_group = obj.vertex_groups.new(name=target_bone_name) |
|
|
| for v in obj.data.vertices: |
| for g in v.groups: |
| if g.group == source_i: |
| target_group.add((v.index,), g.weight, "ADD") |
| obj.vertex_groups.remove(source_group) |
|
|
|
|
| def remove_empty_vgroups(mesh_obj_list: "list[Object]"): |
| if isinstance(mesh_obj_list, Object): |
| mesh_obj_list = [mesh_obj_list] |
| for obj in mesh_obj_list: |
| vertex_groups = obj.vertex_groups |
| groups = {r: None for r in range(len(vertex_groups))} |
|
|
| for vert in obj.data.vertices: |
| for vg in vert.groups: |
| i = vg.group |
| if i in groups: |
| del groups[i] |
|
|
| lis = list(groups) |
| lis.sort(reverse=True) |
| for i in lis: |
| vertex_groups.remove(vertex_groups[i]) |
|
|
|
|
| def set_action(armature_obj: Object, action: Action): |
| if not armature_obj.animation_data: |
| armature_obj.animation_data_create() |
| armature_obj.animation_data.action = action |
| return armature_obj |
|
|
|
|
| def mesh_quads2tris(obj_list: "list[Object]" = None): |
| if not obj_list: |
| obj_list = bpy.context.scene.objects |
| for obj in obj_list: |
| if obj.type == "MESH": |
| with Mode("EDIT", obj): |
| bpy.ops.mesh.quads_convert_to_tris(quad_method="BEAUTY", ngon_method="BEAUTY") |
|
|
|
|
| def get_enabled_addons() -> "list[str]": |
| return [x.module for x in bpy.context.preferences.addons] |
|
|
|
|
| def enable_arp(armature_obj: Object, addon_path=os.path.join(os.path.dirname(__file__), "auto_rig_pro")): |
| import sys |
|
|
| assert os.path.isfile(os.path.join(addon_path, "__init__.py")), "Auto-Rig Pro not found" |
| dirname, addon_name = os.path.split(addon_path) |
| |
| |
| sys.path.insert(0, dirname) |
| with Mode("POSE", armature_obj): |
| |
| |
| bpy.ops.preferences.addon_enable(module=addon_name) |
|
|
|
|
| def retarget(source_armature: Object, target_armature: Object, inplace=False): |
| enable_arp(target_armature) |
| scn = bpy.context.scene |
| scn.source_rig = source_armature.name |
| if inplace: |
| scn.arp_retarget_in_place = True |
| scn.target_rig = target_armature.name |
| bpy.ops.arp.auto_scale() |
| bpy.ops.arp.build_bones_list() |
| hips = scn.bones_map_v2["mixamorig:Hips"] |
| scn.bones_map_index = list(scn.bones_map_v2).index(hips) |
| hips.set_as_root = True |
| bpy.ops.arp.retarget() |
| return target_armature |
|
|
|
|
| def load_mixamo_anim(char_file: str, anim_file: str, do_retarget=False, inplace=False, to_tris=False): |
| char_objs = load_file(char_file) if isinstance(char_file, str) else char_file |
| char_armature = get_armature_obj(char_objs) |
|
|
| anim_objs = load_file(anim_file) |
| anim_armature = get_armature_obj(anim_objs) |
| print(anim_armature) |
| print(anim_armature.animation_data) |
| assert anim_armature.animation_data is not None and len(bpy.data.actions) > 0, f"Animation not found in {anim_file}" |
|
|
| set_action(char_armature, anim_armature.animation_data.action) |
| if do_retarget: |
| retarget(anim_armature, char_armature, inplace=inplace) |
| for action in bpy.data.actions: |
| if action is not char_armature.animation_data.action: |
| bpy.data.actions.remove(action, do_unlink=True) |
| for obj in anim_objs: |
| bpy.data.objects.remove(obj, do_unlink=True) |
|
|
| if to_tris: |
| mesh_quads2tris(char_objs) |
| return char_objs |
|
|