#!/usr/bin/env python # -*- coding: utf-8 -*- """ Tool - DNA Utilities Provides utilities for working with DNA files and data """ import os import sys import json import maya.cmds as cmds import maya.mel as mel # Import configuration import config class DNAUtils: """Utilities for working with DNA files and data""" def __init__(self): """Initialize DNA utilities""" self.pydna_available = False self.pydna_error = None self.dna_calib_available = False self.dna_calib_error = None self.current_dna = None self.current_dna_file = None self.current_dna_version = None # Ensure pydna is in the Python path if config.PYDNA_PATH not in sys.path: sys.path.append(config.PYDNA_PATH) # Ensure DNA Calibration tools are in the Python path if config.DNA_CALIB_PATH not in sys.path: sys.path.append(config.DNA_CALIB_PATH) # Try to import pydna modules try: # First try to import from the specific version directory import pydna self.pydna_available = True print(f"PyDNA module loaded successfully: {config.PYDNA_PATH}") except ImportError as e: self.pydna_error = str(e) # Try to find pydna in alternative locations try: # Try parent directory parent_dir = os.path.dirname(config.PYDNA_PATH) if parent_dir not in sys.path: sys.path.append(parent_dir) import pydna self.pydna_available = True print(f"PyDNA module loaded successfully from alternative location: {parent_dir}") except ImportError: self.pydna_available = False cmds.warning(f"PyDNA module not found, some DNA features may be unavailable: {self.pydna_error}") # Try to import DNA Calibration modules try: # Import DNA Calibration modules import dna_calibration self.dna_calib_available = True print(f"DNA Calibration module loaded successfully: {config.DNA_CALIB_PATH}") except ImportError as e: self.dna_calib_error = str(e) self.dna_calib_available = False cmds.warning(f"DNA Calibration module not found, calibration features may be unavailable: {self.dna_calib_error}") def load_dna(self, file_path): """ Load a DNA file into the scene Args: file_path (str): Path to the DNA file Returns: bool: True if successful, False otherwise """ if not os.path.exists(file_path): error_msg = f"DNA file not found: {file_path}" cmds.warning(error_msg) cmds.confirmDialog( title="DNA loading error", message=error_msg, button=["OK"], defaultButton="OK" ) return False if not self.pydna_available: error_msg = "PyDNA module not found, cannot load DNA file" if self.pydna_error: error_msg += f"\nError details: {self.pydna_error}" cmds.warning(error_msg) cmds.confirmDialog( title="PyDNA module loading error", message=error_msg, button=["OK"], defaultButton="OK" ) return False try: # Import pydna modules import pydna from pydna.dna_reader import DNAReader # Read DNA file reader = DNAReader() dna = reader.read(file_path) if not dna: cmds.warning(f"Unable to read DNA file: {file_path}") return False # Create mesh from DNA mesh_name = os.path.splitext(os.path.basename(file_path))[0] result = self._create_mesh_from_dna(dna, mesh_name) if not result: cmds.warning(f"Unable to create mesh from DNA: {file_path}") return False # Create skeleton from DNA result = self._create_skeleton_from_dna(dna, mesh_name) if not result: cmds.warning(f"Unable to create skeleton from DNA: {file_path}") return False print(f"Successfully loaded DNA file: {file_path}") return True except Exception as e: cmds.warning(f"Error loading DNA file: {str(e)}") return False def save_dna(self, file_path): """ Save the current scene as a DNA file Args: file_path (str): Path to save the DNA file Returns: bool: True if successful, False otherwise """ if not self.pydna_available: cmds.warning("PyDNA module not found, cannot save DNA file") return False try: # Import pydna modules import pydna from pydna.dna_writer import DNAWriter # Get selected mesh and skeleton selection = cmds.ls(selection=True) if not selection: cmds.warning("Please select a mesh or skeleton") return False # Find mesh and skeleton mesh = None skeleton = None for obj in selection: if cmds.objectType(obj) == "transform": shapes = cmds.listRelatives(obj, shapes=True) if shapes and cmds.objectType(shapes[0]) == "mesh": mesh = obj elif cmds.objectType(obj) == "joint": # Find root joint root = obj parent = cmds.listRelatives(root, parent=True) while parent and cmds.objectType(parent[0]) == "joint": root = parent[0] parent = cmds.listRelatives(root, parent=True) skeleton = root if not mesh: cmds.warning("Mesh not found") return False if not skeleton: cmds.warning("Skeleton not found") return False # Create DNA from mesh and skeleton dna = self._create_dna_from_scene(mesh, skeleton) if not dna: cmds.warning(f"Unable to create DNA from scene") return False # Write DNA file writer = DNAWriter() result = writer.write(dna, file_path) if not result: cmds.warning(f"Unable to write DNA file: {file_path}") return False print(f"Successfully saved DNA file: {file_path}") return True except Exception as e: cmds.warning(f"Error saving DNA file: {str(e)}") return False def create_dna(self, mesh, skeleton): """ Create a DNA from a mesh and skeleton Args: mesh (str): Name of the mesh skeleton (str): Name of the skeleton Returns: bool: True if successful, False otherwise """ if not self.pydna_available: cmds.warning("PyDNA module not found, cannot create DNA") return False try: # Import pydna modules import pydna from pydna.dna_creator import DNACreator # Check if mesh and skeleton exist if not cmds.objExists(mesh): cmds.warning(f"Mesh not found: {mesh}") return False if not cmds.objExists(skeleton): cmds.warning(f"Skeleton not found: {skeleton}") return False # Create DNA creator = DNACreator() dna = creator.create(mesh, skeleton) if not dna: cmds.warning(f"Unable to create DNA") return False # Store DNA in scene self._store_dna_in_scene(dna, mesh) print(f"Successfully created DNA from {mesh} and {skeleton}") return True except Exception as e: cmds.warning(f"Error creating DNA: {str(e)}") return False def update_dna(self, mesh, skeleton): """ Update a DNA from a mesh and skeleton Args: mesh (str): Name of the mesh skeleton (str): Name of the skeleton Returns: bool: True if successful, False otherwise """ if not self.pydna_available: cmds.warning("PyDNA module not found, cannot update DNA") return False try: # Import pydna modules import pydna from pydna.dna_updater import DNAUpdater # Check if mesh and skeleton exist if not cmds.objExists(mesh): cmds.warning(f"Mesh not found: {mesh}") return False if not cmds.objExists(skeleton): cmds.warning(f"Skeleton not found: {skeleton}") return False # Get DNA from scene dna = self._get_dna_from_scene(mesh) if not dna: cmds.warning(f"Unable to find DNA data, please create DNA first") return False # Update DNA updater = DNAUpdater() result = updater.update(dna, mesh, skeleton) if not result: cmds.warning(f"Unable to update DNA") return False # Store updated DNA in scene self._store_dna_in_scene(dna, mesh) print(f"Successfully updated DNA") return True except Exception as e: cmds.warning(f"Error updating DNA: {str(e)}") return False def export_dna(self, file_path): """ Export DNA to a file Args: file_path (str): Path to export the DNA file Returns: bool: True if successful, False otherwise """ if not self.pydna_available: cmds.warning("PyDNA module not found, cannot export DNA") return False try: # Import pydna modules import pydna from pydna.dna_writer import DNAWriter # Get selected mesh selection = cmds.ls(selection=True) if not selection: cmds.warning("Please select a mesh") return False mesh = None for obj in selection: if cmds.objectType(obj) == "transform": shapes = cmds.listRelatives(obj, shapes=True) if shapes and cmds.objectType(shapes[0]) == "mesh": mesh = obj break if not mesh: cmds.warning("Mesh not found") return False # Get DNA from scene dna = self._get_dna_from_scene(mesh) if not dna: cmds.warning(f"Unable to find DNA data, please create DNA first") return False # Write DNA file writer = DNAWriter() result = writer.write(dna, file_path) if not result: cmds.warning(f"Unable to write DNA file: {file_path}") return False print(f"Successfully exported DNA file: {file_path}") return True except Exception as e: cmds.warning(f"Error exporting DNA file: {str(e)}") return False def get_dna_attributes(self, dna_file=None): """ Get DNA attributes Args: dna_file (str, optional): Path to the DNA file. If None, get from scene. Returns: dict: Dictionary of DNA attributes """ if not self.pydna_available: cmds.warning("PyDNA module not found, cannot get DNA attributes") return {} try: # Import pydna modules import pydna # Get DNA dna = None if dna_file and os.path.exists(dna_file): # Read DNA file from pydna.dna_reader import DNAReader reader = DNAReader() dna = reader.read(dna_file) else: # Get from scene selection = cmds.ls(selection=True) if not selection: cmds.warning("Please select a mesh or load a DNA file") return {} mesh = None for obj in selection: if cmds.objectType(obj) == "transform": shapes = cmds.listRelatives(obj, shapes=True) if shapes and cmds.objectType(shapes[0]) == "mesh": mesh = obj break if not mesh: cmds.warning("Mesh not found") return {} dna = self._get_dna_from_scene(mesh) if not dna: cmds.warning(f"Unable to find DNA data") return {} # Get attributes attributes = {} # General attributes attributes["general.name"] = dna.getName() attributes["general.version"] = dna.getVersion() attributes["general.topology"] = dna.getTopology() # Mesh attributes mesh_count = dna.getMeshCount() attributes["mesh.count"] = mesh_count for i in range(mesh_count): mesh = dna.getMesh(i) attributes[f"mesh.{i}.name"] = mesh.getName() attributes[f"mesh.{i}.vertex_count"] = mesh.getVertexCount() attributes[f"mesh.{i}.triangle_count"] = mesh.getTriangleCount() # Joint attributes joint_count = dna.getJointCount() attributes["joint.count"] = joint_count for i in range(joint_count): joint = dna.getJoint(i) attributes[f"joint.{i}.name"] = joint.getName() attributes[f"joint.{i}.parent"] = joint.getParent() # BlendShape attributes blendshape_count = dna.getBlendShapeCount() attributes["blendshape.count"] = blendshape_count for i in range(blendshape_count): blendshape = dna.getBlendShape(i) attributes[f"blendshape.{i}.name"] = blendshape.getName() attributes[f"blendshape.{i}.target_count"] = blendshape.getTargetCount() return attributes except Exception as e: cmds.warning(f"Error getting DNA attributes: {str(e)}") return {} def set_dna_attribute(self, attr_name, value): """ Set a DNA attribute Args: attr_name (str): Name of the attribute value (any): Value to set Returns: bool: True if successful, False otherwise """ if not self.pydna_available: cmds.warning("PyDNA module not found, cannot set DNA attribute") return False try: # Import pydna modules import pydna # Get DNA from scene selection = cmds.ls(selection=True) if not selection: cmds.warning("Please select a mesh") return False mesh = None for obj in selection: if cmds.objectType(obj) == "transform": shapes = cmds.listRelatives(obj, shapes=True) if shapes and cmds.objectType(shapes[0]) == "mesh": mesh = obj break if not mesh: cmds.warning("Mesh not found") return False dna = self._get_dna_from_scene(mesh) if not dna: cmds.warning(f"Unable to find DNA data, please create DNA first") return False # Set attribute # Parse attribute name parts = attr_name.split(".") if len(parts) < 2: cmds.warning(f"Invalid attribute name: {attr_name}") return False category = parts[0] if category == "general": if parts[1] == "name": dna.setName(value) elif parts[1] == "version": dna.setVersion(value) elif parts[1] == "topology": dna.setTopology(value) elif category == "mesh" and len(parts) >= 3: try: mesh_index = int(parts[1]) mesh = dna.getMesh(mesh_index) if parts[2] == "name": mesh.setName(value) except (ValueError, IndexError): cmds.warning(f"Invalid mesh index: {parts[1]}") return False elif category == "joint" and len(parts) >= 3: try: joint_index = int(parts[1]) joint = dna.getJoint(joint_index) if parts[2] == "name": joint.setName(value) except (ValueError, IndexError): cmds.warning(f"Invalid joint index: {parts[1]}") return False elif category == "blendshape" and len(parts) >= 3: try: blendshape_index = int(parts[1]) blendshape = dna.getBlendShape(blendshape_index) if parts[2] == "name": blendshape.setName(value) except (ValueError, IndexError): cmds.warning(f"Invalid BlendShape index: {parts[1]}") return False else: cmds.warning(f"Unsupported attribute category: {category}") return False # Store updated DNA in scene self._store_dna_in_scene(dna, mesh) print(f"Successfully set DNA attribute: {attr_name} = {value}") return True except Exception as e: cmds.warning(f"Error setting DNA attribute: {str(e)}") return False def _create_mesh_from_dna(self, dna, name): """ Create a mesh from DNA Args: dna: DNA object name (str): Name for the mesh Returns: bool: True if successful, False otherwise """ try: # Get mesh data from DNA mesh_count = dna.getMeshCount() if mesh_count == 0: cmds.warning("No mesh data in DNA") return False # Create mesh for each DNA mesh for i in range(mesh_count): dna_mesh = dna.getMesh(i) mesh_name = f"{name}_mesh_{i}" # Get vertices vertex_count = dna_mesh.getVertexCount() vertices = [] for j in range(vertex_count): pos = dna_mesh.getVertexPosition(j) vertices.append((pos.x, pos.y, pos.z)) # Get triangles triangle_count = dna_mesh.getTriangleCount() triangles = [] for j in range(triangle_count): tri = dna_mesh.getTriangle(j) triangles.append((tri.v1, tri.v2, tri.v3)) # Create mesh mesh = cmds.polyCreateFacet( name=mesh_name, p=vertices, f=triangles )[0] # Get UVs uv_count = dna_mesh.getUVCount() if uv_count > 0: # Create UV set cmds.polyUVSet(mesh, create=True, uvSet="map1") # Set UVs for j in range(uv_count): uv = dna_mesh.getUV(j) cmds.polyEditUV(f"{mesh}.map1[{j}]", u=uv.u, v=uv.v) return True except Exception as e: cmds.warning(f"Error creating mesh: {str(e)}") return False def _create_skeleton_from_dna(self, dna, name): """ Create a skeleton from DNA Args: dna: DNA object name (str): Name for the skeleton Returns: bool: True if successful, False otherwise """ try: # Get joint data from DNA joint_count = dna.getJointCount() if joint_count == 0: cmds.warning("No joint data in DNA") return False # Create joints joints = {} for i in range(joint_count): dna_joint = dna.getJoint(i) joint_name = dna_joint.getName() parent_index = dna_joint.getParent() # Get joint position pos = dna_joint.getPosition() # Create joint joint = cmds.joint(name=joint_name, p=(pos.x, pos.y, pos.z)) joints[i] = joint # Set joint orientation orient = dna_joint.getOrientation() cmds.setAttr(f"{joint}.rotateX", orient.x) cmds.setAttr(f"{joint}.rotateY", orient.y) cmds.setAttr(f"{joint}.rotateZ", orient.z) # Set parent if parent_index >= 0: parent_joint = joints.get(parent_index) if parent_joint: cmds.parent(joint, parent_joint) # Rename root joint root_joint = joints.get(0) if root_joint: cmds.rename(root_joint, f"{name}_root") return True except Exception as e: cmds.warning(f"Error creating skeleton: {str(e)}") return False def _create_dna_from_scene(self, mesh, skeleton): """ Create a DNA from scene objects Args: mesh (str): Name of the mesh skeleton (str): Name of the skeleton Returns: object: DNA object or None if failed """ try: # Import pydna modules import pydna from pydna.dna_creator import DNACreator # Create DNA creator = DNACreator() dna = creator.create(mesh, skeleton) return dna except Exception as e: cmds.warning(f"Error creating DNA from scene: {str(e)}") return None def _store_dna_in_scene(self, dna, mesh): """ Store DNA in the scene as mesh attributes Args: dna: DNA object mesh (str): Name of the mesh Returns: bool: True if successful, False otherwise """ try: # Import pydna modules import pydna from pydna.dna_writer import DNAWriter # Serialize DNA to JSON writer = DNAWriter() dna_json = writer.toJSON(dna) # Store as mesh attribute if not cmds.attributeQuery("dnaData", node=mesh, exists=True): cmds.addAttr(mesh, longName="dnaData", dataType="string") cmds.setAttr(f"{mesh}.dnaData", dna_json, type="string") return True except Exception as e: cmds.warning(f"Error storing DNA data: {str(e)}") return False def _get_dna_from_scene(self, mesh): """ Get DNA from scene Args: mesh (str): Name of the mesh Returns: object: DNA object or None if not found """ try: # Import pydna modules import pydna from pydna.dna_reader import DNAReader # Check if mesh has DNA data if not cmds.attributeQuery("dnaData", node=mesh, exists=True): return None # Get DNA data dna_json = cmds.getAttr(f"{mesh}.dnaData") if not dna_json: return None # Deserialize DNA from JSON reader = DNAReader() dna = reader.fromJSON(dna_json) # Store the current DNA self.current_dna = dna self.current_dna_version = self._detect_dna_version(dna) return dna except Exception as e: cmds.warning(f"Error getting DNA data: {str(e)}") return None def detect_dna_version(self, dna_file): """ Detect the version of a DNA file Args: dna_file (str): Path to the DNA file Returns: str: DNA version (e.g., 'MH.4', 'MH.3', 'Custom') """ try: # Import pydna modules import pydna from pydna.dna_reader import DNAReader # Read DNA file reader = DNAReader() dna = reader.read(dna_file) if not dna: return "Unknown" return self._detect_dna_version(dna) except Exception as e: cmds.warning(f"Error detecting DNA version: {str(e)}") return "Unknown" def _detect_dna_version(self, dna): """ Detect the version of a DNA object Args: dna: DNA object Returns: str: DNA version (e.g., 'MH.4', 'MH.3', 'Custom') """ try: # Get DNA metadata metadata = dna.getMetaData() if not metadata: return "Unknown" # Check for MetaHuman version if "metahuman" in metadata.lower(): if "4." in metadata: return "MH.4" elif "3." in metadata: return "MH.3" else: return "MetaHuman" # Check for specific markers joint_count = dna.getJointCount() mesh_count = dna.getMeshCount() # MetaHuman 4 typically has 411 joints if joint_count >= 400 and joint_count <= 420: return "MH.4" # MetaHuman 3 typically has 245 joints elif joint_count >= 240 and joint_count <= 260: return "MH.3" else: return "Custom" except Exception as e: cmds.warning(f"Error detecting DNA version: {str(e)}") return "Unknown" def validate_dna(self, dna_file=None): """ Validate a DNA file structure Args: dna_file (str, optional): Path to the DNA file. If None, validate current DNA. Returns: dict: Validation results with issues and warnings """ try: # Import pydna modules import pydna from pydna.dna_reader import DNAReader # Get DNA object dna = None if dna_file: reader = DNAReader() dna = reader.read(dna_file) else: dna = self.current_dna if not dna: return {"valid": False, "errors": ["No DNA data available"], "warnings": []} # Initialize validation results results = { "valid": True, "errors": [], "warnings": [], "info": {} } # Check basic structure joint_count = dna.getJointCount() mesh_count = dna.getMeshCount() blendshape_count = dna.getBlendShapeCount() if hasattr(dna, "getBlendShapeCount") else 0 results["info"] = { "version": self._detect_dna_version(dna), "joint_count": joint_count, "mesh_count": mesh_count, "blendshape_count": blendshape_count } # Validate joints if joint_count == 0: results["errors"].append("No joints found in DNA") results["valid"] = False # Validate meshes if mesh_count == 0: results["errors"].append("No meshes found in DNA") results["valid"] = False # Check for root joint root_joint = None try: root_joint = dna.getJoint(0) except: results["errors"].append("No root joint found") results["valid"] = False # Check for MetaHuman compatibility version = results["info"]["version"] if version in ["MH.4", "MH.3", "MetaHuman"]: # Check for specific MetaHuman joints mh_joints = ["head", "neck", "spine", "pelvis"] found_joints = [] for i in range(joint_count): joint = dna.getJoint(i) joint_name = joint.getName().lower() for mh_joint in mh_joints: if mh_joint in joint_name and mh_joint not in found_joints: found_joints.append(mh_joint) missing_joints = [j for j in mh_joints if j not in found_joints] if missing_joints: results["warnings"].append(f"Missing expected MetaHuman joints: {', '.join(missing_joints)}") # Set final validity results["valid"] = len(results["errors"]) == 0 return results except Exception as e: cmds.warning(f"Error validating DNA: {str(e)}") return {"valid": False, "errors": [str(e)], "warnings": []} def extract_blendshapes_from_dna(self, output_dir, dna_file=None): """ Extract blendshapes from DNA file Args: output_dir (str): Directory to save extracted blendshapes dna_file (str, optional): Path to the DNA file. If None, use current DNA. Returns: list: List of extracted blendshape file paths """ try: # Import pydna modules import pydna from pydna.dna_reader import DNAReader # Get DNA object dna = None if dna_file: reader = DNAReader() dna = reader.read(dna_file) else: dna = self.current_dna if not dna: cmds.warning("No DNA data available for extracting blendshapes") return [] # Check if output directory exists, if not create it if not os.path.exists(output_dir): os.makedirs(output_dir) # Get blendshape count blendshape_count = dna.getBlendShapeCount() if hasattr(dna, "getBlendShapeCount") else 0 if blendshape_count == 0: cmds.warning("No blendshapes found in DNA") return [] # Extract blendshapes extracted_files = [] base_mesh = None # Create base mesh try: mesh_name = "temp_dna_base_mesh" base_mesh = self._create_mesh_from_dna(dna, mesh_name) except Exception as e: cmds.warning(f"Error creating base mesh: {str(e)}") return [] # Extract each blendshape for i in range(blendshape_count): try: blendshape = dna.getBlendShape(i) blendshape_name = blendshape.getName() # Create blendshape mesh bs_mesh_name = f"temp_bs_{blendshape_name}" self._create_blendshape_mesh(dna, i, bs_mesh_name) # Export blendshape mesh output_file = os.path.join(output_dir, f"{blendshape_name}.obj") cmds.select(bs_mesh_name) cmds.file(output_file, force=True, exportSelected=True, type="OBJexport") # Delete temporary mesh cmds.delete(bs_mesh_name) extracted_files.append(output_file) except Exception as e: cmds.warning(f"Error extracting blendshape {i}: {str(e)}") # Clean up if base_mesh and cmds.objExists(base_mesh): cmds.delete(base_mesh) return extracted_files except Exception as e: cmds.warning(f"Error extracting blendshapes: {str(e)}") return [] def _create_blendshape_mesh(self, dna, blendshape_index, name): """ Create a mesh for a specific blendshape Args: dna: DNA object blendshape_index (int): Index of the blendshape name (str): Name for the blendshape mesh Returns: str: Name of the created mesh or None if failed """ try: # Get blendshape blendshape = dna.getBlendShape(blendshape_index) # Get base mesh mesh_index = 0 # Usually the first mesh dna_mesh = dna.getMesh(mesh_index) # Get vertices and topology vertex_count = dna_mesh.getVertexCount() vertices = [] # Apply blendshape deltas to base vertices for i in range(vertex_count): base_pos = dna_mesh.getVertex(i) delta = blendshape.getVertexDelta(i) # Apply delta pos = [base_pos.x + delta.x, base_pos.y + delta.y, base_pos.z + delta.z] vertices.append(pos) # Get face topology face_count = dna_mesh.getFaceCount() faces = [] for i in range(face_count): face = dna_mesh.getFace(i) faces.append([face.v1, face.v2, face.v3]) # Create mesh mesh = cmds.polyCreateFacet(name=name, point=vertices, face=faces)[0] return mesh except Exception as e: cmds.warning(f"Error creating blendshape mesh: {str(e)}") return None