Skip to content

Instantly share code, notes, and snippets.

@aobond2
Last active May 9, 2024 10:16
Show Gist options
  • Save aobond2/0a1e73068e28f0a13efb5fd74b3a7ca5 to your computer and use it in GitHub Desktop.
Save aobond2/0a1e73068e28f0a13efb5fd74b3a7ca5 to your computer and use it in GitHub Desktop.
Validate and export rig
"""
Maya/QT UI template
Maya 2023
"""
import maya.cmds as cmds
import maya.mel as mel
from maya import OpenMayaUI as omui
from shiboken2 import wrapInstance
from PySide2 import QtUiTools, QtCore, QtGui, QtWidgets
from functools import partial # optional, for passing args during signal function calls
import sys
import pymel.core as pm
import os, inspect
import fnmatch
from PySide2.QtGui import QColor
class RigExport(QtWidgets.QWidget):
"""
Create a default tool window.
"""
window = None
def __init__(self, parent = None):
"""
Initialize class.
"""
super(RigExport, self).__init__(parent = parent)
self.setWindowFlags(QtCore.Qt.Window)
# Get script location
current_file = inspect.getfile(inspect.currentframe())
currentPath = os.path.dirname(os.path.abspath(current_file))
# Get the UI
self.widgetPath = str(currentPath) + "\RigExport.ui"
self.widget = QtUiTools.QUiLoader().load(self.widgetPath)
self.widget.setParent(self)
# set initial window size
self.resize(400, 650)
# Variables
# TODO: change this based on texture format we use
self.textureFormat = ".TGA"
self.ARMSSuffix = "_ARMS"
self.normalSuffix = "_N"
self.difuseSuffix = "_D"
self.rootName = "root"
self.boneInfluenceLimit = 5
self.vertexWrongArray = []
self.meshBoneInfluenceOverLimit = []
self.tagNonExport = "NonExport"
self.tagMeshPart = "MeshPart"
self.extraAttributeName = "Tags"
self.meshPartTagAttributeName = "BodyPart"
self.exportArray = []
self.mayaFilePath = cmds.file(q=True, sceneName=True)
self.uassetPrefix = "SK_"
self.uassetSuffix= "_01"
self.materialClean = False
self.failArray = []
self.passString = "Pass"
self.warningString = "Warning"
self.failString = "Fail"
self.errorArray = []
self.jointSCWrongArray = []
self.melScriptPath = "C:/dev/Seraph/MayaPlugins/scripts/RigExporter/MeshBabylonExport.mel"
self.animationMelScriptPath = "C:/dev/Seraph/MayaPlugins/scripts/RigExporter/AnimationBabylonExport.mel"
self.noTextureArray = []
# TODO: Change this
self.filePath = "C:/test/"
self.meshPartArray = [
"Head",
"Hair",
"HandL",
"HandR",
"LegsBase",
"LegsUpper",
"MainBody",
"Shoes",
"TorsoBase",
"TorsoMid",
"TorsoUpper",
"Mouth",
"Hat",
"Glasses",
"Eyes"
]
# locate UI widgets/ reference here
self.btn_close = self.widget.findChild(QtWidgets.QPushButton, 'pushButton')
self.btn_validate = self.widget.findChild(QtWidgets.QPushButton, 'ValidateButton')
self.btn_export = self.widget.findChild(QtWidgets.QPushButton, 'ExportButton')
self.btn_cleanUpMaterial = self.widget.findChild(QtWidgets.QPushButton, 'MaterialCleanUpButton')
self.btn_addTag = self.widget.findChild(QtWidgets.QPushButton, 'addTagButton')
self.btn_meshPartTag = self.widget.findChild(QtWidgets.QPushButton, 'meshPartTagButton')
self.btn_deleteTag = self.widget.findChild(QtWidgets.QPushButton, 'deleteTagsButton')
self.label_extraMaterial = self.widget.findChild(QtWidgets.QLabel, 'ExtraMaterialLabel')
self.label_bindPose = self.widget.findChild(QtWidgets.QLabel, 'BindPoseLabel')
self.label_vertexInfluence = self.widget.findChild(QtWidgets.QLabel, 'VertexInfluenceLabel')
self.label_currentTag = self.widget.findChild(QtWidgets.QLabel, 'currentTagLabel')
self.label_skinCluster = self.widget.findChild(QtWidgets.QLabel, 'SkinClusterLabel')
self.graphic_materialCheck = self.widget.findChild(QtWidgets.QGraphicsView, 'MaterialStatus')
self.graphic_bindPoseCheck = self.widget.findChild(QtWidgets.QGraphicsView, 'BindPoseStatus')
self.graphic_vertexInfluenceCheck = self.widget.findChild(QtWidgets.QGraphicsView, 'VertexInfluenceStatus')
self.graphic_skinClusterCheck = self.widget.findChild(QtWidgets.QGraphicsView, 'SkinClusterStatus')
self.graphic_skinCluster02Check = self.widget.findChild(QtWidgets.QGraphicsView, 'SkinCluster02Status')
self.comboBox_Tags = self.widget.findChild(QtWidgets.QComboBox, 'tagsComboBox')
self.comboBox_meshPartTags = self.widget.findChild(QtWidgets.QComboBox, 'meshPartTagsComboBox')
self.maxBoneSpinBox = self.widget.findChild(QtWidgets.QSpinBox, 'MaxBoneSpinBox')
self.materialTextBrowser = self.widget.findChild(QtWidgets.QTextBrowser, 'MaterialTextBrowser')
self.bindPoseTextBrowser = self.widget.findChild(QtWidgets.QTextBrowser, 'BindPoseTextBrowser')
self.vertexInfluenceTextBrowser = self.widget.findChild(QtWidgets.QTextBrowser, 'VertexInfluenceTextBrowser')
self.skinClusterTextBrowser = self.widget.findChild(QtWidgets.QTextBrowser, 'SkinClusterTextBrowser')
self.skinCluster02TextBrowser = self.widget.findChild(QtWidgets.QTextBrowser, 'SkinClusterTextBrowser_2')
self.exportTextBrowser = self.widget.findChild(QtWidgets.QTextBrowser, 'ExportTextBrowser')
self.btn_zone = self.widget.findChild(QtWidgets.QPushButton, 'ZoneButton')
self.btn_skeletalMeshGLBExport = self.widget.findChild(QtWidgets.QPushButton, 'ExportSetButton')
self.btn_animationGLBExport = self.widget.findChild(QtWidgets.QPushButton, 'ExportGLBButton')
self.btn_batchGLBExport = self.widget.findChild(QtWidgets.QPushButton, 'BatchExportGLBButton')
self.text_animationName = self.widget.findChild(QtWidgets.QTextEdit, 'AnimationNameTextEdit')
self.btn_textureFix = self.widget.findChild(QtWidgets.QPushButton, 'TextureFixButton')
self.btn_render = self.widget.findChild(QtWidgets.QPushButton, 'renderButton')
self.btn_batchRender = self.widget.findChild(QtWidgets.QPushButton, 'batchRenderButton')
self.btn_variantSkeletalMeshGLBExport = self.widget.findChild(QtWidgets.QPushButton, 'ExportTextureVariantButton')
self.btn_updateTags = self.widget.findChild(QtWidgets.QPushButton, 'updateTagsButton')
# assign functionality to buttons
self.btn_validate.clicked.connect(self.clickedValidate)
self.btn_export.clicked.connect(self.exportMeshes)
self.btn_cleanUpMaterial.clicked.connect(self.cleanUpMaterial)
self.btn_addTag.clicked.connect(self.addTag)
self.btn_meshPartTag.clicked.connect(self.meshPartTag)
self.btn_deleteTag.clicked.connect(self.deleteTags)
self.btn_zone.clicked.connect(self.zoneProcess)
self.btn_skeletalMeshGLBExport.clicked.connect(self.skeletalMeshGLBExport)
self.btn_animationGLBExport.clicked.connect(self.animationGLBExport)
self.btn_batchGLBExport.clicked.connect(self.batchGLBExportWithTags)
self.btn_textureFix.clicked.connect(self.textureFix)
self.btn_render.clicked.connect(self.doRender)
self.btn_batchRender.clicked.connect(self.doBatchRender)
self.btn_variantSkeletalMeshGLBExport.clicked.connect(self.variantSkeletalMeshGLBExport)
self.btn_updateTags.clicked.connect(self.updateBodyPartTags)
# Set some button to disable
self.btn_export.setEnabled(False)
self.btn_cleanUpMaterial.setEnabled(False)
self.btn_zone.setEnabled(False)
self.removeSelectionCallback()
self.addSelectionCallback()
self.updateTagComboBox()
def updateTagComboBox(self):
self.comboBox_meshPartTags.clear()
self.comboBox_meshPartTags.addItems(self.meshPartArray)
def addSelectionCallback(self):
global selectionChangedCallback
selectionChangedCallback = pm.scriptJob(event=['SelectionChanged', self.selectLabel])
def removeSelectionCallback(self):
try:
global selectionChangedCallback
if pm.scriptJob(exists=selectionChangedCallback):
pm.scriptJob(kill=selectionChangedCallback)
except:
print ("no selection callback")
def selectLabel(self):
selectedObjects = cmds.ls(sl=True)
self.label_currentTag.setText(str(selectedObjects))
def meshPartTag(self):
print ("MeshPart")
allTagsFromQT = self.comboBox_meshPartTags
currentTagInQT = allTagsFromQT.currentIndex()
selectedObject = cmds.ls(selection=True)
neededTags = self.makeMeshPartEnumArray()
enumOptions = ':'.join(neededTags)
# Add custom attribute
for obj in selectedObject:
if not (cmds.attributeQuery(self.meshPartTagAttributeName, node=obj, exists=True)):
cmds.addAttr(obj, longName=self.meshPartTagAttributeName, attributeType='enum', enumName = enumOptions)
# Set attribute default value
cmds.setAttr('{}.{}'.format(obj, self.meshPartTagAttributeName), currentTagInQT)
def getObjectCenter(self, inputMesh):
bb = cmds.exactWorldBoundingBox(inputMesh)
center = [(bb[0] + bb[3]) / 2, (bb[1] + bb[4]) / 2, (bb[2] + bb[5]) / 2]
distance = max(bb[3] - bb[0], bb[4] - bb[1], bb[5] - bb[2])
return (center, distance)
def addLights(self):
print ("Add lights")
#ambientLight = cmds.ambientLight(intensity=15)
#ambientLight = pm.createNode('aiSkyDomeLight', name='aiSkyDomeLight1')
ambientLight = cmds.shadingNode('aiSkyDomeLight', name='aiSkyDomeLight1', asLight = True)
cmds.setAttr(ambientLight + '.intensity', 1.5)
cmds.setAttr(ambientLight + '.camera', 0)
directionalLight = cmds.directionalLight(rotation=(-52, -50, 0), intensity = 5, rgb = (0.85, 1, 1))
#backLight = cmds.directionalLight(rotation=(-15, 170, 0), intensity = 10, rgb = (1, 0, 0))
#backLight = cmds.directionalLight(rotation=(165, -0.7, 182), intensity = 10, rgb = (1, 0, 0))
backLight = cmds.shadingNode('directionalLight', name='backLight', asLight = True)
cmds.setAttr(backLight + '.intensity', 10)
cmds.setAttr(backLight + '.rotateX', 165)
cmds.setAttr(backLight + '.rotateY', 33)
cmds.setAttr(backLight + '.rotateZ', 182)
'''
cmds.setAttr(f"{directionalLight}.useDepthMapShadows", 1)
cmds.setAttr(f"{directionalLight}.dmapResolution", 1024)
cmds.setAttr(f"{directionalLight}.dmapFilterSize", 10)
cmds.setAttr(f"{backLight}.useDepthMapShadows", 1)
cmds.setAttr(f"{backLight}.dmapResolution", 1024)
cmds.setAttr(f"{backLight}.dmapFilterSize", 5)
'''
return ambientLight, directionalLight, backLight
def getRenderFileName(self):
currentFilePath = cmds.file(q=True, sn=True)
currentFileName = os.path.basename(currentFilePath)
# Get body type from the file name, example = SK_BodyA_Body_01.ma
bodyType = currentFileName.split("_")[1]
characterFolderSplitter = "Characters"
characterFolderAppend = "Characters/Thumbnails/"
currentFilePath = (currentFilePath.split(characterFolderSplitter)[0])
currentFilePath = currentFilePath + characterFolderAppend
# Get current selection name
selectedObjects = cmds.ls(sl=True)
fileName = bodyType + '_' + str(selectedObjects[0])
filePath = currentFilePath + fileName
print (filePath)
return filePath
def setRenderSettings(self):
# TODO: expose this
cmds.setAttr("defaultResolution.width", 512)
cmds.setAttr("defaultResolution.height", 512)
cmds.setAttr("defaultResolution.deviceAspectRatio", 1)
# TODO: Save to file
filePath = self.getRenderFileName()
cmds.setAttr("defaultRenderGlobals.imageFormat", 32)
cmds.setAttr("hardwareRenderingGlobals.multiSampleEnable", 1)
cmds.setAttr("defaultRenderGlobals.imageFilePrefix", filePath, type="string")
# Set renderer to hardware
#cmds.setAttr('defaultRenderGlobals.currentRenderer', 'mayaHardware2', type='string')
cmds.setAttr('defaultRenderGlobals.currentRenderer', 'arnold', type='string')
cmds.setAttr("defaultArnoldDriver.ai_translator", "png", type="string")
def doBatchRender(self):
print ("Batch render")
self.unhideAllMeshes()
currentFile = self.getCurrentFile()
self.checkTag()
exportGLB = True
# Export all MeshPart
# Need to set 2nd argument to True for batch render
self.exportMeshParts(exportGLB, True)
def doRender(self):
# Set render settings here
self.setRenderSettings()
selectedObjects = cmds.ls(selection=True)
print ("-------------------------")
print (selectedObjects)
center, distance = self.getObjectCenter(selectedObjects[0])
camera_position = [center[0], center[1], center[2] + distance]
# Set to render only selected
mel.eval("optionVar -intValue renderViewRenderSelectedObj on;")
# Create new camera
newCamera = cmds.camera(displayResolution=True)
cameraShape = newCamera[1]
# Set the camera's rotation
cmds.xform(newCamera, rotation=[-12, 20, 0])
# Set the camera aperture to correspond to a 1.00 aspect ratio
cmds.setAttr(cameraShape + '.horizontalFilmAperture', 1)
cmds.setAttr(cameraShape + '.verticalFilmAperture', 1)
# Disable other renderable cameras
for cam in cmds.ls(type="camera"):
if cam != cameraShape:
cmds.setAttr(cam + ".renderable", False)
# Enable new camera
cmds.setAttr(cameraShape + ".renderable", True)
cmds.setAttr(cameraShape + ".filmFit", 3)
cmds.lookThru(cameraShape)
# Set camera position
cmds.camera(cameraShape, edit=True, centerOfInterest=distance, position=camera_position)
cmds.camera(cameraShape, e=True, aspectRatio=1.00)
cmds.viewFit(cameraShape, selectedObjects, f=1)
# Add lights
ambientLight, directionalLight, backLight = self.addLights()
cmds.select(clear=True)
for obj in selectedObjects:
cmds.select(obj)
self.getObjectCenter(obj)
pm.runtime.RenderIntoNewWindow()
# Do Render
#newImage = cmds.render(newCamera, x=512, y=512)
# Delete camera
if cmds.objExists(cameraShape):
cmds.delete(cameraShape)
cmds.delete(newCamera[0])
#'''
# Delete lights and its transform
try:
dlRelatives = cmds.listRelatives(directionalLight, parent=True)
cmds.delete(dlRelatives, directionalLight)
alRelatives = cmds.listRelatives(ambientLight, parent=True)
cmds.delete(alRelatives, ambientLight)
blRelatives = cmds.listRelatives(backLight, parent=True)
cmds.delete(blRelatives, backLight)
except:
print ("No lights")
#'''
def processTexture(self):
# Get the material of selected object
theNodes = cmds.ls(sl = True, dag = True, s = True)
shadeEng = cmds.listConnections(theNodes, type = "shadingEngine")
materials = cmds.ls(cmds.listConnections(shadeEng), materials = True)
for material in materials:
textureNode = cmds.listConnections(material, type='file')[0]
# Get the filename from the texture node
currentTextureFilename = cmds.getAttr("%s.fileTextureName" % textureNode)
# Get new path
newTexturePath = str(self.findCorrectTexturePath(currentTextureFilename))
# Update path with new path
cmds.setAttr("%s.fileTextureName" % textureNode, newTexturePath, type="string")
# Convert material to aiStandardMaterial for exporting GLB
self.convertMaterialToaiStandardSurface(shadeEng, material, theNodes, len(materials))
def findCorrectTexturePath(self, textureName):
currentFilePath = os.path.dirname(cmds.file(q=True, sn=True))
characterFolderSplitter = "Characters"
characterFolderAppend = "Characters\Textures"
currentFilePath = (currentFilePath.split(characterFolderSplitter)[0])
targetFileName = textureName.split("/")[-1]
# Check texture folder for matching name
#textureFolder = "C:\dev\M2Main\Source\Engine\M2Content\Characters\Textures"
textureFolder = currentFilePath + characterFolderAppend
for dirpath, dirnames, filenames in os.walk(textureFolder):
for filename in filenames:
if fnmatch.fnmatch(filename, targetFileName):
targetFileName = (os.path.join(dirpath, filename))
for dirpath, dirnames, filenames in os.walk(dirpath):
for filename in filenames:
if fnmatch.fnmatch(filename, targetFileName):
targetFileName = (os.path.join(dirpath, filename))
newPath = targetFileName
return (newPath)
def textureFix(self):
self.processAllMeshesForTexture()
# TODO: Remove this later
self.getFBXExportLocation()
def has_multiple_materials(self, object_name):
# Get all shading groups connected to the object
shading_groups = cmds.listConnections(object_name, type='shadingEngine')
if not shading_groups:
return False
# Get all unique materials connected to the shading groups
materials = set()
for sg in shading_groups:
materials.update(cmds.listConnections(sg + '.surfaceShader'))
# Check if there are multiple materials
return len(materials) > 1
def processAllMeshesForTexture(self):
objects = cmds.ls(dag = True, exactType="mesh")
for obj in objects:
try:
cmds.select(obj)
self.processTexture()
except:
print("------------------")
def getFBXExportLocation(self):
fbxFolderName = "FBX"
currentFilePath = os.getcwd()
fbxPath = (currentFilePath.split("MA")[0]) + fbxFolderName
print ("-------------")
print (fbxPath)
def zoneProcess(self):
print ("Zone Process")
groups, meshes = self.getMeshesAndGroups()
for m in meshes:
print ("---------------------- mesh = " + str(m))
self.boneInfluenceLimit = self.getMaxBoneValue()
skin_Cluster = cmds.ls(cmds.listHistory(m), type='skinCluster')
if not skin_Cluster:
return
#bones = cmds.skinCluster(skin_Cluster[0], query=True, influence=True)
# Get all vertices
vertices = cmds.ls(m + ".vtx[*]", flatten=True)
# Auto unwrap the mesh
# TODO: Do this in UV2 instead
cmds.polyAutoProjection(m, lm=0, pb=0, ibd=1, cm=0, l=2, sc=1)
for vertex in vertices:
boneInfluence = []
vertexIndex = self.getVertexIndex(vertex)
influences = cmds.skinPercent(skin_Cluster[0], vertex, query=True, value=True)
boneNames = pm.skinCluster(skin_Cluster[0], query=True, influence=True)
for boneName, influence in zip(boneNames, influences):
if influence > 0.0:
#print("Bone {}: {}, vertex: {}".format(boneName, influence, vertexIndex))
boneInfluence.append(boneName)
mainBone = boneInfluence[0]
# TODO: Change this later, testing simple unwrap and moving uv
self.moveVertexUV(m, vertexIndex, vertex, mainBone)
# TODO: Remove this return
return
def moveVertexUV(self, meshName, vertexIndex, vertex, boneName):
#print ("Moving vertex")
print (boneName)
# Scale down UV to fit the division
uvScale = 0.03
newU = self.UV2Position(self.parseBoneName(boneName))
cmds.polyEditUV(vertex, u=newU, v=newU, scaleU = uvScale, scaleV = uvScale)
def parseBoneName(self, boneNameInput):
# removing uneeded string from boneName
boneNameInput = str(boneNameInput)
boneNameOutput = boneNameInput.split("'")[0]
return boneNameOutput
def UV2Position(self, boneName):
# Hardcode position here based on bone name. Use Dictionary?
# Magic number UV
boneUVDict = {
"pelvis": 1
}
try:
newU = (boneUVDict[boneName]) /32
except:
newU = 1
return newU
def getVertexIndex(self, vertexInput):
# Simple string operation to get vertex index
return int(vertexInput.split("[")[1].split("]")[0])
def clickedValidate(self):
print("--------- Validate")
self.extraMaterialCheck()
self.bindPoseCheck()
self.vertexInfluenceLimitCheck()
self.skinClusterCheck()
self.jointSkinClusterCheck()
# Enable export button
if len(self.errorArray) == 0:
self.btn_export.setEnabled(True)
self.btn_cleanUpMaterial.setEnabled(True)
def getSkinClustersPerJoint(self, joint_name):
skinClusterList = []
# Get all the deformers connected to the joint
deformers = cmds.listConnections(joint_name, type='skinCluster')
for d in deformers:
if d not in skinClusterList:
skinClusterList.append(d)
return skinClusterList
def get_skin_clusters_in_scene(self):
skin_clusters = []
all_meshes = cmds.ls(type='mesh') # Get a list of all mesh nodes in the scene
for mesh in all_meshes:
skin_cluster = cmds.ls(cmds.listHistory(mesh), type='skinCluster') # Find skin clusters in the history of the mesh
if skin_cluster:
skin_clusters.extend(skin_cluster)
return skin_clusters
def getSkinClusterFromJoints(self):
# Function to get list of skincluster in joints
jointName = self.rootName
allSkinClusters = cmds.ls(type='skinCluster')
skinClusters = []
for sc in allSkinClusters:
influences = cmds.skinCluster(sc, query=True, influence=True)
if jointName in influences or any(child in influences for child in cmds.listRelatives(jointName, children=True, type='joint')):
skinClusters.append(sc)
return skinClusters
def getMaxBoneValue(self):
maxBones = self.maxBoneSpinBox.value()
return maxBones
def getMeshesAndGroups(self):
meshes = cmds.listRelatives(cmds.ls(geometry=True), parent=True, fullPath=True) or []
allTransforms = cmds.ls(type='transform')
# Get transform with children and dont have a shape node as one of its children, that's "Group" in Maya
groups = [node for node in allTransforms if cmds.listRelatives(node, children=True) and not cmds.listRelatives(node, shapes=True)]
return groups, meshes
def checkTag(self):
print ("Check tag")
# Get all objects that's MeshPart, in this case have extra attributes with index 1
groups, meshes = self.getMeshesAndGroups()
# Check groups for tags
for g in groups:
try:
allAttr = cmds.listAttr(g, userDefined = True)
for a in allAttr:
if (a == self.extraAttributeName):
enumValues = cmds.getAttr(f"{g}.{a}")
if enumValues == 1 and (g not in self.exportArray):
self.exportArray.append(g)
except:
continue
# Now check meshes, TODO: this probably can be done in 1 go
for m in meshes:
try:
allAttr = cmds.listAttr(m, userDefined = True)
for x in allAttr:
if (x == self.extraAttributeName):
enumValues = cmds.getAttr(f"{m}.{x}")
if enumValues == 1 and (m not in self.exportArray):
self.exportArray.append(m)
except:
continue
# VALIDATION PART
def compareMaterial(self):
objectWithMat = set()
allMat = set(cmds.ls(mat=True))
for m in allMat:
connectedObjects = cmds.listConnections(m, type="shape")
if connectedObjects:
objectWithMat.update(connectedObjects)
unusedMaterial = allMat - objectWithMat
return (unusedMaterial)
def extraMaterialCheck(self):
self.compareMaterial()
print ("--------- Check extra material")
unusedMaterial = self.compareMaterial()
materialText = self.label_extraMaterial.text()
if (len(unusedMaterial) == 0) or (self.materialClean == True):
self.updateStatusGraphic(self.graphic_materialCheck, self.passString)
self.updateValidationResult(self.passString, self.materialTextBrowser)
else:
self.updateStatusGraphic(self.graphic_materialCheck, self.warningString)
self.updateValidationResult("Click CleanUp to remove unused materials", self.materialTextBrowser)
def updateStatusGraphic(self, graphic, status):
# Function to change color of the graphic
if status == "Pass":
color = QColor(0, 255, 0)
if status == "Warning":
color = QColor(255, 255, 0)
if status == "Fail":
color = QColor(255, 0, 0)
self.errorArray.append(graphic)
background_color = "background-color: rgba({},{},{},{})".format(
color.red(), color.green(), color.blue(), color.alphaF()
)
graphic.setStyleSheet(background_color)
def cleanUpMaterial(self):
print ("--------- Clean up material")
# TODO: Update the function subtract
#unusedMaterial = self.compareMaterial()
#for u in unusedMaterial:
# cmds.delete(u)
self.materialClean = True
mel.eval('MLdeleteUnused')
self.updateStatusGraphic(self.graphic_materialCheck, self.passString)
self.updateValidationResult(self.passString, self.materialTextBrowser)
def bindPoseCheck(self):
print("--------- Bind pose check")
# Get a list of all bind poses in the scene.
bindPoseArray = []
bind_poses = cmds.ls(type='dagPose')
skeleton_bind_poses = [pose for pose in bind_poses if self.rootName in cmds.dagPose(pose, query=True, members=True)]
bindPoseNumber = (len(skeleton_bind_poses))
# Check children bind pose
rootChildren = self.getRootChildren()
for r in rootChildren:
splitString = r.split('|')[-1]
childBindPoses = [pose for pose in bind_poses if splitString in cmds.dagPose(pose, query=True, members=True)]
if childBindPoses not in bindPoseArray:
bindPoseArray.append(childBindPoses)
if bindPoseNumber == 1 and (len(bindPoseArray) == 1):
self.updateStatusGraphic(self.graphic_bindPoseCheck, self.passString)
self.updateValidationResult(self.passString, self.bindPoseTextBrowser)
else:
self.updateStatusGraphic(self.graphic_bindPoseCheck, self.failString)
if bindPoseNumber > 1:
self.updateValidationResult("There are more than 1 bind pose", self.materialTextBrowser)
def getHierarchyRecursive(self, node):
children = cmds.listRelatives(node, children=True, fullPath=True) or []
result = []
for child in children:
result.append(child)
result.extend(self.getHierarchyRecursive(child))
return result
def getRootChildren(self):
rootChild = self.getHierarchyRecursive(self.rootName)
return rootChild
def checkBoneInfluence(self, mesh):
self.boneInfluenceLimit = self.getMaxBoneValue()
skin_Cluster = cmds.ls(cmds.listHistory(mesh), type='skinCluster')
if not skin_Cluster:
return
# Get all vertices
vertices = cmds.ls(mesh + ".vtx[*]", flatten=True)
for vertex in vertices:
influences = cmds.skinPercent(skin_Cluster[0], vertex, query=True, value=True)
# Check if vertex have more then bone influences limit
if len([weight for weight in influences if weight > 0]) > self.boneInfluenceLimit:
print ("Vertex {} in mesh {} has more than {} bone influences.".format(vertex, mesh, self.boneInfluenceLimit))
self.vertexWrongArray.append(vertex)
if mesh not in self.meshBoneInfluenceOverLimit:
self.meshBoneInfluenceOverLimit.append(mesh)
def vertexInfluenceLimitCheck(self):
print("--------- Vertex influence check")
allMeshes = cmds.ls(type='mesh')
for mesh in allMeshes:
self.checkBoneInfluence(mesh)
if len(self.vertexWrongArray) > 0:
print ("FAIL")
self.updateStatusGraphic(self.graphic_vertexInfluenceCheck, self.warningString)
self.updateValidationResult("These meshes have vertex that over the influence limit: \n\n" + str(self.meshBoneInfluenceOverLimit), self.vertexInfluenceTextBrowser)
else:
print ("SUCCESS")
self.updateStatusGraphic(self.graphic_vertexInfluenceCheck, self.passString)
self.updateValidationResult(self.passString, self.vertexInfluenceTextBrowser)
def hasSkinCluster(self, mesh):
skinClusters = cmds.listConnections(mesh, type='skinCluster')
return skinClusters is not None and len(skinClusters) > 0
def findMeshesWithNoSkinClusters(self):
allMeshes = cmds.ls(type='mesh')
meshesWithNoSkinClusterArray = []
for mesh in allMeshes:
if not self.hasSkinCluster(mesh) and not ("ShapeOrig" in mesh): # Ignore ShapeOrig
meshesWithNoSkinClusterArray.append(mesh)
return meshesWithNoSkinClusterArray
def skinClusterCheck(self):
# This is called skinnedMesh check in GUI instead of skin cluster
print ("--------- Skinned Mesh check")
noSkinCluster = self.findMeshesWithNoSkinClusters()
rootSkinCluster = self.getSkinClusterFromJoints() # Get all skin clusters in root
meshesSkinCluster = self.get_skin_clusters_in_scene() # Get meshes skincluster
if not noSkinCluster:
print ("All meshes hace skin cluster")
self.updateStatusGraphic(self.graphic_skinClusterCheck, self.passString)
else:
errorMessage = ("There are meshes with no skin cluster: \n\n" + str(noSkinCluster))
print (errorMessage)
self.updateStatusGraphic(self.graphic_skinClusterCheck, self.warningString)
for m in noSkinCluster:
print (m)
self.updateValidationResult(errorMessage, self.skinClusterTextBrowser)
if not(set(meshesSkinCluster) == set(rootSkinCluster)):
self.updateStatusGraphic(self.graphic_skinClusterCheck, self.failString)
diff = set(meshesSkinCluster).symmetric_difference(set(rootSkinCluster))
print ("There are skin cluster difference between meshes and joints")
print (diff)
self.updateValidationResult("There are skin cluster difference between meshes and joints", self.skinClusterTextBrowser)
def jointSkinClusterCheck(self):
meshesSkinCluster = self.get_skin_clusters_in_scene() # Get meshes skincluster
rootJoint = None
# Get root joint
allJoints = cmds.ls(type="joint")
# Find the joint with no parent (the root joint)
for joint in allJoints:
if not cmds.listRelatives(joint, parent=True):
rootJoint = joint
self.recursiveJointCheck(rootJoint, meshesSkinCluster)
if len(self.jointSCWrongArray) > 0:
self.updateStatusGraphic(self.graphic_skinCluster02Check, self.failString)
self.updateValidationResult(self.jointSCWrongArray, self.skinCluster02TextBrowser)
else:
self.updateStatusGraphic(self.graphic_skinCluster02Check, self.passString)
self.updateValidationResult(self.passString, self.skinCluster02TextBrowser)
def recursiveJointCheck(self, joint, meshesSC):
children = cmds.listRelatives(joint, children=True, type="joint") or []
for child in children:
if child:
jointSCList = self.getSkinClustersPerJoint(child)
# Compare the list
if not(set(jointSCList) == set(meshesSC)):
self.jointSCWrongArray.append(child)
# Continue down hierarchy
self.recursiveJointCheck(child, meshesSC)
def updateValidationResult(self, text, textBrowser):
textBrowser.setPlainText(text)
def getCharacterName(self):
characterName = self.mayaFilePath.split("/")[-1]
characterName = characterName.split(".")[0] # Remove '.fbx'
characterName = characterName.split("_")[1]
return characterName
def getExportedObjects(self, meshPart):
cmds.select(clear=True)
objectToExport = ["root"]
objectToExport.append(meshPart)
# Select the relevant object and 'Root'
for o in objectToExport:
if cmds.objExists(o):
cmds.select(o, add=True)
return objectToExport
def fbxExportOptions(self):
fbxOptions = {
's': True, # Selection Only
'es': True, # Embed Textures
'force': True, # Overwrite if file exists
'ems': True, # Export Edges
'wn': True, # World Normal
'et': True, # Tangents and Binormals
'b': False, # Smoothing Groups
'm': True, # Smooth Mesh
'l': False, # Light
'd': False, # Cameras
'animation': False, # Animation
}
return fbxOptions
def getParentGroup(self,obj):
parentGroup = cmds.ls(obj, long=True)[0].split("|")[1:-1]
parentGroup.reverse()
return (parentGroup[0]).split("_")[0] if parentGroup else None
def unhideAllMeshes(self):
allObjects = cmds.ls()
for obj in allObjects:
try:
cmds.setAttr(obj + ".visibility", True)
except:
continue
def exportMeshParts(self, GLBExport, ThumbnailExport):
print ("EXPORTING")
self.exportTextBrowser.clear()
fbxFolder = "/FBX"
glbFolder = "/GLB"
bodyPartTag = ""
# Get character name
char = self.getCharacterName()
fileDirectory = os.path.dirname(self.mayaFilePath)
fileDirectory = os.path.dirname(fileDirectory) # Up one folder
if GLBExport == False:
fileDirectory += fbxFolder
else:
fileDirectory += glbFolder
# Get mesh part folder from tag in the mesh
# Export path
exportOptions = self.fbxExportOptions()
fileNames = ""
for meshPart in self.exportArray:
currentExportArray = []
try:
parentGroup = self.getParentGroup(meshPart)
bodyPartTag = self.getBodyPartTag(meshPart)
if ThumbnailExport == False:
currentExportArray = self.getExportedObjects(meshPart)
if ThumbnailExport == True:
cmds.select(clear=True)
meshPart = meshPart.split("|")[-1]
currentExportArray.append(meshPart)
for o in currentExportArray:
print (meshPart)
print (o)
if cmds.objExists(o):
cmds.select(o, add=True)
print (currentExportArray)
filePath = fileName = fileDirectory + "/" + bodyPartTag
# Create export folder if not exist
if not os.path.exists(filePath):
os.makedirs(filePath)
fileName = fileDirectory + "/" + bodyPartTag +"/" + self.uassetPrefix + char + "_"
if ThumbnailExport == False:
# Concatenate 2nd value, because the first 1 is always 'Root'
#currentFileName = fileName + (exportArray[1].split("|")[-1])
# TODO: Sort this parent group naming
#currentFileName = fileName + parentGroup + "_" + (exportArray[1].split("|")[-1])
# TODO: Removed parent group name from Alfonso's request
currentFileName = fileName + (currentExportArray[1].split("|")[-1])
if ThumbnailExport == True:
currentFileName = fileName + (currentExportArray[0].split("|")[-1])
fileNames += ("Export success on: " + (currentFileName + "\n" + "\n"))
# Export and parse export options
# FBX EXPORT
if ThumbnailExport == False:
if GLBExport == False:
cmds.file(currentFileName, force=True, options="".join(f"{key}={value}" for key, value in exportOptions.items()), type="FBX export", pr=True, es=True)
# GLB Export
if GLBExport == True:
mel.eval('source "{}"; MeshExport "{}";'.format(self.melScriptPath, currentFileName))
if ThumbnailExport == True:
print ("Do Batch Render")
self.doRender()
except:
# Display error when fail export
failString = "Export fail on: " + str(meshPart)
fileNames += failString
# Update text browser
self.updateValidationResult(fileNames, self.exportTextBrowser)
def getBodyPartTag(self, bodyPart):
# TODO: GET VALUE OF BODY PART HERE, THIS IS NEEDED FOR SETTING EXPORT FOLDER OF MESH/BODY PART
allAttr = cmds.listAttr(bodyPart, userDefined = True)
for a in allAttr:
if (a == self.meshPartTagAttributeName):
enumValues = cmds.getAttr(f"{bodyPart}.{a}")
bodyPartTag = self.meshPartArray[enumValues]
return bodyPartTag
def updateBodyPartTags(self):
allTagsFromQT = self.comboBox_meshPartTags
neededTags = self.makeMeshPartEnumArray()
enumOptions = ':'.join(neededTags)
currentTagInQT = allTagsFromQT.currentIndex()
# Get all object with extra attributes BodyPart
# Compare the member of body part enum in object to the one in code
# If different, update
self.checkTag()
for obj in self.exportArray:
allAttr = cmds.listAttr(obj, userDefined = True)
for a in allAttr:
if (a == self.meshPartTagAttributeName):
enumValues = cmds.getAttr(f"{obj}.{a}")
enumMembers = cmds.attributeQuery(a, node=obj, listEnum=True)[0].split(':')
currentBodyPartTag = (enumMembers[enumValues])
newIndex = (self.meshPartArray.index(currentBodyPartTag))
self.updateArrayWithOrder(self.meshPartArray, enumMembers)
self.reorderArray(self.meshPartArray, enumMembers)
# Delete body part attibute
cmds.deleteAttr(obj + '.' + a)
# Add custom attribute
if not (cmds.attributeQuery(self.meshPartTagAttributeName, node=obj, exists=True)):
cmds.addAttr(obj, longName=self.meshPartTagAttributeName, attributeType='enum', enumName = enumOptions)
# Set attribute default value
cmds.setAttr('{}.{}'.format(obj, self.meshPartTagAttributeName), newIndex)
def updateArrayWithOrder(self, meshPartArray, objectTagArray):
objectTagArraySet = set (objectTagArray)
for item in meshPartArray:
if item not in objectTagArraySet:
objectTagArray.append(item)
def reorderArray(self, arr1, arr2):
indexMap = {elem: index for index, elem in enumerate(arr1)}
arr2.sort(key=lambda x:indexMap.get(x, len(arr1)))
# EXPORT PART
def exportMeshes(self):
print ("Export Meshes")
self.unhideAllMeshes()
# Get all MeshPart objects
self.checkTag()
exportGLB = False
# Export all MeshPart
self.exportMeshParts(exportGLB, False)
# TAGS PART
def makeEnumArray(self):
tagsEnumArray = []
tagsEnumArray.append(self.tagNonExport)
tagsEnumArray.append(self.tagMeshPart)
return tagsEnumArray
def makeMeshPartEnumArray(self):
meshPartEnumArray = []
for i in self.meshPartArray:
meshPartEnumArray.append(i)
return meshPartEnumArray
def addTag(self):
print ("Add tag")
allTagsFromQT = self.comboBox_Tags
currentTagInQT = allTagsFromQT.currentIndex()
selectedObject = cmds.ls(selection=True)
neededTags = self.makeEnumArray()
enumOptions = ':'.join(neededTags)
# Add custom attribute
for obj in selectedObject:
if not (cmds.attributeQuery(self.extraAttributeName, node=obj, exists=True)):
cmds.addAttr(obj, longName=self.extraAttributeName, attributeType='enum', enumName = enumOptions)
# Set attribute default value
cmds.setAttr('{}.{}'.format(obj, self.extraAttributeName), currentTagInQT)
def deleteTags(self):
print ("Delete tags")
selectedObject = cmds.ls(selection=True)
for o in selectedObject:
customAttributes = cmds.listAttr(o, userDefined=True)
try:
for attr in customAttributes:
# Delete the custom attribute
cmds.deleteAttr(o, at=attr)
except:
print ("no attribute")
def getCurrentFile(self):
currentFile = cmds.file(q=True, sceneName=True)
noExt, _ = os.path.splitext(currentFile)
return noExt
# GLTF EXPORT RELATED
def convertMaterialToaiStandardSurface(self, shadeEng, materialInput, obj, matNumber):
if cmds.nodeType(materialInput) != 'aiStandardSurface':
aiMaterial = cmds.shadingNode('aiStandardSurface', asShader=True, name='{}_aiStandardSurface'.format(materialInput))
# Transfer attributes from existing material to new aiStandardSurface material
self.transferAttributes(materialInput, aiMaterial)
# Transfer textures from existing material to new aiStandardSurface material
self.transferTextures(materialInput, aiMaterial)
# Assign aiMAterial
if matNumber == 1:
self.assignaiMaterial(obj, aiMaterial, materialInput)
elif matNumber > 1:
self.replaceMaterial(obj, shadeEng, materialInput, aiMaterial)
# Find normal and ARMS texture
normalTexture, armsTexture, difuseTexture = self.getNormalAndARMS(aiMaterial)
# Swap textures to PNG if not already using one
normalTexture, armsTexture, difuseTexture = self.swapTextureToPNG(normalTexture, armsTexture, difuseTexture)
# Assign normal and ARMS texture to aiMaterial
self.assignNormalARMSTexture(normalTexture, armsTexture, aiMaterial, difuseTexture)
else:
print ("Object already using aiMat")
def assignIndividualTextures(self,filePath, attribute, aiMaterial):
if os.path.isfile(filePath):
fileNode = cmds.shadingNode("file", asTexture=True, name=f"{attribute}MapInput")
cmds.setAttr(f"{fileNode}.fileTextureName", filePath, type='string')
cmds.connectAttr(f"{fileNode}.outColor", f"{aiMaterial}.{attribute}")
def assignNormalARMSTexture(self, normal, arms, aiMaterial, difuse):
# Assign Normal
self.assignIndividualTextures(normal, "normalCamera", aiMaterial)
# Assign ARMS
if os.path.isfile(arms):
# Make file node to load the texture
armsFileNode = cmds.shadingNode("file", asTexture=True, name="armsMapInput")
# Set the file texture name
cmds.setAttr(f"{armsFileNode}.fileTextureName", arms, type="string")
# Connect G to roughness, B to metalness
cmds.connectAttr(f"{armsFileNode}.outColorG", f"{aiMaterial}.specularRoughness")
cmds.connectAttr(f"{armsFileNode}.outColorB", f"{aiMaterial}.metalness")
# Assign Difuse
# Disconnect existing baseColor first
if os.path.isfile(difuse):
connections = cmds.listConnections(aiMaterial + ".baseColor", source=True, destination=False, plugs=True)
if connections:
# Disconnect each connection
for connection in connections:
cmds.disconnectAttr(connection, aiMaterial + ".baseColor")
self.assignIndividualTextures(difuse, "baseColor", aiMaterial)
def getNormalAndARMS(self, materialInput):
existingTexture = self.getTexturesFromAiMaterial(materialInput)
normal, arms, difuse = self.getImageName(existingTexture[0])
return normal, arms, difuse
def getTexturesFromAiMaterial(self, aiMaterialNode):
textures = []
connections = cmds.listConnections(aiMaterialNode, source=True, destination=False, plugs=True)
if connections:
for connection in connections:
if cmds.nodeType(connection) == "file":
textureNode = connection
textures.append(textureNode)
return textures
def getImageName(self, textureNode):
textureNode = (textureNode.split('.outColor')[0])
fileTexturePath = cmds.getAttr(textureNode + ".fileTextureName")
textureFolder = os.path.dirname(fileTexturePath)
textureName = os.path.splitext(os.path.basename(fileTexturePath))[0]
# define suffixes
suffixes = {
'Normal': self.normalSuffix,
'Arms': self.ARMSSuffix,
'Difuse': self.difuseSuffix
}
# Generate texture paths
texturePaths = {}
for key, suffix in suffixes.items():
modifiedTextureName = ('_'.join(textureName.split('_')[:-1])) + suffix
texturePaths[key] = os.path.join(textureFolder, modifiedTextureName + self.textureFormat)
return texturePaths['Normal'], texturePaths['Arms'], texturePaths['Difuse']
def swapTextureToPNG(self, *textures):
newTextures = []
for texture in textures:
if texture.lower().endswith('.tga'):
pngTexture = os.path.splitext(texture)[0] + '.png'
if os.path.exists(pngTexture):
texture = pngTexture
newTextures.append(texture)
return tuple(newTextures)
def replaceMaterial(self, obj, shadingEngine, materialInput, aiMaterial):
for se in shadingEngine:
materials = cmds.ls(cmds.listConnections(se), materials=True)
if materialInput in materials:
cmds.disconnectAttr('%s.outColor' % materialInput, '%s.surfaceShader' % se)
# Connect new mat
cmds.connectAttr('%s.outColor' % aiMaterial, '%s.surfaceShader' % se, force=True)
def assignaiMaterial(self, obj, aiMaterial, materialInput):
cmds.select(obj)
cmds.hyperShade(assign=aiMaterial)
# Delete old material
cmds.delete(materialInput)
def transferAttributes(self, sourceMaterial, destinationMaterial):
attributes = cmds.listAttr(sourceMaterial, userDefined=True, scalar=True)
if attributes:
for attr in attributes:
value = cmds.getAttr(sourceMaterial + '.' + attr)
cmds.setAttr(destinationMaterial + '.' + attr, value)
def transferTextures(self, sourceMaterial, destinationMaterial):
fileNodes = cmds.ls(cmds.listHistory(sourceMaterial), type='file')
if fileNodes:
for fileNode in fileNodes:
texturePath = cmds.getAttr(fileNode + '.fileTextureName')
newFileNode = cmds.shadingNode('file', asTexture=True)
cmds.setAttr(newFileNode + '.fileTextureName', texturePath, type="string")
cmds.connectAttr(newFileNode + '.outColor', destinationMaterial + '.baseColor', force=True)
def skeletalMeshGLBExport(self):
print ("Skeletal mesh export")
currentFile = self.getCurrentFile()
self.checkTag()
exportGLB = True
# Activate export button
mel.eval('source "{}"; MeshExport "{}";'.format(self.melScriptPath, currentFile))
def variantSkeletalMeshGLBExport(self):
textureColourArray = [
'Red',
'Brown',
'Gray',
'Purple',
'Black',
'White',
'Pink',
'Green',
'Cosmic',
'Enlightened',
'Glitch',
'Jade'
]
print ("Variant skeletal mesh export")
# Get the material of selected object
selectedObject = cmds.ls(selection=True)
bodyName = (selectedObject[0].split("_")[1])
theNodes = cmds.ls(sl = True, dag = True, s = True)
shadeEng = cmds.listConnections(theNodes, type = "shadingEngine")
materials = cmds.ls(cmds.listConnections(shadeEng), materials = True)
header = ''
for material in materials:
textureNode = cmds.listConnections(material, type='file')[0]
# Get the filename from the texture node
currentTextureFilename = cmds.getAttr("%s.fileTextureName" % textureNode)
#currentTextureFilename = currentTextureFilename.lower()
if 'png' in currentTextureFilename:
header = (currentTextureFilename.split('_color.png')[0])
header = "_".join(header.split("_")[:-1])
if 'tga' in currentTextureFilename:
header = (currentTextureFilename.split('_color.tga')[0])
header = "_".join(header.split("_")[:-1])
texturesFolder = ("/".join(header.split("/")[:-1])) + "/"
# TODO: Get all path of textures
for texture in textureColourArray:
try:
# GLB EXPORT STUFF
currentFile = "".join((self.getCurrentFile()).split('_01')[:-1])
currentFileFolder = "".join((self.getCurrentFile()).split('MA')[:-1])
currentFileName = os.path.basename(self.getCurrentFile())
needForTexture = currentFile
GLBFolder = currentFileFolder + 'GLB/MainBody/'
currentFile = GLBFolder + currentFileName + '_' + bodyName
currentFile = currentFile + '_' + texture + '_01'
print (currentFile)
# TEXTURES STUFF
#newTexturePath = texturesFolder + 'MoonBirds_Body_' + bodyName + '_' + texture + '_color.png'
newTexturePath = "/".join(needForTexture.split('/')[:-3]) + '/Textures/MoonBirds_Body/MoonBirds_Body_'
newTexturePath = newTexturePath + bodyName + '_' + texture + '_color.png'
if not os.path.exists(newTexturePath):
print ("NOT EXIST")
print (newTexturePath)
# Update path with new path
cmds.setAttr("%s.fileTextureName" % textureNode, newTexturePath, type="string")
# Do export with Babylon Exporter
self.checkTag()
mel.eval('source "{}"; MeshExport "{}";'.format(self.melScriptPath, currentFile))
except:
continue
def batchGLBExportWithTags(self):
self.unhideAllMeshes()
currentFile = self.getCurrentFile()
self.checkTag()
exportGLB = True
# Export all MeshPart
self.exportMeshParts(exportGLB, False)
def animationGLBExport(self):
print ("Animation export")
currentFile = self.getCurrentFile()
# Activate export button
animName = self.text_animationName.toPlainText()
currentFile = (currentFile + '_' + animName)
mel.eval('source "{}"; AnimationExport "{}";'.format(self.animationMelScriptPath, currentFile))
# GENERIC FUNCTIONALITY
def resizeEvent(self, event):
# Called on automatically generated resize event
self.widget.resize(self.width(), self.height())
def closeWindow(self):
# Close window
print ('closing window')
self.removeSelectionCallback()
self.destroy()
def openWindow():
"""
ID Maya and attach tool window.
"""
# Maya uses this so it should always return True
if QtWidgets.QApplication.instance():
# Id any current instances of tool and destroy
for win in (QtWidgets.QApplication.allWindows()):
if 'RigExporterWindow' in win.objectName(): # update this name to match name below
win.destroy()
#QtWidgets.QApplication(sys.argv)
mayaMainWindowPtr = omui.MQtUtil.mainWindow()
mayaMainWindow = wrapInstance(int(mayaMainWindowPtr), QtWidgets.QWidget)
RigExport.window = RigExport(parent = mayaMainWindow)
RigExport.window.setObjectName('RigExporterWindow') # code above uses this to ID any existing windows
RigExport.window.setWindowTitle('Rig Exporter')
RigExport.window.show()
openWindow()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment