Spaces:
Running on Zero
Running on Zero
| from collections import defaultdict | |
| from numpy import ndarray | |
| from typing import Optional, Tuple | |
| import bpy # type: ignore | |
| import logging | |
| import numpy as np | |
| import os | |
| import trimesh | |
| from .abstract import AbstractParser | |
| from ..info.asset import Asset | |
| from mathutils import Vector, Matrix # type: ignore | |
| class BpyParser(AbstractParser): | |
| def load(cls, filepath: str, **kwargs) -> Asset: | |
| clean_bpy() | |
| load(filepath=filepath, **kwargs) | |
| collection = bpy.data.collections.get("glTF_not_exported") | |
| if collection is not None: | |
| for obj in list(collection.objects): | |
| bpy.data.objects.remove(obj, do_unlink=True) | |
| armature = get_armature() | |
| if armature is None: | |
| bones = None | |
| joint_names = None | |
| parents = None | |
| lengths = None | |
| matrix_world = np.eye(4) | |
| matrix_local = None | |
| matrix_basis = None | |
| armature_name = None | |
| else: | |
| bones = armature.pose.bones # list of PoseBone | |
| joint_names = [b.name for b in bones] | |
| parents = [] | |
| lengths = [] | |
| matrix_world = np.array(armature.matrix_world) | |
| obj = armature.parent | |
| while obj is not None: | |
| matrix_world = np.array(obj.matrix_world) @ matrix_world | |
| obj = obj.parent | |
| matrix_local = [] | |
| for pbone in bones: | |
| matrix_local.append(np.array(pbone.bone.matrix_local)) | |
| parents.append(joint_names.index(pbone.parent.name) if pbone.parent is not None else -1) | |
| lengths.append(pbone.bone.length) | |
| matrix_local = np.stack(matrix_local, axis=0) | |
| parents = np.array(parents, dtype=np.int32) | |
| lengths = np.array(lengths, dtype=np.float32) | |
| matrix_basis = get_matrix_basis(bones=bones) | |
| armature_name = armature.name | |
| mesh_dict = extract_mesh(bones=bones) | |
| return Asset( | |
| vertices=mesh_dict['vertices'], | |
| faces=mesh_dict['faces'], | |
| vertex_normals=mesh_dict['vertex_normals'], | |
| face_normals=mesh_dict['face_normals'], | |
| vertex_bias=mesh_dict['vertex_bias'], | |
| face_bias=mesh_dict['face_bias'], | |
| mesh_names=mesh_dict['mesh_names'], | |
| joint_names=joint_names, | |
| parents=parents, | |
| lengths=lengths, | |
| matrix_world=matrix_world, | |
| matrix_local=matrix_local, | |
| matrix_basis=matrix_basis, | |
| armature_name=armature_name, | |
| skin=mesh_dict['skin'], | |
| ) | |
| def export(cls, asset: Asset, filepath: str, **kwargs): | |
| """ | |
| If export obj, kwargs: | |
| precision: int=6, number of decimal places for vertex coordinates | |
| Otherwise, export fbx/glb/gltf using bpy, kwargs: | |
| extrude_scale: float=0.5, if there is no tails in asset, first calculate the average length between parents and sons, then the length of leaf bone is l*extrude_scale. Otherwise do not affect final results. | |
| connect_tail_to_unique_child: bool=False, if True, the tail of a bone with only one child will be exactly at the head of its child. | |
| extrude_from_parent: bool=False, if True, the orientation of the leaf bone will be the same as its parent. | |
| group_per_vertex: int=-1, number of the largest weights to keep for each vertex. -1 means keep all. | |
| add_root: bool=False, if True, add a root bone at (0, 0, 0). | |
| do_not_normalize: bool=False, if True, do not normalize the skinning weights. | |
| collection_name: str='new_collection', name of the new collection to store objects. | |
| add_leaf_bones: bool=False, if True, add a leaf bone at the end of each bone. | |
| """ | |
| ext = os.path.splitext(filepath)[1].lower() | |
| if ext == '.obj': | |
| cls.export_obj(asset, filepath, **kwargs) | |
| elif ext == 'ply': | |
| cls.export_ply(asset, filepath, **kwargs) | |
| else: | |
| cls.export_asset(asset, filepath, **kwargs) | |
| def export_obj( | |
| cls, | |
| asset: Asset, | |
| filepath: str, | |
| precision: int=6, | |
| use_pc: bool=False, | |
| use_normal: bool=False, | |
| use_skeleton: bool=False, | |
| normal_size: float=0.01, | |
| ): | |
| """ | |
| Export the asset as an .obj file. This will ignore skeleton and skinning. | |
| Args: | |
| use_normal: export normals | |
| use_skeleton: export skeleton | |
| """ | |
| asset._build_bias() | |
| if asset.vertices is None or asset.vertex_bias is None: | |
| raise ValueError("do not have vertices or vertex_bias") | |
| if use_normal and asset.vertex_normals is None: | |
| raise ValueError("use_normal is True but do not have vertex_normals") | |
| if not filepath.lower().endswith('.obj'): | |
| filepath += ".obj" | |
| faces = asset.faces | |
| mesh_names = asset.mesh_names | |
| if mesh_names is None: | |
| mesh_names = [f"mesh_{i}" for i in range(asset.P)] | |
| cls._safe_make_dir(filepath) | |
| file = open(filepath, 'w') | |
| lines = [] | |
| tot = 0 | |
| if use_skeleton: | |
| raise NotImplementedError() | |
| for i, mesh_name in enumerate(mesh_names): | |
| lines.append(f'o {mesh_name}\n') | |
| if use_normal: | |
| s = asset.get_vertex_slice(i) | |
| for v, n in zip(asset.vertices[s], asset.vertex_normals[s]): # type: ignore | |
| vv = v + n * normal_size | |
| lines.append(f'v {v[0]:.{precision}f} {v[2]:.{precision}f} {-v[1]:.{precision}f}\n') | |
| lines.append(f'v {vv[0]:.{precision}f} {vv[2]:.{precision}f} {-vv[1]:.{precision}f}\n') | |
| lines.append(f'v {vv[0]:.{precision}f} {vv[2]:.{precision}f} {-vv[1]+0.000001:.{precision}f}\n') | |
| lines.append(f"f {tot+1} {tot+2} {tot+3}\n") | |
| tot += 3 | |
| else: | |
| for v in asset.vertices[asset.get_vertex_slice(i)]: | |
| lines.append(f'v {v[0]:.{precision}f} {v[2]:.{precision}f} {-v[1]:.{precision}f}\n') | |
| if faces is not None and use_pc == False: | |
| for f in faces[asset.get_face_slice(i)]: | |
| lines.append(f"f {f[0]+1} {f[1]+1} {f[2]+1}\n") | |
| file.writelines(lines) | |
| file.close() | |
| def export_ply( | |
| cls, | |
| asset: Asset, | |
| filepath: str, | |
| use_pc: bool=False, | |
| render_skin_id: Optional[int]=None, | |
| ): | |
| """ | |
| Export the asset as an .ply file. This will ignore skeleton and skinning. | |
| """ | |
| import open3d as o3d | |
| asset._build_bias() | |
| if asset.vertices is None or asset.vertex_bias is None: | |
| raise ValueError("do not have vertices or vertex_bias") | |
| if not filepath.lower().endswith('.ply'): | |
| filepath += ".ply" | |
| faces = asset.faces | |
| if use_pc: | |
| faces = None | |
| mesh_names = asset.mesh_names | |
| if mesh_names is None: | |
| mesh_names = [f"mesh_{i}" for i in range(asset.P)] | |
| cls._safe_make_dir(filepath) | |
| if render_skin_id is not None: | |
| if asset.skin is None: | |
| raise ValueError("render_skin_id is not None, but skin of asset is None") | |
| colors = np.stack([ | |
| asset.skin[:, render_skin_id], | |
| np.zeros(asset.N), | |
| 1-asset.skin[:, render_skin_id], | |
| ], axis=1) | |
| else: | |
| colors = None | |
| if faces is None: | |
| pcd = o3d.geometry.PointCloud() | |
| pcd.points = o3d.utility.Vector3dVector(asset.vertices) | |
| if colors is not None: | |
| pcd.colors = o3d.utility.Vector3dVector(colors) | |
| o3d.io.write_point_cloud(filepath, pcd) | |
| else: | |
| mesh = o3d.geometry.TriangleMesh() | |
| mesh.vertices = o3d.utility.Vector3dVector(asset.vertices) | |
| mesh.triangles = o3d.utility.Vector3iVector(faces) | |
| if colors is not None: | |
| mesh.vertex_colors = o3d.utility.Vector3dVector(colors) | |
| o3d.io.write_triangle_mesh(filepath, mesh) | |
| def export_asset(cls, asset: Asset, filepath: str, **kwargs): | |
| use_origin = kwargs.pop('use_origin', False) if 'use_origin' in kwargs else False | |
| if not use_origin: | |
| clean_bpy() | |
| make_asset(asset=asset, **kwargs) | |
| cls._safe_make_dir(filepath) | |
| _, ext = os.path.splitext(filepath) | |
| ext = ext.lower()[1:] | |
| if ext == 'fbx': | |
| if asset.joints is not None and asset.matrix_basis is not None: | |
| logging.warning("Exporting animation, but fbx format is deprecated because the rest pose will not be exported in bpy4.2. Use glb/gltf format instead. See: https://blender.stackexchange.com/questions/273398/blender-export-fbx-lose-the-origin-rest-pose.") | |
| bpy.ops.export_scene.fbx(filepath=filepath, check_existing=False, add_leaf_bones=kwargs.get('add_leaf_bones', False), path_mode='COPY', embed_textures=True, mesh_smooth_type="FACE") | |
| elif ext == 'glb' or ext == 'gltf': | |
| bpy.ops.export_scene.gltf(filepath=filepath) | |
| else: | |
| raise ValueError(f"Unsupported format: {ext}") | |
| def _safe_make_dir(cls, path: str): | |
| if os.path.dirname(path) == '': | |
| return | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| def clean_bpy(): | |
| """Clean all the data in bpy.""" | |
| bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) | |
| data_types = [ | |
| bpy.data.actions, | |
| bpy.data.armatures, | |
| bpy.data.cameras, | |
| bpy.data.collections, | |
| bpy.data.curves, | |
| bpy.data.lights, | |
| bpy.data.materials, | |
| bpy.data.meshes, | |
| bpy.data.objects, | |
| bpy.data.worlds, | |
| bpy.data.node_groups, | |
| bpy.data.images, | |
| bpy.data.textures, | |
| ] | |
| for data_collection in data_types: | |
| for item in data_collection: | |
| data_collection.remove(item) | |
| def load(filepath: str, **kwargs): | |
| """Load a 3D file into bpy.""" | |
| _, ext = os.path.splitext(filepath) | |
| ext = ext.lower()[1:] | |
| if not os.path.exists(filepath): | |
| raise RuntimeError(f"file does not exist: {filepath}") | |
| if ext == "obj": | |
| bpy.ops.wm.obj_import(filepath=filepath) | |
| elif ext == "fbx": | |
| bpy.ops.import_scene.fbx( | |
| filepath=filepath, | |
| ignore_leaf_bones=kwargs.get('ignore_leaf_bones', False), | |
| use_image_search=kwargs.get('use_image_search', True), | |
| ) | |
| elif ext == "glb" or ext == "gltf": | |
| bpy.ops.import_scene.gltf(filepath=filepath, import_pack_images=kwargs.get('import_pack_images', False)) | |
| elif ext == "dae": | |
| bpy.ops.wm.collada_import(filepath=filepath) | |
| elif ext == "blend": | |
| with bpy.data.libraries.load(filepath) as (data_from, data_to): | |
| data_to.objects = data_from.objects | |
| for obj in data_to.objects: | |
| if obj is not None: | |
| bpy.context.collection.objects.link(obj) | |
| elif ext == "bvh": | |
| bpy.ops.import_anim.bvh(filepath=filepath) | |
| else: | |
| raise ValueError(f"unsupported type: {ext}") | |
| def get_armature(): | |
| """Get the armature object in the current scene.""" | |
| armatures = [obj for obj in bpy.context.scene.objects if obj.type == 'ARMATURE'] | |
| if len(armatures) == 0: | |
| return None | |
| return armatures[0] | |
| def extract_mesh(bones=None): | |
| """ | |
| Extract vertices, face_normals, faces and skinning(if possible). | |
| """ | |
| meshes = [] | |
| for v in bpy.data.objects: | |
| if v.type == 'MESH': | |
| meshes.append(v) | |
| index = {} | |
| if bones is not None: | |
| for (id, pbone) in enumerate(bones): | |
| index[pbone.name] = id | |
| total_bones = len(bones) | |
| else: | |
| total_bones = None | |
| mesh_names_list = [] | |
| vertices_list = [] | |
| faces_list = [] | |
| skin_list = [] | |
| vertex_bias = [] | |
| face_bias = [] | |
| cur_vertex_bias = 0 | |
| cur_face_bias = 0 | |
| for obj in meshes: | |
| # directly apply mesh's transformation because armature operates on the transformed mesh | |
| if obj.parent is not None: | |
| m = np.linalg.inv(np.array(obj.parent.matrix_world)) @ np.array(obj.matrix_world) | |
| else: | |
| m = np.array(obj.matrix_world) | |
| matrix_world_rot = m[:3, :3] | |
| matrix_world_bias = m[:3, 3] | |
| rot = matrix_world_rot | |
| total_vertices = len(obj.data.vertices) | |
| vertices = np.zeros((3, total_vertices)) | |
| if total_bones is not None: | |
| skin_weight = np.zeros((total_vertices, total_bones)) | |
| else: | |
| skin_weight = np.zeros((1, 1)) | |
| obj_verts = obj.data.vertices | |
| obj_group_names = [g.name for g in obj.vertex_groups] | |
| faces = [] | |
| normals = [] | |
| for polygon in obj.data.polygons: | |
| edges = polygon.edge_keys | |
| nodes = [] | |
| adj = {} | |
| for edge in edges: | |
| if adj.get(edge[0]) is None: | |
| adj[edge[0]] = [] | |
| adj[edge[0]].append(edge[1]) | |
| if adj.get(edge[1]) is None: | |
| adj[edge[1]] = [] | |
| adj[edge[1]].append(edge[0]) | |
| nodes.append(edge[0]) | |
| nodes.append(edge[1]) | |
| normal = polygon.normal | |
| nodes = list(set(sorted(nodes))) | |
| first = nodes[0] | |
| loop = [] | |
| now = first | |
| vis = {} | |
| while True: | |
| loop.append(now) | |
| vis[now] = True | |
| if vis.get(adj[now][0]) is None: | |
| now = adj[now][0] | |
| elif vis.get(adj[now][1]) is None: | |
| now = adj[now][1] | |
| else: | |
| break | |
| for (second, third) in zip(loop[1:], loop[2:]): | |
| faces.append((first, second, third)) | |
| normals.append(rot @ normal) | |
| faces = np.array(faces, dtype=np.int32) | |
| normals = np.array(normals, dtype=np.float32) | |
| coords = np.array([v.co for v in obj_verts]) | |
| rot_np = np.array(rot) | |
| coords = (rot_np @ coords.T).T + matrix_world_bias | |
| vertices[0:3, :coords.shape[0]] = coords.T | |
| # extract skin | |
| if bones is not None: | |
| vg_lut = {} | |
| for v in obj_verts: | |
| for g in v.groups: | |
| vg_lut[(v.index, g.group)] = g.weight | |
| for bone in bones: | |
| if bone.name not in obj_group_names: | |
| continue | |
| gidx = obj.vertex_groups[bone.name].index | |
| col = index[bone.name] | |
| for v in obj_verts: | |
| w = vg_lut.get((v.index, gidx)) | |
| if w is not None: | |
| skin_weight[v.index, col] = w | |
| vertices = vertices.T | |
| # determine the orientation of the face normal | |
| v0 = vertices[faces[:, 0]] | |
| v1 = vertices[faces[:, 1]] | |
| v2 = vertices[faces[:, 2]] | |
| cross = np.cross(v1-v0, v2-v0) | |
| dot = np.einsum("ij,ij->i", cross, normals) | |
| correct_faces = faces.copy() | |
| mask = dot < 0 | |
| correct_faces[mask, 1], correct_faces[mask, 2] = faces[mask, 2], faces[mask, 1] | |
| mesh_names_list.append(obj.name) | |
| vertices_list.append(vertices) | |
| faces_list.append(correct_faces+cur_vertex_bias) # add bias to faces | |
| if total_bones is not None: | |
| skin_list.append(skin_weight) | |
| cur_vertex_bias += len(vertices) | |
| cur_face_bias += len(faces) | |
| vertex_bias.append(cur_vertex_bias) | |
| face_bias.append(cur_face_bias) | |
| vertices = np.vstack(vertices_list) | |
| faces = np.vstack(faces_list) | |
| mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False, maintain_order=True) | |
| vertex_normals = mesh.vertex_normals | |
| face_normals = mesh.face_normals | |
| return { | |
| 'mesh_names': np.array(mesh_names_list), | |
| 'vertices': vertices, | |
| 'faces': faces, | |
| 'face_normals': face_normals, | |
| 'vertex_normals': vertex_normals, | |
| 'skin': np.vstack(skin_list) if len(skin_list) > 0 else None, | |
| 'vertex_bias': np.array(vertex_bias), | |
| 'face_bias': np.array(face_bias), | |
| } | |
| def get_matrix_basis(bones=None): | |
| if bones is None: | |
| return None | |
| if bpy.data.actions is not None and len(bpy.data.actions) > 0: | |
| action = bpy.data.actions[0] | |
| frames = int(action.frame_range.y - action.frame_range.x) | |
| else: | |
| return None | |
| J = len(bones) | |
| matrix_basis = np.zeros((frames, J, 4, 4)) | |
| matrix_basis[...] = np.eye(4) | |
| for frame in range(frames): | |
| bpy.context.scene.frame_set(frame + 1) | |
| for (id, pbone) in enumerate(bones): | |
| matrix_basis[frame, id] = np.array(pbone.matrix_basis) | |
| return matrix_basis | |
| def make_asset( | |
| asset: Asset, | |
| extrude_scale: float=0.5, | |
| connect_tail_to_unique_child: bool=False, | |
| extrude_from_parent: bool=False, | |
| group_per_vertex: int=-1, | |
| add_root: bool=False, | |
| do_not_normalize: bool=False, | |
| collection_name: str='new_collection', | |
| use_face: bool=True, | |
| ): | |
| """ | |
| Args: | |
| extrude_scale: float=0.5, if there is no tails in asset, first calculate the average length between parents and sons, then the length of leaf bone is l*extrude_scale. Otherwise do not affect final results. | |
| connect_tail_to_unique_child: bool=False, if True, the tail of a bone with only one child will be exactly at the head of its child. | |
| extrude_from_parent: bool=False, if True, the orientation of the leaf bone will be the same as its parent. | |
| group_per_vertex: int=-1, number of the largest weights to keep for each vertex. -1 means keep all. | |
| add_root: bool=False, if True, add a root bone at (0, 0, 0). | |
| do_not_normalize: bool=False, if True, do not normalize the skinning weights. | |
| collection_name: str='new_collection', name of the new collection to store objects. | |
| use_face: bool=True, if False, do not export faces. | |
| """ | |
| collection = bpy.data.collections.new(collection_name) | |
| bpy.context.scene.collection.children.link(collection) | |
| # 1. if there are meshes, make meshes | |
| objects = [] | |
| mesh_names = [] | |
| for v in bpy.data.objects: | |
| if v.type == 'MESH': | |
| objects.append(v) | |
| mesh_names.append(v.name) | |
| if len(objects) == 0: | |
| mesh_names = [f"mesh_{i}" for i in range(asset.P)] | |
| if len(objects)==0 and asset.vertices is not None: | |
| if asset.mesh_names is not None: | |
| mesh_names = asset.mesh_names | |
| for i in range(asset.P): | |
| mesh = bpy.data.meshes.new(f"data_{mesh_names[i]}") | |
| v = asset.vertices[asset.get_vertex_slice(i)] | |
| if not use_face or (asset.faces is None or asset.face_bias is None or asset.vertex_bias is None): | |
| mesh.from_pydata(v, [], []) | |
| else: | |
| if i == 0: | |
| mesh.from_pydata(v, [], asset.faces[asset.get_face_slice(i)]) | |
| else: | |
| mesh.from_pydata(v, [], asset.faces[asset.get_face_slice(i)]-asset.vertex_bias[i-1]) | |
| mesh.update() | |
| # make object from mesh | |
| object = bpy.data.objects.new(mesh_names[i], mesh) | |
| objects.append(object) | |
| # add object to scene collection | |
| collection.objects.link(object) | |
| # 2. if there is armature, process tails and make armature | |
| if len(bpy.data.armatures) > 0: | |
| armature = bpy.data.armatures[0] | |
| armature_name = armature.name | |
| joint_names = [b.name for b in armature.bones] | |
| else: | |
| armature = None | |
| armature_name = 'Armature' | |
| joint_names = asset.joint_names if asset.joint_names is not None else [f"bone_{i}" for i in range(asset.J)] | |
| if armature is None and asset.joints is not None and asset.parents is not None: | |
| joints = asset.joints | |
| if asset.tails is None: | |
| tails = joints.copy() | |
| connect_tail_to_unique_child = True | |
| extrude_from_parent = True | |
| else: | |
| tails = asset.tails | |
| root_tail = False | |
| root_id = asset.root | |
| length_sum = 0. | |
| sons = defaultdict(list) | |
| for i in range(len(asset.parents)): | |
| p = asset.parents[i] | |
| if p == -1: | |
| continue | |
| sons[p].append(i) | |
| length_sum += np.linalg.norm(joints[i] - joints[p]) | |
| if asset.J <= 1: | |
| length = 1.0 | |
| else: | |
| length_avg = length_sum / max(len(asset.parents) - 1, 1) | |
| length = length_avg * extrude_scale | |
| for i in range(len(asset.parents)): | |
| p = asset.parents[i] | |
| if p == -1: | |
| continue | |
| sons[p].append(i) | |
| d = np.linalg.norm(joints[i] - joints[p]) | |
| if d <= length * 1e-2: | |
| max_d = max(length, 1e-5) | |
| joints[i] += np.random.randn(3) * max_d * 1e-2 | |
| if connect_tail_to_unique_child: | |
| for i in range(len(asset.parents)): | |
| if len(sons[i]) == 1: | |
| child = sons[i][0] | |
| tails[i] = joints[child] | |
| if root_id == i: | |
| root_tail = True | |
| if extrude_from_parent: | |
| for i in range(len(asset.parents)): | |
| if len(sons[i]) != 1 and asset.parents[i] != -1: | |
| p = asset.parents[i] | |
| d = joints[i] - joints[p] | |
| if np.linalg.norm(d) < 1e-6: | |
| d = np.array([0., 0., 1.]) # in case son.head == parent.head | |
| else: | |
| d = d / np.linalg.norm(d) | |
| tails[i] = joints[i] + d * length | |
| if root_tail is False: | |
| tails[root_id] = joints[root_id] + np.array([0., 0., length]) | |
| bpy.ops.object.armature_add(enter_editmode=True) | |
| armature = bpy.data.armatures.get('Armature') | |
| armature_name = asset.armature_name if asset.armature_name is not None else 'Armature' | |
| edit_bones = armature.edit_bones | |
| if add_root: | |
| bone_root = edit_bones.get('Bone') | |
| root_name = 'Root' | |
| x = 0 | |
| while root_name in joint_names: | |
| root_name = f'Root_{x}' | |
| x += 1 | |
| bone_root.name = root_name | |
| bone_root.tail = Vector((joints[0, 0], joints[0, 1], joints[0, 2])) | |
| else: | |
| bone_root = edit_bones.get('Bone') | |
| bone_root.name = joint_names[0] | |
| bone_root.head = Vector((joints[0, 0], joints[0, 1], joints[0, 2])) | |
| bone_root.tail = Vector((tails[0, 0], tails[0, 1], tails[0, 2])) | |
| def extrude_bone( | |
| edit_bones, | |
| name: str, | |
| parent_name: str, | |
| head: Tuple[float, float, float], | |
| tail: Tuple[float, float, float], | |
| ): | |
| bone = edit_bones.new(name) | |
| bone.head = Vector((head[0], head[1], head[2])) | |
| bone.tail = Vector((tail[0], tail[1], tail[2])) | |
| bone.name = name | |
| parent_bone = edit_bones.get(parent_name) | |
| bone.parent = parent_bone | |
| bone.use_connect = False | |
| assert not np.isnan(head).any(), f"nan found in head of bone {name}" | |
| assert not np.isnan(tail).any(), f"nan found in tail of bone {name}" | |
| for u in asset.dfs_order: | |
| if add_root is False and u==0: | |
| continue | |
| pname = joint_names[u] if asset.parents[u] == -1 else joint_names[asset.parents[u]] | |
| extrude_bone(edit_bones, joint_names[u], pname, joints[u], tails[u]) | |
| bpy.ops.object.mode_set(mode='OBJECT') | |
| # 3. if there is skin, set vertex groups | |
| if asset.skin is not None and armature is not None and len(objects) > 0: | |
| # must set to object mode to enable parent_set | |
| bpy.ops.object.mode_set(mode='OBJECT') | |
| N = len(objects) | |
| objects = bpy.data.objects | |
| for o in bpy.context.selected_objects: | |
| o.select_set(False) | |
| for i in range(N): | |
| skin = asset.skin[asset.get_vertex_slice(i)] | |
| ob = objects[mesh_names[i]] | |
| armature_b = bpy.data.objects[armature_name] | |
| ob.select_set(True) | |
| armature_b.select_set(True) | |
| bpy.ops.object.parent_set(type='ARMATURE_NAME') | |
| # sparsify | |
| argsorted = np.argsort(-skin, axis=1) | |
| vertex_group_reweight = skin[np.arange(skin.shape[0])[..., None], argsorted] | |
| group_per_vertex = min(group_per_vertex, skin.shape[1]) | |
| if group_per_vertex == -1: | |
| group_per_vertex = vertex_group_reweight.shape[-1] | |
| if not do_not_normalize: | |
| vertex_group_reweight = vertex_group_reweight / vertex_group_reweight[..., :group_per_vertex].sum(axis=1)[...,None] | |
| # clean vertex groups first in case skin exists | |
| for name in joint_names: | |
| ob.vertex_groups[name].remove(range(990)) | |
| for v, w in enumerate(skin): | |
| for ii in range(group_per_vertex): | |
| j = argsorted[v, ii] | |
| n = joint_names[j] | |
| ob.vertex_groups[n].add([v], vertex_group_reweight[v, ii], 'REPLACE') | |
| def to_matrix(x: ndarray): | |
| return Matrix((x[0, :], x[1, :], x[2, :], x[3, :])) | |
| if asset.matrix_world is None: | |
| matrix_world = to_matrix(np.eye(4)) | |
| else: | |
| matrix_world = to_matrix(asset.matrix_world) | |
| if armature is not None: | |
| bpy.data.objects[armature_name].matrix_world = matrix_world | |
| # 4. if there is animation, set keyframes | |
| if asset.matrix_basis is not None and asset.matrix_local is not None and armature is not None: | |
| matrix_basis = asset.matrix_basis | |
| matrix_local = asset.matrix_local | |
| objects = bpy.data.objects | |
| for o in bpy.context.selected_objects: | |
| o.select_set(False) | |
| armature = bpy.data.objects[armature_name] | |
| armature.select_set(True) | |
| armature.matrix_world = matrix_world | |
| frames = matrix_basis.shape[0] | |
| # change matrix_local | |
| bpy.context.view_layer.objects.active = armature | |
| bpy.ops.object.mode_set(mode='EDIT') | |
| for (id, name) in enumerate(joint_names): | |
| # matrix_local of pose bone | |
| bpy.context.active_object.data.edit_bones[id].matrix = to_matrix(matrix_local[id]) | |
| bpy.ops.object.mode_set(mode='OBJECT') | |
| for (id, name) in enumerate(joint_names): | |
| pbone = armature.pose.bones.get(name) | |
| for frame in range(frames): | |
| bpy.context.scene.frame_set(frame + 1) | |
| q = to_matrix(matrix_basis[frame, id]) | |
| if pbone.rotation_mode == "QUATERNION": | |
| pbone.rotation_quaternion = q.to_quaternion() | |
| pbone.keyframe_insert(data_path = 'rotation_quaternion') | |
| else: | |
| pbone.rotation_euler = q.to_euler() | |
| pbone.keyframe_insert(data_path = 'rotation_euler') | |
| pbone.location = q.to_translation() | |
| pbone.keyframe_insert(data_path = 'location') | |
| bpy.ops.object.mode_set(mode='OBJECT') | |
| def _umeyama_similarity(src: ndarray, tgt: ndarray) -> ndarray: | |
| assert src.shape == tgt.shape | |
| n = src.shape[0] | |
| src_mean = src.mean(axis=0) | |
| tgt_mean = tgt.mean(axis=0) | |
| src_c = src - src_mean | |
| tgt_c = tgt - tgt_mean | |
| # cross-covariance | |
| C = (src_c.T @ tgt_c) / n | |
| U, S, Vt = np.linalg.svd(C) | |
| R = Vt.T @ U.T | |
| if np.linalg.det(R) < 0: | |
| Vt[-1, :] *= -1 | |
| R = Vt.T @ U.T | |
| var_src = (src_c ** 2).sum() / n | |
| scale = S.sum() / var_src | |
| t = tgt_mean - scale * R @ src_mean | |
| T = np.eye(4) | |
| T[:3, :3] = scale * R | |
| T[:3, 3] = t | |
| return T | |
| def _pca_similarity( | |
| src: ndarray, | |
| tgt: ndarray, | |
| max_points: int=4096, | |
| ) -> ndarray: | |
| if src.shape[0] > max_points: | |
| src = src[np.random.choice(src.shape[0], max_points, replace=False)] | |
| if tgt.shape[0] > max_points: | |
| tgt = tgt[np.random.choice(tgt.shape[0], max_points, replace=False)] | |
| src_mean = src.mean(axis=0) | |
| tgt_mean = tgt.mean(axis=0) | |
| src_c = src - src_mean | |
| tgt_c = tgt - tgt_mean | |
| U_src, _, _ = np.linalg.svd(src_c.T @ src_c) | |
| U_tgt, _, _ = np.linalg.svd(tgt_c.T @ tgt_c) | |
| R = U_tgt @ U_src.T | |
| if np.linalg.det(R) < 0: | |
| U_tgt[:, -1] *= -1 | |
| R = U_tgt @ U_src.T | |
| scale = np.sqrt((tgt_c ** 2).sum() / (src_c ** 2).sum()) | |
| t = tgt_mean - scale * R @ src_mean | |
| T = np.eye(4) | |
| T[:3, :3] = scale * R | |
| T[:3, 3] = t | |
| return T | |
| def estimate_similarity_transform( | |
| src: ndarray, | |
| tgt: ndarray, | |
| max_points: int=4096, | |
| ) -> ndarray: | |
| """ | |
| src: (N, 3) | |
| tgt: (M, 3) | |
| return: (4, 4) similarity transform matrix | |
| """ | |
| if src.shape[0] == tgt.shape[0]: | |
| return _umeyama_similarity(src, tgt) | |
| return _pca_similarity(src, tgt, max_points) | |
| def transfer_rigging( | |
| source_asset: Asset, | |
| target_path: str, | |
| export_path: str, | |
| **kwargs, | |
| ): | |
| assert source_asset.matrix_local is not None | |
| assert source_asset.parents is not None | |
| target_asset = BpyParser.load(filepath=target_path) | |
| bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) | |
| data_types = [ | |
| bpy.data.actions, | |
| bpy.data.armatures, | |
| ] | |
| for data_collection in data_types: | |
| for item in data_collection: | |
| data_collection.remove(item) | |
| source_vertices = source_asset.vertices # (n, 3) | |
| target_vertices = target_asset.vertices # (m, 3) | |
| assert source_vertices is not None and target_vertices is not None | |
| target_asset.matrix_local = source_asset.matrix_local.copy() | |
| target_asset.matrix_basis = source_asset.matrix_basis.copy() if source_asset.matrix_basis is not None else None | |
| source_joints = source_asset.joints | |
| assert source_joints is not None | |
| max_points = kwargs.pop('max_points', 4096) if kwargs.get('max_points') is not None else 4096 | |
| T = estimate_similarity_transform(src=source_vertices, tgt=target_vertices, max_points=max_points) | |
| source_joints_h = np.concatenate([ | |
| source_joints, np.ones((len(source_joints), 1)) | |
| ], axis=1) | |
| target_joints = (T @ source_joints_h.T).T[:, :3] | |
| target_asset.matrix_local[:, :3, 3] = target_joints | |
| target_asset.parents = source_asset.parents.copy() | |
| target_asset.lengths = source_asset.lengths.copy() if source_asset.lengths is not None else None | |
| target_asset.joint_names = source_asset.joint_names.copy() if source_asset.joint_names is not None else None | |
| if source_asset.skin is not None: | |
| from scipy.spatial import cKDTree | |
| source_skin = source_asset.skin # (n, J) | |
| source_vertices_h = np.concatenate([ | |
| source_vertices, np.ones((len(source_vertices), 1)) | |
| ], axis=1) | |
| source_vertices = (T @ source_vertices_h.T).T[:, :3] | |
| tree = cKDTree(source_vertices) | |
| dists, idx = tree.query(target_vertices, k=1) | |
| target_asset.skin = source_skin[idx] | |
| BpyParser.export(target_asset, export_path, use_origin=True, **kwargs) | |
| clean_bpy() |