├── Qtpy ├── LICENSE ├── Qt.py ├── Qt.pyc ├── __init__.py └── __init__.pyc ├── README.md ├── __init__.py ├── license.txt ├── uExport.py └── uExport.ui /Qtpy/LICENSE: -------------------------------------------------------------------------------- 1 | Qt.py | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Marcus Ottosson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Qtpy/Qt.py: -------------------------------------------------------------------------------- 1 | """Map all bindings to PySide2 2 | 3 | This module replaces itself with the most desirable binding. 4 | 5 | Project goals: 6 | Qt.py was born in the film and visual effects industry to address 7 | the growing need for the development of software capable of running 8 | with more than one flavour of the Qt bindings for Python - PySide, 9 | PySide2, PyQt4 and PyQt5. 10 | 11 | 1. Build for one, run with all 12 | 2. Explicit is better than implicit 13 | 3. Support co-existence 14 | 15 | Default resolution order: 16 | - PySide2 17 | - PyQt5 18 | - PySide 19 | - PyQt4 20 | 21 | Usage: 22 | >> import sys 23 | >> from Qt import QtWidgets 24 | >> app = QtWidgets.QApplication(sys.argv) 25 | >> button = QtWidgets.QPushButton("Hello World") 26 | >> button.show() 27 | >> app.exec_() 28 | 29 | """ 30 | 31 | import os 32 | import sys 33 | import shutil 34 | 35 | self = sys.modules[__name__] 36 | 37 | self.__version__ = "0.6.1" 38 | 39 | self.__added__ = list() # All unique members of Qt.py 40 | self.__remapped__ = list() # Members copied from elsewhere 41 | self.__modified__ = list() # Existing members modified in some way 42 | 43 | # Below members are set dynamically on import relative the original binding. 44 | self.__qt_version__ = "0.0.0" 45 | self.__binding__ = "None" 46 | self.__binding_version__ = "0.0.0" 47 | self.load_ui = lambda fname: None 48 | self.translate = lambda context, sourceText, disambiguation, n: None 49 | self.setSectionResizeMode = lambda *args, **kwargs: None 50 | 51 | 52 | def convert(lines): 53 | """Convert compiled .ui file from PySide2 to Qt.py 54 | 55 | Arguments: 56 | lines (list): Each line of of .ui file 57 | 58 | Usage: 59 | >> with open("myui.py") as f: 60 | .. lines = convert(f.readlines()) 61 | 62 | """ 63 | 64 | def parse(line): 65 | line = line.replace("from PySide2 import", "from Qt import") 66 | line = line.replace("QtWidgets.QApplication.translate", 67 | "Qt.QtCompat.translate") 68 | return line 69 | 70 | parsed = list() 71 | for line in lines: 72 | line = parse(line) 73 | parsed.append(line) 74 | 75 | return parsed 76 | 77 | 78 | def _remap(object, name, value, safe=True): 79 | """Prevent accidental assignment of existing members 80 | 81 | Arguments: 82 | object (object): Parent of new attribute 83 | name (str): Name of new attribute 84 | value (object): Value of new attribute 85 | safe (bool): Whether or not to guarantee that 86 | the new attribute was not overwritten. 87 | Can be set to False under condition that 88 | it is superseded by extensive testing. 89 | 90 | """ 91 | 92 | if os.getenv("QT_TESTING") is not None and safe: 93 | # Cannot alter original binding. 94 | if hasattr(object, name): 95 | raise AttributeError("Cannot override existing name: " 96 | "%s.%s" % (object.__name__, name)) 97 | 98 | # Cannot alter classes of functions 99 | if type(object).__name__ != "module": 100 | raise AttributeError("%s != 'module': Cannot alter " 101 | "anything but modules" % object) 102 | 103 | elif hasattr(object, name): 104 | # Keep track of modifications 105 | self.__modified__.append(name) 106 | 107 | self.__remapped__.append(name) 108 | 109 | setattr(object, name, value) 110 | 111 | 112 | def _add(object, name, value): 113 | """Append to self, accessible via Qt.QtCompat""" 114 | self.__added__.append(name) 115 | setattr(self, name, value) 116 | 117 | 118 | def _pyqt5(): 119 | import PyQt5.Qt 120 | from PyQt5 import QtCore, QtWidgets, uic 121 | 122 | _remap(QtCore, "Signal", QtCore.pyqtSignal) 123 | _remap(QtCore, "Slot", QtCore.pyqtSlot) 124 | _remap(QtCore, "Property", QtCore.pyqtProperty) 125 | 126 | _add(PyQt5, "__binding__", PyQt5.__name__) 127 | _add(PyQt5, "load_ui", lambda fname: uic.loadUi(fname)) 128 | _add(PyQt5, "translate", lambda context, sourceText, disambiguation, n: ( 129 | QtCore.QCoreApplication(context, sourceText, 130 | disambiguation, n))) 131 | _add(PyQt5, 132 | "setSectionResizeMode", 133 | QtWidgets.QHeaderView.setSectionResizeMode) 134 | 135 | _maintain_backwards_compatibility(PyQt5) 136 | 137 | return PyQt5 138 | 139 | 140 | def _pyqt4(): 141 | # Attempt to set sip API v2 (must be done prior to importing PyQt4) 142 | import sip 143 | try: 144 | sip.setapi("QString", 2) 145 | sip.setapi("QVariant", 2) 146 | sip.setapi("QDate", 2) 147 | sip.setapi("QDateTime", 2) 148 | sip.setapi("QTextStream", 2) 149 | sip.setapi("QTime", 2) 150 | sip.setapi("QUrl", 2) 151 | except AttributeError: 152 | raise ImportError 153 | # PyQt4 < v4.6 154 | except ValueError: 155 | # API version already set to v1 156 | raise ImportError 157 | 158 | import PyQt4.Qt 159 | from PyQt4 import QtCore, QtGui, uic 160 | 161 | _remap(PyQt4, "QtWidgets", QtGui) 162 | _remap(QtCore, "Signal", QtCore.pyqtSignal) 163 | _remap(QtCore, "Slot", QtCore.pyqtSlot) 164 | _remap(QtCore, "Property", QtCore.pyqtProperty) 165 | _remap(QtCore, "QItemSelection", QtGui.QItemSelection) 166 | _remap(QtCore, "QStringListModel", QtGui.QStringListModel) 167 | _remap(QtCore, "QItemSelectionModel", QtGui.QItemSelectionModel) 168 | _remap(QtCore, "QSortFilterProxyModel", QtGui.QSortFilterProxyModel) 169 | _remap(QtCore, "QAbstractProxyModel", QtGui.QAbstractProxyModel) 170 | 171 | try: 172 | from PyQt4 import QtWebKit 173 | _remap(PyQt4, "QtWebKitWidgets", QtWebKit) 174 | except ImportError: 175 | # QtWebkit is optional in Qt , therefore might not be available 176 | pass 177 | 178 | _add(PyQt4, "QtCompat", self) 179 | _add(PyQt4, "__binding__", PyQt4.__name__) 180 | _add(PyQt4, "load_ui", lambda fname: uic.loadUi(fname)) 181 | _add(PyQt4, "translate", lambda context, sourceText, disambiguation, n: ( 182 | QtCore.QCoreApplication(context, sourceText, 183 | disambiguation, None, n))) 184 | _add(PyQt4, "setSectionResizeMode", QtGui.QHeaderView.setResizeMode) 185 | 186 | _maintain_backwards_compatibility(PyQt4) 187 | 188 | return PyQt4 189 | 190 | 191 | def _pyside2(): 192 | import PySide2 193 | from PySide2 import QtGui, QtWidgets, QtCore, QtUiTools 194 | 195 | _remap(QtCore, "QStringListModel", QtGui.QStringListModel) 196 | 197 | _add(PySide2, "__binding__", PySide2.__name__) 198 | _add(PySide2, "load_ui", lambda fname: QtUiTools.QUiLoader().load(fname)) 199 | _add(PySide2, "translate", lambda context, sourceText, disambiguation, n: ( 200 | QtCore.QCoreApplication(context, sourceText, 201 | disambiguation, None, n))) 202 | _add(PySide2, 203 | "setSectionResizeMode", 204 | QtWidgets.QHeaderView.setSectionResizeMode) 205 | 206 | _maintain_backwards_compatibility(PySide2) 207 | 208 | return PySide2 209 | 210 | 211 | def _pyside(): 212 | import PySide 213 | from PySide import QtGui, QtCore, QtUiTools 214 | 215 | _remap(PySide, "QtWidgets", QtGui) 216 | _remap(QtCore, "QSortFilterProxyModel", QtGui.QSortFilterProxyModel) 217 | _remap(QtCore, "QStringListModel", QtGui.QStringListModel) 218 | _remap(QtCore, "QItemSelection", QtGui.QItemSelection) 219 | _remap(QtCore, "QItemSelectionModel", QtGui.QItemSelectionModel) 220 | _remap(QtCore, "QAbstractProxyModel", QtGui.QAbstractProxyModel) 221 | 222 | try: 223 | from PySide import QtWebKit 224 | _remap(PySide, "QtWebKitWidgets", QtWebKit) 225 | except ImportError: 226 | # QtWebkit is optional in Qt, therefore might not be available 227 | pass 228 | 229 | _add(PySide, "__binding__", PySide.__name__) 230 | _add(PySide, "load_ui", lambda fname: QtUiTools.QUiLoader().load(fname)) 231 | _add(PySide, "translate", lambda context, sourceText, disambiguation, n: ( 232 | QtCore.QCoreApplication(context, sourceText, 233 | disambiguation, None, n))) 234 | _add(PySide, "setSectionResizeMode", QtGui.QHeaderView.setResizeMode) 235 | 236 | _maintain_backwards_compatibility(PySide) 237 | 238 | return PySide 239 | 240 | 241 | def _log(text, verbose): 242 | if verbose: 243 | sys.stdout.write(text + "\n") 244 | 245 | 246 | def cli(args): 247 | """Qt.py command-line interface""" 248 | import argparse 249 | 250 | parser = argparse.ArgumentParser() 251 | parser.add_argument("--convert", 252 | help="Path to compiled Python module, e.g. my_ui.py") 253 | parser.add_argument("--compile", 254 | help="Accept raw .ui file and compile with native " 255 | "PySide2 compiler.") 256 | parser.add_argument("--stdout", 257 | help="Write to stdout instead of file", 258 | action="store_true") 259 | parser.add_argument("--stdin", 260 | help="Read from stdin instead of file", 261 | action="store_true") 262 | 263 | args = parser.parse_args(args) 264 | 265 | if args.stdout: 266 | raise NotImplementedError("--stdout") 267 | 268 | if args.stdin: 269 | raise NotImplementedError("--stdin") 270 | 271 | if args.compile: 272 | raise NotImplementedError("--compile") 273 | 274 | if args.convert: 275 | sys.stdout.write("#\n" 276 | "# WARNING: --convert is an ALPHA feature.\n#\n" 277 | "# See https://github.com/mottosso/Qt.py/pull/132\n" 278 | "# for details.\n" 279 | "#\n") 280 | 281 | # 282 | # ------> Read 283 | # 284 | with open(args.convert) as f: 285 | lines = convert(f.readlines()) 286 | 287 | backup = "%s_backup%s" % os.path.splitext(args.convert) 288 | sys.stdout.write("Creating \"%s\"..\n" % backup) 289 | shutil.copy(args.convert, backup) 290 | 291 | # 292 | # <------ Write 293 | # 294 | with open(args.convert, "w") as f: 295 | f.write("".join(lines)) 296 | 297 | sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) 298 | 299 | 300 | def init(): 301 | """Try loading each binding in turn 302 | 303 | Please note: the entire Qt module is replaced with this code: 304 | sys.modules["Qt"] = binding() 305 | 306 | This means no functions or variables can be called after 307 | this has executed. 308 | 309 | For debugging and testing, this module may be accessed 310 | through `Qt.__shim__`. 311 | 312 | """ 313 | 314 | preferred = os.getenv("QT_PREFERRED_BINDING") 315 | verbose = os.getenv("QT_VERBOSE") is not None 316 | bindings = (_pyside2, _pyqt5, _pyside, _pyqt4) 317 | 318 | if preferred: 319 | # Internal flag (used in installer) 320 | if preferred == "None": 321 | self.__wrapper_version__ = self.__version__ 322 | return 323 | 324 | preferred = preferred.split(os.pathsep) 325 | available = { 326 | "PySide2": _pyside2, 327 | "PyQt5": _pyqt5, 328 | "PySide": _pyside, 329 | "PyQt4": _pyqt4 330 | } 331 | 332 | try: 333 | bindings = [available[binding] for binding in preferred] 334 | except KeyError: 335 | raise ImportError( 336 | "Available preferred Qt bindings: " 337 | "\n".join(preferred) 338 | ) 339 | 340 | for binding in bindings: 341 | _log("Trying %s" % binding.__name__, verbose) 342 | 343 | try: 344 | binding = binding() 345 | 346 | except ImportError as e: 347 | _log(" - ImportError(\"%s\")" % e, verbose) 348 | continue 349 | 350 | else: 351 | # Reference to this module 352 | binding.__shim__ = self 353 | binding.QtCompat = self 354 | 355 | sys.modules.update({ 356 | __name__: binding, 357 | 358 | # Fix #133, `from Qt.QtWidgets import QPushButton` 359 | __name__ + ".QtWidgets": binding.QtWidgets 360 | 361 | }) 362 | 363 | return 364 | 365 | # If not binding were found, throw this error 366 | raise ImportError("No Qt binding were found.") 367 | 368 | 369 | def _maintain_backwards_compatibility(binding): 370 | """Add members found in prior versions up till the next major release 371 | 372 | These members are to be considered deprecated. When a new major 373 | release is made, these members are removed. 374 | 375 | """ 376 | 377 | for member in ("__binding__", 378 | "__binding_version__", 379 | "__qt_version__", 380 | "__added__", 381 | "__remapped__", 382 | "__modified__", 383 | "convert", 384 | "load_ui", 385 | "translate"): 386 | setattr(binding, member, getattr(self, member)) 387 | self.__added__.append(member) 388 | 389 | setattr(binding, "__wrapper_version__", self.__version__) 390 | self.__added__.append("__wrapper_version__") 391 | 392 | 393 | cli(sys.argv[1:]) if __name__ == "__main__" else init() 394 | -------------------------------------------------------------------------------- /Qtpy/Qt.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisevans3d/uExport/ae4ecb8a13864e621d90fa779199cd1bcd089a3f/Qtpy/Qt.pyc -------------------------------------------------------------------------------- /Qtpy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisevans3d/uExport/ae4ecb8a13864e621d90fa779199cd1bcd089a3f/Qtpy/__init__.py -------------------------------------------------------------------------------- /Qtpy/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisevans3d/uExport/ae4ecb8a13864e621d90fa779199cd1bcd089a3f/Qtpy/__init__.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | uExport 2 | ============ 3 | 4 | Overview 5 | --------------- 6 | uExport is a simple tool to automatically export complex characters from Maya to Unreal Engine 4. It works by marking up the scene with metadata related to what rendermeshes and skeletons compose a 'character'. 7 | 8 | When scenes are saved with this markup, you can open any Maya scene and automatically export exactly what content is supposed to be going to UE4, even in a batch process. 9 | 10 | Here's a simple test where one uExport node has been created; which is exposed to the user like so: 11 | ![alt tag](http://chrisevans3d.com/files/github/uExport_simple.gif) 12 | 13 | __Features:__ 14 | * Serialize export info into Maya scene 15 | * exported path 16 | * user friendly asset name 17 | * fbx name 18 | * fbx export settings 19 | * export skeleton 20 | * rendermeshes 21 | * LODs 22 | * blendshapes 23 | * UI to create and edit export markup 24 | * Fire arbitrary python scripts assoc with any export (useful for LODs) 25 | * Simple sanity checking 26 | * No skeleton set 27 | * Meshes not skinned 28 | 29 | Under the Hood 30 | --------------- 31 | Under the hood, this data is serialized to disk using this network node: 32 | ![alt tag](http://chrisevans3d.com/files/github/uNode.PNG) 33 | 34 | Advanced Usage 35 | --------------- 36 | You can have multiple uExport nodes in one scene, not only to represent each character, but even in one export file when breaking up characters into multiple exported FBX files on disk: 37 | ![alt tag](http://chrisevans3d.com/files/github/uexport01.png) 38 | 39 | __Batching__
40 | If you are using uExport to markup your characters, there are sample server tasks in the [mayaTaskServer repo](https://github.com/chrisevans3d/mayaTaskServer) to batch exports and animations. 41 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from uExport import * -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Christopher Evans chris.evans@gmail.com 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /uExport.py: -------------------------------------------------------------------------------- 1 | ''' 2 | uExport 3 | Christopher Evans, Version 0.1, Feb 2014 4 | @author = Chris Evans 5 | version = 0.85 6 | 7 | Add this to a shelf: 8 | import uExport as ue 9 | uExportToolWindow = ue.show() 10 | 11 | ''' 12 | 13 | import os 14 | import re 15 | import time 16 | import stat 17 | import types 18 | 19 | from cStringIO import StringIO 20 | import xml.etree.ElementTree as xml 21 | import json 22 | import functools 23 | 24 | import maya.cmds as cmds 25 | import maya.OpenMayaUI as openMayaUI 26 | import maya.mel as mel 27 | 28 | # legacy support 29 | from Qtpy.Qt import QtWidgets, QtCore, QtGui 30 | 31 | mayaApi = cmds.about(api=True) 32 | if mayaApi >= 201700: 33 | import shiboken2 as shiboken 34 | import pyside2uic as pysideuic 35 | else: 36 | import shiboken 37 | import pysideuic 38 | 39 | def show(): 40 | global uExportToolWindow 41 | try: 42 | uExportToolWindow.close() 43 | except: 44 | pass 45 | 46 | uExportToolWindow = uExportTool() 47 | uExportToolWindow.show() 48 | return uExportToolWindow 49 | 50 | def loadUiType(uiFile): 51 | """ 52 | Pyside lacks the "loadUiType" command, so we have to convert the ui file to py code in-memory first 53 | and then execute it in a special frame to retrieve the form_class. 54 | http://tech-artists.org/forum/showthread.php?3035-PySide-in-Maya-2013 (ChrisE) 55 | """ 56 | parsed = xml.parse(uiFile) 57 | widget_class = parsed.find('widget').get('class') 58 | form_class = parsed.find('class').text 59 | 60 | with open(uiFile, 'r') as f: 61 | o = StringIO() 62 | frame = {} 63 | 64 | pysideuic.compileUi(f, o, indent=0) 65 | pyc = compile(o.getvalue(), '', 'exec') 66 | exec pyc in frame 67 | 68 | #Fetch the base_class and form class based on their type in the xml from designer 69 | form_class = frame['Ui_%s'%form_class] 70 | base_class = eval('QtWidgets.%s'%widget_class) 71 | return form_class, base_class 72 | 73 | def getMayaWindow(): 74 | ptr = openMayaUI.MQtUtil.mainWindow() 75 | if ptr is not None: 76 | return shiboken.wrapInstance(long(ptr), QtWidgets.QWidget) 77 | 78 | try: 79 | selfDirectory = os.path.dirname(__file__) 80 | uiFile = selfDirectory + '/uExport.ui' 81 | except: 82 | uiFile = 'D:\\Build\\usr\\jeremy_ernst\\MayaTools\\General\\Scripts\\epic\\rigging\\uExport\\uExport.ui' 83 | form_class, base_class = loadUiType(uiFile) 84 | 85 | if os.path.isfile(uiFile): 86 | form_class, base_class = loadUiType(uiFile) 87 | else: 88 | cmds.error('Cannot find UI file: ' + uiFile) 89 | 90 | 91 | 92 | #these are used by both classes below, this method is usually in the utils lib at Epic 93 | def attrExists(attr): 94 | if '.' in attr: 95 | node, att = attr.split('.') 96 | return cmds.attributeQuery(att, node=node, ex=1) 97 | else: 98 | cmds.warning('attrExists: No attr passed in: ' + attr) 99 | return False 100 | 101 | def msgConnect(attribFrom, attribTo, debug=0): 102 | # TODO needs a mode to dump all current connections (overwrite/force) 103 | objFrom, attFrom = attribFrom.split('.') 104 | objTo, attTo = attribTo.split('.') 105 | if debug: print 'msgConnect>>> Locals:', locals() 106 | if not attrExists(attribFrom): 107 | cmds.addAttr(objFrom, longName=attFrom, attributeType='message') 108 | if not attrExists(attribTo): 109 | cmds.addAttr(objTo, longName=attTo, attributeType='message') 110 | 111 | # check that both atts, if existing are msg atts 112 | for a in (attribTo, attribFrom): 113 | if cmds.getAttr(a, type=1) != 'message': 114 | cmds.warning('msgConnect: Attr, ' + a + ' is not a message attribute. CONNECTION ABORTED.') 115 | return False 116 | 117 | try: 118 | return cmds.connectAttr(attribFrom, attribTo, f=True) 119 | except Exception as e: 120 | print e 121 | return False 122 | 123 | def findRelatedSkinCluster(skinObject): 124 | '''Python implementation of MEL command: http://takkun.nyamuuuu.net/blog/archives/592''' 125 | 126 | skinShape = None 127 | skinShapeWithPath = None 128 | hiddenShape = None 129 | hiddenShapeWithPath = None 130 | 131 | cpTest = cmds.ls( skinObject, typ="controlPoint" ) 132 | if len( cpTest ): 133 | skinShape = skinObject 134 | 135 | else: 136 | rels = cmds.listRelatives( skinObject ) 137 | if rels == None: return False 138 | for r in rels : 139 | cpTest = cmds.ls( "%s|%s" % ( skinObject, r ), typ="controlPoint" ) 140 | if len( cpTest ) == 0: 141 | continue 142 | 143 | io = cmds.getAttr( "%s|%s.io" % ( skinObject, r ) ) 144 | if io: 145 | continue 146 | 147 | visible = cmds.getAttr( "%s|%s.v" % ( skinObject, r ) ) 148 | if not visible: 149 | hiddenShape = r 150 | hiddenShapeWithPath = "%s|%s" % ( skinObject, r ) 151 | continue 152 | 153 | skinShape = r 154 | skinShapeWithPath = "%s|%s" % ( skinObject, r ) 155 | break 156 | 157 | if skinShape: 158 | if len( skinShape ) == 0: 159 | if len( hiddenShape ) == 0: 160 | return None 161 | 162 | else: 163 | skinShape = hiddenShape 164 | skinShapeWithPath = hiddenShapeWithPath 165 | 166 | clusters = cmds.ls( typ="skinCluster" ) 167 | for c in clusters: 168 | geom = cmds.skinCluster( c, q=True, g=True ) 169 | for g in geom: 170 | if g == skinShape or g == skinShapeWithPath: 171 | return c 172 | 173 | return None 174 | 175 | def getParents(item): 176 | parents = [] 177 | current_item = item 178 | current_parent = current_item.parent() 179 | 180 | # Walk up the tree and collect all parent items of this item 181 | while not current_parent is None: 182 | parents.append(current_parent) 183 | current_item = current_parent 184 | current_parent = current_item.parent() 185 | return parents 186 | 187 | ######################################################################## 188 | ## UEXPORT CLASS 189 | ######################################################################## 190 | 191 | class uExport(object): 192 | ''' 193 | Just a little basket to store things. 194 | TODO: Add properties to get/set values 195 | TODO: add logic to check that meshes exist across LODs 196 | ''' 197 | def __init__(self, node): 198 | 199 | #update for new LOD attrs instead of just rendermeshes 200 | if not attrExists(node + '.rendermeshes_LOD0'): 201 | if attrExists(node + '.rendermesh'): 202 | if cmds.listConnections(node + '.rendermesh'): 203 | lod0meshes = cmds.listConnections(node + '.rendermesh') 204 | for mesh in lod0meshes: 205 | msgConnect(node + '.rendermeshes_LOD0', mesh + '.uExport') 206 | cmds.deleteAttr(node, at='rendermesh') 207 | else: 208 | cmds.addAttr(node, longName='rendermeshes_LOD0', attributeType='message') 209 | else: 210 | cmds.addAttr(node, longName='rendermeshes_LOD0', attributeType='message') 211 | 212 | #add the other lod attrs 213 | cmds.addAttr(node, longName='rendermeshes_LOD1', attributeType='message') 214 | cmds.addAttr(node, longName='rendermeshes_LOD2', attributeType='message') 215 | cmds.addAttr(node, longName='rendermeshes_LOD3', attributeType='message') 216 | cmds.addAttr(node, longName='rendermeshes_LOD4', attributeType='message') 217 | 218 | #TODO: don't assume 4 LODs 219 | if not attrExists(node + '.export_script_LOD0'): 220 | cmds.addAttr(node, longName='export_script_LOD0', dt='string') 221 | cmds.addAttr(node, longName='export_script_LOD1', dt='string') 222 | cmds.addAttr(node, longName='export_script_LOD2', dt='string') 223 | cmds.addAttr(node, longName='export_script_LOD3', dt='string') 224 | cmds.addAttr(node, longName='export_script_LOD4', dt='string') 225 | 226 | if not attrExists(node + '.fbx_name_LOD0'): 227 | cmds.addAttr(node, longName='fbx_name_LOD0', dt='string') 228 | cmds.addAttr(node, longName='fbx_name_LOD1', dt='string') 229 | cmds.addAttr(node, longName='fbx_name_LOD2', dt='string') 230 | cmds.addAttr(node, longName='fbx_name_LOD3', dt='string') 231 | cmds.addAttr(node, longName='fbx_name_LOD4', dt='string') 232 | 233 | self.export_root = cmds.listConnections(node + '.export_root') 234 | 235 | self.version = cmds.getAttr(node + '.uexport_ver') 236 | self.node = node 237 | self.name = node.split('|')[-1] 238 | self.asset_name = node 239 | self.folder_path = None 240 | self.fbxPropertiesDict = None 241 | 242 | 243 | if attrExists(node + '.asset_name'): 244 | self.asset_name = cmds.getAttr(node + '.asset_name') 245 | if attrExists(node + '.fbx_name'): 246 | self.fbx_name = cmds.getAttr(node + '.fbx_name') 247 | if attrExists(node + '.folder_path'): 248 | self.folder_path = cmds.getAttr(node + '.folder_path') 249 | 250 | #ART MetaData 251 | #TO DO: Move to properties 252 | if attrExists(node + '.joint_mover_template'): 253 | self.joint_mover_template = cmds.getAttr(node + '.joint_mover_template') 254 | if attrExists(node + '.skeleton_template'): 255 | self.skeleton_template = cmds.getAttr(node + '.skeleton_template') 256 | if attrExists(node + '.pre_script'): 257 | self.pre_script = cmds.getAttr(node + '.pre_script') 258 | if attrExists(node + '.post_script'): 259 | self.post_script = cmds.getAttr(node + '.post_script') 260 | if attrExists(node + '.export_file'): 261 | self.export_file = cmds.getAttr(node + '.export_file') 262 | if attrExists(node + '.anim_file'): 263 | self.anim_file = cmds.getAttr(node + '.anim_file') 264 | if attrExists(node + '.skeleton_uasset'): 265 | self.skeleton_uasset = cmds.getAttr(node + '.skeleton_uasset') 266 | if attrExists(node + '.skelmesh_uasset'): 267 | self.skelmesh_uasset = cmds.getAttr(node + '.skelmesh_uasset') 268 | if attrExists(node + '.physics_uasset'): 269 | self.physics_uasset = cmds.getAttr(node + '.physics_uasset') 270 | if attrExists(node + '.thumbnail_large'): 271 | self.thumbnail_large = cmds.getAttr(node + '.thumbnail_large') 272 | if attrExists(node + '.thumbnail_small'): 273 | self.thumbnail_small = cmds.getAttr(node + '.thumbnail_small') 274 | 275 | ## Built in methods 276 | ######################################################################## 277 | def getLodDicts(self): 278 | lodDicts = {} 279 | lodDicts[0] = {'meshes':self.rendermeshes_LOD0, 'export_script':self.export_script_LOD0, 'fbx_name':self.fbx_name_LOD0} 280 | lodDicts[1] = {'meshes':self.rendermeshes_LOD1, 'export_script':self.export_script_LOD1, 'fbx_name':self.fbx_name_LOD1} 281 | lodDicts[2] = {'meshes':self.rendermeshes_LOD2, 'export_script':self.export_script_LOD2, 'fbx_name':self.fbx_name_LOD2} 282 | lodDicts[3] = {'meshes':self.rendermeshes_LOD3, 'export_script':self.export_script_LOD3, 'fbx_name':self.fbx_name_LOD3} 283 | lodDicts[4] = {'meshes':self.rendermeshes_LOD4, 'export_script':self.export_script_LOD4, 'fbx_name':self.fbx_name_LOD4} 284 | return lodDicts 285 | 286 | def getShaderDict(self): 287 | shaderDict = {} 288 | for mesh in self.rendermeshes_ALL: 289 | for shader in self.getAssocShaders(mesh): 290 | if shader not in shaderDict.keys(): 291 | shaderDict[shader] = [mesh] 292 | else: 293 | shaderDict[shader].append(mesh) 294 | if shaderDict: 295 | return shaderDict 296 | else: 297 | return False 298 | 299 | def getAssocShaders(self, mesh): 300 | shapes = cmds.listRelatives(mesh, shapes=1, f=True) 301 | shadingGrps = cmds.listConnections(shapes,type='shadingEngine') 302 | shaders = cmds.ls(cmds.listConnections(shadingGrps),materials=1) 303 | return shaders 304 | 305 | def connectRenderMeshes(self, renderMeshes, LOD=0): 306 | try: 307 | cmds.undoInfo(openChunk=True) 308 | lodAttr = None 309 | if LOD >=0 or LOD <=4: 310 | lodAttr = self.node + '.rendermeshes_LOD' + str(LOD) 311 | conns = cmds.listConnections(lodAttr, plugs=1, destination=1) 312 | if conns: 313 | for conn in cmds.listConnections(lodAttr, plugs=1, destination=1): 314 | cmds.disconnectAttr(lodAttr, conn) 315 | if lodAttr: 316 | for mesh in renderMeshes: 317 | msgConnect(lodAttr, mesh + '.uExport') 318 | else: 319 | cmds.error('connectRenderMeshes>>> please specify a LOD integer (0-4) for your meshes') 320 | 321 | except Exception as e: 322 | print e 323 | finally: 324 | cmds.undoInfo(closeChunk=True) 325 | 326 | def getFbxExportPropertiesDict(self): 327 | if not attrExists(self.node + '.fbxPropertiesDict'): 328 | cmds.addAttr(self.node, longName='fbxPropertiesDict', dt='string') 329 | self.fbxPropertiesDict = {'animInterpolation':'quaternion', 'upAxis':'default', 'triangulation':False} 330 | cmds.setAttr(self.node + '.fbxPropertiesDict', json.dumps(self.fbxPropertiesDict), type='string') 331 | return self.fbxPropertiesDict 332 | else: 333 | self.fbxPropertiesDict = json.loads(cmds.getAttr(self.node + '.fbxPropertiesDict')) 334 | return self.fbxPropertiesDict 335 | 336 | ## Properties 337 | ######################################################################## 338 | 339 | #return and set the rendermeshes per LOD 340 | @property 341 | def rendermeshes_LOD0(self): 342 | conns = cmds.listConnections(self.node + '.rendermeshes_LOD0') 343 | if conns: 344 | return conns 345 | else: return [] 346 | @rendermeshes_LOD0.setter 347 | def rendermeshes_LOD0(self, meshes): 348 | self.connectRenderMeshes(meshes, LOD=0) 349 | 350 | @property 351 | def rendermeshes_LOD1(self): 352 | conns = cmds.listConnections(self.node + '.rendermeshes_LOD1') 353 | if conns: 354 | return conns 355 | else: return [] 356 | @rendermeshes_LOD1.setter 357 | def rendermeshes_LOD1(self, meshes): 358 | self.connectRenderMeshes(meshes, LOD=1) 359 | 360 | @property 361 | def rendermeshes_LOD2(self): 362 | conns = cmds.listConnections(self.node + '.rendermeshes_LOD2') 363 | if conns: 364 | return conns 365 | else: return [] 366 | @rendermeshes_LOD2.setter 367 | def rendermeshes_LOD2(self, meshes): 368 | self.connectRenderMeshes(meshes, LOD=2) 369 | 370 | @property 371 | def rendermeshes_LOD3(self): 372 | conns = cmds.listConnections(self.node + '.rendermeshes_LOD3') 373 | if conns: 374 | return conns 375 | else: return [] 376 | @rendermeshes_LOD3.setter 377 | def rendermeshes_LOD3(self, meshes): 378 | self.connectRenderMeshes(meshes, LOD=3) 379 | 380 | @property 381 | def rendermeshes_LOD4(self): 382 | conns = cmds.listConnections(self.node + '.rendermeshes_LOD4') 383 | if conns: 384 | return conns 385 | else: return [] 386 | @rendermeshes_LOD4.setter 387 | def rendermeshes_LOD4(self, meshes): 388 | self.connectRenderMeshes(meshes, LOD=4) 389 | 390 | #number of lods 391 | @property 392 | def lodNum(self): 393 | att = self.node + '.lodNum' 394 | if attrExists(att): 395 | return cmds.getAttr(att) 396 | else: 397 | cmds.addAttr(self.node, ln='lodNum', at='byte') 398 | cmds.setAttr(att, 4) 399 | return cmds.getAttr(att) 400 | 401 | @lodNum.setter 402 | def lodNum(self, meshes): 403 | att = self.node + '.lodNum' 404 | if attrExists(att): 405 | return cmds.setAttr(att) 406 | else: 407 | cmds.addAttr(self.node, ln='lodNum', at='byte') 408 | cmds.setAttr(att, 4) 409 | return cmds.setAttr(att) 410 | 411 | #return ALL lod geometry 412 | @property 413 | def rendermeshes_ALL(self): 414 | meshes = [] 415 | meshes.extend(self.rendermeshes_LOD0) 416 | meshes.extend(self.rendermeshes_LOD1) 417 | meshes.extend(self.rendermeshes_LOD2) 418 | meshes.extend(self.rendermeshes_LOD3) 419 | meshes.extend(self.rendermeshes_LOD4) 420 | return meshes 421 | 422 | #return and set export script paths 423 | @property 424 | def export_script_LOD0(self): 425 | return cmds.getAttr(self.node + '.export_script_LOD0') 426 | @export_script_LOD0.setter 427 | def export_script_LOD0(self, path): 428 | cmds.setAttr(self.node + '.export_script_LOD0', path, type='string') 429 | 430 | @property 431 | def export_script_LOD1(self): 432 | return cmds.getAttr(self.node + '.export_script_LOD1') 433 | @export_script_LOD1.setter 434 | def export_script_LOD1(self, path): 435 | cmds.setAttr(self.node + '.export_script_LOD1', path, type='string') 436 | 437 | @property 438 | def export_script_LOD2(self): 439 | return cmds.getAttr(self.node + '.export_script_LOD2') 440 | @export_script_LOD2.setter 441 | def export_script_LOD2(self, path): 442 | cmds.setAttr(self.node + '.export_script_LOD2', path, type='string') 443 | 444 | @property 445 | def export_script_LOD3(self): 446 | return cmds.getAttr(self.node + '.export_script_LOD3') 447 | @export_script_LOD3.setter 448 | def export_script_LOD3(self, path): 449 | cmds.setAttr(self.node + '.export_script_LOD3', path, type='string') 450 | 451 | @property 452 | def export_script_LOD4(self): 453 | return cmds.getAttr(self.node + '.export_script_LOD4') 454 | @export_script_LOD4.setter 455 | def export_script_LOD4(self, path): 456 | cmds.setAttr(self.node + '.export_script_LOD4', path, type='string') 457 | 458 | 459 | #return and set fbx export names 460 | @property 461 | def fbx_name_LOD0(self): 462 | return cmds.getAttr(self.node + '.fbx_name_LOD0') 463 | @fbx_name_LOD0.setter 464 | def fbx_name_LOD0(self, name): 465 | cmds.setAttr(self.node + '.fbx_name_LOD0', name, type='string') 466 | 467 | @property 468 | def fbx_name_LOD1(self): 469 | return cmds.getAttr(self.node + '.fbx_name_LOD1') 470 | @fbx_name_LOD1.setter 471 | def fbx_name_LOD1(self, name): 472 | cmds.setAttr(self.node + '.fbx_name_LOD1', name, type='string') 473 | 474 | @property 475 | def fbx_name_LOD2(self): 476 | return cmds.getAttr(self.node + '.fbx_name_LOD2') 477 | @fbx_name_LOD2.setter 478 | def fbx_name_LOD2(self, name): 479 | cmds.setAttr(self.node + '.fbx_name_LOD2', name, type='string') 480 | 481 | @property 482 | def fbx_name_LOD3(self): 483 | return cmds.getAttr(self.node + '.fbx_name_LOD3') 484 | @fbx_name_LOD3.setter 485 | def fbx_name_LOD3(self, name): 486 | cmds.setAttr(self.node + '.fbx_name_LOD3', name, type='string') 487 | 488 | @property 489 | def fbx_name_LOD4(self): 490 | return cmds.getAttr(self.node + '.fbx_name_LOD4') 491 | @fbx_name_LOD4.setter 492 | def fbx_name_LOD4(self, path): 493 | cmds.setAttr(self.node + '.fbx_name_LOD4', path, type='string') 494 | 495 | 496 | #return joints 497 | @property 498 | def joints(self): 499 | if self.export_root: 500 | returnMe = [] 501 | children = cmds.listRelatives(self.export_root, type='joint',allDescendents=True) 502 | if children: 503 | returnMe.extend(children) 504 | 505 | returnMe.append(self.export_root[0]) 506 | return returnMe 507 | @joints.setter 508 | def joints(self): 509 | print 'Joints returned by walking hierarchy from the root, not directly settable.' 510 | 511 | #return fbxExportDict 512 | @property 513 | def fbxExportProperties(self): 514 | return self.getFbxExportPropertiesDict() 515 | @fbxExportProperties.setter 516 | def fbxExportProperties(self, dict): 517 | self.fbxPropertiesDict = dict 518 | cmds.setAttr(self.node + '.fbxPropertiesDict', json.dumps(self.fbxPropertiesDict), type='string') 519 | 520 | ######################################################################## 521 | ## UEXPORT TOOL 522 | ######################################################################## 523 | 524 | 525 | class uExportTool(base_class, form_class): 526 | title = 'uExportTool 0.8' 527 | 528 | currentMesh = None 529 | currentSkin = None 530 | currentInf = None 531 | currentVerts = None 532 | currentNormalization = None 533 | 534 | scriptJobNum = None 535 | copyCache = None 536 | 537 | jointLoc = None 538 | 539 | iconLib = {} 540 | iconPath = os.environ.get('MAYA_LOCATION', None) + '/icons/' 541 | iconLib['joint'] = QtGui.QIcon(QtGui.QPixmap(iconPath + 'kinJoint.png')) 542 | iconLib['ikHandle'] = QtGui.QIcon(QtGui.QPixmap(iconPath + 'kinHandle.png')) 543 | iconLib['transform'] = QtGui.QIcon(QtGui.QPixmap(iconPath + 'orientJoint.png')) 544 | 545 | def __init__(self, parent=getMayaWindow()): 546 | self.closeExistingWindow() 547 | super(uExportTool, self).__init__(parent) 548 | 549 | 550 | self.setupUi(self) 551 | self.setWindowTitle(self.title) 552 | self.fbxVerLbl.setText('fbx plugin ' + str(self.fbxVersion()) + ' ') 553 | 554 | wName = openMayaUI.MQtUtil.fullName(long(shiboken.getCppPointer(self)[0])) 555 | 556 | ## Connect UI 557 | ######################################################################## 558 | self.export_BTN.clicked.connect(self.export_FN) 559 | self.createUexportNode_BTN.clicked.connect(self.createUexportNode_FN) 560 | self.replaceUnknownNodes.clicked.connect(self.replaceUnknownNodes_FN) 561 | self.refreshBTN.clicked.connect(self.refreshUI) 562 | self.getTexturesP4BTN.clicked.connect(self.getTexturesP4_FN) 563 | 564 | # TODO: Add save settings, setting p4 menu for now 565 | self.p4CHK.setChecked(False) 566 | 567 | self.workSpaceCMB.currentIndexChanged.connect(self.workspaceSelected) 568 | 569 | #context menu 570 | self.export_tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 571 | self.export_tree.customContextMenuRequested.connect(self.openMenu) 572 | 573 | self.export_tree.itemClicked.connect(self.check_status) 574 | self.export_tree.itemClicked.connect(self.itemClicked) 575 | self.missingFilesTree.itemClicked.connect(self.itemClicked) 576 | 577 | 578 | #check for p4 lib 579 | yourP4Module = 'p4python.P4' 580 | try: 581 | __import__(yourP4Module) 582 | except ImportError: 583 | print 'Perforce lib not found.' 584 | self.perforce = False 585 | else: 586 | self.perforce = True 587 | print 'Perforce lib found.' 588 | 589 | #Connect event filter to grab right/left click 590 | self.export_tree.viewport().installEventFilter(self) 591 | self.mousePress = None 592 | 593 | self.snapRoot_CMB.setHidden(True) 594 | self.refreshUI() 595 | 596 | ## GENERAL 597 | ######################################################################## 598 | 599 | #quick mesh check 600 | def isMesh(self, node): 601 | rels = cmds.listRelatives(node, children=True, s=True) 602 | if rels: 603 | for shp in rels: 604 | if cmds.nodeType(shp)=='mesh': 605 | return True 606 | return False 607 | 608 | def setOutlinerToShowAssetContents(self): 609 | mel.eval('outlinerEditor -e -showContainerContents 1 outlinerPanel1; outlinerEditor -e \ 610 | -showContainedOnly 0 outlinerPanel1;') 611 | 612 | def closeExistingWindow(self): 613 | for qt in QtWidgets.qApp.topLevelWidgets(): 614 | try: 615 | if qt.__class__.__name__ == self.__class__.__name__: 616 | qt.deleteLater() 617 | print 'uExport: Closed ' + str(qt.__class__.__name__) 618 | except: 619 | pass 620 | 621 | def convertSkelSettingsToNN(delete=1): 622 | orig = 'SkeletonSettings_Cache' 623 | if cmds.objExists(orig): 624 | if cmds.nodeType(orig) == 'unknown': 625 | new = cmds.createNode('network') 626 | for att in cmds.listAttr(orig): 627 | if not cmds.attributeQuery(att, node=new, exists=1): 628 | typ = cmds.attributeQuery(att, node=orig, at=1) 629 | if typ == 'typed': 630 | cmds.addAttr(new, longName=att, dt='string') 631 | if cmds.getAttr(orig + '.' + att): 632 | cmds.setAttr(new + '.' + att, cmds.getAttr(orig + '.' + att), type='string') 633 | elif typ == 'enum': 634 | cmds.addAttr(new, longName=att, at='enum', enumName=cmds.attributeQuery(att, node=orig, listEnum=1)[0]) 635 | cmds.delete(orig) 636 | cmds.rename(new, 'SkeletonSettings_Cache') 637 | 638 | def fbxVersion(self): 639 | for plugin in cmds.pluginInfo(q=True, listPlugins=True): 640 | if "fbxmaya" in plugin: 641 | return cmds.pluginInfo(plugin, q=True, version=True) 642 | 643 | def replaceUnknownNodes_FN(self): 644 | self.convertSkelSettingsToNN() 645 | 646 | def isBlendshape(self, mesh): 647 | future = cmds.listHistory(mesh, future=1) 648 | isShape = False 649 | for node in future: 650 | if cmds.nodeType(node) == 'blendShape': 651 | return True 652 | return False 653 | 654 | def LOD_transferWeights(meshes, jointsToRemove, jointToTransferTo, debug=1, pruneWeights=0.001, *args): 655 | ''' 656 | Original function by Charles Anderson @ Epic Games 657 | ''' 658 | for mesh in meshes: 659 | 660 | # Find the skin cluster for the current mesh 661 | cluster = findCluster(mesh) 662 | 663 | if debug: 664 | print "MESH: ", mesh 665 | print "CLUSTER: ", cluster 666 | 667 | # Prune weights on the current mesh 668 | if pruneWeights: 669 | cmds.skinPercent(cluster, mesh, prw=pruneWeights) 670 | 671 | # Find all of the current influences on the current skin cluster. 672 | meshInfluences = cmds.skinCluster(cluster, q=True, inf=True) 673 | #print "Current Influences: ", meshInfluences 674 | 675 | for joint in jointsToRemove: 676 | if joint in meshInfluences: 677 | #print "Current Joint: ", joint 678 | 679 | # If the jointToTransferTo is not already an influence on the current mesh then add it. 680 | currentInfluences = cmds.skinCluster(cluster, q=True, inf=True) 681 | if jointToTransferTo not in currentInfluences: 682 | cmds.skinCluster(cluster, e=True, wt=0, ai=jointToTransferTo) 683 | 684 | # Now transfer all of the influences we want to remove onto the jointToTransferTo. 685 | for x in range(cmds.polyEvaluate(mesh, v=True)): 686 | #print "TRANSFERRING DATA....." 687 | value = cmds.skinPercent(cluster, (mesh+".vtx["+str(x)+"]"), t=joint, q=True) 688 | if value > 0: 689 | cmds.skinPercent(cluster, (mesh+".vtx["+str(x)+"]"), tmw=[joint, jointToTransferTo]) 690 | 691 | # Remove unused influences 692 | currentInfluences = cmds.skinCluster(cluster, q=True, inf=True) 693 | #print "Current Influences: ", currentInfluences 694 | influencesToRemove = [] 695 | weightedInfs = cmds.skinCluster(cluster, q=True, weightedInfluence=True) 696 | #print "Weighted Influences: ", weightedInfs 697 | for inf in currentInfluences: 698 | #print "Influence: ", inf 699 | if inf not in weightedInfs: 700 | #print "Update Influences to Remove List: ", inf 701 | influencesToRemove.append(inf) 702 | 703 | #print "ToRemove Influences: ", influencesToRemove 704 | if influencesToRemove != []: 705 | for inf in influencesToRemove: 706 | cmds.skinCluster(cluster, e=True, ri=inf) 707 | 708 | ## UI RELATED 709 | ######################################################################## 710 | 711 | #event filter to grab and discern right/left click 712 | def eventFilter(self, source, event): 713 | if (event.type() == QtCore.QEvent.MouseButtonPress and event.button() == QtCore.Qt.RightButton and source is self.export_tree.viewport()): 714 | self.mousePress = 'right' 715 | elif (event.type() == QtCore.QEvent.MouseButtonPress and event.button() == QtCore.Qt.LeftButton and source is self.export_tree.viewport()): 716 | self.mousePress = 'left' 717 | return super(uExportTool, self).eventFilter(source, event) 718 | 719 | #contextual menus 720 | def openMenu(self, position): 721 | menu = QtWidgets.QMenu() 722 | clickedWid = self.export_tree.itemAt(position) 723 | 724 | if 'P4_FILE_LIST' in self.export_tree.itemAt(position).text(0): 725 | checkOut = menu.addAction("Check out files") 726 | pos = self.export_tree.mapToGlobal(position) 727 | action = menu.exec_(pos) 728 | 729 | if action: 730 | print 'checkin gout files' 731 | 732 | if 'LOD ' in clickedWid.text(0): 733 | addMeshes = menu.addAction("Add selected meshes") 734 | removeMeshes = menu.addAction("Remove selected meshes") 735 | resetMeshes = menu.addAction("Set only selected meshes") 736 | 737 | pos = self.export_tree.mapToGlobal(position) 738 | action = menu.exec_(pos) 739 | 740 | if action: 741 | if action == addMeshes: 742 | meshes = [mesh for mesh in cmds.ls(sl=1) if self.isMesh(mesh)] 743 | uNode = clickedWid.uExport 744 | lod = clickedWid.lod 745 | existingMeshes = eval('uNode.rendermeshes_LOD' + str(lod)) 746 | existingMeshes.extend(meshes) 747 | uNode.connectRenderMeshes(existingMeshes, LOD=clickedWid.lod) 748 | self.refreshUI() 749 | elif action == removeMeshes: 750 | meshes = [mesh for mesh in cmds.ls(sl=1) if self.isMesh(mesh)] 751 | uNode = clickedWid.uExport 752 | lod = clickedWid.lod 753 | existingMeshes = eval('uNode.rendermeshes_LOD' + str(lod)) 754 | newMeshes = [mesh for mesh in existingMeshes if mesh not in meshes] 755 | uNode.connectRenderMeshes(newMeshes, LOD=lod) 756 | self.refreshUI() 757 | elif action == resetMeshes: 758 | newMeshes = cmds.ls(sl=1) 759 | uNode = clickedWid.uExport 760 | lod = clickedWid.lod 761 | uNode.connectRenderMeshes(newMeshes, LOD=lod) 762 | self.refreshUI() 763 | 764 | if not self.export_tree.itemAt(position).parent(): 765 | rootRewire = menu.addAction("Re-wire root joint attr to current selected joint") 766 | 767 | meshSubmenu = menu.addMenu('EDIT RENDERMESHES >>') 768 | 769 | addLOD0 = meshSubmenu.addAction("Add selected as LOD0 render meshes") 770 | addLOD1 = meshSubmenu.addAction("Add selected as LOD1 render meshes") 771 | addLOD2 = meshSubmenu.addAction("Add selected as LOD2 render meshes") 772 | addLOD3 = meshSubmenu.addAction("Add selected as LOD3 render meshes") 773 | addLOD4 = meshSubmenu.addAction("Add selected as LOD4 render meshes") 774 | 775 | scriptSubmenu = menu.addMenu('ADD EXPORT SCRIPTS >>') 776 | 777 | addScriptLOD0 = scriptSubmenu.addAction("Add LOD0 export script") 778 | addScriptLOD1 = scriptSubmenu.addAction("Add LOD1 export script") 779 | addScriptLOD2 = scriptSubmenu.addAction("Add LOD2 export script") 780 | addScriptLOD3 = scriptSubmenu.addAction("Add LOD3 export script") 781 | addScriptLOD4 = scriptSubmenu.addAction("Add LOD4 export script") 782 | 783 | pos = self.export_tree.mapToGlobal(position) 784 | action = menu.exec_(pos) 785 | 786 | 787 | if action: 788 | if action == rootRewire: 789 | index = self.export_tree.selectedIndexes()[0] 790 | uExportNode = self.export_tree.itemFromIndex(index).uExport.node 791 | root = cmds.ls(sl=1) 792 | if len(root) == 1: 793 | self.connectRoot(uExportNode, root[0]) 794 | else: 795 | cmds.error('Select a single joint, you == fail, bro. ' + str(root)) 796 | 797 | elif action in (addLOD0, addLOD1, addLOD2, addLOD3, addLOD4): 798 | for index in self.export_tree.selectedIndexes(): 799 | uExportNode = self.export_tree.itemFromIndex(index).uExport 800 | meshes = cmds.ls(sl=1) 801 | #TODO: check if theyre actually meshes 802 | if action == addLOD0: uExportNode.rendermeshes_LOD0 = meshes 803 | if action == addLOD1: uExportNode.rendermeshes_LOD1 = meshes 804 | if action == addLOD2: uExportNode.rendermeshes_LOD2 = meshes 805 | if action == addLOD3: uExportNode.rendermeshes_LOD3 = meshes 806 | if action == addLOD4: uExportNode.rendermeshes_LOD4 = meshes 807 | 808 | elif action in (addScriptLOD0, addScriptLOD1, addScriptLOD2, addScriptLOD3, addScriptLOD4): 809 | for index in self.export_tree.selectedIndexes(): 810 | uExportNode = self.export_tree.itemFromIndex(index).uExport 811 | 812 | fileName,_ = QtWidgets.QFileDialog.getOpenFileName(self, 813 | "Choose a Python Script", '', 814 | "Python (*.py);;All Files (*)") 815 | 816 | if fileName: 817 | if action == addScriptLOD0: uExportNode.export_script_LOD0 = fileName 818 | if action == addScriptLOD1: uExportNode.export_script_LOD1 = fileName 819 | if action == addScriptLOD2: uExportNode.export_script_LOD2 = fileName 820 | if action == addScriptLOD3: uExportNode.export_script_LOD3 = fileName 821 | if action == addScriptLOD4: uExportNode.export_script_LOD4 = fileName 822 | 823 | self.refreshUI() 824 | 825 | def refreshUI(self): 826 | start = time.time() 827 | 828 | self.export_tree.clear() 829 | self.missingFilesTree.clear() 830 | self.uNodes = [] 831 | for node in self.getExportNodes(): 832 | self.uNodes.append(uExport(node)) 833 | self.buildExportTree(self.uNodes) 834 | self.buildMissingFilesTree() 835 | 836 | 837 | if self.p4CHK.isChecked(): 838 | if self.perforce: 839 | self.getTexturesP4BTN.setEnabled(True) 840 | self.getP4Workspaces() 841 | 842 | # put export into the uExport class later 843 | 844 | elapsed = (time.time() - start) 845 | print 'uExport>>> Refreshed in %.2f seconds.' % elapsed 846 | 847 | 848 | def buildExportTree(self, uNodes): 849 | for uNode in uNodes: 850 | 851 | red = QtGui.QColor(200, 75, 75, 255) 852 | widRed = QtGui.QColor(200, 75, 75, 100) 853 | blue = QtGui.QColor(50, 130, 210, 255) 854 | widBlue = QtGui.QColor(50, 130, 210, 100) 855 | 856 | #top level 857 | wid1 = QtWidgets.QTreeWidgetItem() 858 | font = wid1.font(0) 859 | font.setPointSize(15) 860 | 861 | wid1.setText(0,uNode.asset_name) 862 | wid1.uExport = uNode 863 | 864 | wid1.setText(1, uNode.version) 865 | self.export_tree.addTopLevelItem(wid1) 866 | wid1.setExpanded(True) 867 | wid1.setFont(0,font) 868 | 869 | font = wid1.font(0) 870 | font.setPointSize(10) 871 | 872 | wid1.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsSelectable) 873 | wid1.setCheckState(0, QtCore.Qt.Checked) 874 | 875 | wid1.setBackground(0, QtGui.QColor(widBlue)) 876 | 877 | #mesh branch 878 | meshTop = QtWidgets.QTreeWidgetItem() 879 | meshes = uNode.rendermeshes_LOD0 880 | if meshes: 881 | meshTop.setText(0, 'RENDER MESHES: (' + str(len(uNode.rendermeshes_ALL)) + ') LODS: (' + str(uNode.lodNum) + ')') 882 | else: 883 | meshTop.setText(0, 'RENDER MESHES: NONE') 884 | meshTop.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable) 885 | meshTop.setCheckState(0, QtCore.Qt.Checked) 886 | wid1.addChild(meshTop) 887 | meshTop.setExpanded(True) 888 | 889 | allMeshRelated = [] 890 | 891 | #get mat info 892 | start = time.time() 893 | shaderDict = uNode.getShaderDict() 894 | elapsed = (time.time() - start) 895 | print 'uExport>>> Built shader dict for ' + uNode.asset_name + ' in %.2f seconds.' % elapsed 896 | 897 | #meshes 898 | lodDicts = uNode.getLodDicts() 899 | if lodDicts: 900 | for lodNum in lodDicts: 901 | lodWid = QtWidgets.QTreeWidgetItem() 902 | 903 | #get mesh info 904 | meshes = lodDicts[lodNum]['meshes'] 905 | numMeshes = '0' 906 | if meshes: 907 | numMeshes = str(len(meshes)) 908 | else: 909 | lodWid.setForeground(0,red) 910 | continue 911 | 912 | numShaders = 0 913 | usedShaders = [] 914 | 915 | if shaderDict: 916 | numShaders = len(shaderDict.keys()) 917 | for mat in shaderDict.keys(): 918 | for mesh in meshes: 919 | if mesh in shaderDict[mat]: 920 | usedShaders.append(mat) 921 | else: 922 | lodWid.setForeground(0,red) 923 | wid1.setBackground(0, widRed) 924 | 925 | usedShaders = set(usedShaders) 926 | 927 | widText = 'LOD ' + str(lodNum) + ' (' + numMeshes + ' meshes) (' + str(len(usedShaders)) + ' materials)' 928 | if lodDicts[lodNum]['export_script']: 929 | widText += ' > Export Script: ' + lodDicts[lodNum]['export_script'].split('/')[-1] 930 | 931 | lodWid.setText(0, widText) 932 | 933 | #add metadata for later use 934 | lodWid.lod = lodNum 935 | lodWid.uExport = uNode 936 | 937 | meshTop.addChild(lodWid) 938 | 939 | #create mesh top widget 940 | meshLodTop = QtWidgets.QTreeWidgetItem() 941 | meshLodTop.setText(0, 'MESHES (' + numMeshes + ')') 942 | lodWid.addChild(meshLodTop) 943 | 944 | matLodTop = QtWidgets.QTreeWidgetItem() 945 | 946 | if meshes: 947 | for mesh in meshes: 948 | meshWid = QtWidgets.QTreeWidgetItem() 949 | meshWid.setText(0, mesh) 950 | meshLodTop.addChild(meshWid) 951 | 952 | if not findRelatedSkinCluster(mesh): 953 | if self.isBlendshape(mesh): 954 | meshWid.setForeground(0, blue) 955 | meshWid.setText(0, mesh + ' (blendshape)') 956 | else: 957 | meshWid.setForeground(0, red) 958 | wid1.setBackground(0, widRed) 959 | meshWid.setText(0, mesh + ': NO SKINCLUSTER') 960 | for item in getParents(meshWid): 961 | item.setExpanded(True) 962 | meshWid.selectMe = [mesh] 963 | meshLodTop.selectMe = meshes 964 | 965 | else: 966 | meshWid = QtWidgets.QTreeWidgetItem() 967 | meshWid.setText(0, 'NONE') 968 | meshLodTop.addChild(meshWid) 969 | 970 | #create mat top widget 971 | matLodTop = QtWidgets.QTreeWidgetItem() 972 | lodWid.addChild(matLodTop) 973 | 974 | usedShaders = list(set(usedShaders)) 975 | 976 | for shader in usedShaders: 977 | matWid = QtWidgets.QTreeWidgetItem() 978 | matWid.setText(0, shader) 979 | matWid.setText(1, str(shaderDict[shader])) 980 | matLodTop.addChild(matWid) 981 | matWid.selectMe = [shader] 982 | self.export_tree.sortItems(0, QtCore.Qt.SortOrder(0)) 983 | 984 | matLodTop.setText(0, 'MATERIALS (' + str(len(usedShaders)) + ')') 985 | matLodTop.selectMe = usedShaders 986 | 987 | lodWid.selectMe = usedShaders + meshes 988 | 989 | allMeshRelated.extend(lodWid.selectMe) 990 | 991 | meshTop.selectMe = allMeshRelated 992 | 993 | 994 | 995 | #anim branch 996 | animTop = QtWidgets.QTreeWidgetItem() 997 | 998 | animTop.selectMe = uNode.joints 999 | 1000 | if uNode.export_root: 1001 | jnts = uNode.joints 1002 | if jnts: 1003 | animTop.setText(0, 'ANIMATION: (' + str(len(uNode.joints)) + ' JOINTS)') 1004 | else: 1005 | animTop.setText(0, 'ANIMATION: NO SKELETON ROOT SET') 1006 | animTop.setForeground(0, red) 1007 | wid1.setBackground(0, widRed) 1008 | animTop.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable) 1009 | animTop.setCheckState(0, QtCore.Qt.Checked) 1010 | wid1.addChild(animTop) 1011 | 1012 | #anims 1013 | animWid = QtWidgets.QTreeWidgetItem() 1014 | animWid.setText(0, '<< CURRENT TIME RANGE >>') 1015 | animTop.addChild(animWid) 1016 | 1017 | 1018 | #meta branch 1019 | metaTop = QtWidgets.QTreeWidgetItem() 1020 | metaTop.setText(0, 'METADATA') 1021 | wid1.addChild(metaTop) 1022 | 1023 | # uexport node meta 1024 | ueWid = QtWidgets.QTreeWidgetItem() 1025 | ueWid.setText(0, 'UEXPORT NODE: ' + str(uNode.node)) 1026 | metaTop.addChild(ueWid) 1027 | ueWid.selectMe = uNode.node 1028 | 1029 | #export path meta 1030 | fpathWid = QtWidgets.QTreeWidgetItem() 1031 | fpathWid.setText(0, 'EXPORT_FOLDER_PATH: ' + str(uNode.folder_path)) 1032 | metaTop.addChild(fpathWid) 1033 | 1034 | fbxInfoRoot = QtWidgets.QTreeWidgetItem() 1035 | fbxInfoRoot.setText(0, 'FBX_INFO ') 1036 | metaTop.addChild(fbxInfoRoot) 1037 | fbxInfoRoot.setExpanded(True) 1038 | #fbx files meta 1039 | fbxFiles = QtWidgets.QTreeWidgetItem() 1040 | fbxFiles.setText(0, 'files ') 1041 | fbxInfoRoot.addChild(fbxFiles) 1042 | fileNum = 0 1043 | for lodNum in lodDicts: 1044 | if lodDicts[lodNum]['fbx_name']: 1045 | fbxMeshWid = QtWidgets.QTreeWidgetItem() 1046 | fbxMeshWid.setText(0, lodDicts[lodNum]['fbx_name']) 1047 | fbxFiles.addChild(fbxMeshWid) 1048 | fileNum += 1 1049 | fbxFiles.setText(0, 'files on disk (' + str(fileNum) + ')') 1050 | 1051 | #fbx settings meta 1052 | fbxProps = uNode.fbxExportProperties 1053 | fbxDefault = 1 1054 | 1055 | fbxSettings = QtWidgets.QTreeWidgetItem() 1056 | fbxSettings.setText(0, 'export settings (default)') 1057 | if fbxProps != {u'animInterpolation':u'quaternion', u'upAxis':u'default', u'triangulation':False}: 1058 | fbxSettings.setText(0, 'export settings (non-default)') 1059 | fbxDefault = 0 1060 | fbxInfoRoot.addChild(fbxSettings) 1061 | 1062 | #fbx settings 1063 | interpType = QtWidgets.QTreeWidgetItem() 1064 | interpType_CMB = self.makeTreeCmb(['INTERP TYPE: Quaternion','INTERP TYPE: Euler', 'INTERP TYPE: Resample'], 160) 1065 | interpType_CMB.currentIndexChanged.connect(functools.partial(self.setAnimInterpolation, interpType_CMB, uNode)) 1066 | 1067 | if fbxProps['animInterpolation'].lower() == 'quaternion': 1068 | interpType_CMB.setCurrentIndex(0) 1069 | elif fbxProps['animInterpolation'].lower() == 'euler': 1070 | interpType_CMB.setCurrentIndex(1) 1071 | else: 1072 | interpType_CMB.setCurrentIndex(2) 1073 | 1074 | 1075 | upAxis = QtWidgets.QTreeWidgetItem() 1076 | upAxis_CMB = self.makeTreeCmb(['UP AXIS: Default','UP AXIS: Y', 'UP AXIS: Z'], 150) 1077 | upAxis_CMB.currentIndexChanged.connect(functools.partial(self.setUpAxis, upAxis_CMB, uNode)) 1078 | 1079 | if fbxProps['upAxis'].lower() == 'default': 1080 | upAxis_CMB.setCurrentIndex(0) 1081 | elif fbxProps['upAxis'].lower() == 'y': 1082 | upAxis_CMB.setCurrentIndex(1) 1083 | else: 1084 | upAxis_CMB.setCurrentIndex(2) 1085 | 1086 | #triangulate checkbox and logic to write properties 1087 | triangulate = QtWidgets.QTreeWidgetItem() 1088 | triangulate_CHK = QtWidgets.QCheckBox(parent=self.export_tree) 1089 | triangulate_CHK.setText('Triangulate mesh') 1090 | triangulate_CHK.setChecked(fbxProps['triangulation']) 1091 | triangulate_CHK.stateChanged.connect(functools.partial(self.setTriangulation, triangulate_CHK, uNode)) 1092 | 1093 | fbxSettings.addChild(interpType) 1094 | fbxSettings.addChild(upAxis) 1095 | fbxSettings.addChild(triangulate) 1096 | self.export_tree.setItemWidget(interpType, 0, interpType_CMB) 1097 | self.export_tree.setItemWidget(upAxis, 0, upAxis_CMB) 1098 | self.export_tree.setItemWidget(triangulate, 0, triangulate_CHK) 1099 | 1100 | #if the settings don't match the defaults, set expanded for the user to keep an eye 1101 | if not fbxDefault: 1102 | fbxSettings.setExpanded(True) 1103 | 1104 | #p4 1105 | if self.p4CHK.isChecked(): 1106 | p4FileList = QtWidgets.QTreeWidgetItem() 1107 | p4FileList.setText(0, 'P4_FILE_LIST') 1108 | metaTop.addChild(p4FileList) 1109 | for lodNum in lodDicts: 1110 | if lodDicts[lodNum]['fbx_name']: 1111 | p4DepotFile = self.getP4Location(lodDicts[lodNum]['fbx_name']) 1112 | if p4DepotFile: 1113 | p4FileWid = QtWidgets.QTreeWidgetItem() 1114 | p4FileWid.setText(0, p4DepotFile[1]) 1115 | p4FileList.addChild(p4FileWid) 1116 | 1117 | #set tool tip with p4 info 1118 | p4FileWid.setToolTip(0, p4DepotFile[0] + '\n' + p4DepotFile[2] + '\n' + p4DepotFile[3]) 1119 | wid1.sortChildren(0, QtCore.Qt.SortOrder(0)) 1120 | 1121 | def makeTreeCmb(self, items, width, select=None): 1122 | if items: 1123 | cmb = QtWidgets.QComboBox(parent=self.export_tree) 1124 | cmb.addItems(items) 1125 | cmb.setMaximumWidth(width) 1126 | cmb.setMaximumHeight(15) 1127 | return cmb 1128 | else: 1129 | return False 1130 | 1131 | #functions for auto-generated UI in the tree 1132 | def setTriangulation(self, chk, uNode, *args): 1133 | fbxDict = uNode.fbxExportProperties 1134 | if chk.isChecked(): 1135 | fbxDict['triangulation'] = True 1136 | uNode.fbxExportProperties = fbxDict 1137 | else: 1138 | fbxDict['triangulation'] = False 1139 | uNode.fbxExportProperties = fbxDict 1140 | 1141 | def setUpAxis(self, cmb, uNode, *args): 1142 | fbxDict = uNode.fbxExportProperties 1143 | index = cmb.currentIndex() 1144 | if index == 0: 1145 | fbxDict['upAxis'] = 'default' 1146 | uNode.fbxExportProperties = fbxDict 1147 | elif index == 1: 1148 | fbxDict['upAxis'] = 'y' 1149 | uNode.fbxExportProperties = fbxDict 1150 | else: 1151 | fbxDict['upAxis'] = 'z' 1152 | uNode.fbxExportProperties = fbxDict 1153 | 1154 | def setAnimInterpolation(self, cmb, uNode, *args): 1155 | fbxDict = uNode.fbxExportProperties 1156 | index = cmb.currentIndex() 1157 | if index == 0: 1158 | fbxDict['animInterpolation'] = 'quaternion' 1159 | uNode.fbxExportProperties = fbxDict 1160 | elif index == 1: 1161 | fbxDict['animInterpolation'] = 'euler' 1162 | uNode.fbxExportProperties = fbxDict 1163 | else: 1164 | fbxDict['animInterpolation'] = 'resample' 1165 | uNode.fbxExportProperties = fbxDict 1166 | 1167 | def buildMissingFilesTree(self): 1168 | missingFileDict = self.missingNodes() 1169 | for f in missingFileDict.keys(): 1170 | wid1 = QtWidgets.QTreeWidgetItem() 1171 | font = wid1.font(0) 1172 | font.setPointSize(15) 1173 | 1174 | wid1.setText(0,f) 1175 | 1176 | wid1.setText(2, missingFileDict[f]['path']) 1177 | wid1.setText(3, missingFileDict[f]['node']) 1178 | self.missingFilesTree.addTopLevelItem(wid1) 1179 | wid1.selectMe = missingFileDict[f]['node'] 1180 | 1181 | self.missingFilesTree.header().resizeSections(QtWidgets.QHeaderView.ResizeToContents) 1182 | 1183 | def createUexportNode_FN(self): 1184 | if cmds.ls(sl=1): 1185 | if self.useRoot_CHK.isChecked(): 1186 | rootName = self.rootName_CMB.currentText() 1187 | self.create(renderMeshes=cmds.ls(sl=1), rootJoint=rootName, lods=self.lodNum_SPIN.value()) 1188 | else: 1189 | #TODO: modal picker with joint filter 1190 | pass 1191 | 1192 | def getTexturesP4_FN(self): 1193 | missingFileDict = self.missingNodes() 1194 | self.rePathFileNodesP4(missingFileDict) 1195 | 1196 | def export_FN(self): 1197 | #get what should be export in an exportTask fn 1198 | exportWidgets = self.getExportNodeWidgets() 1199 | for wid in exportWidgets: 1200 | snapConst = None 1201 | if self.snapRoot_CHK.isChecked(): 1202 | node = self.snapRoot_CMB.currentText() 1203 | try: 1204 | if cmds.objExists(node): 1205 | snapConst = cmds.parentConstraint(node, wid.uExport.export_root) 1206 | else: 1207 | cmds.error('Unable to find node: ' + node) 1208 | except: 1209 | cmds.warning('Could not constrain root' + wid.uExport.export_root + ', is it already constrained?') 1210 | #export 1211 | mpath = cmds.file(sceneName=1, q=1) 1212 | fname = mpath.split('/')[-1] 1213 | fpath = mpath.replace(fname,'').replace('/','\\') 1214 | if wid.uExport.fbx_name: 1215 | if wid.uExport.fbx_name != '': 1216 | fname = wid.uExport.fbx_name 1217 | if wid.uExport.folder_path: 1218 | if wid.uExport.folder_path != '': 1219 | fpath = wid.uExport.folder_path 1220 | 1221 | #prompt user for save location 1222 | userPath = str() 1223 | if self.suppressSaveCHK.isChecked(): 1224 | userPath = "{0}/{1}".format(wid.uExport.folder_path, wid.uExport.fbx_name) 1225 | if not os.path.isfile(userPath): 1226 | userPath = QtWidgets.QFileDialog.getSaveFileName(caption = 'Export ' + wid.uExport.name + ' to:', filter='FBX Files (*.fbx)', dir=fpath + '\\' + fname)[0] 1227 | else: 1228 | userPath = QtWidgets.QFileDialog.getSaveFileName(caption = 'Export ' + wid.uExport.name + ' to:', filter='FBX Files (*.fbx)', dir=fpath + '\\' + fname)[0] 1229 | 1230 | if userPath: 1231 | wid.uExport.fbx_name = userPath.split('/')[-1] 1232 | wid.uExport.folder_path = userPath.replace(wid.uExport.fbx_name,'') 1233 | 1234 | #if set to save loc, save it 1235 | if self.rememberSaveCHK.isChecked(): 1236 | cmds.setAttr(wid.uExport.name + '.folder_path', wid.uExport.folder_path, type='string') 1237 | cmds.setAttr(wid.uExport.name + '.fbx_name', wid.uExport.fbx_name, type='string') 1238 | 1239 | #set initial lod save names 1240 | if not wid.uExport.fbx_name_LOD0: 1241 | FbxBaseName = wid.uExport.fbx_name.split('.')[0] 1242 | wid.uExport.fbx_name_LOD0 = wid.uExport.fbx_name 1243 | wid.uExport.fbx_name_LOD1 = FbxBaseName + '_LOD1.fbx' 1244 | wid.uExport.fbx_name_LOD2 = FbxBaseName + '_LOD2.fbx' 1245 | wid.uExport.fbx_name_LOD3 = FbxBaseName + '_LOD3.fbx' 1246 | wid.uExport.fbx_name_LOD4 = FbxBaseName + '_LOD4.fbx' 1247 | 1248 | 1249 | meshChk = 1 1250 | animChk = 1 1251 | for c in range(0, wid.childCount()): 1252 | if 'MESH' in wid.child(c).text(0): 1253 | if wid.child(c).checkState(0) == QtCore.Qt.Checked: 1254 | pass 1255 | else: 1256 | meshChk = 0 1257 | 1258 | start = time.time() 1259 | #put export into the uExport class later 1260 | export_success = self.export(wid.uExport, path=userPath, mesh=meshChk) 1261 | if export_success: 1262 | elapsed = (time.time() - start) 1263 | print 'uExport>>> Exported ', wid.text(0), 'to', userPath ,'in %.2f seconds.' % elapsed 1264 | 1265 | else: 1266 | cmds.warning('Invalid path specified: [' + str(userPath) + ']') 1267 | #cleanup constraint 1268 | if snapConst: cmds.delete(snapConst) 1269 | 1270 | def getExportNodeWidgets(self): 1271 | nodes = [] 1272 | for i in range(0, self.export_tree.topLevelItemCount()): 1273 | if self.export_tree.topLevelItem(i).checkState(0) == QtCore.Qt.Checked: 1274 | nodes.append(self.export_tree.topLevelItem(i)) 1275 | return nodes 1276 | 1277 | 1278 | def check_status(self): 1279 | for i in range(0, self.export_tree.topLevelItemCount()): 1280 | if self.export_tree.topLevelItem(i).checkState(0) == QtCore.Qt.Unchecked: 1281 | for c in range(0, self.export_tree.topLevelItem(i).childCount()): 1282 | self.export_tree.topLevelItem(i).child(c).setCheckState(0,QtCore.Qt.Unchecked) 1283 | 1284 | def itemClicked(self, *args): 1285 | #select nodes if there is selection metadata 1286 | if self.mousePress is 'left': 1287 | if hasattr(args[0], 'selectMe'): 1288 | cmds.select(args[0].selectMe) 1289 | 1290 | 1291 | ## P4 CRAP 1292 | ######################################################################## 1293 | #check that P4 exists 1294 | def getP4Location(self, asset, root='//depot/ArtSource/', debug=1): 1295 | from p4python.P4 import P4, P4Exception 1296 | 1297 | if asset: 1298 | p4 = P4() 1299 | try: p4.connect() 1300 | except: 1301 | print 'Cannot connect to P4!' 1302 | return False 1303 | 1304 | try: 1305 | file = p4.run_files(root + '...' + asset) 1306 | depotLoc = file[0]['depotFile'] 1307 | describe = p4.run_describe(file[0]['change']) 1308 | return [describe[0]['user'], depotLoc, describe[0]['desc'], file[0]['change']] 1309 | except Exception as e: 1310 | print "findFileP4>>>> Cannot find file.", asset 1311 | if debug: print e 1312 | return False 1313 | finally: 1314 | p4.disconnect() 1315 | 1316 | def workspaceSelected(self): 1317 | self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat,QtCore.QSettings.SystemScope, 'uExport', 'settings') 1318 | self.settings.setFallbacksEnabled(False) 1319 | # setPath() to try to save to current working directory 1320 | self.settings.setPath(QtCore.QSettings.IniFormat,QtCore.QSettings.SystemScope, './uExport_settings.ini') 1321 | self.settings.value('workspace', self.workSpaceCMB.currentText()) 1322 | 1323 | def getP4Workspaces(self): 1324 | import socket 1325 | from p4python.P4 import P4, P4Exception 1326 | 1327 | #get computer name 1328 | host = socket.gethostname() 1329 | 1330 | p4 = P4() 1331 | try: p4.connect() 1332 | except: 1333 | print 'Cannot connect to P4!' 1334 | return False 1335 | 1336 | for ws in p4.run('clients', '-u', p4.user): 1337 | try: 1338 | if ws['Host'] == host: 1339 | self.workSpaceCMB.addItem(ws['client']) 1340 | except Exception as e: 1341 | print e 1342 | p4.disconnect() 1343 | 1344 | def colorTreeWidgetItemByName(self, tree, text, color): 1345 | root = tree.invisibleRootItem() 1346 | child_count = root.childCount() 1347 | retWid = None 1348 | for i in range(child_count): 1349 | wid = root.child(i) 1350 | if wid.text(0) == text: 1351 | wid.setForeground(0, color) 1352 | wid.setForeground(1, color) 1353 | wid.setForeground(2, color) 1354 | wid.setForeground(3, color) 1355 | retWid = wid 1356 | if retWid: 1357 | return retWid 1358 | 1359 | def rePathFileNodesP4(self, missingFileDict, debug=1, checkHash=False): 1360 | from p4python.P4 import P4, P4Exception 1361 | 1362 | if missingFileDict: 1363 | p4 = P4() 1364 | print self.workSpaceCMB.currentText() 1365 | p4.client = str(self.workSpaceCMB.currentText()) 1366 | try: p4.connect() 1367 | except: 1368 | print 'Cannot connect to P4!' 1369 | return False 1370 | 1371 | for f in missingFileDict.keys(): 1372 | floppedPath = missingFileDict[f]['path'].replace('\\', '/') + f 1373 | pathBreak = floppedPath.split('/') 1374 | 1375 | #find parents since there are dupe files in p4 often 1376 | parent = None 1377 | if len(pathBreak) > 1: 1378 | parent = pathBreak[-2] 1379 | parentParent = None 1380 | if len(pathBreak) > 2: 1381 | parentParent = pathBreak[-3] 1382 | 1383 | depotFileToGrab = None 1384 | 1385 | files = None 1386 | try: 1387 | files = p4.run_files(self.p4RootLINE.text() + '...' + f) 1388 | except: 1389 | pass 1390 | if files: 1391 | if len(files) > 1: 1392 | if debug: 1393 | print 'rePathFileNodesP4>>>> Multiple files [', len(files), '] found in depot search path with the name: ' + f 1394 | print 'P4 file paths:' 1395 | for i in range(0, len(files)): 1396 | print files[i]['depotFile'] 1397 | for i in range(0, len(files)): 1398 | try: 1399 | newParent = files[i]['depotFile'].split('/')[-2] 1400 | if newParent == parent: 1401 | if 'delete' in p4.run('fstat', files[i]['depotFile'])[0]['headAction']: 1402 | print 'INVALID PATH: p4 asset head revision is deleted or moved. depotFile:', files[i]['depotFile'] 1403 | continue 1404 | depotFileToGrab = files[i]['depotFile'] 1405 | continue 1406 | if checkHash: 1407 | dupeCheck(files[i]['depotFile']) 1408 | 1409 | #color widget red, change INFO 1410 | widc = self.colorTreeWidgetItemByName(self.missingFilesTree, f, QtGui.QColor(200, 75, 75, 255)) 1411 | widc.setText(1, 'NOT FOUND') 1412 | 1413 | except Exception as e: 1414 | if debug: 1415 | print e 1416 | print 'rePathFileNodesP4>>>> No file path similarities to path:', floppedPath 1417 | else: 1418 | print 'rePathFileNodesP4>>>> File found on depot: ', files[0]['depotFile'] 1419 | depotFileToGrab = files[0]['depotFile'] 1420 | 1421 | else: 1422 | print 'rePathFileNodesP4>>>> FILE NOT FOUND IN PERFORCE SEARCH PATH - fName:', f, 'searchPath:', self.p4RootLINE.text() 1423 | #color widget red, change INFO 1424 | widc = self.colorTreeWidgetItemByName(self.missingFilesTree, f, QtGui.QColor(200, 75, 75, 255)) 1425 | widc.setText(1, 'NOT FOUND') 1426 | 1427 | 1428 | #if we found it in the depot 1429 | if depotFileToGrab: 1430 | print 'GRABBING: ', depotFileToGrab 1431 | try: 1432 | p4.run('sync', ' -f', depotFileToGrab) 1433 | except Exception as e: 1434 | if 'up-to-date' in e.message: 1435 | pass 1436 | else: 1437 | print e 1438 | 1439 | fileFstat = p4.run('fstat', depotFileToGrab)[0] 1440 | print fileFstat 1441 | newPath = fileFstat['clientFile'] 1442 | newPath = newPath.replace('\\','/') 1443 | 1444 | if debug: print 'rePathFileNodesP4>>>> NEW PATH>>', newPath, '\n\n' 1445 | cmds.setAttr((missingFileDict[f]['node'] + '.fileTextureName'), newPath, type='string') 1446 | #color widget green, change info 1447 | widc = self.colorTreeWidgetItemByName(self.missingFilesTree, f, QtGui.QColor(40, 230, 160, 255)) 1448 | widc.setText(1, 'FOUND') 1449 | 1450 | self.repaint() 1451 | 1452 | p4.disconnect() 1453 | 1454 | 1455 | ## UEXPORT NODE 1456 | ######################################################################## 1457 | def getExportNodes(self): 1458 | return cmds.ls('*.uexport_ver', o=1, r=1) 1459 | 1460 | def connectRoot(self, uNode, root, rewire=1): 1461 | try: 1462 | cmds.undoInfo(openChunk=True) 1463 | if rewire: 1464 | conns = cmds.listConnections(uNode + '.export_root', plugs=1, source=1) 1465 | if conns: 1466 | for conn in conns: 1467 | cmds.disconnectAttr(conn, uNode + '.export_root') 1468 | 1469 | if not attrExists(root+'.export'): 1470 | cmds.addAttr(root, longName='export', attributeType='message') 1471 | cmds.connectAttr(root + '.export', uNode + '.export_root' ) 1472 | except Exception as e: 1473 | print e 1474 | finally: 1475 | cmds.undoInfo(closeChunk=True) 1476 | 1477 | def create(self, renderMeshes=None, rootJoint=None, strName='uExport', lods=1): 1478 | 1479 | uExportNode = None 1480 | if cmds.objExists(strName): 1481 | #later re-hook up 1482 | #set uExport 1483 | cmds.warning('uExport>>>>> uExport node already exists with name: ' + strName) 1484 | pass 1485 | else: 1486 | try: 1487 | cmds.undoInfo(openChunk=True) 1488 | 1489 | text, ok = QtWidgets.QInputDialog.getText(None, 'Creating uExport Node', 'Enter node name:', text='uExport') 1490 | if text: 1491 | strName = text 1492 | 1493 | uExportNode = cmds.group(em=1, name=strName) 1494 | cmds.addAttr(uExportNode, ln='export_root', at='message') 1495 | cmds.addAttr(uExportNode, ln='materials', at='message') 1496 | cmds.addAttr(uExportNode, ln='uexport_ver', dt='string') 1497 | cmds.setAttr(uExportNode + '.uexport_ver', '1.0', type='string') 1498 | cmds.addAttr(uExportNode, ln='folder_path', dt='string') 1499 | cmds.addAttr(uExportNode, ln='asset_name', dt='string') 1500 | cmds.addAttr(uExportNode, ln='fbx_name', dt='string') 1501 | cmds.addAttr(uExportNode, ln='lodNum', at='byte') 1502 | 1503 | cmds.setAttr(uExportNode + '.lodNum', lods) 1504 | 1505 | if self.createArtMetadata_CHK.isChecked(): 1506 | #not used atm 1507 | cmds.addAttr(uExportNode, ln='joint_mover_template', dt='string') 1508 | cmds.addAttr(uExportNode, ln='skeleton_template', dt='string') 1509 | cmds.addAttr(uExportNode, ln='pre_script', dt='string') 1510 | cmds.addAttr(uExportNode, ln='post_script', dt='string') 1511 | cmds.addAttr(uExportNode, ln='export_file', dt='string') 1512 | cmds.addAttr(uExportNode, ln='anim_file', dt='string') 1513 | cmds.addAttr(uExportNode, ln='skeleton_uasset', dt='string') 1514 | cmds.addAttr(uExportNode, ln='skelmesh_uasset', dt='string') 1515 | cmds.addAttr(uExportNode, ln='physics_uasset', dt='string') 1516 | cmds.addAttr(uExportNode, ln='thumbnail_large', dt='string') 1517 | cmds.addAttr(uExportNode, ln='thumbnail_small', dt='string') 1518 | 1519 | text, ok = QtWidgets.QInputDialog.getText(None, 'Defining Asset', 'Enter asset name:', text='myAsset') 1520 | 1521 | if text: 1522 | #internal ART stuff 1523 | cmds.setAttr(uExportNode + '.asset_name', text, type='string') 1524 | 1525 | except Exception as e: 1526 | cmds.warning(e) 1527 | print 'Locals: ' + str(locals()) 1528 | finally: 1529 | cmds.undoInfo(closeChunk=True) 1530 | 1531 | if uExportNode: 1532 | 1533 | uNode = uExport(uExportNode) 1534 | 1535 | try: 1536 | if renderMeshes: 1537 | uNode.rendermeshes_LOD0 = renderMeshes 1538 | 1539 | #rootJoint 1540 | if rootJoint: 1541 | if not attrExists(rootJoint+'.export'): 1542 | cmds.addAttr(rootJoint, longName='export', attributeType='message') 1543 | cmds.connectAttr(rootJoint + '.export', uExportNode + '.export_root') 1544 | else: 1545 | cmds.warning('No root joint or could not find root: ' + str(rootJoint)) 1546 | 1547 | except Exception as e: 1548 | print cmds.warning(e) 1549 | print 'Locals: ' + str(locals()) 1550 | 1551 | self.tabWidget.setCurrentIndex(0) 1552 | self.refreshUI() 1553 | 1554 | 1555 | ## MISSING TEXTURES 1556 | ######################################################################## 1557 | 1558 | def missingNodes(self): 1559 | fileNodes = cmds.ls(type='file') 1560 | missingFiles = {} 1561 | for f in fileNodes: 1562 | filePath = cmds.getAttr(f + '.fileTextureName') 1563 | if filePath != '': 1564 | if not cmds.file(filePath, exists=1, q=1): 1565 | fileName = filePath.split('/')[-1] 1566 | path = filePath.replace(fileName,'') 1567 | missingFiles[fileName] = {'path':path, 'node':f} 1568 | return missingFiles 1569 | 1570 | 1571 | ## SANITY CHECK 1572 | ######################################################################## 1573 | 1574 | #general methods 1575 | def hasPrefix(self, obj, prefix, fix=1): 1576 | if obj.startswith(prefix): 1577 | return True 1578 | else: 1579 | return False 1580 | 1581 | #triggers 1582 | #TODO: remove issue class usage, hook up 1583 | ''' 1584 | def matCheck(self, meshes, debug=1, mNum=1): 1585 | import re 1586 | 1587 | issues = [] 1588 | allShaders = [] 1589 | 1590 | for mesh in meshes: 1591 | 1592 | shaders = [] 1593 | shaders.extend(getAssocShaders(mesh)) 1594 | 1595 | shaders = set(shaders) 1596 | if debug: 1597 | print 'Shaders found:', shaders 1598 | 1599 | for shader in shaders: 1600 | 1601 | allShaders.append(shader) 1602 | 1603 | shaderNum = re.search(r'\d+$', shader).group() 1604 | if not shaderNum: 1605 | issue = Issue(shader, 'Shader [' + shader + '] does not end in a number', assocNode=mesh) 1606 | issues.append(issue) 1607 | 1608 | if not hasPrefix(shader,'M_'): 1609 | issue = Issue(shader, 'Shader [' + shader + '] does not have the prefix \'M_\'', assocNode=mesh) 1610 | issues.append(issue) 1611 | #FIX 1612 | if shader.startswith('m_'): 1613 | print shader 1614 | shader[0:1] = 'M_' 1615 | #cmds.rename(shader, ) 1616 | 1617 | if '_skin' not in shader: 1618 | issue = Issue(shader, 'Shader [' + shader + '] does not have the suffix \'_skin\'', assocNode=mesh) 1619 | issues.append(issue) 1620 | 1621 | shaderDict = {} 1622 | for shader in set(allShaders): 1623 | shaderNum = re.search(r'\d+$', shader).group() 1624 | if shaderNum in shaderDict.keys(): 1625 | issue = Issue(shader, 'Shader [' + shader + '] shader with slot number already exists: ' + shaderDict[shaderNum], assocNode=shaderDict[shaderNum]) 1626 | issues.append(issue) 1627 | else: 1628 | shaderDict[shaderNum] = shader 1629 | 1630 | if mNum: 1631 | for key in shaderDict.keys(): 1632 | pass 1633 | #cmds.rename(shaderDict[key], ) 1634 | 1635 | return issues 1636 | ''' 1637 | 1638 | 1639 | ## EXPORT 1640 | ######################################################################## 1641 | 1642 | #TODO: Find and export blendshape meshes! 1643 | def setExportFlags(self, uNode): 1644 | 1645 | # set export properties from the fbxExportPropertiesDict of the uNode 1646 | fbxDict = uNode.fbxExportProperties 1647 | if fbxDict['triangulation'] == True: 1648 | mel.eval("FBXExportTriangulate -v true") 1649 | else: 1650 | mel.eval("FBXExportTriangulate -v false") 1651 | 1652 | # Mesh 1653 | mel.eval("FBXExportSmoothingGroups -v true") 1654 | mel.eval("FBXExportHardEdges -v false") 1655 | mel.eval("FBXExportTangents -v true") 1656 | mel.eval("FBXExportInstances -v false") 1657 | mel.eval("FBXExportInAscii -v true") 1658 | mel.eval("FBXExportSmoothMesh -v false") 1659 | 1660 | # Animation 1661 | mel.eval("FBXExportBakeResampleAnimation -v true") 1662 | mel.eval("FBXExportBakeComplexAnimation -v true") 1663 | mel.eval("FBXExportBakeComplexStart -v "+str(cmds.playbackOptions(minTime=1, q=1))) 1664 | mel.eval("FBXExportBakeComplexEnd -v "+str(cmds.playbackOptions(maxTime=1, q=1))) 1665 | mel.eval("FBXExportReferencedAssetsContent -v true") 1666 | mel.eval("FBXExportBakeComplexStep -v 1") 1667 | mel.eval("FBXExportUseSceneName -v false") 1668 | mel.eval("FBXExportQuaternion -v quaternion") 1669 | mel.eval("FBXExportShapes -v true") 1670 | mel.eval("FBXExportSkins -v true") 1671 | 1672 | if fbxDict['animInterpolation'] == 'euler': 1673 | mel.eval("FBXExportQuaternion -v euler") 1674 | elif fbxDict['animInterpolation'] == 'resample': 1675 | mel.eval("FBXExportQuaternion -v resample") 1676 | 1677 | if fbxDict['upAxis'].lower() == 'y': 1678 | print 'FBX EXPORT OVERRIDE: setting y up axis' 1679 | mel.eval("FBXExportUpAxis y") 1680 | elif fbxDict['upAxis'].lower() == 'z': 1681 | print 'FBX EXPORT OVERRIDE: setting Z up axis' 1682 | mel.eval("FBXExportUpAxis z") 1683 | 1684 | #garbage we don't want 1685 | # Constraints 1686 | mel.eval("FBXExportConstraints -v false") 1687 | # Cameras 1688 | mel.eval("FBXExportCameras -v false") 1689 | # Lights 1690 | mel.eval("FBXExportLights -v false") 1691 | # Embed Media 1692 | mel.eval("FBXExportEmbeddedTextures -v false") 1693 | # Connections 1694 | mel.eval("FBXExportInputConnections -v false") 1695 | 1696 | def export(self, uNode, mesh=1, anim=1, path=None,bake=False): 1697 | 1698 | # check if the file is checked out/writeable 1699 | if os.path.isfile(path): 1700 | file_stat = os.stat(path)[0] 1701 | if not file_stat & stat.S_IWRITE: 1702 | message = "Please ensure you have {0} checked out".format(path) 1703 | return QtWidgets.QMessageBox.information(QtWidgets.QWidget(), "Export Warning", message) 1704 | 1705 | toExport = [] 1706 | 1707 | if not path: 1708 | oldPath = path 1709 | path = cmds.file(sceneName=1, q=1) 1710 | cmds.warning('No valid path set for export [' + str(oldPath) + ']/nExporting to Maya file loc: ' + path) 1711 | toExport.extend(cmds.listRelatives(uNode.export_root, type='joint',allDescendents=True,f=1)) 1712 | 1713 | 1714 | #kvassey -- adding support for baking to root in rig before export 1715 | if self.bakeRoot_CHK.isChecked(): 1716 | print "Bake Root Checked" 1717 | #copy root skeleton with input connections under new group 1718 | cmds.select(cl=True) 1719 | currRoot = uNode.export_root 1720 | exportSkel = cmds.duplicate(currRoot, un=True, rc=False, po=False) 1721 | tempGrp = cmds.group(exportSkel[0]) 1722 | #rename root joint 1723 | dupRoot = cmds.rename(exportSkel[0], currRoot) 1724 | #bake 1725 | startTime = cmds.playbackOptions(min=True, q=True) 1726 | endTime = cmds.playbackOptions(max=True, q=True) 1727 | cmds.bakeResults(dupRoot, sm=True, hi="below", s=True, sb=1, dic=True, t=(startTime, endTime)) 1728 | 1729 | #move FBX export inside here, skip rest. 1730 | #toExport.extend(cmds.listRelatives(cmds.listConnections(dupRoot), type='joint',allDescendents=True)) 1731 | #cmds.select(toExport) 1732 | cmds.select(dupRoot, r=True, hi=True) 1733 | toExport = cmds.ls(sl=True, type='joint') 1734 | cmds.select(toExport, r=True) 1735 | self.setExportFlags(uNode) 1736 | # Export! 1737 | print "FBXExport -f \""+ path +"\" -s" 1738 | mel.eval("FBXExport -f \""+ path +"\" -s") 1739 | 1740 | cmds.delete(tempGrp) 1741 | toExport = [] 1742 | 1743 | 1744 | #Assuming the meshes are skinned, maybe work with static later 1745 | else: 1746 | exportScript = False 1747 | 1748 | if mesh: 1749 | #we're not exporting LODs 1750 | if not self.exportLODs_CHK.isChecked(): 1751 | meshes = uNode.rendermeshes_LOD0 1752 | if meshes: 1753 | toExport.extend(uNode.rendermeshes_LOD0) 1754 | else: 1755 | cmds.warning('uExport>>> export: No rendermeshes found.') 1756 | #ok, we're exporting LODs 1757 | else: 1758 | lodDicts = uNode.getLodDicts() 1759 | if lodDicts: 1760 | if self.exportLODs_CHK.isChecked(): 1761 | for lodNum in lodDicts: 1762 | #check that there are meshes to be exported at this LOD 1763 | if lodDicts[lodNum]['meshes']: 1764 | #check if there is a script 1765 | if lodDicts[lodNum]['export_script']: 1766 | if self.resetAfterExport_CHK.isChecked(): 1767 | # Warn the user that this operation will save their file. 1768 | saveResult = 'Yes' 1769 | if not self.suppressSaveCHK.isChecked(): 1770 | saveResult = cmds.confirmDialog(title = "Save Warning", message="In order to continue your file must be saved. Would you like to save it? If yes it will be saved and after the operation is complete your file will be re-opened.", button = ["Save and Continue", "Continue Without Saving"], defaultButton='Save and Continue', cancelButton='Continue Without Saving', dismissString='Continue Without Saving') 1771 | if saveResult == 'Yes': 1772 | cmds.file(save=True) 1773 | else: 1774 | cmds.warning('You chose not to save changes. Big-boy pants.') 1775 | 1776 | filePath = lodDicts[lodNum]['export_script'] 1777 | 1778 | exportScript = True 1779 | if os.path.isfile(filePath): 1780 | uExportNode = uNode 1781 | execfile(filePath) 1782 | else: 1783 | cmds.error('UEXPORT>> Cannot find export script: ' + filePath) 1784 | 1785 | #add items for export 1786 | toExport.extend(lodDicts[lodNum]['meshes']) 1787 | toExport.extend(cmds.listRelatives(uNode.export_root, type='joint',allDescendents=True, f=1)) 1788 | 1789 | #setup export 1790 | cmds.select(toExport) 1791 | self.setExportFlags(uNode) 1792 | new_fpath = path[:-4] + '_LOD' + str(lodNum) + '.fbx' 1793 | 1794 | #look for lod name overrides 1795 | if lodDicts[lodNum]['fbx_name']: 1796 | justFilePath = path.replace(path.split('/')[-1],'') 1797 | new_fpath = justFilePath + lodDicts[lodNum]['fbx_name'] 1798 | 1799 | # Export! 1800 | print "FBXExport -f \"" + new_fpath + "\" -s" 1801 | mel.eval("FBXExport -f \"" + new_fpath + "\" -s") 1802 | 1803 | if exportScript: 1804 | # Re-open the file without saving. 1805 | fullPath = cmds.file(q = True, sceneName = True) 1806 | cmds.file(fullPath, open=True, f=True) 1807 | 1808 | #clear sel and export list 1809 | cmds.select(d=1) 1810 | toExport = [] 1811 | 1812 | if not self.exportLODs_CHK.isChecked(): 1813 | if anim: 1814 | toExport.extend(cmds.listRelatives(uNode.export_root, type='joint', allDescendents=True, f=1) or []) 1815 | 1816 | cmds.select(toExport) 1817 | self.setExportFlags(uNode) 1818 | # Export! 1819 | print "FBXExport -f \""+ path +"\" -s" 1820 | mel.eval("FBXExport -f \""+ path +"\" -s") 1821 | return True 1822 | 1823 | 1824 | if __name__ == '__main__': 1825 | show() 1826 | -------------------------------------------------------------------------------- /uExport.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 464 10 | 572 11 | 12 | 13 | 14 | uExport v1 15 | 16 | 17 | 18 | 2 19 | 20 | 21 | 3 22 | 23 | 24 | 25 | 26 | 0 27 | 28 | 29 | 30 | EXPORT 31 | 32 | 33 | 34 | 2 35 | 36 | 37 | 3 38 | 39 | 40 | 41 | 42 | QFrame::Plain 43 | 44 | 45 | true 46 | 47 | 48 | 49 | 1 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Export LODs 60 | 61 | 62 | 63 | 64 | 65 | 66 | Qt::Horizontal 67 | 68 | 69 | 70 | 40 71 | 20 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Qt::Horizontal 80 | 81 | 82 | 83 | 40 84 | 20 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | false 93 | 94 | 95 | 96 | 7 97 | 75 98 | true 99 | 100 | 101 | 102 | fbxVer 103 | 104 | 105 | 106 | 107 | 108 | 109 | p4connect 110 | 111 | 112 | true 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 16 123 | 75 124 | true 125 | 126 | 127 | 128 | EXPORT SELECTED ITEMS 129 | 130 | 131 | 132 | 133 | 134 | 135 | 3 136 | 137 | 138 | QLayout::SetMinimumSize 139 | 140 | 141 | 142 | 143 | 144 | 67 145 | 16777215 146 | 147 | 148 | 149 | 150 | 10 151 | 75 152 | true 153 | 154 | 155 | 156 | REFRESH 157 | 158 | 159 | 160 | 161 | 162 | 163 | Qt::Horizontal 164 | 165 | 166 | 167 | 40 168 | 20 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | Remember save locations 177 | 178 | 179 | true 180 | 181 | 182 | 183 | 184 | 185 | 186 | Suppress save dlg 187 | 188 | 189 | true 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | ADV 200 | 201 | 202 | 203 | 204 | 205 | 206 | 11 207 | 75 208 | true 209 | 210 | 211 | 212 | UEXPORT NODE MANAGEMENT 213 | 214 | 215 | 216 | 2 217 | 218 | 219 | 3 220 | 221 | 222 | 223 | 224 | 225 | 226 | false 227 | 228 | 229 | 230 | 10 231 | 50 232 | false 233 | 234 | 235 | 236 | Ask me to choose root 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 10 245 | 50 246 | false 247 | 248 | 249 | 250 | Use root name: 251 | 252 | 253 | true 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 16777215 262 | 21 263 | 264 | 265 | 266 | 267 | 10 268 | 50 269 | false 270 | 271 | 272 | 273 | true 274 | 275 | 276 | 277 | root 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 10 287 | 50 288 | false 289 | 290 | 291 | 292 | LOD# 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 33 301 | 21 302 | 303 | 304 | 305 | 306 | 10 307 | 50 308 | false 309 | 310 | 311 | 312 | 10 313 | 314 | 315 | 4 316 | 317 | 318 | 319 | 320 | 321 | 322 | Qt::Horizontal 323 | 324 | 325 | 326 | 40 327 | 20 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 8 341 | 75 342 | true 343 | 344 | 345 | 346 | Stamp ART mData 347 | 348 | 349 | 350 | 351 | 352 | 353 | false 354 | 355 | 356 | 357 | 8 358 | 50 359 | false 360 | 361 | 362 | 363 | D:\Build\usr\jeremy_ernst\MayaTools 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 8 374 | 375 | 376 | 377 | Stamp Perforce metadata 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | CREATE NODE 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 11 400 | 75 401 | true 402 | 403 | 404 | 405 | FBX EXPORT OPTIONS 406 | 407 | 408 | 409 | 2 410 | 411 | 412 | 3 413 | 414 | 415 | 416 | 417 | 418 | 419 | Animation Interpolation: 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 10 428 | 50 429 | false 430 | 431 | 432 | 433 | Quaternion 434 | 435 | 436 | true 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 10 445 | 50 446 | false 447 | 448 | 449 | 450 | Euler 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 10 459 | 50 460 | false 461 | 462 | 463 | 464 | Resample 465 | 466 | 467 | 468 | 469 | 470 | 471 | Qt::Horizontal 472 | 473 | 474 | 475 | 40 476 | 20 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | Stated Up Axis: 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 10 497 | 50 498 | false 499 | 500 | 501 | 502 | 503 | Scene Default 504 | 505 | 506 | 507 | 508 | Y 509 | 510 | 511 | 512 | 513 | Z 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | Qt::Horizontal 522 | 523 | 524 | 525 | 40 526 | 20 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 11 541 | 75 542 | true 543 | 544 | 545 | 546 | LODSCRIPT EXPORT OPTIONS 547 | 548 | 549 | 550 | 551 | 552 | 553 | 10 554 | 50 555 | false 556 | 557 | 558 | 559 | Reset scenes after export scripts 560 | 561 | 562 | true 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 10 571 | 50 572 | false 573 | 574 | 575 | 576 | Suppress LOD save dlgs 577 | 578 | 579 | true 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | Qt::Vertical 590 | 591 | 592 | 593 | 20 594 | 40 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | TOOLS 604 | 605 | 606 | 607 | 3 608 | 609 | 610 | 3 611 | 612 | 613 | 3 614 | 615 | 616 | 2 617 | 618 | 619 | 620 | 621 | 622 | 75 623 | true 624 | 625 | 626 | 627 | CINEMATICS 628 | 629 | 630 | 631 | 2 632 | 633 | 634 | 3 635 | 636 | 637 | 638 | 639 | 640 | 641 | true 642 | 643 | 644 | 645 | 50 646 | false 647 | 648 | 649 | 650 | Snap root: 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 155 659 | 0 660 | 661 | 662 | 663 | 664 | 50 665 | false 666 | 667 | 668 | 669 | true 670 | 671 | 672 | 673 | NONE 674 | 675 | 676 | 677 | 678 | Kite_Boy:master_anim 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | Qt::Horizontal 687 | 688 | 689 | 690 | 40 691 | 20 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 50 703 | false 704 | 705 | 706 | 707 | Bake Root (exporting attrs on joints pre Maya 2016 Ext 2) 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 75 719 | true 720 | 721 | 722 | 723 | A.R.T. UTILITIES 724 | 725 | 726 | 727 | 2 728 | 729 | 730 | 3 731 | 732 | 733 | 734 | 735 | 736 | 10 737 | 75 738 | true 739 | 740 | 741 | 742 | SWAP UNKNOWN ART NODES WITH NETWORK NODES 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 75 754 | true 755 | 756 | 757 | 758 | PERFORCE OPTIONS 759 | 760 | 761 | 762 | 2 763 | 764 | 765 | 3 766 | 767 | 768 | 769 | 770 | 3 771 | 772 | 773 | 774 | 775 | 2 776 | 777 | 778 | 779 | 780 | 781 | 50 782 | false 783 | 784 | 785 | 786 | Project Root: 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 50 795 | false 796 | 797 | 798 | 799 | //depot/ArtSource/Orion/ 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 3 809 | 810 | 811 | 812 | 813 | 814 | 1 815 | 0 816 | 817 | 818 | 819 | 820 | 71 821 | 16777215 822 | 823 | 824 | 825 | WorkSpace: 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 75 844 | true 845 | 846 | 847 | 848 | MISSING TEXTURES 849 | 850 | 851 | 852 | 2 853 | 854 | 855 | 3 856 | 857 | 858 | 859 | 860 | 4 861 | 862 | 863 | 864 | FILE 865 | 866 | 867 | 868 | 869 | INFO 870 | 871 | 872 | 873 | 874 | PATH 875 | 876 | 877 | 878 | 879 | NODE 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | false 890 | 891 | 892 | 893 | 11 894 | 75 895 | true 896 | 897 | 898 | 899 | FIND, SYNC, AND REPATH FILES (P4) 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | createArtMetadata_CHK 918 | toggled(bool) 919 | artPath 920 | setEnabled(bool) 921 | 922 | 923 | 104 924 | 421 925 | 926 | 927 | 193 928 | 423 929 | 930 | 931 | 932 | 933 | 934 | --------------------------------------------------------------------------------