jkorstad's picture
Correctly add UniRig source files
f499d3b
'''
inject the result in res.npz into model.vrm and exports as res_textured.vrm
'''
import argparse
import yaml
import os
import numpy as np
from numpy import ndarray
from typing import Tuple, Union, List
import argparse
from tqdm import tqdm
from box import Box
from scipy.spatial import cKDTree
import open3d as o3d
import itertools
import bpy
from mathutils import Vector
from ..data.raw_data import RawData, RawSkin
from ..data.extract import process_mesh, process_armature, get_arranged_bones
def parser():
parser = argparse.ArgumentParser()
parser.add_argument('--config', type=str)
parser.add_argument('--num_runs', type=int)
parser.add_argument('--id', type=int)
return parser.parse_args()
def 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)
def load(filepath: str, return_armature: bool=False):
if return_armature:
old_objs = set(bpy.context.scene.objects)
if not os.path.exists(filepath):
raise ValueError(f'File {filepath} does not exist !')
try:
if filepath.endswith(".vrm"):
# enable vrm addon and load vrm model
bpy.ops.preferences.addon_enable(module='vrm')
bpy.ops.import_scene.vrm(
filepath=filepath,
use_addon_preferences=True,
extract_textures_into_folder=False,
make_new_texture_folder=False,
set_shading_type_to_material_on_import=False,
set_view_transform_to_standard_on_import=True,
set_armature_display_to_wire=True,
set_armature_display_to_show_in_front=True,
set_armature_bone_shape_to_default=True,
disable_bake=True, # customized option for better performance
)
elif filepath.endswith(".fbx") or filepath.endswith(".FBX"):
bpy.ops.import_scene.fbx(filepath=filepath, ignore_leaf_bones=False, use_image_search=False)
elif filepath.endswith(".glb") or filepath.endswith(".gltf"):
bpy.ops.import_scene.gltf(filepath=filepath, import_pack_images=False)
elif filepath.endswith(".dae"):
bpy.ops.wm.collada_import(filepath=filepath)
elif filepath.endswith(".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)
else:
raise ValueError(f"not suported type {filepath}")
except:
raise ValueError(f"failed to load {filepath}")
if return_armature:
armature = [x for x in set(bpy.context.scene.objects)-old_objs if x.type=="ARMATURE"]
if len(armature)==0:
return None
if len(armature)>1:
raise ValueError(f"multiple armatures found")
armature = armature[0]
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
for bone in bpy.data.armatures[0].edit_bones:
bone.roll = 0. # change all roll to 0. to prevent weird behaviour
bpy.ops.object.mode_set(mode='OBJECT')
armature.select_set(False)
bpy.ops.object.select_all(action='DESELECT')
return armature
def get_skin(arranged_bones):
meshes = []
for v in bpy.data.objects:
if v.type == 'MESH':
meshes.append(v)
index = {}
for (id, pbone) in enumerate(arranged_bones):
index[pbone.name] = id
_dict_skin = {}
total_bones = len(arranged_bones)
for obj in meshes:
total_vertices = len(obj.data.vertices)
skin_weight = np.zeros((total_vertices, total_bones))
obj_group_names = [g.name for g in obj.vertex_groups]
obj_verts = obj.data.vertices
for bone in arranged_bones:
if bone.name not in obj_group_names:
continue
gidx = obj.vertex_groups[bone.name].index
bone_verts = [v for v in obj_verts if gidx in [g.group for g in v.groups]]
for v in bone_verts:
which = [id for id in range(len(v.groups)) if v.groups[id].group==gidx]
w = v.groups[which[0]].weight
skin_weight[v.index, index[bone.name]] = w
_dict_skin[obj.name] = {
'skin': skin_weight,
}
skin = np.concatenate([
_dict_skin[d]['skin'] for d in _dict_skin
], axis=0)
return skin
def axis(a: np.ndarray):
b = np.concatenate([-a[:, 0:1], -a[:, 1:2], a[:, 2:3]], axis=1)
return b
def get_correct_orientation_kdtree(a: np.ndarray, b: np.ndarray, bones: np.ndarray, num: int=16384) -> np.ndarray:
'''
a: sampled_vertiecs
b: mesh_vertices
'''
min_loss = float('inf')
best_transformed = a.copy()
axis_permutations = list(itertools.permutations([0, 1, 2]))
sign_combinations = [(x, y, z) for x in [1, -1]
for y in [1, -1]
for z in [1, -1]]
_bones = bones.copy()
for perm in axis_permutations:
permuted_a = a[np.random.permutation(a.shape[0])[:num]][:, perm]
for signs in sign_combinations:
transformed = permuted_a * np.array(signs)
tree = cKDTree(transformed)
distances, indices = tree.query(b)
current_loss = distances.mean()
if current_loss < min_loss: # prevent from mirroring
min_loss = current_loss
best_transformed = a[:, perm] * np.array(signs)
bones[:, :3] = _bones[:, :3][:, perm] * np.array(signs)
bones[:, 3:] = _bones[:, 3:][:, perm] * np.array(signs)
return best_transformed, bones
def denormalize_vertices(mesh_vertices: ndarray, vertices: ndarray, bones: ndarray) -> np.ndarray:
min_vals = np.min(mesh_vertices, axis=0)
max_vals = np.max(mesh_vertices, axis=0)
center = (min_vals + max_vals) / 2
scale = np.max(max_vals - min_vals) / 2
denormalized_vertices = vertices * scale + center
denormalized_bones = bones * scale
denormalized_bones[:, :3] += center
denormalized_bones[:, 3:] += center
return denormalized_vertices, denormalized_bones
def make_armature(
vertices: ndarray,
bones: ndarray, # (joint, tail)
parents: list[Union[int, None]],
names: list[str],
skin: ndarray,
group_per_vertex: int=4,
add_root: bool=False,
is_vrm: bool=False,
):
context = bpy.context
mesh_vertices = []
for ob in bpy.data.objects:
if ob.type != 'MESH':
continue
m = np.array(ob.matrix_world)
matrix_world_rot = m[:3, :3]
matrix_world_bias = m[:3, 3]
for v in ob.data.vertices:
mesh_vertices.append(matrix_world_rot @ np.array(v.co) + matrix_world_bias)
mesh_vertices = np.stack(mesh_vertices)
vertices, bones = denormalize_vertices(mesh_vertices, vertices, bones)
bpy.ops.object.add(type="ARMATURE", location=(0, 0, 0))
armature = context.object
if hasattr(armature.data, 'vrm_addon_extension'):
armature.data.vrm_addon_extension.spec_version = "1.0"
humanoid = armature.data.vrm_addon_extension.vrm1.humanoid
is_vrm = True
bpy.ops.object.mode_set(mode="EDIT")
edit_bones = armature.data.edit_bones
if add_root:
bone_root = edit_bones.new('Root')
bone_root.name = 'Root'
bone_root.head = (0., 0., 0.)
bone_root.tail = (bones[0, 0], bones[0, 1], bones[0, 2])
J = len(names)
def extrude_bone(
name: Union[None, str],
parent_name: Union[None, str],
head: Tuple[float, float, float],
tail: Tuple[float, float, float],
):
bone = edit_bones.new(name)
bone.head = (head[0], head[1], head[2])
bone.tail = (tail[0], tail[1], tail[2])
bone.name = name
if parent_name is None:
return
parent_bone = edit_bones.get(parent_name)
bone.parent = parent_bone
bone.use_connect = False # always False currently
vertices, bones = get_correct_orientation_kdtree(vertices, mesh_vertices, bones)
for i in range(J):
if add_root:
pname = 'Root' if parents[i] is None else names[parents[i]]
else:
pname = None if parents[i] is None else names[parents[i]]
extrude_bone(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)
argsorted = np.argsort(-skin, axis=1)
vertex_group_reweight = skin[np.arange(skin.shape[0])[..., None], argsorted]
vertex_group_reweight = vertex_group_reweight / vertex_group_reweight[..., :group_per_vertex].sum(axis=1)[...,None]
vertex_group_reweight = np.nan_to_num(vertex_group_reweight)
tree = cKDTree(vertices)
for ob in objects:
if ob.type != 'MESH':
continue
ob.select_set(True)
armature.select_set(True)
bpy.ops.object.parent_set(type='ARMATURE_NAME')
vis = []
for x in ob.vertex_groups:
vis.append(x.name)
n_vertices = []
m = np.array(ob.matrix_world)
matrix_world_rot = m[:3, :3]
matrix_world_bias = m[:3, 3]
for v in ob.data.vertices:
n_vertices.append(matrix_world_rot @ np.array(v.co) + matrix_world_bias)
n_vertices = np.stack(n_vertices)
_, index = tree.query(n_vertices)
for v, co in enumerate(tqdm(n_vertices)):
for ii in range(group_per_vertex):
i = argsorted[index[v], ii]
if i >= len(names):
continue
n = names[i]
if n not in ob.vertex_groups:
continue
ob.vertex_groups[n].add([v], vertex_group_reweight[index[v], ii], 'REPLACE')
armature.select_set(False)
ob.select_set(False)
# set vrm bones link
if is_vrm:
armature.data.vrm_addon_extension.spec_version = "1.0"
humanoid.human_bones.hips.node.bone_name = "J_Bip_C_Hips"
humanoid.human_bones.spine.node.bone_name = "J_Bip_C_Spine"
humanoid.human_bones.chest.node.bone_name = "J_Bip_C_Chest"
humanoid.human_bones.neck.node.bone_name = "J_Bip_C_Neck"
humanoid.human_bones.head.node.bone_name = "J_Bip_C_Head"
humanoid.human_bones.left_upper_leg.node.bone_name = "J_Bip_L_UpperLeg"
humanoid.human_bones.left_lower_leg.node.bone_name = "J_Bip_L_LowerLeg"
humanoid.human_bones.left_foot.node.bone_name = "J_Bip_L_Foot"
humanoid.human_bones.right_upper_leg.node.bone_name = "J_Bip_R_UpperLeg"
humanoid.human_bones.right_lower_leg.node.bone_name = "J_Bip_R_LowerLeg"
humanoid.human_bones.right_foot.node.bone_name = "J_Bip_R_Foot"
humanoid.human_bones.left_upper_arm.node.bone_name = "J_Bip_L_UpperArm"
humanoid.human_bones.left_lower_arm.node.bone_name = "J_Bip_L_LowerArm"
humanoid.human_bones.left_hand.node.bone_name = "J_Bip_L_Hand"
humanoid.human_bones.right_upper_arm.node.bone_name = "J_Bip_R_UpperArm"
humanoid.human_bones.right_lower_arm.node.bone_name = "J_Bip_R_LowerArm"
humanoid.human_bones.right_hand.node.bone_name = "J_Bip_R_Hand"
bpy.ops.vrm.assign_vrm1_humanoid_human_bones_automatically(armature_name="Armature")
def merge(
path: str,
output_path: str,
vertices: ndarray,
joints: ndarray,
skin: ndarray,
parents: List[Union[None, int]],
names: List[str],
tails: ndarray,
add_root: bool=False,
is_vrm: bool=False,
):
'''
Merge skin and bone into original file.
'''
clean_bpy()
try:
load(path)
except Exception as e:
print(f"Failed to load {path}: {e}")
return
for c in bpy.data.armatures:
bpy.data.armatures.remove(c)
bones = np.concatenate([joints, tails], axis=1)
# if the result is weired, orientation may be wrong
make_armature(
vertices=vertices,
bones=bones,
parents=parents,
names=names,
skin=skin,
group_per_vertex=4,
add_root=add_root,
is_vrm=is_vrm,
)
dirpath = os.path.dirname(output_path)
if dirpath != '':
os.makedirs(dirpath, exist_ok=True)
try:
if is_vrm:
bpy.ops.export_scene.vrm(filepath=output_path)
elif output_path.endswith(".fbx") or output_path.endswith(".FBX"):
bpy.ops.export_scene.fbx(filepath=output_path, add_leaf_bones=True)
elif output_path.endswith(".glb") or output_path.endswith(".gltf"):
bpy.ops.export_scene.gltf(filepath=output_path)
elif output_path.endswith(".dae"):
bpy.ops.wm.collada_export(filepath=output_path)
elif output_path.endswith(".blend"):
with bpy.data.libraries.load(output_path) as (data_from, data_to):
data_to.objects = data_from.objects
else:
raise ValueError(f"not suported type {output_path}")
except:
raise ValueError(f"failed to export {output_path}")
def str2bool(v):
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
def nullable_string(val):
if not val:
return None
return val
def parse():
parser = argparse.ArgumentParser()
parser.add_argument('--require_suffix', type=str, required=True)
parser.add_argument('--num_runs', type=int, required=True)
parser.add_argument('--id', type=int, required=True)
parser.add_argument('--data_config', type=str, required=False)
parser.add_argument('--skeleton_config', type=str, required=False)
parser.add_argument('--skin_config', type=str, required=False)
parser.add_argument('--merge_dir', type=str, required=False)
parser.add_argument('--merge_name', type=str, required=False)
parser.add_argument('--add_root', type=str2bool, required=False, default=False)
parser.add_argument('--source', type=nullable_string, required=False, default=None)
parser.add_argument('--target', type=nullable_string, required=False, default=None)
parser.add_argument('--output', type=nullable_string, required=False, default=None)
return parser.parse_args()
def transfer(source: str, target: str, output: str, add_root: bool=False):
try:
armature = load(filepath=source, return_armature=True)
assert armature is not None
except Exception as e:
print(f"failed to load {source}")
return
vertices, faces = process_mesh()
arranged_bones = get_arranged_bones(armature)
skin = get_skin(arranged_bones)
joints, tails, parents, names, matrix_local = process_armature(armature, arranged_bones)
merge(
path=target,
output_path=output,
vertices=vertices,
joints=joints,
skin=skin,
parents=parents,
names=names,
tails=tails,
add_root=add_root,
)
if __name__ == "__main__":
args = parse()
if args.source is not None or args.target is not None:
assert args.source is not None and args.target is not None
transfer(args.source, args.target, args.output, args.add_root)
exit()
data_config = Box(yaml.safe_load(open(args.data_config, "r")))
skeleton_config = Box(yaml.safe_load(open(args.skeleton_config, "r")))
skin_config = Box(yaml.safe_load(open(args.skin_config, "r")))
num_runs = args.num_runs
id = args.id
require_suffix = args.require_suffix.split(',')
merge_dir = args.merge_dir
merge_name = args.merge_name
add_root = args.add_root
input_dataset_dir = data_config.input_dataset_dir
dataset_name = data_config.output_dataset_dir
skin_output_dataset_dir = skin_config.writer.output_dir
skin_name = skin_config.writer.export_npz
skeleton_output_dataset_dir = skeleton_config.writer.output_dir
skeleton_name = skeleton_config.writer.export_npz
def make_path(output_dataset_dir, dataset_name, root, file_name):
if output_dataset_dir is None:
return os.path.join(
dataset_name,
os.path.relpath(root, input_dataset_dir),
file_name,
)
return os.path.join(
output_dataset_dir,
dataset_name,
os.path.relpath(root, input_dataset_dir),
file_name,
)
files = []
for root, dirs, f in os.walk(input_dataset_dir):
for file in f:
if file.split('.')[-1] in require_suffix:
file_name = file.removeprefix("./")
suffix = file.split('.')[-1]
# remove suffix
file_name = '.'.join(file_name.split('.')[:-1])
skin_path = make_path(skin_output_dataset_dir, dataset_name, root, os.path.join(file_name, skin_name+'.npz'))
skeleton_path = make_path(skeleton_output_dataset_dir, dataset_name, root, os.path.join(file_name, skeleton_name+'.npz'))
merge_path = make_path(merge_dir, dataset_name, root, os.path.join(file_name, merge_name+"."+suffix))
# check if inference result exists
if os.path.exists(skin_path) and os.path.exists(skeleton_path):
files.append((os.path.join(root, file), skin_path, skeleton_path, merge_path))
num_files = len(files)
print("num_files", num_files)
gap = num_files // num_runs
start = gap * id
end = gap * (id + 1)
if id+1==num_runs:
end = num_files
files = sorted(files)
if end!=-1:
files = files[:end]
tot = 0
for file in tqdm(files[start:]):
origin_file = file[0]
skin_path = file[1]
skeleton_path = file[2]
merge_file = file[3]
raw_skin = RawSkin.load(path=skin_path)
raw_data = RawData.load(path=skeleton_path)
try:
merge(
path=origin_file,
output_path=merge_file,
vertices=raw_skin.vertices,
joints=raw_skin.joints,
skin=raw_skin.skin,
parents=raw_data.parents,
names=raw_data.names,
tails=raw_data.tails,
add_root=add_root,
is_vrm=(raw_data.cls=='vroid'),
)
except Exception as e:
print(f"failed to merge {origin_file}: {e}")