Metahuman_DNA_Calibration/examples/dna_viewer_demo.py
2022-11-29 14:25:24 +01:00

355 lines
13 KiB
Python

"""
This example demonstrates generating functional rig in maya scene and exporting fbx per lod.
- usage in command line:
mayapy dna_viewer_demo.py
NOTE: Script cannot be called with Python, it must be called with mayapy.
- usage in Maya:
1. copy whole content of this file to Maya Script Editor
2. change value of ROOT_DIR to absolute path of dna_calibration, e.g. `c:/dna_calibration` in Windows or `/home/user/dna_calibration`. Important:
Use `/` (forward slash), because Maya uses forward slashes in path.
- customization:
- change CHARACTER_NAME to Taro, or name of file which is copied to /data/dna
- change ADD_COLOR_VERTEX to True, if you want to import fbx in Unreal Engine with painted vertices for fallowing cases:
- vertex normals that are going to be updated during import in Unreal Engine, its vertices must be painted with green color.
- for potential future GeneSplicer usage, skinwights on vertices which will need update in character mixing process, must be painted with blue color.
Expected:
- script will generate maya scene Ada.mb in OUTPUT_DIR
- script will generate workspace.mel in OUTPUT_DIR
- script will copy original Ada.dna file to OUTPUT_DIR
Expected: script will generate <PATH TO NEW DNA FILE>.
NOTE: The directory referenced by the given path must exist. If the directory does not exist, the script is going to fail.
NOTE: If running on Linux, please make sure to append the LD_LIBRARY_PATH with absolute path to the lib/linux directory before running the example:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:<path-to-lib-linux-dir>
"""
import logging
from pathlib import Path
try:
from maya import standalone
try:
standalone.initialize(name="python")
except Exception:
logging.warning("Starting Maya cannot be done in Maya")
except ImportError as e:
logging.error("DNAViewer needs to be run with Maya2022")
raise ImportError(e) from e
from os import environ, makedirs
from os import path as ospath
from shutil import copyfile
from sys import path as syspath
from sys import platform
# if you use Maya, use absolute path
ROOT_DIR = f"{ospath.dirname(ospath.abspath(__file__))}/..".replace("\\", "/")
OUTPUT_DIR = f"{ROOT_DIR}/output"
DATA_DIR = f"{ROOT_DIR}/data"
EXAMPLES_DIR = f"{ROOT_DIR}/examples"
ROOT_LIB_DIR = f"{ROOT_DIR}/lib"
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"
)
# 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, LIB_DIR)
syspath.insert(0, EXAMPLES_DIR)
syspath.insert(0, DATA_DIR)
# Setting constants that will be used
FACIAL_ROOT_NAME = "FACIAL_C_FacialRoot"
CHARACTER_NAME = "Ada"
ADD_COLOR_VERTEX = False
DNA_DIR = f"{DATA_DIR}/dna"
BODY_DIR = f"{DATA_DIR}/body"
OUTPUT_DIR = f"{ROOT_DIR}/output"
CHARACTER_DNA = f"{DNA_DIR}/{CHARACTER_NAME}.dna"
ANALOG_GUI = f"{DATA_DIR}/analog_gui.ma"
GUI = f"{DATA_DIR}/gui.ma"
AFTER_ASSEMBLY_SCRIPT = f"{DATA_DIR}/after_assembly_script.py"
ROTATION = [90.0, 0.0, 0.0]
FBX_UP_AXIS = "z"
BODY_FILE = f"{BODY_DIR}/fem_skeleton.ma"
ADD_MESH_NAME_TO_BLEND_SHAPE_CHANNEL_NAME = True
FACIAL_ROOT_JOINTS = ["FACIAL_C_FacialRoot", "FACIAL_C_Neck1Root", "FACIAL_C_Neck2Root"]
NECK_JOINTS = ["spine_04",
"clavicle_l",
"clavicle_r",
"upperarm_out_l",
"upperarm_fwd_l",
"upperarm_bck_l",
"neck_01",
"neck_02",
"head"]
from vtx_color import VTX_COLOR_VALUES, VTX_COLOR_MESHES, MESH_SHADER_MAPPING
from maya import cmds, mel
from dna_viewer import assemble_rig, build_meshes, create_build_options, load_dna
from dna_viewer.util.skin_weights_maya import SkinWeightsMaya
from dna import DataLayer_All, FileStream, Status, BinaryStreamReader, BinaryStreamWriter
from dnacalib import (
CommandSequence,
DNACalibDNAReader,
RotateCommand,
)
# Loads the builtin plugin needed for FBX
cmds.loadPlugin("fbxmaya.mll")
class Assemble:
def __init__(self):
self.rotate_dna_path = f"{CHARACTER_DNA}.rotate.dna"
self.dna = load_dna(CHARACTER_DNA)
def assemble_rig(self):
# Creates the rig
assemble_rig(
dna=self.dna,
gui_path=GUI,
analog_gui_path=ANALOG_GUI,
with_attributes_on_root_joint=True,
with_key_frames=True,
aas_path=AFTER_ASSEMBLY_SCRIPT,
add_mesh_name_to_blend_shape_channel_name=ADD_MESH_NAME_TO_BLEND_SHAPE_CHANNEL_NAME
)
# Renames and saves the scene
cmds.file(rename=f"{OUTPUT_DIR}/{CHARACTER_NAME}.mb")
cmds.file(save=True)
# copy dna fila and workspace file alongside generated scene
copyfile(CHARACTER_DNA, f"{OUTPUT_DIR}/{CHARACTER_NAME}.dna")
def get_dna(self):
if ROTATION == [0.0, 0.0, 0.0]:
return self.dna
return self.prepare_rotated_dna()
def export_fbx_for_all_lods(self):
dna = self.get_dna()
reader = self.load_dna_reader()
for lod in range(dna.get_lod_count()):
self.export_fbx_for_lod(dna, lod)
self.cleanup()
def cleanup(self):
path = Path(self.rotate_dna_path)
if path.exists():
path.unlink()
def export_fbx_for_lod(self, dna, lod):
# Creates the meshes for the given lod
meshes = self.build_meshes_for_lod(dna, lod)
# Executes FBX relate commands from the imported plugin
self.create_head_and_body_scene(meshes)
self.set_fbx_options()
# Saves the result
if ADD_COLOR_VERTEX:
self.add_shader()
self.set_vertex_color()
self.export_fbx(lod, meshes)
def create_shader(self, 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(self):
for shader_name, meshes in MESH_SHADER_MAPPING.items():
shading_group = self.create_shader(shader_name)
for mesh in meshes:
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(self):
for m, meshName in enumerate(VTX_COLOR_MESHES):
try:
cmds.select(meshName)
except Exception as e:
print(f"Skipped adding vtx color for mesh {meshName}. Reason {e}")
continue
for v, rgb in enumerate(VTX_COLOR_VALUES[m]):
cmds.polyColorPerVertex(f"{meshName}.vtx[{v}]", g=rgb[1], b=rgb[2])
def create_head_and_body_scene(self, mesh_names):
scene_mesh_names = []
skinweights = []
skinweights_maya = SkinWeightsMaya()
for mesh_name in mesh_names:
if cmds.objExists(mesh_name):
scene_mesh_names.append(mesh_name)
skinweights.append(skinweights_maya.get_skin_weights_from_scene(mesh_name))
cmds.delete(f"{mesh_name}_skinCluster")
cmds.parent(FACIAL_ROOT_JOINTS[0], world=True)
cmds.parent(FACIAL_ROOT_JOINTS[1], world=True)
cmds.parent(FACIAL_ROOT_JOINTS[2], world=True)
cmds.delete(NECK_JOINTS[0])
cmds.file(BODY_FILE, options="v=0", type="mayaAscii", i=True)
cmds.parent(FACIAL_ROOT_JOINTS[1], NECK_JOINTS[6])
cmds.parent(FACIAL_ROOT_JOINTS[2], NECK_JOINTS[7])
cmds.parent(FACIAL_ROOT_JOINTS[0], NECK_JOINTS[8])
for mesh_name, skinweight in zip(scene_mesh_names, skinweights):
self.create_skin_cluster(skinweight.joints, mesh_name, f"{mesh_name}_skinCluster", skinweight.no_of_influences)
skinweights_maya.set_skin_weights_to_scene(mesh_name, skinweight)
def create_skin_cluster(self, 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 build_meshes_for_lod(self, dna, lod):
# Sets the parameters needed to build the meshes with
build_options = create_build_options(
add_joints=True,
add_blend_shapes=True,
add_skin=True,
add_ctrl_attributes_on_root_joint=True,
add_animated_map_attributes_on_root_joint=True,
add_mesh_name_to_blend_shape_channel_name=ADD_MESH_NAME_TO_BLEND_SHAPE_CHANNEL_NAME,
add_key_frames=True
)
# Builds and returns the created mesh paths in the scene
return build_meshes(
dna=dna,
options=build_options,
group_by_lod=False,
lod_list=[lod],
create_new_scene=True,
)
def set_fbx_options(self):
# 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(FBX_UP_AXIS)
# Deselects objects in Maya
cmds.select(clear=True)
def export_fbx(self, lod, meshes):
# Selects every mesh in the given lod
for item in meshes:
cmds.select(item, add=True)
# Adds facial root joint to selection
cmds.select(FACIAL_ROOT_NAME, add=True)
# Sets the file path
export_file_name = f"{OUTPUT_DIR}/{CHARACTER_NAME}_lod{lod}.fbx"
# Exports the fbx
mel.eval(f'FBXExport -f "{export_file_name}" -s true')
def prepare_rotated_dna(self):
reader = self.load_dna_reader()
# Copies DNA contents and will serve as input/output parameter to commands
calibrated = DNACalibDNAReader(reader)
commands = self.build_command_list()
# Modifies calibrated DNA in-place
commands.run(calibrated)
self.save_dna(calibrated)
return load_dna(f"{CHARACTER_DNA}.rotate.dna")
def load_dna_reader(self):
stream = FileStream(CHARACTER_DNA, 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(self, reader):
stream = FileStream(f"{CHARACTER_DNA}.rotate.dna", 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}")
def build_command_list(self):
rotate = RotateCommand(ROTATION, [0.0, 0.0, 0.0])
# Abstraction to collect all commands into a sequence, and run them with only one invocation
commands = CommandSequence()
commands.add(rotate)
return commands
assemble = Assemble()
makedirs(OUTPUT_DIR, exist_ok=True)
# this fixes warning when calling this script with headless maya Warning: line 1: Unknown object type: HIKCharacterNode
mel.eval(f"HIKCharacterControlsTool;")
# generate workspace.mel
mel.eval(f'setProject "{OUTPUT_DIR}";')
# Export FBX for each lod
assemble.export_fbx_for_all_lods()
# Assembles the rig and saves the scene
assemble.assemble_rig()