470 lines
14 KiB
Python
470 lines
14 KiB
Python
import os
|
|
import json
|
|
|
|
# Defining common paths
|
|
ROOT_DIR = "D:/work/MetaHuman-DNA-Calibration"
|
|
WORK_DIR = "D:/work/UnrealFest"
|
|
# Body file
|
|
orig_body_file = f"{WORK_DIR}/model/f_srt_unw_body_rig.ma"
|
|
|
|
# Temp folder
|
|
temp_dir = f"{WORK_DIR}/temp"
|
|
|
|
# Output folder
|
|
output_dir = f"{WORK_DIR}/output/scale"
|
|
|
|
# Create folders
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
if not os.path.exists(temp_dir):
|
|
os.makedirs(temp_dir)
|
|
|
|
# Scaled body files
|
|
scaled_skeleton_file = os.path.join(temp_dir, "scaled_skeleton_body.ma")
|
|
scaled_body_file = os.path.join(temp_dir, "scaled_body.ma")
|
|
|
|
########################### RIGGING PART ###############################################################################
|
|
|
|
# Scaling body
|
|
|
|
from maya import cmds
|
|
|
|
# Step 1: Open body scene
|
|
cmds.file(orig_body_file, options="v=0", type="mayaAscii", i=True)
|
|
|
|
# Step 2: Save skin cluster values in JSON file
|
|
for i in range(4):
|
|
cmds.deformerWeights(f"body_lod{i}_sw.json", path=temp_dir, deformer=f"f_srt_unw_body_lod{i}_mesh_skinCluster",
|
|
weightPrecision=16, weightTolerance=0.0000001, export=True)
|
|
|
|
# Step 3: Manual work in Maya
|
|
|
|
# Scale body and unbind driver skeleton
|
|
|
|
cmds.select('root', replace=True)
|
|
cmds.select(hi=True)
|
|
cmds.delete(cmds.ls(sl=True, type='constraint'))
|
|
|
|
# Step 4: Read joints from skin cluster JSON file
|
|
|
|
################
|
|
def get_joints_from_file(file_path):
|
|
deformer_data = {}
|
|
with open(file_path) as def_file:
|
|
deformer_data = json.load(def_file)
|
|
|
|
joints_in_cluster = []
|
|
children = deformer_data["deformerWeight"]["weights"]
|
|
for child in children:
|
|
joints_in_cluster.append(child["source"])
|
|
print(joints_in_cluster)
|
|
return joints_in_cluster
|
|
# cmds.select(joints_in_cluster, add=True)
|
|
|
|
def load_skin_weights(mesh, path, file_name, max_influence):
|
|
|
|
|
|
# create skinCluster
|
|
joints = get_joints_from_file(path + "/" + file_name)
|
|
if not joints:
|
|
raise RuntimeError(f"Could not find joints in skinweights file {path}")
|
|
joints.append(mesh)
|
|
skinCluster = cmds.skinCluster(joints, tsb=True)[0]
|
|
# zero everything, was seeing weird values perhaps from initial default weights.
|
|
# cmds.skinPercent(skinCluster,mesh,pruneWeights=100,normalize=False)
|
|
# load new weights
|
|
cmds.deformerWeights(file_name, path=path, deformer=skinCluster, im=True, method='index')
|
|
|
|
# set skinCluster settings
|
|
cmds.setAttr(skinCluster + '.normalizeWeights', 1)
|
|
cmds.skinCluster(skinCluster, e=True, forceNormalizeWeights=True)
|
|
cmds.setAttr(skinCluster + '.maintainMaxInfluences', 1)
|
|
cmds.setAttr(skinCluster + '.maxInfluences', max_influence)
|
|
|
|
for i in range(4):
|
|
influence = 8
|
|
if i == 3:
|
|
influence = 4
|
|
load_skin_weights(f"f_srt_unw_body_lod{i}_meshShape", temp_dir, f"body_lod{i}_sw.json", influence)
|
|
#############
|
|
|
|
|
|
# Step 5: Save body files
|
|
cmds.file(rename=str(scaled_body_file))
|
|
cmds.file(save=True, force=True)
|
|
|
|
|
|
# Step 5: Prepare skeleton file and save
|
|
cmds.file(rename=str(scaled_skeleton_file))
|
|
cmds.file(save=True, force=True)
|
|
|
|
|
|
########################################################################################################################
|
|
############# CALIBRATION PART #########################################################################################
|
|
|
|
from os import environ
|
|
from sys import path as syspath
|
|
from sys import platform
|
|
import contextlib
|
|
|
|
MAYA_VERSION = "2022" # or 2023
|
|
ROOT_LIB_DIR = f"{ROOT_DIR}/lib/Maya{MAYA_VERSION}"
|
|
if platform == "win32":
|
|
LIB_DIR = f"{ROOT_LIB_DIR}/windows"
|
|
elif platform == "linux":
|
|
LIB_DIR = f"{ROOT_LIB_DIR}/linux"
|
|
else:
|
|
raise OSError(
|
|
"OS not supported, please compile dependencies and add value to LIB_DIR"
|
|
)
|
|
DATA_DIR = f"{ROOT_DIR}/data"
|
|
|
|
# Add bin directory to maya plugin path
|
|
if "MAYA_PLUG_IN_PATH" in environ:
|
|
separator = ":" if platform == "linux" else ";"
|
|
environ["MAYA_PLUG_IN_PATH"] = separator.join([environ["MAYA_PLUG_IN_PATH"], LIB_DIR])
|
|
else:
|
|
environ["MAYA_PLUG_IN_PATH"] = LIB_DIR
|
|
|
|
# Adds directories to path
|
|
syspath.insert(0, ROOT_DIR)
|
|
syspath.insert(0, DATA_DIR)
|
|
syspath.insert(0, LIB_DIR)
|
|
|
|
# Imports
|
|
from maya import cmds, mel
|
|
|
|
from dna_viewer import (
|
|
DNA,
|
|
Config,
|
|
RigConfig,
|
|
build_meshes,
|
|
build_rig,
|
|
get_skin_weights_from_scene,
|
|
set_skin_weights_to_scene
|
|
)
|
|
from dnacalib import (
|
|
CommandSequence,
|
|
DNACalibDNAReader,
|
|
RenameJointCommand,
|
|
ScaleCommand,
|
|
SetBlendShapeTargetDeltasCommand,
|
|
SetVertexPositionsCommand,
|
|
VectorOperation_Add,
|
|
VectorOperation_Interpolate,
|
|
SetNeutralJointTranslationsCommand,
|
|
SetNeutralJointRotationsCommand,
|
|
SetLODsCommand,
|
|
TranslateCommand,
|
|
SetSkinWeightsCommand,
|
|
RemoveJointCommand,
|
|
RotateCommand
|
|
)
|
|
|
|
from dna import (
|
|
BinaryStreamReader,
|
|
BinaryStreamWriter,
|
|
DataLayer_All,
|
|
FileStream,
|
|
Status,
|
|
)
|
|
from vtx_color import MESH_SHADER_MAPPING, VTX_COLOR_MESHES, VTX_COLOR_VALUES
|
|
|
|
|
|
# Methods
|
|
def read_dna(path):
|
|
stream = FileStream(path, FileStream.AccessMode_Read, FileStream.OpenMode_Binary)
|
|
reader = BinaryStreamReader(stream, DataLayer_All)
|
|
reader.read()
|
|
if not Status.isOk():
|
|
status = Status.get()
|
|
raise RuntimeError(f"Error loading DNA: {status.message}")
|
|
return reader
|
|
|
|
|
|
def save_dna(reader, path):
|
|
stream = FileStream(path, FileStream.AccessMode_Write, FileStream.OpenMode_Binary)
|
|
writer = BinaryStreamWriter(stream)
|
|
writer.setFrom(reader)
|
|
writer.write()
|
|
|
|
if not Status.isOk():
|
|
status = Status.get()
|
|
raise RuntimeError(f"Error saving DNA: {status.message}")
|
|
|
|
print(f"DNA {path} successfully saved.")
|
|
|
|
|
|
def assemble_scene(dna_path, analog_gui_path, gui_path,
|
|
additional_assemble_script):
|
|
dna = DNA(dna_path)
|
|
config = RigConfig(
|
|
gui_path=gui_path,
|
|
analog_gui_path=analog_gui_path,
|
|
aas_path=additional_assemble_script,
|
|
add_animated_map_attributes_on_root_joint=True,
|
|
add_key_frames=True,
|
|
add_mesh_name_to_blend_shape_channel_name=True
|
|
)
|
|
|
|
# Creates the rig
|
|
build_rig(dna=dna, config=config)
|
|
|
|
translation = cmds.xform("neck_01", ws=True, query=True, translation=True)
|
|
cmds.xform("CTRL_faceGUI", ws=True, t=[translation[0] + 20, translation[1] + 5, translation[2]])
|
|
|
|
|
|
def prepare_rotated_dna(dna_path, rotated_dna_path):
|
|
reader = read_dna(dna_path)
|
|
|
|
# Copies DNA contents and will serve as input/output parameter to commands
|
|
calibrated = DNACalibDNAReader(reader)
|
|
|
|
# Modifies calibrated DNA in-place
|
|
rotate = RotateCommand([90.0, 0.0, 0.0], [0.0, 0.0, 0.0])
|
|
rotate.run(calibrated)
|
|
|
|
save_dna(calibrated, rotated_dna_path)
|
|
return DNA(rotated_dna_path)
|
|
|
|
|
|
def get_dna(dna_path, rotated_dna_path):
|
|
if up_axis == "z":
|
|
return prepare_rotated_dna(dna_path, rotated_dna_path)
|
|
return DNA(dna_path)
|
|
|
|
|
|
def build_meshes_for_lod(dna, lod):
|
|
# Create config
|
|
config = Config(
|
|
group_by_lod=False,
|
|
create_display_layers=False,
|
|
lod_filter=[lod],
|
|
add_mesh_name_to_blend_shape_channel_name=True,
|
|
)
|
|
|
|
# Builds and returns the created mesh paths in the scene
|
|
return build_meshes(dna, config)
|
|
|
|
|
|
def create_skin_cluster(influences, mesh, skin_cluster_name, maximum_influences):
|
|
cmds.select(influences[0], replace=True)
|
|
cmds.select(mesh, add=True)
|
|
skinCluster = cmds.skinCluster(
|
|
toSelectedBones=True,
|
|
name=skin_cluster_name,
|
|
maximumInfluences=maximum_influences,
|
|
skinMethod=0,
|
|
obeyMaxInfluences=True,
|
|
)
|
|
if len(influences) > 1:
|
|
cmds.skinCluster(
|
|
skinCluster, edit=True, addInfluence=influences[1:], weight=0.0
|
|
)
|
|
return skinCluster
|
|
|
|
|
|
def create_head_and_body_scene(mesh_names, body_file, neck_joints, root_joint, facial_root_joints):
|
|
scene_mesh_names = []
|
|
skinweights = []
|
|
|
|
for mesh_name in mesh_names:
|
|
if cmds.objExists(mesh_name):
|
|
scene_mesh_names.append(mesh_name)
|
|
skinweights.append(get_skin_weights_from_scene(mesh_name))
|
|
cmds.delete(f"{mesh_name}_skinCluster")
|
|
|
|
for facial_joint in facial_root_joints:
|
|
cmds.parent(facial_joint, world=True)
|
|
cmds.delete(root_joint)
|
|
|
|
cmds.file(body_file, options="v=0", type="mayaAscii", i=True)
|
|
if up_axis == "y":
|
|
cmds.joint("root", edit=True, orientation=[-90.0, 0.0, 0.0])
|
|
for facial_joint, neck_joint in zip(facial_root_joints, neck_joints):
|
|
cmds.parent(facial_joint, neck_joint)
|
|
|
|
for mesh_name, skinweight in zip(scene_mesh_names, skinweights):
|
|
create_skin_cluster(
|
|
skinweight.joints,
|
|
mesh_name,
|
|
f"{mesh_name}_skinCluster",
|
|
skinweight.no_of_influences,
|
|
)
|
|
set_skin_weights_to_scene(mesh_name, skinweight)
|
|
|
|
|
|
def set_fbx_options(orientation):
|
|
# Executes FBX relate commands from the imported plugin
|
|
min_time = cmds.playbackOptions(minTime=True, query=True)
|
|
max_time = cmds.playbackOptions(maxTime=True, query=True)
|
|
|
|
cmds.FBXResetExport()
|
|
mel.eval("FBXExportBakeComplexAnimation -v true")
|
|
mel.eval(f"FBXExportBakeComplexStart -v {min_time}")
|
|
mel.eval(f"FBXExportBakeComplexEnd -v {max_time}")
|
|
mel.eval("FBXExportConstraints -v true")
|
|
mel.eval("FBXExportSkeletonDefinitions -v true")
|
|
mel.eval("FBXExportInputConnections -v true")
|
|
mel.eval("FBXExportSmoothingGroups -v true")
|
|
mel.eval("FBXExportSkins -v true")
|
|
mel.eval("FBXExportShapes -v true")
|
|
mel.eval("FBXExportCameras -v false")
|
|
mel.eval("FBXExportLights -v false")
|
|
cmds.FBXExportUpAxis(orientation)
|
|
# Deselects objects in Maya
|
|
cmds.select(clear=True)
|
|
|
|
|
|
def create_shader(name):
|
|
cmds.shadingNode("blinn", asShader=True, name=name)
|
|
|
|
shading_group = str(
|
|
cmds.sets(
|
|
renderable=True,
|
|
noSurfaceShader=True,
|
|
empty=True,
|
|
name=f"{name}SG",
|
|
)
|
|
)
|
|
cmds.connectAttr(f"{name}.outColor", f"{shading_group}.surfaceShader")
|
|
return shading_group
|
|
|
|
|
|
def add_shader(lod):
|
|
for shader_name, meshes in MESH_SHADER_MAPPING.items():
|
|
shading_group = create_shader(shader_name)
|
|
for mesh in meshes:
|
|
if f"lod{lod}" in mesh:
|
|
try:
|
|
cmds.select(mesh, replace=True)
|
|
cmds.sets(edit=True, forceElement=shading_group)
|
|
except Exception as e:
|
|
print(f"Skipped adding shader for mesh {mesh}. Reason {e}")
|
|
|
|
|
|
def set_vertex_color(lod):
|
|
for m, mesh_name in enumerate(VTX_COLOR_MESHES):
|
|
try:
|
|
if f"lod{lod}" in mesh_name:
|
|
cmds.select(mesh_name)
|
|
for v, rgb in enumerate(VTX_COLOR_VALUES[m]):
|
|
cmds.polyColorPerVertex(f"{mesh_name}.vtx[{v}]", g=rgb[1], b=rgb[2])
|
|
except Exception as e:
|
|
print(f"Skipped adding vtx color for mesh {mesh_name}. Reason {e}")
|
|
continue
|
|
|
|
|
|
def export_fbx(lod_num, meshes, root_jnt, chr_name, fbx_dir):
|
|
# Selects every mesh in the given lod
|
|
for item in meshes:
|
|
cmds.select(item, add=True)
|
|
# Adds facial root joint to selection
|
|
cmds.select(root_jnt, add=True)
|
|
# Sets the file path
|
|
export_file_name = f"{fbx_dir}/{chr_name}_lod{lod_num}.fbx"
|
|
# Exports the fbx
|
|
mel.eval(f'FBXExport -f "{export_file_name}" -s true')
|
|
|
|
|
|
def export_body_fbx(lod, mesh, body_root, fbx_dir):
|
|
# Selects mesh
|
|
cmds.select(mesh, add=True)
|
|
# Adds facial root joint to selection
|
|
cmds.select(body_root, add=True)
|
|
# Sets the file path
|
|
export_file_name = f"{fbx_dir}/{character_name}_body_lod{lod}.fbx"
|
|
# Exports the fbx
|
|
mel.eval(f'FBXExport -f "{export_file_name}" -s true')
|
|
# Deselects all
|
|
cmds.select(clear=True)
|
|
|
|
|
|
|
|
def export_fbx_for_lod(dna, lod, add_vtx_color, chr_name, body_file, fbx_dir, neck_joints, root_joint,
|
|
fbx_root_jnt, facial_root_joints, orientation):
|
|
# Creates the meshes for the given lod
|
|
result = build_meshes_for_lod(dna, lod)
|
|
meshes = result.get_all_meshes()
|
|
# Executes FBX relate commands from the imported plugin
|
|
create_head_and_body_scene(meshes, body_file, neck_joints, root_joint, facial_root_joints)
|
|
set_fbx_options(orientation)
|
|
# Saves the result
|
|
if add_vtx_color:
|
|
add_shader(lod)
|
|
set_vertex_color(lod)
|
|
export_fbx(lod, meshes, fbx_root_jnt, chr_name, fbx_dir)
|
|
|
|
|
|
# Setting paths that will be used
|
|
|
|
# Original MH DNA
|
|
character_dna = f"{WORK_DIR}/dna/Lena.dna"
|
|
|
|
# Final DNA
|
|
final_dna = f"{WORK_DIR}/dna/Lena_resized.dna"
|
|
|
|
# Rotated DNA
|
|
rotated_dna = f"{WORK_DIR}/dna/Lena_rotated.dna"
|
|
|
|
# Result scene
|
|
review_scene = f"{temp_dir}/scaled_scene.mb"
|
|
|
|
# Scene misc files
|
|
gui_path = f"{DATA_DIR}/mh4/gui.ma"
|
|
analog_gui_path = f"{DATA_DIR}/analog_gui.ma"
|
|
aas_path = f"{DATA_DIR}/mh4/additional_assemble_script.py"
|
|
|
|
# Consts
|
|
up_axis = "z"
|
|
head_mesh = "head_lod0_mesh"
|
|
facial_root_joints = ["FACIAL_C_FacialRoot", "FACIAL_C_Neck1Root", "FACIAL_C_Neck2Root"]
|
|
neck_joints = ["head", "neck_01", "neck_02"]
|
|
root_joint = "spine_04"
|
|
facial_root = "FACIAL_C_FacialRoot"
|
|
fbx_root = "root"
|
|
character_name = "Lena"
|
|
add_vtx_color = True
|
|
#############################################
|
|
|
|
|
|
# DNA calibration steps
|
|
|
|
# Step 1: Scale whole rig
|
|
reader = read_dna(character_dna)
|
|
calibrated = DNACalibDNAReader(reader)
|
|
command = ScaleCommand(0.85, [0.0, 0.0, 0.0])
|
|
command.run(calibrated)
|
|
|
|
# Save DNA
|
|
save_dna(calibrated, final_dna)
|
|
|
|
# Step 2: Check result
|
|
assemble_scene(final_dna, analog_gui_path, gui_path, aas_path)
|
|
cmds.file(rename=review_scene)
|
|
cmds.file(save=True)
|
|
#
|
|
# ###################################################################################
|
|
# Export FBX
|
|
# Loads the builtin plugin needed for FBX
|
|
cmds.loadPlugin("fbxmaya.mll")
|
|
|
|
# Generate workspace.mel
|
|
mel.eval(f'setProject "{output_dir}";')
|
|
|
|
# Export FBX for each lod
|
|
cmds.upAxis(ax=up_axis)
|
|
|
|
dna_for_export = get_dna(final_dna, rotated_dna)
|
|
for lod_index in range(dna_for_export.get_lod_count()):
|
|
export_fbx_for_lod(dna_for_export, lod_index, add_vtx_color,
|
|
character_name, scaled_skeleton_file, output_dir, neck_joints, root_joint, fbx_root,
|
|
facial_root_joints, up_axis)
|
|
|
|
with contextlib.suppress(FileNotFoundError):
|
|
os.remove(rotated_dna)
|
|
|
|
cmds.file(force=True, new=True)
|
|
cmds.file(scaled_body_file, options="v=0", type="mayaAscii", i=True)
|
|
for lod in range(4):
|
|
export_body_fbx(lod, f"f_srt_unw_body_lod{lod}_mesh", "root", output_dir) |