TressFX_Exporter.py 86 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR MIT
  5. #
  6. #
  7. #-------------------------------------------------------------------------------------
  8. #
  9. # Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved.
  10. #
  11. # Permission is hereby granted, free of charge, to any person obtaining a copy
  12. # of this software and associated documentation files (the "Software"), to deal
  13. # in the Software without restriction, including without limitation the rights
  14. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. # copies of the Software, and to permit persons to whom the Software is
  16. # furnished to do so, subject to the following conditions:
  17. #
  18. # The above copyright notice and this permission notice shall be included in
  19. # all copies or substantial portions of the Software.
  20. #
  21. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. # THE SOFTWARE.
  28. #
  29. #-------------------------------------------------------------------------------------
  30. #
  31. # 1. Copy tressFX_Exporter.py into Maya plug-in path. The easy place would be C:\Users\YOUR_USER_NAME\Documents\maya\plug-ins\
  32. # 2. If you want to install TressFX menu, go to Windows->Plug-in Managers menu and check Loaded and Auto load for TressFX_Exporter.py.
  33. # 3. Then TressFX menu would appear in the top menu bar.
  34. # 4. Within it, there should be export menu item. Click it and it will bring up TressFX Exporter window.
  35. # 5. Alternatively, without loading TressFX plugin in Plug-in Managers, you can simply run the following python script commands in Script Editor
  36. # and bring up TressFX Exporter window.
  37. #
  38. # import TressFX_Exporter
  39. # reload(TressFX_Exporter)
  40. # TressFX_Exporter.UI()
  41. #
  42. # Or, it's easy to run this method for loading from a script, from python (python tab of script editor)
  43. #
  44. # Following is a script to unload and load this plugin. It may be useful to reload the plugin quickly during the development.
  45. # import maya.cmds as cmds
  46. # cmds.unloadPlugin('TressFX_Exporter.py')
  47. # cmds.loadPlugin('TressFX_Exporter.py')
  48. from functools import partial
  49. import maya.cmds as cmds
  50. import maya.OpenMaya as OpenMaya
  51. import maya.OpenMayaAnim as OpenMayaAnim
  52. import maya.OpenMayaMPx as OpenMayaMPx
  53. import maya.mel as mel
  54. import pymel.core as pymel
  55. import ctypes
  56. import random
  57. import sys
  58. import re
  59. from maya.OpenMaya import MIntArray, MDagPathArray
  60. selected_mesh_shape_name = ''
  61. joint_sel_list_names = []
  62. singleItemGroup_Select = ''
  63. customRootGroup_Select = ''
  64. jointSubsetGroup_Select = ''
  65. hairTab_Select_Joints = ''
  66. hairTab_Custom_Root = ''
  67. defaultJointRootIndex = 0
  68. defaultJointRootWeight = 0.0
  69. customUVRange_Select = ''
  70. customUVRange_Values = ''
  71. tressfx_exporter_version = '4.1.23'
  72. # Don't change the following maximum joints per vertex value. It must match the one in TressFX loader and simulation
  73. TRESSFX_MAX_INFLUENTIAL_BONE_COUNT = 4
  74. import TressFX_Exporter
  75. bReload = True
  76. if(bReload):
  77. reload(TressFX_Exporter)
  78. def InstallShelf():
  79. #----------------------
  80. # Add a shelf
  81. #----------------------
  82. shelfName = "TressFX"
  83. layout = mel.eval('$tmp=$gShelfTopLevel')
  84. if cmds.shelfLayout(shelfName, query=True, exists=True): # If the shelf exists, delete buttons and delete shelf
  85. for buttons in cmds.shelfLayout(shelfName, query=True, childArray=True) or []:
  86. cmds.deleteUI(buttons)
  87. cmds.setParent(layout + '|' + shelfName)
  88. else: # If the shelf doesn't exist, create a new one
  89. cmds.setParent(layout)
  90. cmds.shelfLayout(shelfName)
  91. # create a button
  92. cmds.shelfButton( label='Export',
  93. command='import TressFX_Exporter\nTressFX_Exporter.UI()',
  94. #command='TressFX_Exporter.UI()',
  95. sourceType='python',
  96. annotation='Launch TressFX exporter',
  97. #image='', # This empty icon image will cause a warning message like 'Pixmap file not found, using default.'.
  98. style='textOnly')
  99. def initializePlugin(mobject):
  100. mplugin = OpenMayaMPx.MFnPlugin(mobject, "TressFX", tressfx_exporter_version, "Any")
  101. # install menu
  102. gMainWindow = mel.eval('$temp1=$gMainWindow')
  103. if cmds.menu('TressFX', exists=True):
  104. cmds.deleteUI('TressFX')
  105. tressfx_top_menu = cmds.menu('TressFX', parent=gMainWindow, tearOff=False, label='TressFX')
  106. cmds.menuItem(parent = tressfx_top_menu,
  107. label='Export Hair/Fur',
  108. command = 'import TressFX_Exporter\nTressFX_Exporter.UI()')
  109. cmds.menuItem(parent = tressfx_top_menu,
  110. label='Export Collision Mesh',
  111. command = 'import TressFX_Exporter\nTressFX_Exporter.CollisionUI()')
  112. # install shelf
  113. #InstallShelf()
  114. def uninitializePlugin(mobject):
  115. mplugin = OpenMayaMPx.MFnPlugin(mobject)
  116. try:
  117. # Close TressFX plugin windows if it is already open.
  118. if cmds.window("TressFXExporterUI", exists = True):
  119. cmds.deleteUI("TressFXExporterUI")
  120. if cmds.window("TressFXCollisionMeshUI", exists = True):
  121. cmds.deleteUI("TressFXCollisionMeshUI")
  122. # uninstall menu
  123. if cmds.menu('TressFX', exists=True):
  124. cmds.deleteUI('TressFX')
  125. except:
  126. sys.stderr.write("Failed to uninitialize TressFX plugin")
  127. raise
  128. return
  129. # Helper class to show progress bar for lengthy process
  130. class ProgressBar:
  131. def __init__ (self,title,steps):
  132. self.window = pymel.window(title, sizeable=False)
  133. pymel.columnLayout()
  134. self.progressControls = []
  135. self.progressbar = pymel.progressBar(maxValue=steps, width=300)
  136. pymel.showWindow( self.window )
  137. self.progressbar.step(0)
  138. def Kill(self):
  139. pymel.deleteUI(self.window)
  140. def Increment(self):
  141. self.progressbar.step(1)
  142. def GetAllDAGObjects(mFnType, maxDepth = 1):
  143. #dagIterator = OpenMaya.MItDag( OpenMaya.MItDag.kBreadthFirst, OpenMaya.MFn.kJoint ) #could use kBreadthFirst to find top of each 'type' (curve, bones, xgen)
  144. dagIterator = OpenMaya.MItDag( OpenMaya.MItDag.kBreadthFirst, mFnType ) #could use kBreadthFirst to find top of each 'type' (curve, bones, xgen)
  145. #todo: Notes for making it easier to use Xgen directly
  146. # kJoint
  147. #kNurbsCurve (parent is kTransform)
  148. #kTransform for mesh (like sphere, and shpae is kPluginShape but not helpful since xgGuides are kTransform and kPluginShape too!)
  149. #and xgen collection is kPluginTransformNode (depth1), description is kTransform again
  150. #and the other sub items are both kTransform
  151. # Will need to find better way to identify xgen, mesh/skinclusters, for joints
  152. #perhaps joints have connection??
  153. # This reference to the MFnDagNode function set will be needed
  154. # to obtain information about the DAG objects.
  155. dagNodeFn = OpenMaya.MFnDagNode()
  156. listOfDagNodes = []
  157. # Traverse the scene.
  158. while( not dagIterator.isDone() ):
  159. # Obtain the current item.
  160. dagObject = dagIterator.currentItem()
  161. # Extract the depth of the DAG object.
  162. depth = dagIterator.depth()
  163. if depth > maxDepth:
  164. return listOfDagNodes
  165. else:
  166. listOfDagNodes.append(dagObject)
  167. # Make our MFnDagNode function set operate on the current DAG object.
  168. dagNodeFn.setObject( dagObject )
  169. # Extract the DAG object's name.
  170. name = dagNodeFn.name()
  171. #print name + ' (' + dagObject.apiTypeStr() + ') depth: ' + str( depth )
  172. # Iterate to the next item.
  173. dagIterator.next()
  174. return listOfDagNodes
  175. def GetNurbCurvesByGroup():
  176. return GetAllDAGObjects(OpenMaya.MFn.kNurbsCurve, 4)
  177. def GetAvailableRigs():
  178. return GetAllDAGObjects(OpenMaya.MFn.kJoint, 1)
  179. def GetSkinClustersByRig():
  180. return
  181. #currently undefined. Maya doesn't seem to have a python api for getting to the
  182. #Utilities (tab) of Xgen, or rendering. both are needed to render out guides,
  183. # then convert guides to nurbs curves.
  184. #def GetXGenGroups():
  185. # return
  186. def GetSkinClusterInfluenceObjectsNames():
  187. #-------------------------
  188. # Get skin cluster object
  189. #-------------------------
  190. if selected_mesh_shape_name == '':
  191. cmds.warning("TressFX: Cannot retrieve skin cluster, base mesh must be set.\n")
  192. return
  193. skinClusterName = ''
  194. skinClusters = cmds.listHistory(selected_mesh_shape_name)
  195. skinClusters = cmds.ls(skinClusters, type="skinCluster")
  196. if skinClusters:
  197. skinClusterName = skinClusters[0]
  198. else:
  199. cmds.warning('TressFX: No skin cluster found on '+ selected_mesh_shape_name)
  200. return
  201. # get the MFnSkinCluster using skinClusterName
  202. selList = OpenMaya.MSelectionList()
  203. selList.add(skinClusterName)
  204. skinClusterNode = OpenMaya.MObject()
  205. selList.getDependNode(0, skinClusterNode)
  206. skinFn = OpenMayaAnim.MFnSkinCluster(skinClusterNode)
  207. dagPaths = MDagPathArray()
  208. skinFn.influenceObjects(dagPaths)
  209. skInfluenceNames = []
  210. # get joint names
  211. for i in range(dagPaths.length()):
  212. influenceName = dagPaths[i].partialPathName()
  213. skInfluenceNames.append(influenceName) #we want the partial path, even if it includes NS here (for comparision with user selected list)
  214. return skInfluenceNames
  215. def UI():
  216. # prevents multiple windows
  217. if cmds.window("TressFXExporterUI", exists = True):
  218. cmds.deleteUI("TressFXExporterUI")
  219. global joint_sel_list_names
  220. joint_sel_list_names = [] #clear joint list, can't guarantee clear() method will be present (need python 3.3 minimum)
  221. window_width = 550
  222. window_height = 525
  223. windowTitle = 'TressFX Hair/Fur'
  224. window = cmds.window("TressFXExporterUI", title = windowTitle, w=window_width, h = window_height, mnb=False, sizeable=False)
  225. mainLayout = cmds.columnLayout(columnAlign = 'center') # In case you want to see the area of the main layout, use backgroundColor = (1, 1, 1) as an argument
  226. #cmds.separator(h=10)
  227. mainTabs = cmds.tabLayout(imw = 5, imh = 5)
  228. #cmds.rowColumnLayout( numberOfColumns=2, columnWidth=[ (1,10),(2,window_width-10) ])
  229. #cmds.separator(style='none', width=10)
  230. hairTab_SelectTarget = cmds.rowColumnLayout( numberOfColumns=3, columnWidth=[ (1,300),(2,10),(3,170) ], columnAlign = [ (1,'center'),(2,'center'),(3,'center') ], parent = mainTabs )
  231. hairTab_SetOptions = cmds.rowColumnLayout( numberOfColumns=3, columnWidth=[ (1,300),(2,10),(3,170) ], columnAlign = [ (1,'center'),(2,'center'),(3,'center') ], parent = mainTabs )
  232. hairTab_Export = cmds.rowColumnLayout( numberOfColumns=3, columnWidth=[ (1,300),(2,10),(3,170) ], columnAlign = [ (1,'center'),(2,'center'),(3,'center') ], parent = mainTabs )
  233. cmds.tabLayout( mainTabs, edit=True, tabLabel=((hairTab_SelectTarget, 'Select Hair/Mesh/Rig'), (hairTab_SetOptions, 'Choose Options'), (hairTab_Export, 'Export Files')) )
  234. #Selection tab items - selecting the rig, xgen, curves, skincluster/meshes
  235. cmds.setParent(hairTab_SelectTarget)
  236. global singleItemGroup_Select
  237. singleItemGroup_Select = cmds.columnLayout(columnAlign = 'center', parent = hairTab_SelectTarget)
  238. cmds.separator(style='none',h=50)
  239. cmds.button(label="Set the base mesh", w=200, h=25, command=SetBaseMesh)
  240. cmds.separator(style='none',h=20)
  241. cmds.textField("MeshNameLabel", w=200, editable=False)
  242. cmds.separator(style='none',h=10)
  243. global customRootGroup_Select
  244. customRootGroup_Select = cmds.columnLayout(columnAlign = 'center', parent = singleItemGroup_Select)
  245. cmds.checkBox("UseCustomJointRootLabel", label='Use Custom Joint Root', align='left', enable=False, changeCommand=UseCustomJointRoot, parent = customRootGroup_Select )
  246. cmds.separator(style='none',h=10) #renormalize so weights add up to one
  247. cmds.checkBox("RenormalizeFinalPairs", label='Re-Normalize Final Weights (sumMaxInfluences=1)', align='left', enable=False)
  248. cmds.separator(style='none',h=10)
  249. global jointSubsetGroup_Select
  250. jointSubsetGroup_Select = cmds.columnLayout(columnAlign = 'center', parent = singleItemGroup_Select)
  251. cmds.checkBox("UseJointSubsetLabel", label='Use Joints Subset Only', align='left', enable=False, changeCommand=UseJointSubset, parent = jointSubsetGroup_Select )
  252. cmds.separator(style='none',h=10)
  253. #Options tab items - vertices per strand, sampling, random, unreal/directx/special formats
  254. #cmds.intField("numVerticesPerStrand", w=30, minValue=4, maxValue=64, value=16 )
  255. cmds.setParent(hairTab_SetOptions)
  256. cmds.separator(style='none',h=50)
  257. cmds.separator(style='none',h=50)
  258. cmds.separator(style='none',h=50)
  259. cmds.text(label='Number of vertices per strand:', align='left', parent = hairTab_SetOptions)
  260. cmds.separator(style='none', width=10)
  261. cmds.optionMenu("numberOfStrandsOptionMenu", label='')
  262. cmds.menuItem(label='4')
  263. cmds.menuItem(label='8')
  264. cmds.menuItem(label='16')
  265. cmds.menuItem(label='32')
  266. cmds.menuItem(label='64')
  267. cmds.text(label='Minimum curve length:', align='left')
  268. cmds.separator(style='none',h=30)
  269. cmds.floatField("minCurveLength", minValue=0, value=0, w=70)
  270. cmds.text(label='Sample every N curves:', align='left')
  271. cmds.separator(style='none', width=10)
  272. cmds.optionMenu("samplingOptionMenu", label='')
  273. cmds.menuItem(label='1')
  274. cmds.menuItem(label='2')
  275. cmds.menuItem(label='4')
  276. cmds.menuItem(label='8')
  277. cmds.menuItem(label='16')
  278. cmds.menuItem(label='32')
  279. cmds.menuItem(label='64')
  280. cmds.menuItem(label='128')
  281. cmds.text(label='Sample start offset[0-32]:', align='left')
  282. cmds.separator(style='none',h=30)
  283. cmds.intField("curveOffset", w=70, minValue=0, maxValue=32, value=0 )
  284. cmds.text(label='Scale Scene:', align='left')
  285. cmds.separator(style='none', width=10)
  286. cmds.optionMenu('scalingOptionMenu', label='') #TressFX does not know about the auto-scaling an engine might do (such as FBX cm->m) but you can manually set scaling here to adjust for what the engine will want
  287. cmds.menuItem(label='0.001')
  288. cmds.menuItem(label='0.01')
  289. cmds.menuItem(label='0.1')
  290. cmds.menuItem(label='1.0')
  291. cmds.menuItem(label='10.0')
  292. cmds.menuItem(label='100.0')
  293. cmds.menuItem(label='1000.0')
  294. cmds.optionMenu('scalingOptionMenu', edit=True, value='1.0')
  295. singleItemGroup_Options = cmds.columnLayout(columnAlign = 'center', parent = hairTab_SetOptions)
  296. cmds.separator(style='none',h=50)
  297. cmds.checkBox("bothEndsImmovable", label='Both ends immovable')
  298. cmds.separator (style='none', width=10)
  299. cmds.checkBox("InvertZ", label='Invert Z-axis of Hairs', value = False)
  300. cmds.checkBox("randomStrandCheckBox", label='Randomize strands for LOD', value = False)
  301. cmds.separator(style='none',h=30)
  302. cmds.text(label='Unreal 4.x Options', align='center')
  303. cmds.checkBox("useZUp", label='Make Z-Up Direction', value = True) #if user requests z-up, and maya not using Z (but Y as up), will swap y and z coordinates of point
  304. cmds.separator(style='none',h=30)
  305. cmds.text(label='DirectX 11/12 Options', align='center')
  306. cmds.checkBox("InvertYForUVs", label='Invert Y-axis of UV coordinates', value = True, changeCommand=AllowCustomUV )
  307. global customUVRange_Select
  308. customUVRange_Select = cmds.columnLayout(columnAlign = 'center', parent = singleItemGroup_Options)
  309. cmds.checkBox("useNonUniformUVRange", label='Using Non-Uniform UV Range', value = False, align='left', enable=True, changeCommand=UseCustomUV, parent = customUVRange_Select )
  310. cmds.separator(style='none',h=10)
  311. #Export Specific Items
  312. cmds.setParent(hairTab_Export)
  313. singleItemGroup_Export = cmds.columnLayout(columnAlign = 'center', parent = hairTab_Export)
  314. cmds.separator(style='none',h=50)
  315. cmds.text(label='Export (TressFX 4.x)', align='center')
  316. cmds.checkBox("exportHairCheckBox", label='Export hair data (*.tfx)', value = True)
  317. cmds.checkBox("exportBoneCheckBox", label='Export bone data (*.tfxbone)', value = True)
  318. cmds.separator(style='none',h=30)
  319. cmds.text(label='ErrorMode (TressFX 4.x)', align='center')
  320. cmds.checkBox("ignoreUVErrorsCheckBox", label='ignore TFX UVcoord Errors', value = True)
  321. cmds.checkBox("removeNamespace", label='remove Namespace from bones', value = True) #key when importing one .ma/.mb into another
  322. cmds.separator(style='none',h=30)
  323. cmds.text(label='Export (TressFX 3.0, deprecated)', align='center')
  324. cmds.checkBox("exportSkinCheckBox", label='Export skin data (*.tfxskin)', value = False)
  325. cmds.separator(style='none',h=30)
  326. cmds.button(label="Export!", w=300, h=50, command=DoExport)
  327. #cmds.separator(style='none',h=15)
  328. #cmds.button(label="Export collision mesh", w=170, h=30, command=DoExportCollisionMesh)
  329. #cmds.button(label="Goto Bind Pose", w=220, h=30, command=GotoBindPose)
  330. cmds.separator(h=20)
  331. cmds.setParent(mainLayout)
  332. version_text = 'v' + tressfx_exporter_version
  333. cmds.text(label=version_text, width=window_width-25, align='right')
  334. cmds.separator(style='none',h=20)
  335. global selected_mesh_shape_name
  336. selected_mesh_shape_name = ''
  337. cmds.showWindow(window)
  338. # After showWindow, resize the window to enforce its size. This is a workaround of Maya's bug.
  339. cmds.window(window, edit=True, widthHeight=(window_width, window_height))
  340. def CollisionUI():
  341. # prevents multiple windows
  342. if cmds.window("TressFXCollisionMeshUI", exists = True):
  343. cmds.deleteUI("TressFXExporterUI")
  344. window_width = 320
  345. window_height = 300
  346. windowTitle = 'TressFX Collision'
  347. window = cmds.window("TressFXCollisionMeshUI", title = windowTitle, w=window_width, h = window_height, mnb=False, sizeable=False)
  348. mainLayout = cmds.columnLayout(columnAlign = 'center') # In case you want to see the area of the main layout, use backgroundColor = (1, 1, 1) as an argument
  349. cmds.rowColumnLayout( numberOfColumns=3, columnWidth=[ (1,10),(2,window_width-20), (3, 10) ], rowSpacing = (1,10), parent=mainLayout)
  350. cmds.separator(style='none', width=10)
  351. cmds.separator(style='none', width=10)
  352. cmds.separator(style='none', width=10)
  353. cmds.separator(style='none', width=10)
  354. cmds.button(label="Set the collision mesh", w=120, h=25, command=partial(SetBaseMesh, True))
  355. cmds.separator(style='none', width=10)
  356. cmds.separator(style='none', width=10)
  357. cmds.textField("MeshNameLabel", w=220, editable=False)
  358. cmds.separator(style='none', width=10)
  359. cmds.separator(style='none', width=10, height=20)
  360. cmds.separator(style='none', width=10)
  361. cmds.separator(style='none', width=10)
  362. subwidth = (300 - 20)/4
  363. cmds.rowColumnLayout( numberOfColumns=4, columnWidth=[ (1,10),(2, subwidth), (3, subwidth), (4, 10) ], rowSpacing = (1,10), parent=mainLayout)
  364. cmds.separator(style='none', width=10)
  365. cmds.text(label='Scale Scene:', align='left')
  366. cmds.optionMenu('scalingCollisionOptionMenu', label='') #TressFX does not know about the auto-scaling an engine might do (such as FBX cm->m) but you can manually set scaling here to adjust for what the engine will want
  367. cmds.menuItem(label='0.001')
  368. cmds.menuItem(label='0.01')
  369. cmds.menuItem(label='0.1')
  370. cmds.menuItem(label='1.0')
  371. cmds.menuItem(label='10.0')
  372. cmds.menuItem(label='100.0')
  373. cmds.menuItem(label='1000.0')
  374. cmds.optionMenu('scalingCollisionOptionMenu', edit=True, value='1.0')
  375. cmds.separator(style='none', width=10)
  376. #cmds.setParent('..')
  377. #cmds.rowColumnLayout( numberOfColumns=1, columnWidth=[ (1,window_width-20) ], rowSpacing = (1,10), parent=mainLayout)
  378. cmds.rowColumnLayout( numberOfColumns=3, columnWidth=[ (1,10),(2,window_width-20), (3, 10) ], rowSpacing = (1,10), parent=mainLayout)
  379. cmds.separator(style='none', width=10, height=20)
  380. cmds.separator(style='none', width=10)
  381. cmds.separator(style='none', width=10)
  382. cmds.separator(style='none', width=10)
  383. cmds.text(label='ErrorMode (TressFX 4.x)', align='center')
  384. cmds.separator(style='none', width=10)
  385. cmds.separator(style='none', width=10)
  386. cmds.checkBox("removeNamespaceCM", label='remove Namespace from bones', value = True)
  387. cmds.separator(style='none', width=10)
  388. cmds.separator(style='none', width=10)
  389. cmds.checkBox("RenormalizeFinalPairs", label='Re-Normalize Final Weights (sumMaxInfluences=1)', align='left', enable=True)
  390. cmds.separator(style='none', width=10)
  391. #cmds.checkBox("noDupBoneWts", label='test:no dup nonzero bone wts per vtx', value = False) #error checking. todo: may want this, or may not need it
  392. cmds.separator(style='none', width=10)
  393. cmds.button(label="Export", w=170, h=30, command=DoExportCollisionMesh)
  394. cmds.separator(style='none', width=10)
  395. cmds.separator(style='none', width=10)
  396. cmds.separator(style='none', width=10)
  397. version_text = 'v' + tressfx_exporter_version
  398. cmds.text(label=version_text, width=window_width-25, align='right', parent=mainLayout)
  399. global selected_mesh_shape_name
  400. selected_mesh_shape_name = ''
  401. cmds.showWindow(window)
  402. # After showWindow, resize the window to enforce its size. This is a workaround of a Maya bug.
  403. cmds.window(window, edit=True, widthHeight=(window_width, window_height))
  404. def AllowCustomUV(*args):
  405. bEnableCustomUV = cmds.checkBox("InvertYForUVs", q = True, v = True)
  406. if (bEnableCustomUV):
  407. cmds.checkBox("useNonUniformUVRange", edit = True, enable = True)
  408. else:
  409. cmds.checkBox("useNonUniformUVRange", edit = True, enable = False)
  410. return
  411. def UseCustomUV(*args):
  412. #if using DirectX invert-y option, means we are inverting the v of uv coordinates, and allow the user the option of specifying
  413. #a custom uv range other than 0-1. This means that they have an actual non-uniform range. We do not check to see if that is true or not.
  414. #mainly because I couldn't find a way for Maya to tell me that info
  415. bUseCustomUV = cmds.checkBox("useNonUniformUVRange", q = True, v = True)
  416. global customUVRange_Values
  417. if (bUseCustomUV):
  418. if cmds.rowColumnLayout(customUVRange_Values, exists = True):
  419. cmds.deleteUI(customUVRange_Values, layout=True)
  420. customUVRange_Values = cmds.rowColumnLayout( numberOfColumns=4, columnWidth=[ (1,50),(2,80),(3,50),(4,80) ], columnAlign = [ (1,'center'),(2,'center'),(3,'center'),(4,'center') ], parent = customUVRange_Select )
  421. cmds.setParent(customUVRange_Values)
  422. cmds.text( "U min:", w=20)
  423. cmds.floatField( "uMin", w=50, edit=False, pre=2, value = 0.0)
  424. cmds.text( "U max:", w=20)
  425. cmds.floatField( "uMax", w=50, edit=False, pre=2, value = 1.0)
  426. cmds.text( "V min:", w=20)
  427. cmds.floatField( "vMin", w=50, edit=False, pre=2, value = 0.0)
  428. cmds.text( "V max:", w=20)
  429. cmds.floatField( "vMax", w=50, edit=False, pre=2, value = 1.0)
  430. else:
  431. if cmds.rowColumnLayout(customUVRange_Values, exists = True):
  432. cmds.deleteUI(customUVRange_Values, layout=True)
  433. return
  434. def CreateCustomRootUI(jointNameRoot):
  435. global defaultJointRootIndex
  436. global defaultJointRootWeight
  437. global hairTab_Custom_Root
  438. hairTab_Custom_Root = cmds.columnLayout(columnAlign = 'center', parent = customRootGroup_Select )
  439. hairTab_Custom_Root1 = cmds.rowColumnLayout( numberOfColumns=2, columnWidth=[ (1,100),(2,100) ], columnAlign = [ (1,'center'),(2,'center') ], parent = hairTab_Custom_Root )
  440. cmds.setParent(hairTab_Custom_Root1)
  441. cmds.button("Set Joint", w=50, h=25, enable=True, visible=True, command=SetRootJoint)
  442. cmds.button("Clear Joint", w=50, h=25, enable=True, visible=True, command=ClearRootJoint)
  443. hairTab_Custom_Root2 = cmds.rowColumnLayout( numberOfColumns=2, columnWidth=[ (1,100),(2,100) ], columnAlign = [ (1,'center'),(2,'center') ], parent = hairTab_Custom_Root )
  444. cmds.setParent(hairTab_Custom_Root2)
  445. cmds.text( "New Root:", w=50)
  446. cmds.text( "JointRootName", w=50, label=jointNameRoot)
  447. hairTab_Custom_Root3 = cmds.rowColumnLayout( numberOfColumns=2, columnWidth=[ (1,100),(2,100) ], columnAlign = [ (1,'center'),(2,'center') ], parent = hairTab_Custom_Root )
  448. cmds.setParent(hairTab_Custom_Root2)
  449. cmds.text( "Weight (0-1)", w=50)
  450. cmds.floatField( "JointRootWeightFloat", w=50, edit=False, minValue = 0.0, maxValue = 1.0, pre=2, value = defaultJointRootWeight, changeCommand=SetRootWeight)
  451. return
  452. def DeleteCustomRootUI():
  453. global hairTab_Custom_Root
  454. #destroy the panel/float input with the root joint selection and weight (includes all children)
  455. if cmds.columnLayout(hairTab_Custom_Root, exists = True):
  456. cmds.deleteUI(hairTab_Custom_Root, layout=True)
  457. return
  458. def UseCustomJointRoot(*args):
  459. #todo: automatically add the custom root to the joint subset (only if a joint subset is being used)
  460. #create UI for setting a custom root (joint) and its weight (only used when hit a 0-weight case)
  461. bUseCustomRoot = cmds.checkBox("UseCustomJointRootLabel", q = True, v = True)
  462. global defaultJointRootIndex
  463. global defaultJointRootWeight
  464. if (bUseCustomRoot):
  465. rootName = GetJointNameFromListByIndex(0)
  466. CreateCustomRootUI(rootName)
  467. else:
  468. DeleteCustomRootUI()
  469. defaultJointRootIndex = 0
  470. defaultJointRootWeight = 0.0
  471. return
  472. def GetJointNameFromListByIndex(index):
  473. skinClusterJointList = GetSkinClusterInfluenceObjectsNames()
  474. #check to make sure we have joints (influencers) bound to mesh (skin)
  475. if len(skinClusterJointList) == 0:
  476. cmds.warning("TressFX: Could not retrieve joint name by index. Base mesh has no skin cluster influencers (check to see if skin is bound)")
  477. return "none"
  478. if (index < 0) or (index >= len(skinClusterJointList)):
  479. cmds.warning("TressFX: Could not retrieve joint name by index. Index out of bounds.")
  480. return "none"
  481. return skinClusterJointList[index]
  482. def GetJointIndexFromList(jointName):
  483. skinClusterJointList = GetSkinClusterInfluenceObjectsNames()
  484. jointIndex = 0
  485. #check to make sure we have joints (influencers) bound to mesh (skin)
  486. if len(skinClusterJointList) == 0:
  487. cmds.warning("TressFX: Could not retrieve joint index (ret joint index=0). Base mesh has no skin cluster influencers (check to see if skin is bound)")
  488. for i in range(len(skinClusterJointList)):
  489. if (jointName == skinClusterJointList[i]):
  490. jointIndex = i
  491. return jointIndex
  492. def SetRootWeight(value):
  493. global defaultJointRootWeight
  494. defaultJointRootWeight = value #the value is already constrained to be between 0 and 1, when the user hits <return> or eventually hits another button, etc to force a return, we get notified.
  495. return
  496. def SetRootJoint(*args):
  497. #------------------------------
  498. # Set joint to be used as the default 'root joint' used when there are 0 weights during influence (for hair strands) calculations
  499. #-------------------------------
  500. if selected_mesh_shape_name == '':
  501. cmds.warning("TressFX: To select custom root joint, base mesh must be set.\n")
  502. return
  503. sel_list = OpenMaya.MSelectionList()
  504. OpenMaya.MGlobal.getActiveSelectionList(sel_list)
  505. if sel_list.length() == 0:
  506. return
  507. if sel_list.length() > 1:
  508. cmds.warning("TressFX: Can only select one joint to be *root*")
  509. return
  510. skinClusterJointList = GetSkinClusterInfluenceObjectsNames()
  511. #check to make sure we have joints (influencers) bound to mesh (skin)
  512. if len(skinClusterJointList) == 0:
  513. cmds.warning("TressFX: Base mesh has no skin cluster influencers (check to see if skin is bound)")
  514. return
  515. dagPath = OpenMaya.MDagPath()
  516. sel_list.getDagPath(0, dagPath) #index 0, first item in the selection list
  517. jointFn = OpenMaya.MFnDagNode(dagPath)
  518. joint_name = jointFn.name()
  519. if dagPath.apiType() != OpenMaya.MFn.kJoint: #must be a joint, nothing else
  520. cmds.warning('TressFX: Not a joint, root must be a joint: objectname = ' + joint_name)
  521. return
  522. elif not (joint_name in skinClusterJointList):
  523. cmds.warning('TressFX: Joint not in influencer list for skin cluster/base mesh: joint = ' + joint_name)
  524. return
  525. else:
  526. global defaultJointRootIndex
  527. global defaultJointRootWeight
  528. DeleteCustomRootUI()
  529. defaultJointRootIndex = 0
  530. defaultJointRootWeight = 0.0
  531. defaultJointRootIndex = GetJointIndexFromList(joint_name)
  532. CreateCustomRootUI(joint_name)
  533. cmds.showWindow("TressFXExporterUI")
  534. return
  535. def ClearRootJoint(*args):
  536. global defaultJointRootIndex
  537. global defaultJointRootWeight
  538. DeleteCustomRootUI()
  539. defaultJointRootIndex = 0
  540. defaultJointRootWeight = 0.0
  541. defaultName = GetJointNameFromListByIndex(defaultJointRootIndex)
  542. CreateCustomRootUI(defaultName)
  543. cmds.showWindow("TressFXExporterUI")
  544. return
  545. def CreateJointSubsetUI():
  546. #adds ability to only look at (allow) a subset of the joints for the hair influencers
  547. # (good when you have a lot of joints that are 0, like facial weights, to prevent situation with a zero weight/hair that will not track)
  548. global hairTab_Select_Joints
  549. hairTab_Select_Joints = cmds.columnLayout(columnAlign = 'center', parent = jointSubsetGroup_Select )
  550. hairTab_Select_Joints1 = cmds.rowColumnLayout( numberOfColumns=3, columnWidth=[ (1,100),(2,100),(3,100) ], columnAlign = [ (1,'center'),(2,'center'),(3,'center') ], parent = hairTab_Select_Joints )
  551. cmds.setParent(hairTab_Select_Joints1)
  552. cmds.button("Add joints", w=50, h=25, enable=True, visible=True, command=AddJointSet)
  553. cmds.button("Delete joints", w=50, h=25, enable=True, visible=True, command=DeleteJointSet)
  554. cmds.button("Clear All joints", w=50, h=25, enable=True, visible=True, command=ClearJointSet)
  555. pane_list = cmds.paneLayout("JointNamePane", parent=hairTab_Select_Joints)
  556. cmds.textScrollList( "JointNameScrollList", w=300, numberOfRows=8, e=False, allowMultiSelection=True, parent = pane_list, append=joint_sel_list_names)
  557. return
  558. def DeleteJointSubsetUI():
  559. #delete the UI, but not responsible for clearing the joints selected to joint_sel_list_names
  560. global hairTab_Select_Joints
  561. #destroy the panel/float input with the root subset selection controls (includes all children)
  562. if cmds.columnLayout(hairTab_Select_Joints, exists = True):
  563. cmds.deleteUI(hairTab_Select_Joints, layout=True)
  564. return
  565. def UseJointSubset(*args):
  566. bUseJointSubset = cmds.checkBox("UseJointSubsetLabel", q = True, v = True)
  567. if selected_mesh_shape_name == '':
  568. cmds.warning("TressFX: To select custom joint subset, base mesh must be set.\n")
  569. bUseJointSubset = False
  570. cmds.checkBox("UseJointSubsetLabel", editable=True, v=False)
  571. return
  572. if (bUseJointSubset):
  573. CreateJointSubsetUI()
  574. cmds.warning("TressFX: Using a subset can result in loss of hair tracking accuracy.\n Use with caution and only on bones that should never have affect on that skin subsection. \n Useful for masking out dead weights and non-skin joints like weaponry.")
  575. else:
  576. #destroy the panels with the joints/controls and clear the joint set list
  577. DeleteJointSubsetUI()
  578. return
  579. def ClearJointSet(*args):
  580. global joint_sel_list_names
  581. joint_sel_list_names = []
  582. DeleteJointSubsetUI()
  583. CreateJointSubsetUI()
  584. return
  585. def DeleteJointSet(*args):
  586. global joint_sel_list_names
  587. if cmds.columnLayout(hairTab_Select_Joints, exists = True):
  588. numSelectedItems = cmds.textScrollList( "JointNameScrollList", query=True, nsi=True)
  589. if numSelectedItems > 0:
  590. selectedItems = cmds.textScrollList( "JointNameScrollList", query=True, si=True)
  591. else:
  592. print('TressFX: Nothing deleted: no joints selected from joint subset list')
  593. return
  594. for item in selectedItems:
  595. if item in joint_sel_list_names:
  596. joint_sel_list_names.remove(item)
  597. DeleteJointSubsetUI()
  598. CreateJointSubsetUI()
  599. return
  600. def AddJointSet(*args):
  601. #------------------------------
  602. # Create a list of joints to be used in the mapping (and exclude those not in this list)
  603. #-------------------------------
  604. if selected_mesh_shape_name == '':
  605. cmds.warning("TressFX: To select joint subset, base mesh must be set.\n")
  606. return
  607. sel_list = OpenMaya.MSelectionList()
  608. OpenMaya.MGlobal.getActiveSelectionList(sel_list)
  609. if sel_list.length() == 0:
  610. return
  611. skinClusterJointList = GetSkinClusterInfluenceObjectsNames()
  612. #check to make sure we have joints (influencers) bound to mesh (skin)
  613. if len(skinClusterJointList) == 0:
  614. cmds.warning("TressFX: Base mesh has no skin cluster influencers (check to see if skin is bound)")
  615. return
  616. # Create iterator through list of selected object
  617. selection_iter = OpenMaya.MItSelectionList(sel_list)
  618. obj = OpenMaya.MObject()
  619. # Loop though iterator objects
  620. global joint_sel_list_names
  621. while not selection_iter.isDone():
  622. selection_iter.getDependNode(obj)
  623. dagPath = OpenMaya.MDagPath.getAPathTo(obj)
  624. #print dagPath.fullPathName()
  625. jointFn = OpenMaya.MFnDagNode(dagPath)
  626. joint_name = jointFn.name()
  627. if not (joint_name in skinClusterJointList):
  628. cmds.warning('TressFX: Joint not in influencer list for skin cluster/base mesh: joint = ' + joint_name)
  629. elif joint_name in joint_sel_list_names:
  630. cmds.warning('TressFX: Joint already in list: joint = ' + joint_name) #skip it but continue iterating
  631. elif dagPath.apiType() != OpenMaya.MFn.kJoint: #must be a joint, nothing else
  632. cmds.warning('TressFX: Not a joint, can only add joints to this list: objectname = ' + joint_name) #skip it but continue iterating
  633. else:
  634. joint_sel_list_names.append(joint_name)
  635. selection_iter.next()
  636. DeleteJointSubsetUI()
  637. CreateJointSubsetUI()
  638. cmds.showWindow("TressFXExporterUI")
  639. return
  640. def GetIndicesSubsetInfluenceObjects(skinFn, dagPaths ):
  641. #get the joints, but if we are limiting to a subset of joints use that, otherwise use full joint set attached to skincluster
  642. # we also will check the subset and reject any bones that are not contained in dagPaths (which lists the bones for that skincluster)
  643. boneSubsetIndices = []
  644. numberItems = 0
  645. if len(joint_sel_list_names) != 0:
  646. jointList = joint_sel_list_names
  647. numberItems = len(joint_sel_list_names)
  648. else:
  649. cmds.warning("TressFX: Joint Subset Activated, but no items in subset. Using full joints.")
  650. return boneSubsetIndices
  651. for k in range(numberItems):
  652. influenceName = jointList[k]
  653. for i in range(dagPaths.length()):
  654. if (influenceName == dagPaths[i].partialPathName() ):
  655. boneSubsetIndices.append(skinFn.indexForInfluenceObject(dagPaths[i]))
  656. return boneSubsetIndices
  657. def GetInfluenceObjects(dagPaths ):
  658. #get all the joints (influence objects)..currently TressFX 4.x needs all the bone names and indices exported, not just a subset
  659. boneNames = []
  660. #do we need to remove the namespace if present?
  661. bRemoveNS = cmds.checkBox("removeNamespace", q = True, v = True)
  662. numberItems = 0
  663. jointList = dagPaths
  664. numberItems = jointList.length()
  665. # get joint names
  666. for i in range(numberItems):
  667. influenceName = jointList[i].partialPathName()
  668. if bRemoveNS:
  669. influenceNameList = re.split("[:]", influenceName)
  670. if len(influenceNameList) != 1: #has a namespace prefix
  671. boneNames.append(influenceNameList[-1])
  672. else:
  673. boneNames.append(influenceNameList[0])
  674. else:
  675. boneNames.append(influenceName) # Need to remove namespace?
  676. return boneNames
  677. def SetBaseMesh(bCollision = False, *args):
  678. #------------------------------
  679. # Find the selected mesh shape
  680. #-------------------------------
  681. #collision also uses this, need to differentiate (currently col should not have same options available)
  682. # i.e. doesn't hide/disable controls like the Hair side
  683. sel_list = OpenMaya.MSelectionList()
  684. OpenMaya.MGlobal.getActiveSelectionList(sel_list)
  685. if sel_list.length() == 0:
  686. return
  687. dagPath = OpenMaya.MDagPath()
  688. sel_list.getDagPath(0, dagPath)
  689. if not dagPath.hasFn(OpenMaya.MFn.kMesh): #if the node along this path doesn't support kMesh, we reject it and let the user know
  690. cmds.warning('TressFX: Selection not a kMesh')
  691. #note: in this case, we are not resetting the base mesh or any controls from current values (empty or not)
  692. return
  693. global selected_mesh_shape_name
  694. #is it already open and we are just changing base meshes? (Currently, export of hair only)
  695. if (selected_mesh_shape_name != '') and not (bCollision == True):
  696. #subcontrols may already be enabled, so reset them to defaults for this base mesh, or if the selected object isn't a mesh
  697. cmds.checkBox("RenormalizeFinalPairs", edit=True, value=False)
  698. cmds.checkBox("UseCustomJointRootLabel", edit=True, value =False)
  699. cmds.checkBox("UseJointSubsetLabel", edit=True, value=False)
  700. UseCustomJointRoot()
  701. UseJointSubset()
  702. #proceed as normal to set the new mesh and get its name
  703. cmds.textField("MeshNameLabel", edit=True, text='')
  704. selected_mesh_shape_name = ''
  705. dagPath.extendToShape()
  706. meshFn = OpenMaya.MFnMesh(dagPath)
  707. selected_mesh_shape_name = meshFn.name()
  708. cmds.textField("MeshNameLabel", edit=True, text=meshFn.name())
  709. #enable options
  710. if not (bCollision == True):
  711. cmds.checkBox("RenormalizeFinalPairs", edit=True, enable=True)
  712. cmds.checkBox("UseCustomJointRootLabel", edit=True, enable=True)
  713. cmds.checkBox("UseJointSubsetLabel", edit=True, enable=True)
  714. return
  715. def DoExport(*args):
  716. # TODO: Set the time frame to the first one or go to the bind pose?
  717. # first_frame = cmds.playbackOptions(minTime=True, query=True)
  718. # cmds.currentTime(first_frame)
  719. bExportHairCheckBox = cmds.checkBox("exportHairCheckBox", q = True, v = True)
  720. bExportSkinCheckBox = cmds.checkBox("exportSkinCheckBox", q = True, v = True)
  721. bExportBoneCheckBox = cmds.checkBox("exportBoneCheckBox", q = True, v = True)
  722. #----------------------------------------
  723. # collect selected nurbs spline curves.
  724. #----------------------------------------
  725. minCurveLength = cmds.floatField("minCurveLength",q = True, v = True)
  726. curves = GetSelectedNurbsCurves(minCurveLength)
  727. if len(curves) == 0:
  728. cmds.warning("TressFX: No nurbs curves were selected!")
  729. return
  730. else:
  731. print("TressFX: %d curves were selected.\n" % len(curves))
  732. #-------------------------------
  733. # Find the selected mesh shape
  734. #-------------------------------
  735. meshShapedagPath = OpenMaya.MDagPath()
  736. if selected_mesh_shape_name == '':
  737. meshShapedagPath = None
  738. else:
  739. allObject = cmds.ls(selected_mesh_shape_name)
  740. cmds.select(allObject) # TODO: This selection makes hair curves unselected. This is not a problem but just inconvenient for users if they want to keep the curves selected.
  741. sel_list = OpenMaya.MSelectionList()
  742. OpenMaya.MGlobal.getActiveSelectionList(sel_list)
  743. if sel_list.length() == 0:
  744. meshShapedagPath = None
  745. else:
  746. sel_list.getDagPath(0, meshShapedagPath)
  747. meshShapedagPath.extendToShape() # get mesh shape
  748. # if none of export checkboxes were selected, then exit.
  749. if bExportHairCheckBox == 0 and bExportSkinCheckBox == 0 and bExportBoneCheckBox == 0:
  750. cmds.warning("TressFX: Please select checkbox for exporting data")
  751. return
  752. rootPositions = []
  753. #-------------------
  754. # Export a tfx file
  755. #-------------------
  756. if bExportHairCheckBox:
  757. basicFilter = "*.tfx"
  758. filepath = cmds.fileDialog2(fileFilter=basicFilter, dialogStyle=2, caption="Save a tfx binary file(*.tfx)", fileMode=0)
  759. if filepath == None or len(filepath) == 0:
  760. return
  761. bRandomize = cmds.checkBox("randomStrandCheckBox", q = True, v = True)
  762. if bRandomize:
  763. random.shuffle(curves)
  764. rootPositions = SaveTFXBinaryFile(filepath[0], curves, meshShapedagPath)
  765. #------------------------------------------------------------------------------------
  766. # collect root positions for tfxskin or tfxbone in case SaveTFXBinaryFile was not run
  767. #------------------------------------------------------------------------------------
  768. if bExportHairCheckBox == 0 and (bExportSkinCheckBox == 1 or bExportBoneCheckBox == 1):
  769. for i in xrange(len(curves)):
  770. curveFn = curves[i]
  771. rootPos = OpenMaya.MPoint()
  772. curveFn.getPointAtParam(0, rootPos, OpenMaya.MSpace.kObject) # kWorld?
  773. rootPositions.append(rootPos)
  774. if bExportSkinCheckBox == 1 or bExportBoneCheckBox == 1:
  775. if selected_mesh_shape_name == '':
  776. cmds.warning("TressFX: To export skin or bone data, base mesh must be set.\n")
  777. return
  778. #--------------------
  779. # Save tfxskin file
  780. #--------------------
  781. if bExportSkinCheckBox == 1:
  782. basicFilter_tfxskin = "*.tfxskin"
  783. filepath = cmds.fileDialog2(fileFilter=basicFilter_tfxskin, dialogStyle=2, caption="Save a tfxskin file(*.tfxskin)", fileMode=0)
  784. if filepath == None or len(filepath) == 0:
  785. return
  786. SaveTFXSkinBinaryFile(filepath[0], meshShapedagPath, rootPositions)
  787. #------------------------
  788. # Save the tfxbone file.
  789. #------------------------
  790. if bExportBoneCheckBox == 1:
  791. basicFilter = "*.tfxbone"
  792. filepath = cmds.fileDialog2(fileFilter=basicFilter, dialogStyle=2, caption="Save a tfx bone file(*.tfxbone)", fileMode=0)
  793. if filepath == None or len(filepath) == 0:
  794. return
  795. #we will check for a joint subset in the save tfxbone function
  796. SaveTFXBoneBinaryFile(filepath[0], selected_mesh_shape_name, meshShapedagPath, rootPositions)
  797. return
  798. # TODO: Do we need to enforce to go to the bind pose or let user do it?
  799. def GotoBindPose(*args):
  800. joints = cmds.ls(type='joint')
  801. rootJoints = []
  802. for j in joints:
  803. while cmds.listRelatives(j, p=True):
  804. parent = cmds.listRelatives(j, p=True)
  805. j = parent[0]
  806. rootJoints.append(j)
  807. rootJoints = set(rootJoints)
  808. for rootJoint in rootJoints:
  809. cmds.select(rootJoint)
  810. bindpose = cmds.dagPose(q=True, bindPose=True)
  811. cmds.dagPose(bindpose[0] , restore=True)
  812. cmds.select(cl=True)
  813. return
  814. class WeightJointIndexPair:
  815. weight = 0
  816. joint_index = -1
  817. # For sorting
  818. def __lt__(self, other):
  819. return self.weight > other.weight
  820. # vertexIndices is three vertex indices belong to one triangle
  821. def GetSortedWeightsFromTriangleVertices(_maxJointsPerVertex, vertexIndices, jointIndexArray, weightArray, baryCoord):
  822. final_pairs = []
  823. for j in range(_maxJointsPerVertex):
  824. for i in range(3):
  825. vertex_index = vertexIndices[i]
  826. bary = baryCoord[i]
  827. weight = weightArray[vertex_index*_maxJointsPerVertex + j] * bary
  828. joint_index = jointIndexArray[vertex_index*_maxJointsPerVertex + j]
  829. bFound = False
  830. for k in range(len(final_pairs)):
  831. if final_pairs[k].joint_index == joint_index:
  832. final_pairs[k].weight += weight
  833. bFound = True
  834. break
  835. if bFound == False:
  836. pair = WeightJointIndexPair()
  837. pair.weight = weight
  838. pair.joint_index = joint_index
  839. final_pairs.append(pair)
  840. # Set joint index zero if the weight is zero. (old way)
  841. # User can now selected (new UI) to define their own 'root' joint and have it be non-zero (0-1 float range)
  842. # (often if there is an issue with a strand anchoring and not moving (all weights 0) or if they
  843. # are using a subset of joints and masking out the rest..and want a root from that group)
  844. # Needed with complex real world skeletal rigs where you often have weights purposely set to 0
  845. # (like in a facial rig) on the main skincluster
  846. final_pairs_sum = 0
  847. for i in xrange(len(final_pairs)):
  848. final_pairs_sum += final_pairs[i].weight
  849. if final_pairs[i].weight == 0:
  850. final_pairs[i].joint_index = defaultJointRootIndex
  851. final_pairs[i].weight = defaultJointRootWeight
  852. final_pairs.sort()
  853. # TODO: Is it needed to make the sum of weight equal to 1?
  854. # New UI: now has checkbox option to let you re-normalize the weights to 1 (optional only)
  855. bReNormalizeFinalPairs = cmds.checkBox("RenormalizeFinalPairs", q = True, v = True)
  856. if (bReNormalizeFinalPairs == True) and (final_pairs_sum > 0):
  857. for i in xrange(len(final_pairs)):
  858. final_pairs[i].weight /= final_pairs_sum
  859. # number of elements of final_pairs could be more than _maxJointsPerVertex but it should be at least _maxJointsPerVertex.
  860. # If you really want it to be exactly _maxJointsPerVertex, you can try to pop out elements.
  861. return final_pairs
  862. # p0, p1, p2 are three vertices of a triangle and p is inside the triangle
  863. def ComputeBarycentricCoordinates(p0, p1, p2, p):
  864. v0 = p1 - p0
  865. v1 = p2 - p0
  866. v2 = p - p0
  867. v00 = v0 * v0
  868. v01 = v0 * v1
  869. v11 = v1 * v1
  870. v20 = v2 * v0
  871. v21 = v2 * v1
  872. d = v00 * v11 - v01 * v01
  873. v = (v11 * v20 - v01 * v21) / d # TODO: Do I need to care about divide-by-zero case?
  874. w = (v00 * v21 - v01 * v20) / d
  875. u = 1.0 - v - w
  876. # make sure u, v, w are non-negative. It could happen sometimes.
  877. u = max(u, 0)
  878. v = max(v, 0)
  879. w = max(w, 0)
  880. # normalize barycentric coordinates so that their sum is equal to 1
  881. sum = u + v + w
  882. u /= sum
  883. v /= sum
  884. w /= sum
  885. return [u, v, w]
  886. def SaveTFXBoneBinaryFile(filepath, selected_mesh_shape_name, meshShapedagPath, rootPositions):
  887. #---------------------------------------------------------------------------
  888. # Build a face/triangle index list to convert face index into triangle index
  889. #---------------------------------------------------------------------------
  890. faceIter = OpenMaya.MItMeshPolygon(meshShapedagPath)
  891. triangleCount = 0
  892. faceTriaIndexList = []
  893. index = 0
  894. util = OpenMaya.MScriptUtil()
  895. util.createFromInt(0)
  896. while not faceIter.isDone():
  897. faceTriaIndexList.append(triangleCount)
  898. if faceIter.hasValidTriangulation():
  899. numTrianglesPtr = util.asIntPtr()
  900. faceIter.numTriangles(numTrianglesPtr)
  901. numTriangles = util.getInt(numTrianglesPtr)
  902. triangleCount += numTriangles
  903. faceIter.next()
  904. #print "TressFX: Triangle count:%d\n" % triangleCount
  905. #----------------------
  906. # Find the closest face
  907. #----------------------
  908. meshFn = OpenMaya.MFnMesh(meshShapedagPath)
  909. meshIntersector = OpenMaya.MMeshIntersector()
  910. meshIntersector.create(meshShapedagPath.node())
  911. triangleCounts = OpenMaya.MIntArray()
  912. triangleVertexIndices = OpenMaya.MIntArray() # the size of this array is three times of the number of total triangles
  913. meshFn.getTriangles(triangleCounts, triangleVertexIndices)
  914. vertexTriangleList = []
  915. triangleIdForStrandsList = []
  916. baryCoordList = []
  917. uvCoordList = []
  918. pointOnMeshList = []
  919. progressBar = ProgressBar('Collecting bone data', len(rootPositions))
  920. for i in range(len(rootPositions)):
  921. rootPoint = rootPositions[i]
  922. # Find the closest point info
  923. meshPt = OpenMaya.MPointOnMesh()
  924. meshIntersector.getClosestPoint(rootPoint, meshPt)
  925. pt = meshPt.getPoint()
  926. pointOnMesh = OpenMaya.MPoint()
  927. pointOnMesh.x = pt.x
  928. pointOnMesh.y = pt.y
  929. pointOnMesh.z = pt.z
  930. pointOnMeshList.append(pointOnMesh)
  931. # Find face index
  932. faceId = meshPt.faceIndex()
  933. # Find triangle index
  934. triangleId = faceTriaIndexList[faceId] + meshPt.triangleIndex()
  935. dummy = OpenMaya.MScriptUtil()
  936. dummyIntPtr = dummy.asIntPtr()
  937. triangleId_local = meshPt.triangleIndex() # This values is either 0 or 1. It is not a global triangle index. triangleId is the global triangle index.
  938. pointArray = OpenMaya.MPointArray()
  939. vertIdList = OpenMaya.MIntArray()
  940. faceIter.setIndex(faceId, dummyIntPtr)
  941. faceIter.getTriangle(triangleId_local, pointArray, vertIdList, OpenMaya.MSpace.kWorld )
  942. vertexTriangleList.append((vertIdList[0], vertIdList[1], vertIdList[2]))
  943. triangleIdForStrandsList.append(triangleId)
  944. # Find three vertex indices for the triangle. Following two lines should give us three correct vertex indices for the triangle.
  945. # I haven't really verified though.
  946. #vertexIndex = [triangleVertexIndices[triangleId*3], triangleVertexIndices[triangleId*3+1], triangleVertexIndices[triangleId*3+2]]
  947. vertexIndex = [vertIdList[0], vertIdList[1], vertIdList[2]]
  948. # Find barycentric coordinates
  949. uvw = OpenMaya.MPoint()
  950. # Somehow, below code gives me negative barycentric coordinates sometimes.
  951. # uPtr = OpenMaya.MScriptUtil().asFloatPtr()
  952. # vPtr = OpenMaya.MScriptUtil().asFloatPtr()
  953. # meshPt.getBarycentricCoords(uPtr,vPtr)
  954. # uvw.x = OpenMaya.MScriptUtil(uPtr).asFloat()
  955. # uvw.y = OpenMaya.MScriptUtil(vPtr).asFloat()
  956. # uvw.z = 1.0 - uvw.x - uvw.y
  957. # Instead getting barycentric coords from getBarycentricCoords, we compute it by the following function.
  958. uvw_a = ComputeBarycentricCoordinates(pointArray[0], pointArray[1], pointArray[2], pointOnMesh)
  959. uvw.x = uvw_a[0]
  960. uvw.y = uvw_a[1]
  961. uvw.z = uvw_a[2]
  962. # barycentric coordinates should be non-zero
  963. #uvw.x = max(uvw.x, 0)
  964. #uvw.y = max(uvw.y, 0)
  965. #uvw.z = max(uvw.z, 0)
  966. # normalize barycentric coordinates so that their sum is equal to 1
  967. #sum = uvw.x + uvw.y + uvw.z
  968. #uvw.x /= sum
  969. #uvw.y /= sum
  970. #uvw.z /= sum
  971. baryCoordList.append(uvw)
  972. # Find UV coordinates - We don't really use UV coords for tfxbone file.
  973. # util = OpenMaya.MScriptUtil()
  974. # util.createFromList([0.0, 0.0], 2)
  975. # uv_ptr = util.asFloat2Ptr()
  976. # meshFn.getUVAtPoint(rootPoint, uv_ptr)
  977. # u = OpenMaya.MScriptUtil.getFloat2ArrayItem(uv_ptr, 0, 0)
  978. # v = OpenMaya.MScriptUtil.getFloat2ArrayItem(uv_ptr, 0, 1)
  979. # uv_coord = OpenMaya.MPoint()
  980. # uv_coord.x = u
  981. # uv_coord.y = v
  982. # uv_coord.z = 0
  983. # uvCoordList.append(uv_coord)
  984. # update progress gui
  985. progressBar.Increment()
  986. progressBar.Kill()
  987. #-------------------------
  988. # Get skin cluster object
  989. #-------------------------
  990. skinClusterName = ''
  991. skinClusters = cmds.listHistory(selected_mesh_shape_name)
  992. skinClusters = cmds.ls(skinClusters, type="skinCluster")
  993. if skinClusters:
  994. skinClusterName = skinClusters[0]
  995. else:
  996. cmds.warning('TressFX: No skin cluster found on '+ selected_mesh_shape_name)
  997. return
  998. #print skinClusterName
  999. #---------------------------------------------------------------------------------------------------
  1000. # TODO: Try the following method.
  1001. # skins = filter(lambda skin: mesh.getShape() in skin.getOutputGeometry(), ls(type='skinCluster'))
  1002. # if len(skins) > 0 :
  1003. # skin = skins[0]
  1004. # skinJoints = skin.influenceObjects();
  1005. # root = skinJoints[0]
  1006. # while root.getParent() :
  1007. # root = root.getParent()
  1008. # skin.getWeights(mesh.verts[index])
  1009. # select(root, hierarchy=True, replace=True)
  1010. # joints = ls(selection=True, transforms=True, type='joint')
  1011. #---------------------------------------------------------------------------------------------------
  1012. # get the MFnSkinCluster using skinClusterName
  1013. selList = OpenMaya.MSelectionList()
  1014. selList.add(skinClusterName)
  1015. skinClusterNode = OpenMaya.MObject()
  1016. selList.getDependNode(0, skinClusterNode)
  1017. skinFn = OpenMayaAnim.MFnSkinCluster(skinClusterNode)
  1018. dagPaths = MDagPathArray()
  1019. skinFn.influenceObjects(dagPaths)
  1020. # influence object is a joint
  1021. influenceObjectsNames = []
  1022. influenceObjectsNames = GetInfluenceObjects(dagPaths)
  1023. influenceSubsetIndices = []
  1024. #check to see if using subset and got the indices to match
  1025. bUseJointSubset = cmds.checkBox("UseJointSubsetLabel", q = True, v = True)
  1026. if (bUseJointSubset == True):
  1027. influenceSubsetIndices = GetIndicesSubsetInfluenceObjects(skinFn, dagPaths )
  1028. if (len(influenceSubsetIndices) == 0):
  1029. cmds.warning("TressFX: Joint Subset activated but failed to get matching indices, defaulting to full joint set")
  1030. bUseJointSubset = False
  1031. #do we have bones?
  1032. if (len(influenceObjectsNames) == 0):
  1033. cmds.warning('TressFX: NO INFLUENCES FOR SKINCLUSTER FOUND! ')
  1034. else: #how many bones?
  1035. numBonesNeeded = len(influenceObjectsNames)
  1036. cmds.warning('TressFX: NOTICE: Make sure TressFX Max Allowed Bones (in a skeleton) is set to greater than: ' + str(numBonesNeeded))
  1037. skinMeshes = cmds.skinCluster(skinClusterName, query=1, geometry=1)
  1038. geoIter = OpenMaya.MItGeometry(meshShapedagPath)
  1039. infCount = OpenMaya.MScriptUtil()
  1040. infCountPtr = infCount.asUintPtr()
  1041. numVertices = geoIter.count()
  1042. weightArray = [0] * TRESSFX_MAX_INFLUENTIAL_BONE_COUNT * numVertices # joint weight array for all vertices. Each vertex will have TRESSFX_MAX_INFLUENTIAL_BONE_COUNT weights.
  1043. # It is initialized with zero for empty weight in case there are less weights than TRESSFX_MAX_INFLUENTIAL_BONE_COUNT .
  1044. jointIndexArray = [-1] * TRESSFX_MAX_INFLUENTIAL_BONE_COUNT * numVertices # joint index array for all vertices. It is initialized with -1 for an empty element in case
  1045. # there are less weights than TRESSFX_MAX_INFLUENTIAL_BONE_COUNT .
  1046. # collect bone weights for all vertices in the mesh
  1047. index = 0
  1048. while geoIter.isDone() == False:
  1049. weights = OpenMaya.MDoubleArray()
  1050. skinFn.getWeights(meshShapedagPath, geoIter.currentItem(), weights, infCountPtr)
  1051. weightJointIndexPairs = []
  1052. sumWtsThisVertex = 0.0
  1053. for i in range(len(weights)):
  1054. sumWtsThisVertex += weights[i]
  1055. pair = WeightJointIndexPair()
  1056. pair.weight = weights[i]
  1057. if (bUseJointSubset == True) and (i not in influenceSubsetIndices) and (pair.weight > 0.0):
  1058. #print("JointSubset: Setting pair weight:%5.2f to zero for bone index: %d (%s)\n" % (pair.weight, i, influenceObjectsNames[i]))
  1059. pair.weight = 0.0 #don't use this weight, not in subset of joints we want
  1060. pair.joint_index = i
  1061. weightJointIndexPairs.append(pair)
  1062. weightJointIndexPairs.sort()
  1063. a = 0
  1064. sumWtAdjusted = 0.0
  1065. for j in range(min(len(weightJointIndexPairs), TRESSFX_MAX_INFLUENTIAL_BONE_COUNT )):
  1066. weightArray[index*TRESSFX_MAX_INFLUENTIAL_BONE_COUNT + a] = weightJointIndexPairs[j].weight
  1067. jointIndexArray[index*TRESSFX_MAX_INFLUENTIAL_BONE_COUNT + a] = weightJointIndexPairs[j].joint_index
  1068. a += 1
  1069. sumWtAdjusted += weightJointIndexPairs[j].weight
  1070. index += 1
  1071. geoIter.next()
  1072. #------------------------
  1073. # Save the tfxbone file.
  1074. #------------------------
  1075. progressBar = ProgressBar('Saving a tfxbone file', len(influenceObjectsNames) + len(triangleIdForStrandsList))
  1076. f = open(filepath, "wb")
  1077. # Number of Bones
  1078. f.write(ctypes.c_int(len(influenceObjectsNames)))
  1079. # Write all bone (joint) names
  1080. for i in range(len(influenceObjectsNames)):
  1081. # Bone Joint Index
  1082. f.write(ctypes.c_int(i))
  1083. # Size of the string, add 1 to leave room for the nullterminate.
  1084. f.write(ctypes.c_int(len(influenceObjectsNames[i]) + 1))
  1085. # Print the characters of the string 1 by 1.
  1086. for j in range(len(influenceObjectsNames[i])):
  1087. f.write(ctypes.c_byte(ord(influenceObjectsNames[i][j])))
  1088. # Add a zero to null terminate the string.
  1089. f.write(ctypes.c_byte(0))
  1090. progressBar.Increment()
  1091. # Number of Strands
  1092. f.write(ctypes.c_int(len(triangleIdForStrandsList)))
  1093. for i in range(len(triangleIdForStrandsList)):
  1094. triangleId = triangleIdForStrandsList[i]
  1095. # three vertex indices from one triangle - Following two lines should work equally but I haven't verified it yet.
  1096. #vertexIndices = [triangleVertexIndices[triangleId*3], triangleVertexIndices[triangleId*3+1], triangleVertexIndices[triangleId*3+2]]
  1097. vertexIndices = vertexTriangleList[i]
  1098. baryCoord = baryCoordList[i]
  1099. #todo: Investigate if we are getting outlier case where a baryCoord is causing a strand to go to zero?
  1100. # We should be using sorted (i.e. of all weights, only top 4 and
  1101. # which will multiply barycoord against the weight and if we get a weight with 0 in the final four, it is set to the default root,
  1102. # which is joint0 at wt0 (which, if there are no other non-zero influences, would freeze the hair in place)
  1103. # This is why a custom root setting was added, with a settable weight (i.e. non zero), just in case we get tiny barycoords that force
  1104. # a resulting weight to zero (and have few bones influencing that hair, such that all the weight goes to zero)
  1105. #get final pairs (post adjustment by baryCoord)
  1106. weightJointIndexPairs = GetSortedWeightsFromTriangleVertices(TRESSFX_MAX_INFLUENTIAL_BONE_COUNT , vertexIndices, jointIndexArray, weightArray, baryCoord)
  1107. # Index, the rest should be self explanatory.
  1108. f.write(ctypes.c_int(i))
  1109. for j in range(4):
  1110. joint_index = 0
  1111. weight = 0.0
  1112. try:
  1113. joint_index = weightJointIndexPairs[j].joint_index
  1114. weight = weightJointIndexPairs[j].weight
  1115. except:
  1116. print("TressFX: Saving Bone file, exception getting joint_index/bone weight for triangleId %d in StrandsList" % i) #big error: throw exception to stop
  1117. pass
  1118. f.write(ctypes.c_int(joint_index))
  1119. f.write(ctypes.c_float(weight))
  1120. progressBar.Increment()
  1121. f.close()
  1122. progressBar.Kill()
  1123. return
  1124. def RecursiveSearchCurve(curves, objNode, minCurveLength):
  1125. if objNode.hasFn(OpenMaya.MFn.kNurbsCurve):
  1126. curveFn = OpenMaya.MFnNurbsCurve(objNode)
  1127. curveLength = curveFn.length()
  1128. # We only export open type curves.
  1129. if curveFn.form() == OpenMaya.MFnNurbsCurve.kOpen:
  1130. if curveLength >= minCurveLength:
  1131. curves.append(curveFn)
  1132. elif objNode.hasFn(OpenMaya.MFn.kTransform):
  1133. objFn = OpenMaya.MFnTransform(objNode)
  1134. for j in range(objFn.childCount()):
  1135. childObjNode = objFn.child(j)
  1136. RecursiveSearchCurve(curves, childObjNode, minCurveLength)
  1137. def GetSelectedNurbsCurves(minCurveLength):
  1138. slist = OpenMaya.MSelectionList()
  1139. OpenMaya.MGlobal.getActiveSelectionList( slist )
  1140. iter = OpenMaya.MItSelectionList(slist)
  1141. curves = []
  1142. # Find all nurbs curves under the selected node recursively.
  1143. #TODO. Confirm that this should be doing a 'select hierarachy' so user doesn't have to...
  1144. for i in range(slist.length()):
  1145. selObj = OpenMaya.MObject()
  1146. slist.getDependNode(i, selObj)
  1147. RecursiveSearchCurve(curves, selObj, minCurveLength)
  1148. return curves
  1149. class TressFXTFXFileHeader(ctypes.Structure):
  1150. _fields_ = [('version', ctypes.c_float),
  1151. ('numHairStrands', ctypes.c_uint),
  1152. ('numVerticesPerStrand', ctypes.c_uint),
  1153. ('offsetVertexPosition', ctypes.c_uint),
  1154. ('offsetStrandUV', ctypes.c_uint),
  1155. ('offsetVertexUV', ctypes.c_uint),
  1156. ('offsetStrandThickness', ctypes.c_uint),
  1157. ('offsetVertexColor', ctypes.c_uint),
  1158. ('reserved', ctypes.c_uint * 32)]
  1159. class tressfx_float4(ctypes.Structure):
  1160. _fields_ = [('x', ctypes.c_float),
  1161. ('y', ctypes.c_float),
  1162. ('z', ctypes.c_float),
  1163. ('w', ctypes.c_float)]
  1164. class tressfx_float2(ctypes.Structure):
  1165. _fields_ = [('x', ctypes.c_float),
  1166. ('y', ctypes.c_float)]
  1167. def SaveTFXBinaryFile(filepath, curves, meshShapedagPath):
  1168. numCurves = len(curves)
  1169. numVerticesPerStrand = cmds.optionMenu("numberOfStrandsOptionMenu", query=True, value=True)
  1170. numVerticesPerStrand = int(numVerticesPerStrand)
  1171. #check to see if we need to scale the points
  1172. sceneScale = cmds.optionMenu('scalingOptionMenu', query=True, value=True)
  1173. sceneScale = float(sceneScale)
  1174. print("TressFX: Saving TFX Binary:scene scale multiplier = %f" % sceneScale) #todo: make sure scaling doesn't affect uv issues (if there is an issue, that is)
  1175. #sampling options
  1176. curveSample = cmds.optionMenu("samplingOptionMenu", query=True, value=True)
  1177. curveSample = int(curveSample)
  1178. curveIndex_Offset = cmds.intField("curveOffset",q = True, v = True)
  1179. #sanity check
  1180. if curveIndex_Offset >= numCurves:
  1181. cmds.warning('TressFX: Curve Offset requested Greater or Equal to actual Number of Curves - Please Lower Offset Value')
  1182. return
  1183. bothEndsImmovable = cmds.checkBox("bothEndsImmovable",q = True, v = True)
  1184. invertZ = cmds.checkBox("InvertZ",q = True, v = True)
  1185. useZUp = cmds.checkBox("useZUp",q = True, v = True)
  1186. bChangeUpToZ = False
  1187. if (useZUp == True):
  1188. #query current up axis in use
  1189. currentAxis = cmds.upAxis(q = True, axis = True)
  1190. if (currentAxis == 'y'):
  1191. bChangeUpToZ = True
  1192. cmds.warning("TressFX: Maya currently using Y axis as UP, Z up requested: doing y/z swap")
  1193. elif (currentAxis != 'z'):
  1194. cmds.warning("TressFX: Problem detected, attempt to detect Maya UP axis setting failed. No change to default UP axis (no y/z swap)")
  1195. ignoreUVErrors = cmds.checkBox("ignoreUVErrorsCheckBox", q = True, v = True)
  1196. #useCurl = cmds.checkBox("useCurl",q = True, v = True)
  1197. rootPositions = []
  1198. tfxHeader = TressFXTFXFileHeader()
  1199. tfxHeader.version = 4.0
  1200. tfxHeader.numHairStrands = int(numCurves//curveSample) #number of curves may be a subset if we are sampling a subset only
  1201. tfxHeader.numVerticesPerStrand = numVerticesPerStrand
  1202. tfxHeader.offsetVertexPosition = ctypes.sizeof(TressFXTFXFileHeader)
  1203. tfxHeader.offsetStrandUV = 0
  1204. tfxHeader.offsetVertexUV = 0
  1205. tfxHeader.offsetStrandThickness = 0
  1206. tfxHeader.offsetVertexColor = 0
  1207. meshFn = None
  1208. meshIntersector = None
  1209. #if sampling at a lower amount than the full curve set, div by sample over entire set, then mult by sample to jump
  1210. #if offseting, subtract offset amount from full range of loop, so when it's added to the index
  1211. #it won't overshoot the actual number of curves range
  1212. adjustedCurveRange = int(numCurves//curveSample) - curveIndex_Offset #floor division is //
  1213. #sanity check 2
  1214. if curveIndex_Offset >= adjustedCurveRange:
  1215. cmds.warning('TressFX: Curve Offset requested Greater or Equal to subset:Sampled Curves - Please Lower Offset Value')
  1216. return
  1217. # if meshShapedagPath is passed then let's get strand texture coords. To do this, we need MFnMesh and MMeshIntersector objects.
  1218. if meshShapedagPath != None:
  1219. meshFn = OpenMaya.MFnMesh(meshShapedagPath)
  1220. meshIntersector = OpenMaya.MMeshIntersector()
  1221. meshIntersector.create(meshShapedagPath.node())
  1222. # tfxHeader.offsetStrandUV = tfxHeader.offsetVertexPosition + numCurves * numVerticesPerStrand * ctypes.sizeof(tressfx_float4)
  1223. tfxHeader.offsetStrandUV = tfxHeader.offsetVertexPosition + adjustedCurveRange * numVerticesPerStrand * ctypes.sizeof(tressfx_float4)
  1224. bInvertYForUVs = cmds.checkBox("InvertYForUVs",q = True, v = True)
  1225. #totalProgress = numCurves*numVerticesPerStrand #old, non adjusted for sampling of curves
  1226. totalProgress = adjustedCurveRange*numVerticesPerStrand
  1227. if meshFn != None:
  1228. #totalProgress = numCurves*numVerticesPerStrand + numCurves #old, non adjusted for sampling of curves
  1229. totalProgress = adjustedCurveRange*numVerticesPerStrand + adjustedCurveRange
  1230. progressBar = ProgressBar('Saving a tfx file', totalProgress)
  1231. f = open(filepath, "wb")
  1232. f.write(tfxHeader)
  1233. #if sampling at a lower amount than the full curve set, div by sample over entire set, then mult by sample to jump
  1234. #if offseting, subtract offset amount from full range of loop, so when it's added to the index
  1235. #it won't overshoot the actual number of curves range
  1236. #adjustedCurveRange = int(numCurves//curveSample) - curveIndex_Offset #floor division is //
  1237. for i in xrange(adjustedCurveRange):
  1238. curveFn = curves[(i*curveSample) + curveIndex_Offset] #adjusted curve range to accomodate sampling and offsetting into the curve set
  1239. # getting Min/Max value of the nurbs curve
  1240. min = OpenMaya.MScriptUtil()
  1241. min.createFromDouble(0)
  1242. minPtr = min.asDoublePtr()
  1243. max = OpenMaya.MScriptUtil()
  1244. max.createFromDouble(0)
  1245. maxPtr = max.asDoublePtr()
  1246. curveFn.getKnotDomain(minPtr, maxPtr)
  1247. min_val = OpenMaya.MScriptUtil(minPtr).asDouble()
  1248. max_val = OpenMaya.MScriptUtil(maxPtr).asDouble()
  1249. for j in range(0, numVerticesPerStrand):
  1250. param = j/ float(numVerticesPerStrand-1)
  1251. pos = OpenMaya.MPoint()
  1252. param = param * (max_val - min_val)
  1253. curveFn.getPointAtParam(param, pos, OpenMaya.MSpace.kObject) # kObject
  1254. p = tressfx_float4()
  1255. p.x = pos.x
  1256. p.y = pos.y
  1257. if invertZ:
  1258. p.z = -pos.z # flip in z-axis
  1259. else:
  1260. p.z = pos.z
  1261. #if invertY: #no use case currently
  1262. # p.y = -pos.y
  1263. #else:
  1264. # p.y = pos.y
  1265. if (bChangeUpToZ == True): #Unreal uses Z as up (not Y), and Maya is currently using Y (so we need to swap values)
  1266. temp = p.y
  1267. p.y = p.z
  1268. p.z = temp #not sure if can use p.yz = p.zy (or if supported on all possible python builds used with this exporter) so will use old fashioned way
  1269. # w component is an inverse mass
  1270. if j == 0 or j == 1: # the first two vertices are immovable always.
  1271. p.w = 0
  1272. else:
  1273. p.w = 1.0
  1274. if j == (numVerticesPerStrand-1) and bothEndsImmovable: #fix the last vertice of strand
  1275. p.w = 0
  1276. if (sceneScale != 1.0):
  1277. #print('tfx scaling doing it...not 1.0')
  1278. p.x = p.x * sceneScale
  1279. p.y = p.y * sceneScale
  1280. p.z = p.z * sceneScale
  1281. f.write(p)
  1282. progressBar.Increment()
  1283. # # find face index
  1284. rootPos = OpenMaya.MPoint()
  1285. curveFn.getPointAtParam(0, rootPos, OpenMaya.MSpace.kObject) # must be kObject
  1286. rootPositions.append(rootPos)
  1287. # if meshShapedagPath is passed then let's get strand texture coords by using raycasting to the mesh from each root position of hair strand.
  1288. if meshFn != None:
  1289. #last known good u,v
  1290. #in case we hit bad uv points, we can at least try to set them closer to the
  1291. #last u,v set (versus 0,0)
  1292. u_lkg = 0.0
  1293. v_lkg = 0.0
  1294. uMin = 0.0
  1295. uMax = 1.0
  1296. vMin = 0.0
  1297. vMax = 1.0
  1298. #query to see if we have a user defined custom uv bounding box
  1299. bCustomUVRange = cmds.checkBox("useNonUniformUVRange",q = True, v = True)
  1300. if (bCustomUVRange == True) and (bInvertYForUVs == True):
  1301. #only need the min and max of v (currently)
  1302. uMin = cmds.floatField("uMin", q = True, v = True)
  1303. uMax = cmds.floatField("uMax", q = True, v = True)
  1304. vMin = cmds.floatField("vMin", q = True, v = True)
  1305. vMax = cmds.floatField("vMax", q = True, v = True)
  1306. cmds.warning("TressFX: Using custom UV range. This v range (%5.2f, %5.2f) will be used during DirectX y-flipping ('v' reflect) for uv coordinates." % (vMin, vMax))
  1307. print ("TressFX: UV bounding box is: u[%5.2f, %5.2f], v[%5.2f, %5.2f]" % (uMin, uMax, vMin, vMax))
  1308. for i in range(len(rootPositions)):
  1309. rootPoint = rootPositions[i]
  1310. # Find UV coordinates
  1311. util = OpenMaya.MScriptUtil()
  1312. util.createFromList([0.0, 0.0], 2)
  1313. uv_ptr = util.asFloat2Ptr()
  1314. try:
  1315. meshFn.getUVAtPoint(rootPoint, uv_ptr)
  1316. u = OpenMaya.MScriptUtil.getFloat2ArrayItem(uv_ptr, 0, 0)
  1317. v = OpenMaya.MScriptUtil.getFloat2ArrayItem(uv_ptr, 0, 1)
  1318. u_lkg = u
  1319. v_lkg = v
  1320. except:
  1321. #if NOT strict mode' then ok to give point a default 0,0 uv point value, else kill the file
  1322. #cmds.warning('Exception Hit! meshFn.getUVAtPoint failed for rootPoint')
  1323. if ignoreUVErrors:
  1324. cmds.warning('TressFX: UV point error Exception (strict mode off): UV point failed for rootPoint->Ignoring Exception, using last known good (lkg) as uv instead')
  1325. u = u_lkg
  1326. v = v_lkg
  1327. else:
  1328. f.close()
  1329. progressBar.Kill()
  1330. cmds.warning('TressFX: UV point error Exception (strict mode on): UV point failed for rootPoint->Failing to create TFX. Deleting the open TFX file: ' + filepath)
  1331. cmds.sysFile(filepath, delete=True) #remove the damaged file
  1332. return
  1333. uv_coord = tressfx_float2()
  1334. uv_coord.x = u
  1335. uv_coord.y = v
  1336. if (bInvertYForUVs == True):
  1337. uv_coord.y = vMax - uv_coord.y + vMin #uv_coord.y = 1.0 - uv_coord.y # DirectX has it inverted, uniform means a typical bounding box of 0-1 u and v, so we would actually use 1- v + 0 to invert
  1338. #print "uv:%g, %g\n" % (uv_coord.x, uv_coord.y)
  1339. f.write(uv_coord)
  1340. progressBar.Increment()
  1341. f.close()
  1342. progressBar.Kill()
  1343. return rootPositions
  1344. class TressFXSkinFileObject(ctypes.Structure):
  1345. _fields_ = [('version', ctypes.c_uint),
  1346. ('numHairs', ctypes.c_uint),
  1347. ('numTriangles', ctypes.c_uint),
  1348. ('reserved1', ctypes.c_uint * 31),
  1349. ('hairToMeshMap_Offset', ctypes.c_uint),
  1350. ('perStrandUVCoordniate_Offset', ctypes.c_uint),
  1351. ('reserved1', ctypes.c_uint * 31)]
  1352. class HairToTriangleMapping(ctypes.Structure):
  1353. _fields_ = [('mesh', ctypes.c_uint),
  1354. ('triangle', ctypes.c_uint),
  1355. ('barycentricCoord_x', ctypes.c_float),
  1356. ('barycentricCoord_y', ctypes.c_float),
  1357. ('barycentricCoord_z', ctypes.c_float),
  1358. ('reserved', ctypes.c_uint)]
  1359. def SaveTFXSkinBinaryFile(filepath, meshShapedagPath, rootPositions):
  1360. #---------------------------------------------------------------------------
  1361. # Build a face/triangle index list to convert face index into triangle index
  1362. #---------------------------------------------------------------------------
  1363. faceIter = OpenMaya.MItMeshPolygon(meshShapedagPath)
  1364. triangleCount = 0
  1365. faceTriaIndexList = []
  1366. index = 0
  1367. bInvertYForUVs = cmds.checkBox("InvertYForUVs",q = True, v = True)
  1368. util = OpenMaya.MScriptUtil()
  1369. util.createFromInt(0)
  1370. while not faceIter.isDone():
  1371. faceTriaIndexList.append(triangleCount)
  1372. if faceIter.hasValidTriangulation():
  1373. numTrianglesPtr = util.asIntPtr()
  1374. faceIter.numTriangles(numTrianglesPtr)
  1375. numTriangles = util.getInt(numTrianglesPtr)
  1376. triangleCount += numTriangles
  1377. faceIter.next()
  1378. #----------------------
  1379. # Find the closest face
  1380. #----------------------
  1381. meshFn = OpenMaya.MFnMesh(meshShapedagPath)
  1382. meshIntersector = OpenMaya.MMeshIntersector()
  1383. meshIntersector.create(meshShapedagPath.node())
  1384. faceIdList = []
  1385. baryCoordList = []
  1386. uvCoordList = []
  1387. progressBar = ProgressBar('Collecting skin data', len(rootPositions))
  1388. for i in range(len(rootPositions)):
  1389. rootPoint = rootPositions[i]
  1390. # Find the closest point info
  1391. meshPt = OpenMaya.MPointOnMesh()
  1392. meshIntersector.getClosestPoint(rootPoint, meshPt)
  1393. pt = meshPt.getPoint()
  1394. pointOnMesh = OpenMaya.MPoint()
  1395. pointOnMesh = pt
  1396. # Find face index
  1397. faceId = meshPt.faceIndex()
  1398. # Find triangle index
  1399. triangleId = faceTriaIndexList[faceId] + meshPt.triangleIndex()
  1400. faceIdList.append(triangleId)
  1401. # Find barycentric coordinates
  1402. uPtr = OpenMaya.MScriptUtil().asFloatPtr()
  1403. vPtr = OpenMaya.MScriptUtil().asFloatPtr()
  1404. meshPt.getBarycentricCoords(uPtr,vPtr)
  1405. uvw = OpenMaya.MPoint()
  1406. uvw.x = OpenMaya.MScriptUtil(uPtr).asFloat()
  1407. uvw.y = OpenMaya.MScriptUtil(vPtr).asFloat()
  1408. uvw.z = 1.0 - uvw.x - uvw.y
  1409. baryCoordList.append(uvw)
  1410. # TODO: Why are there negative barycentric coords?
  1411. #if uvw.x < 0 or uvw.y < 0 or uvw.z < 0:
  1412. # print 'uvw:', uvw.x, uvw.y, uvw.z
  1413. # Find UV coordinates
  1414. util = OpenMaya.MScriptUtil()
  1415. util.createFromList([0.0, 0.0], 2)
  1416. uv_ptr = util.asFloat2Ptr()
  1417. meshFn.getUVAtPoint(rootPoint, uv_ptr)
  1418. u = OpenMaya.MScriptUtil.getFloat2ArrayItem(uv_ptr, 0, 0)
  1419. v = OpenMaya.MScriptUtil.getFloat2ArrayItem(uv_ptr, 0, 1)
  1420. uv_coord = OpenMaya.MPoint()
  1421. uv_coord.x = u
  1422. uv_coord.y = v
  1423. uv_coord.z = 0
  1424. uvCoordList.append(uv_coord)
  1425. # update progress gui
  1426. progressBar.Increment()
  1427. progressBar.Kill()
  1428. #--------------------
  1429. # Save a tfxskin file
  1430. #--------------------
  1431. tfxSkinObj = TressFXSkinFileObject()
  1432. tfxSkinObj.version = 1
  1433. tfxSkinObj.numHairs = len(faceIdList)
  1434. tfxSkinObj.numTriangles = 0
  1435. tfxSkinObj.hairToMeshMap_Offset = ctypes.sizeof(TressFXSkinFileObject)
  1436. tfxSkinObj.perStrandUVCoordniate_Offset = tfxSkinObj.hairToMeshMap_Offset + len(faceIdList) * ctypes.sizeof(HairToTriangleMapping)
  1437. f = open(filepath, "wb")
  1438. f.write(tfxSkinObj)
  1439. progressBar = ProgressBar('Saving a tfxskin file', len(faceIdList) + len(uvCoordList))
  1440. for i in xrange(len(faceIdList)):
  1441. mapping = HairToTriangleMapping()
  1442. mapping.mesh = 0
  1443. mapping.triangle = faceIdList[i]
  1444. uvw = baryCoordList[i]
  1445. mapping.barycentricCoord_x = uvw.x
  1446. mapping.barycentricCoord_y = uvw.y
  1447. mapping.barycentricCoord_z = uvw.z
  1448. f.write(mapping)
  1449. progressBar.Increment()
  1450. # per strand uv coordinate
  1451. for i in xrange(len(uvCoordList)):
  1452. uv_coord = uvCoordList[i]
  1453. p = Point()
  1454. p.x = uv_coord.x
  1455. if bInvertYForUVs:
  1456. p.y = 1.0 - uv_coord.y # DirectX has it inverted
  1457. p.z = uv_coord.z
  1458. f.write(p)
  1459. progressBar.Increment()
  1460. f.close()
  1461. progressBar.Kill()
  1462. return
  1463. def GetSortedWeightsFromOneVertex(_maxJointsPerVertex, vertexIndex, jointIndexArray, weightArray):
  1464. final_pairs = []
  1465. sumFinal = 0.0
  1466. #noDupNonzeroWts = cmds.checkBox("noDupBoneWts", q = True, v = True)
  1467. for j in range(_maxJointsPerVertex):
  1468. weight = weightArray[vertexIndex*_maxJointsPerVertex + j]
  1469. joint_index = jointIndexArray[vertexIndex*_maxJointsPerVertex + j]
  1470. bFound = False
  1471. for k in range(len(final_pairs)):
  1472. if final_pairs[k].joint_index == joint_index:
  1473. #if noDupNonzeroWts:
  1474. # if final_pairs[k].weight == 0: #no dup bones case
  1475. # final_pairs[k].weight += weight
  1476. #else:
  1477. # final_pairs[k].weight += weight
  1478. final_pairs[k].weight += weight
  1479. bFound = True
  1480. break
  1481. if bFound == False:
  1482. pair = WeightJointIndexPair()
  1483. pair.weight = weight
  1484. pair.joint_index = joint_index
  1485. final_pairs.append(pair)
  1486. #Set joint index to the user defined root/root weight (default is joint 0 with 0 weight) if the weight is zero.
  1487. for i in xrange(len(final_pairs)):
  1488. if final_pairs[i].weight == 0:
  1489. final_pairs[i].joint_index = defaultJointRootIndex
  1490. final_pairs[i].weight = defaultJointRootWeight
  1491. sumFinal += final_pairs[i].weight
  1492. #Normalize so that the sum of the final pairs weight is 1.0 (must be done after we have a final summation of the total weight)
  1493. bNormalizeVtWts = cmds.checkBox("RenormalizeFinalPairs", q = True, v = True)
  1494. if (bNormalizeVtWts == True):
  1495. if (sumFinal > 0.0):
  1496. for i in xrange(len(final_pairs)):
  1497. final_pairs[i].weight /= sumFinal
  1498. else:
  1499. cmds.warning("TressFX: problem with final weights for vertex: %d, total weight <= 0.0: Try using joint subset + custom root joint/weight to compensate." % vertexIndex)
  1500. final_pairs.sort()
  1501. # number of elements of final_pairs could be more than _maxJointsPerVertex but it should be at least _maxJointsPerVertex.
  1502. # If you really want it to be exactly _maxJointsPerVertex, you can try to pop out elements.
  1503. return final_pairs
  1504. def is_match(regex, text):
  1505. pattern = re.compile(regex, text)
  1506. return pattern.search(text) is not None
  1507. def ExportMesh(filepath, meshShapedagPath):
  1508. meshFn = OpenMaya.MFnMesh(meshShapedagPath)
  1509. meshIntersector = OpenMaya.MMeshIntersector()
  1510. meshIntersector.create(meshShapedagPath.node())
  1511. faceIdList = []
  1512. baryCoordList = []
  1513. points = OpenMaya.MPointArray()
  1514. meshFn.getPoints(points, OpenMaya.MSpace.kWorld)
  1515. normals = OpenMaya.MFloatVectorArray()
  1516. meshFn.getVertexNormals(False, normals, OpenMaya.MSpace.kWorld)
  1517. triangleCounts = OpenMaya.MIntArray()
  1518. triangleVertexIndices = OpenMaya.MIntArray() # the size of this array is three times of the number of total triangles
  1519. meshFn.getTriangles(triangleCounts, triangleVertexIndices)
  1520. #-------------------------
  1521. # Get skin cluster object
  1522. #-------------------------
  1523. skinClusterName = ''
  1524. mesh_shape_name = meshFn.name()
  1525. skinClusters = cmds.listHistory(mesh_shape_name)
  1526. skinClusters = cmds.ls(skinClusters, type="skinCluster")
  1527. if skinClusters:
  1528. skinClusterName = skinClusters[0]
  1529. else:
  1530. cmds.warning('TressFX: No skin cluster found on '+ mesh_shape_name)
  1531. return
  1532. #print skinClusterName
  1533. # get the MFnSkinCluster using skinClusterName
  1534. selList = OpenMaya.MSelectionList()
  1535. selList.add(skinClusterName)
  1536. skinClusterNode = OpenMaya.MObject()
  1537. selList.getDependNode(0, skinClusterNode)
  1538. skinFn = OpenMayaAnim.MFnSkinCluster(skinClusterNode)
  1539. dagPaths = MDagPathArray()
  1540. skinFn.influenceObjects(dagPaths)
  1541. # influence object is a joint
  1542. influenceObjectsNames = []
  1543. #do we need to remove the namespace if present?
  1544. bRemoveNSColMesh = cmds.checkBox("removeNamespaceCM", q = True, v = True)
  1545. #check if we need to scale the points
  1546. sceneScaleCol = cmds.optionMenu('scalingCollisionOptionMenu', query=True, value=True)
  1547. sceneScaleCol = float(sceneScaleCol)
  1548. print("TressFX: Exporting Collision Mesh:scene scale multiplier = %f" % sceneScaleCol) #Scaling, must apply to collision and hair exporting
  1549. # get joint names
  1550. #currently a collision mesh will parse through all the joints for that skinCluster (no limiting to a subset at this time)
  1551. for i in range(dagPaths.length()):
  1552. influenceName = dagPaths[i].partialPathName()
  1553. if bRemoveNSColMesh:
  1554. influenceNameList = re.split("[:]", influenceName)
  1555. if len(influenceNameList) != 1: #has a namespace prefix
  1556. influenceObjectsNames.append(influenceNameList[-1])
  1557. else:
  1558. influenceObjectsNames.append(influenceNameList[0])
  1559. else:
  1560. influenceObjectsNames.append(influenceName) # Need to remove namespace?
  1561. #influenceObjectsNames.append(influenceName) # Need to remove namespace?
  1562. skinMeshes = cmds.skinCluster(skinClusterName, query=1, geometry=1)
  1563. geoIter = OpenMaya.MItGeometry(meshShapedagPath)
  1564. infCount = OpenMaya.MScriptUtil()
  1565. infCountPtr = infCount.asUintPtr()
  1566. numVertices = geoIter.count()
  1567. weightArray = [0] * TRESSFX_MAX_INFLUENTIAL_BONE_COUNT * numVertices # joint weight array for all vertices. Each vertex will have TRESSFX_MAX_INFLUENTIAL_BONE_COUNT weights.
  1568. # It is initialized with zero for empty weight in case there are less weights than TRESSFX_MAX_INFLUENTIAL_BONE_COUNT .
  1569. jointIndexArray = [-1] * TRESSFX_MAX_INFLUENTIAL_BONE_COUNT * numVertices # joint index array for all vertices. It is initialized with -1 for an empty element in case
  1570. # there are less weights than TRESSFX_MAX_INFLUENTIAL_BONE_COUNT .
  1571. # collect bone weights for all vertices in the mesh
  1572. index = 0
  1573. progressBar = ProgressBar('Collect data', numVertices)
  1574. while geoIter.isDone() == False:
  1575. weights = OpenMaya.MDoubleArray()
  1576. skinFn.getWeights(meshShapedagPath, geoIter.currentItem(), weights, infCountPtr)
  1577. weightJointIndexPairs = []
  1578. for i in range(len(weights)):
  1579. pair = WeightJointIndexPair()
  1580. pair.weight = weights[i]
  1581. pair.joint_index = i
  1582. weightJointIndexPairs.append(pair)
  1583. weightJointIndexPairs.sort()
  1584. a = 0
  1585. for j in range(min(len(weightJointIndexPairs), TRESSFX_MAX_INFLUENTIAL_BONE_COUNT )):
  1586. weightArray[index*TRESSFX_MAX_INFLUENTIAL_BONE_COUNT + a] = weightJointIndexPairs[j].weight
  1587. jointIndexArray[index*TRESSFX_MAX_INFLUENTIAL_BONE_COUNT + a] = weightJointIndexPairs[j].joint_index
  1588. a += 1
  1589. index += 1
  1590. progressBar.Increment()
  1591. geoIter.next()
  1592. progressBar.Kill()
  1593. #----------------------------------------------------------
  1594. # We collected all necessary data. Now save them in file.
  1595. #----------------------------------------------------------
  1596. totalProgress = points.length() + triangleVertexIndices.length() / 3 + len(influenceObjectsNames)
  1597. progressBar = ProgressBar('Export collision mesh', totalProgress)
  1598. f = open(filepath, "w")
  1599. f.write("# TressFX collision mesh exported by TressFX Exporter in Maya\n")
  1600. # Write all bone (joint) names
  1601. f.write("numOfBones %g\n" % (len(influenceObjectsNames)))
  1602. f.write("# bone index, bone name\n")
  1603. for i in range(len(influenceObjectsNames)):
  1604. f.write("%d %s\n" % (i, influenceObjectsNames[i]))
  1605. progressBar.Increment()
  1606. # write vertex positions and skinning data
  1607. f.write("numOfVertices %g\n" % (points.length()))
  1608. f.write("# vertex index, vertex position x, y, z, normal x, y, z, joint index 0, joint index 1, joint index 2, joint index 3, weight 0, weight 1, weight 2, weight 3\n")
  1609. for vertexIndex in xrange(points.length()):
  1610. point = points[vertexIndex]
  1611. #do we have any scaling?
  1612. if (sceneScaleCol != 1.0):
  1613. #print("export: doing scaling..not 1.0")
  1614. point.x = point.x * sceneScaleCol
  1615. point.y = point.y * sceneScaleCol
  1616. point.z = point.z * sceneScaleCol
  1617. normal = normals[vertexIndex]
  1618. weightJointIndexPairs = GetSortedWeightsFromOneVertex(TRESSFX_MAX_INFLUENTIAL_BONE_COUNT , vertexIndex, jointIndexArray, weightArray)
  1619. f.write("%g %g %g %g %g %g %g %g %g %g %g %g %g %g %g\n" % (vertexIndex, point.x, point.y, point.z, normal.x, normal.y, normal.z, weightJointIndexPairs[0].joint_index, weightJointIndexPairs[1].joint_index, weightJointIndexPairs[2].joint_index, weightJointIndexPairs[3].joint_index,
  1620. weightJointIndexPairs[0].weight, weightJointIndexPairs[1].weight, weightJointIndexPairs[2].weight, weightJointIndexPairs[3].weight))
  1621. #todo: make this error checking optional, most of the time, it is unneeded
  1622. sumWts = 0.0
  1623. sumWts = weightJointIndexPairs[0].weight + weightJointIndexPairs[1].weight + weightJointIndexPairs[2].weight+ weightJointIndexPairs[3].weight
  1624. if sumWts <= 0.0:
  1625. cmds.warning('TressFX: this vertex(%d) has zero total weight or negative weights' % vertexIndex)
  1626. if sumWts > 1.001: #python seems to think 1 is greater than 1.0, so adding a threshold
  1627. cmds.warning('TressFX: this vertex(%d) has bone weights that total > 1 [%f]' % (vertexIndex, sumWts))
  1628. progressBar.Increment()
  1629. # write triangle face indices
  1630. f.write("numOfTriangles %g\n" % (triangleVertexIndices.length() / 3))
  1631. f.write("# triangle index, vertex index 0, vertex index 1, vertex index 2\n")
  1632. for i in range(triangleVertexIndices.length() / 3):
  1633. f.write("%g %d %d %d\n" % (i, triangleVertexIndices[i * 3 + 0], triangleVertexIndices[i * 3 + 1], triangleVertexIndices[i * 3 + 2]))
  1634. progressBar.Increment()
  1635. f.close()
  1636. progressBar.Kill()
  1637. return
  1638. def DoExportCollisionMesh(*args):
  1639. #------------------------------
  1640. # Find the selected mesh shape
  1641. #------------------------------
  1642. meshShapedagPath = OpenMaya.MDagPath()
  1643. if selected_mesh_shape_name == '':
  1644. cmds.warning("TressFX: To export skin or bone data, base mesh must be set.\n")
  1645. return
  1646. allObject = cmds.ls(selected_mesh_shape_name)
  1647. cmds.select(allObject) # TODO: This selection makes hair curves unselected. This is not a problem but just inconvenient for users if they want to keep the curves selected.
  1648. sel_list = OpenMaya.MSelectionList()
  1649. OpenMaya.MGlobal.getActiveSelectionList(sel_list)
  1650. if sel_list.length() == 0:
  1651. return
  1652. sel_list.getDagPath(0, meshShapedagPath)
  1653. meshShapedagPath.extendToShape() # get mesh shape
  1654. basicFilter = "*.tfxmesh"
  1655. filepath = cmds.fileDialog2(fileFilter=basicFilter, dialogStyle=2, caption="Save a tfx collision mesh file(*.tfxmesh)", fileMode=0)
  1656. if filepath == None or len(filepath) == 0:
  1657. return
  1658. ExportMesh(filepath[0], meshShapedagPath)
  1659. return