2025-04-17 13:00:39 +08:00

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)