# Copyright Epic Games, Inc. All Rights Reserved. import os import json from maya import cmds from special_projects.publish_tools.fbx_cmd import fbx_export from special_projects.publish_tools.utils import find_top_joints from special_projects.rigging.rbf_node import RBFNode class RBFNodeExporter(object): """ Utility class to export a RBFsolver node, exports a JSON and FBX >>> from special_projects.rigging.rbf_node.export import RBFNodeExporter >>> node = 'my_UE4RBFSolver_node' >>> asset_name = 'my_asset_name' >>> export_directory = 'my:/export/directory' >>> exporter = RBFNodeExporter(node, asset_name, export_directory) >>> exporter.export() Result: export_directory/node.json export_directory/node.fbx """ def __init__(self, node, asset_name, export_directory): """ Initialize RBF Exporter """ self.set_node(node) self.set_asset_name(asset_name) self.set_export_directory(export_directory) self._set_fbx_export_path() self._set_json_export_path() def node(self): return self._node def set_node(self, node): if not cmds.objectType(node, isAType=RBFNode.node_type): raise TypeError('Invalid "{}" node: "{}"'.format(RBFNode.node_type, node)) self._node = RBFNode(node) def asset_name(self): return self._asset_name def set_asset_name(self, asset_name): self._asset_name = asset_name def export_directory(self): return self._export_directory def set_export_directory(self, directory): if os.path.isdir(directory) and os.path.exists(directory): self._export_directory = directory else: raise IOError('Export directory "{}" does not exists'.format(directory)) def fbx_export_path(self): return self._fbx_export_path def _set_fbx_export_path(self): self._fbx_export_path = os.path.join(self._export_directory, '{}.fbx'.format(self._node)) def json_export_path(self): return self._json_export_path def _set_json_export_path(self): self._json_export_path = os.path.join(self._export_directory, '{}.json'.format(self._node)) # ------------------------------------------------------------------------------------- def export(self): """ Exports FBX and sidecar JSON file """ self.fbx_export() self.json_export() def json_export(self): """ Exports JSON sidecar """ self.node().name_unnamed_poses() config = self._node.data() config['export_fbx'] = self.fbx_export_path() config['asset_name'] = self.asset_name() with open(self.json_export_path(), 'w') as outfile: json.dump(config, outfile, sort_keys=0, indent=4, separators=(",", ":")) def fbx_export(self): """ Exports baked poses to FBX """ self.node().name_unnamed_poses() self.bake_poses() cmds.select(self.root_joint()) fbx_export(self.fbx_export_path(), animation=True, bake_complex_animation=True, bake_complex_start=0, bake_complex_end=cmds.playbackOptions(q=True, maxTime=True), up_axis='z') # ------------------------------------------------------------------------------------- def blendshape_nodes(self): """ Finds blendshape nodes from driven attributes """ driven_attributes = self._node.driven_attributes(type='blendShape') blendshape_nodes = list() for output in driven_attributes: for attribute in output: blendshape_nodes.append(attribute.split('.')[0]) return list(set(blendshape_nodes)) def meshes(self): """ Finds meshes from blendshape nodes """ meshes = list() blendShapes = self.blendshape_nodes() if blendShapes: for blendShape in blendShapes: meshes.extend(cmds.deformer(blendShape, q=True, geometry=True)) meshes = list(set(meshes)) meshes = cmds.listRelatives(meshes, parent=True) return meshes def root_joint(self): """ Finds root joint from meshes """ meshes = self.meshes() if meshes: skeleton = list() for mesh in meshes: skin_cluster = cmds.ls(cmds.findDeformers(mesh), type='skinCluster') if skin_cluster: skin_cluster = skin_cluster[0] influences = cmds.skinCluster(skin_cluster, q=True, inf=True) if influences: skeleton.extend(influences) else: raise RuntimeError('No influences found for "{}"'.format(skin_cluster)) else: cmds.warning('No skinCluster found for "{}"'.format(mesh)) root_joints = find_top_joints(skeleton) else: skeleton = self._node.drivers() # for driver in self._node.drivers(): # driven = driver.replace('_drv', '') # if cmds.objExists(driven): # skeleton.append(driven) root_joints = find_top_joints(skeleton) if not root_joints: raise RuntimeError('No root joint found for "{}"'.format(self._node)) return root_joints[0] def add_root_attributes(self, root_joint): """ Adds RBFNode driven attributes to root_joint """ pose_root_attributes = dict() poses = self._node.poses() for pose in poses: driven_attributes = self._node.pose_driven_attributes(pose) # add type flag! current_pose = list() for attribute in driven_attributes: node, target = attribute.split('.') root_attribute = '{}.{}'.format(root_joint, target) if root_attribute not in current_pose: if not cmds.objExists(root_attribute): cmds.addAttr(root_joint, ln=target, at='double', k=True) else: input_connection = cmds.listConnections(root_attribute, s=True, d=False, plugs=True) if input_connection: cmds.disconnectAttr(input_connection[0], root_attribute) # cmds.connectAttr(attribute, root_attribute) current_pose.append(root_attribute) pose_root_attributes[pose] = current_pose return pose_root_attributes def bake_poses(self): """ Bakes the RBFNode poses in the timeline for FBX export """ for anim_curve_type in ['animCurveTL', 'animCurveTA', 'animCurveTU']: cmds.delete(cmds.ls(type=anim_curve_type)) pose_root_attributes = self.add_root_attributes(self.root_joint()) for frame, pose in enumerate(self._node.poses()): # go to pose self._node.go_to_pose(pose) # key controllers if self._node.num_controllers(): for controller in self._node.controllers(): cmds.setKeyframe(controller, t=frame, inTangentType='linear', outTangentType='step') # or key drivers else: for driver in self._node.drivers(): cmds.setKeyframe(driver, t=frame, inTangentType='linear', outTangentType='step') root_attributes = pose_root_attributes.get(pose, []) for root_attribute in root_attributes: input_connection = cmds.listConnections(root_attribute, s=True, d=False, plugs=True) if input_connection: cmds.disconnectAttr(input_connection[0], root_attribute) # Key Driven Before/After cmds.setAttr(root_attribute, 0) if frame == len(self._node.poses()) - 1: cmds.setKeyframe(root_attribute, t=(frame - 1), inTangentType='linear', outTangentType='linear') else: cmds.setKeyframe(root_attribute, t=((frame - 1), (frame + 1)), inTangentType='linear', outTangentType='linear') # Key Driven cmds.setAttr(root_attribute, 1) cmds.setKeyframe(root_attribute, t=frame, inTangentType='linear', outTangentType='linear') # set start-end frames end_frame = len(self._node.poses()) - 1 cmds.playbackOptions(minTime=0, maxTime=end_frame, animationStartTime=0, animationEndTime=end_frame) cmds.dgdirty(a=True) class RBFPoseExporterBatch(object): """ Utility class to export multiple RBFsolver nodes, exports a JSON and FBX for each solver >>> solvers = cmds.ls(type='UE4RBFSolverNode') >>> rig_scene = r'D:\Build\UE5_Main\Collaboration\Frosty\ArtSource\Character\Hero\Kenny\Rig\Kenny_Rig.ma' >>> export_directory = r'D:\test\class' >>> asset_name = 'Kenny' >>> rbf_exporter_batch = RBFPoseExporterBatch(solvers, asset_name, export_directory, rig_scene) Result: export_directory/node1.json export_directory/node1.fbx export_directory/node2.json export_directory/node2.fbx """ def __init__(self, nodes, asset_name, export_directory, rig_scene): self.set_pose_exporter(nodes, asset_name, export_directory) self.set_rig_scene(rig_scene) def pose_exporter(self): return self._poseExporter def asset_name(self): return self._asset_name def export_directory(self): return self._export_directory def set_pose_exporter(self, nodes, asset_name, export_directory): if not hasattr(nodes, '__iter__'): nodes = [nodes] exporters = list() for node in nodes: if cmds.objectType(node, isAType=RBFNodeExporter.node_type): exporters.append(RBFNodeExporter(node, asset_name, export_directory)) if not len(exporters): raise RuntimeError('No valid {} objects found'.format(RBFNodeExporter.node_type)) self._poseExporter = exporters self._asset_name = asset_name self._export_directory = export_directory def rig_scene(self): return self._rig_scene def set_rig_scene(self, rig_scene): if not os.path.exists(rig_scene): raise IOError('Rig scene "{}" does not exists'.format(rig_scene)) self._rig_scene = rig_scene # ------------------------------------------------------------------------------------- def export(self, run_in_subprocess=True): # TO-DO: Implement run_in_subprocess for exporter in self.pose_exporter(): cmds.file(self.rig_scene(), open=True, force=True) exporter.export()