from collections import defaultdict from typing import Union, Tuple import argparse import bpy # type: ignore import numpy as np import trimesh from mathutils import Vector # type: ignore def export_fbx( path: str, vertices: np.ndarray, faces: Union[np.ndarray, None], bones: np.ndarray, parents: list[Union[int, None]], names: list[str], vertex_group: np.ndarray, dfs_order: list[int], group_per_vertex: int=4, ): # clean bpy for c in bpy.data.actions: bpy.data.actions.remove(c) for c in bpy.data.armatures: bpy.data.armatures.remove(c) for c in bpy.data.cameras: bpy.data.cameras.remove(c) for c in bpy.data.collections: bpy.data.collections.remove(c) for c in bpy.data.images: bpy.data.images.remove(c) for c in bpy.data.materials: bpy.data.materials.remove(c) for c in bpy.data.meshes: bpy.data.meshes.remove(c) for c in bpy.data.objects: bpy.data.objects.remove(c) for c in bpy.data.textures: bpy.data.textures.remove(c) # make mesh mesh = bpy.data.meshes.new('mesh') mesh.from_pydata(vertices, [], faces) mesh.update() # make object from mesh object = bpy.data.objects.new('character', mesh) # make collection collection = bpy.data.collections.new('CA_collection') bpy.context.scene.collection.children.link(collection) # add object to scene collection collection.objects.link(object) # deselect mesh # mesh.select_set(False) bpy.ops.object.armature_add(enter_editmode=True) armature = bpy.data.armatures.get('Armature') edit_bones = armature.edit_bones bone_root = edit_bones.get('Bone') J = len(names) def extrude_bone( edit_bones, name: str, parent_name: str, head: Tuple[float, float, float], tail: Tuple[float, float, float], is_root: bool=False, ): if is_root: bone = bone_root else: 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 if parent_name is not None: parent_bone = edit_bones.get(parent_name) bone.parent = parent_bone else: bone.parent = None bone.use_connect = True for k in range(J): i = dfs_order[k] edit_bones = armature.edit_bones if parents[i] is None: # root extrude_bone(edit_bones, names[i], None, bones[i, :3], bones[i, 3:], is_root=True) else: pname = 'Root' if parents[i] is None else names[parents[i]] extrude_bone(edit_bones, names[i], pname, bones[i, :3], bones[i, 3:]) # must set to object mode to enable parent_set bpy.ops.object.mode_set(mode='OBJECT') objects = bpy.data.objects for o in bpy.context.selected_objects: o.select_set(False) ob = objects['character'] arm = bpy.data.objects['Armature'] ob.select_set(True) arm.select_set(True) bpy.ops.object.parent_set(type='ARMATURE_NAME') vis = [] for x in ob.vertex_groups: vis.append(x.name) #sparsify nGroupPerVertex = group_per_vertex argsorted = np.argsort(-vertex_group, axis=1) vertex_group_reweight = vertex_group[np.arange(vertex_group.shape[0])[..., None],argsorted] vertex_group_reweight = vertex_group_reweight / vertex_group_reweight[..., :nGroupPerVertex].sum(axis=1)[...,None] for v, w in enumerate(vertex_group): for ii in range(nGroupPerVertex): i = argsorted[v,ii] if i >= len(names): continue n = names[i] if n not in vis: continue ob.vertex_groups[n].add([v], vertex_group_reweight[v, ii], 'REPLACE') bpy.ops.export_scene.fbx(filepath=path, check_existing=False, add_leaf_bones=False) def parse(): parser = argparse.ArgumentParser() parser.add_argument("--mesh", type=str, required=True, help="obj path of original mesh") parser.add_argument("--rig", type=str, required=True, help="path of prediction rigging") parser.add_argument("--output", type=str, required=False, default="res.fbx") return parser.parse_args() if __name__ == "__main__": args = parse() mesh_path = args.mesh rig_path = args.rig output_path = args.output # change to face -y axis rot = np.array([ [1.0, 0.0, 0.0], [0.0, 0.0,-1.0], [0.0, 1.0, 0.0], ]) # load original model mesh = trimesh.load(mesh_path, force='mesh', process=False, maintain_order=True) vertices = (rot @ mesh.vertices.T).T faces = mesh.faces N = vertices.shape[0] # handle RigNet output f_info = open(rig_path, 'r') joint_pos = {} joint_hier = {} joint_skin = [] id_mapping = {} name_mapping = {} parent_mapping = {} root_name = None root_pos = None tot = 0 for line in f_info: word = line.split() if word[0] == 'joints': joint_pos[word[1]] = rot @ np.array([float(word[2]), float(word[3]), float(word[4])]) id_mapping[word[1]] = tot name_mapping[tot] = word[1] tot += 1 if word[0] == 'root': root_pos = joint_pos[word[1]] root_name = word[1] if word[0] == 'hier': if word[1] not in joint_hier.keys(): joint_hier[word[1]] = [word[2]] else: joint_hier[word[1]].append(word[2]) if word[0] == 'skin': skin_item = word[1:] joint_skin.append(skin_item) f_info.close() J = len(joint_pos) bones = np.zeros((J, 6)) parents = [] names = [] for name in joint_hier: for son in joint_hier[name]: parent_mapping[son] = name son = defaultdict(list) for i in range(J): name = name_mapping[i] names.append(name) parents.append(None if name==root_name else id_mapping[parent_mapping[name]]) if name != root_name: son[id_mapping[parent_mapping[name]]].append(i) # extrude tails for blender for i in range(J): name = name_mapping[i] head = joint_pos[name] tail = head + np.array([0., 0., 0.1]) if len(joint_hier.get(name, [])) == 1: tail = joint_pos[joint_hier[name][0]] elif name != root_name: pname = name_mapping[parents[i]] direction = joint_pos[name] - joint_pos[pname] tail = head + direction * 0.5 bones[i, :3] = head bones[i, 3:] = tail # assign skin weight skin = np.zeros((N, J)) for skin_item in joint_skin: u = int(skin_item[0]) for j in range(1, len(skin_item), 2): id = id_mapping[skin_item[j]] w = skin_item[j + 1] skin[u, id] = w dfs_order = [] Q = [id_mapping[root_name]] while Q: u = Q.pop() dfs_order.append(u) Q.extend(son[u]) export_fbx( path=output_path, vertices=vertices, faces=faces, bones=bones, parents=parents, names=names, vertex_group=skin, dfs_order=dfs_order, )