scripted/splitUVCmd.py

scripted/splitUVCmd.py
1 #-
2 # ==========================================================================
3 # Copyright (C) 1995 - 2006 Autodesk, Inc. and/or its licensors. All
4 # rights reserved.
5 #
6 # The coded instructions, statements, computer programs, and/or related
7 # material (collectively the "Data") in these files contain unpublished
8 # information proprietary to Autodesk, Inc. ("Autodesk") and/or its
9 # licensors, which is protected by U.S. and Canadian federal copyright
10 # law and by international treaties.
11 #
12 # The Data is provided for use exclusively by You. You have the right
13 # to use, modify, and incorporate this Data into other products for
14 # purposes authorized by the Autodesk software license agreement,
15 # without fee.
16 #
17 # The copyright notices in the Software and this entire statement,
18 # including the above license grant, this restriction and the
19 # following disclaimer, must be included in all copies of the
20 # Software, in whole or in part, and all derivative works of
21 # the Software, unless such copies or derivative works are solely
22 # in the form of machine-executable object code generated by a
23 # source language processor.
24 #
25 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
26 # AUTODESK DOES NOT MAKE AND HEREBY DISCLAIMS ANY EXPRESS OR IMPLIED
27 # WARRANTIES INCLUDING, BUT NOT LIMITED TO, THE WARRANTIES OF
28 # NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
29 # PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE, OR
30 # TRADE PRACTICE. IN NO EVENT WILL AUTODESK AND/OR ITS LICENSORS
31 # BE LIABLE FOR ANY LOST REVENUES, DATA, OR PROFITS, OR SPECIAL,
32 # DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES, EVEN IF AUTODESK
33 # AND/OR ITS LICENSORS HAS BEEN ADVISED OF THE POSSIBILITY
34 # OR PROBABILITY OF SUCH DAMAGES.
35 #
36 # ==========================================================================
37 #+
38 
39 import maya.OpenMaya as OpenMaya
40 import maya.OpenMayaMPx as OpenMayaMPx
41 import sys
42 
43 import polyModifier
44 
45 def statusError(message):
46  fullMsg = "Status failed: %s\n" % message
47  sys.stderr.write(fullMsg)
48  OpenMaya.MGlobal.displayError(fullMsg)
49  raise # called from exception handlers only, reraise exception
50 
51 
52 kPluginCmdName = "spSplitUV"
53 kPluginNodeTypeName = "spSplitUVNode"
54 kPluginNodeId = OpenMaya.MTypeId(0x87013)
55 
56 
57 #####################################################################
58 ## COMMAND ##########################################################
59 #####################################################################
60 
61 # Overview:
62 #
63 # The purpose of the splitUV command is to unshare (split) any selected UVs
64 # on a given object.
65 #
66 # How it works:
67 #
68 # This command is based on the polyModifierCmd. It relies on the polyModifierCmd
69 # to manage "how" the effects of the splitUV operation are applied (ie. directly
70 # on the mesh or through a modifier node). See polyModifier.py for more details
71 #
72 # To understand the algorithm behind the splitUV operation, refer to splitUVFty
73 #
74 # Limitations:
75 #
76 # (1) Can only operate on a single mesh at a given time. If there are more than one
77 # mesh with selected UVs, only the first mesh found in the selection list is
78 # operated on.
79 #
80 
81 class splitUV(polyModifier.polyModifierCmd):
82  def __init__(self):
83  polyModifier.polyModifierCmd.__init__(self)
84  # Selected UVs
85  #
86  # Note: The MObject, fComponentList, is only ever accessed on a single call to the plugin.
87  # It is never accessed between calls and is stored on the class for access in the
88  # overriden initModifierNode() method.
89  #
90  self.__fComponentList = OpenMaya.MObject()
91  self.__fSelUVs = OpenMaya.MIntArray()
92  self.__fSplitUVFactory = splitUVFty()
93 
94 
95  def isUndoable(self):
96  return True
97 
98 
99  def doIt(self, args):
100  """
101  implements the scripted splitUV command.
102 
103  Arguments:
104  args - the argument list that was passes to the command from MEL
105  """
106  # Parse the selection list for objects with selected UV components.
107  # To simplify things, we only take the first object that we find with
108  # selected UVs and operate on that object alone.
109  #
110  # All other objects are ignored and return warning messages indicating
111  # this limitation.
112  #
113  selList = OpenMaya.MSelectionList()
114  OpenMaya.MGlobal.getActiveSelectionList(selList)
115  selListIter = OpenMaya.MItSelectionList(selList)
116 
117  # The splitUV node only accepts a component list input, so we build
118  # a component list using MFnComponentListData.
119  #
120  # MIntArrays could also be passed into the node to represent the uvIds,
121  # but are less storage efficient than component lists, since consecutive
122  # components are bundled into a single entry in component lists.
123  #
124  compListFn = OpenMaya.MFnComponentListData()
125  compListFn.create()
126  found = False
127  foundMultiple = False
128 
129  while not selListIter.isDone():
130  dagPath = OpenMaya.MDagPath()
131  component = OpenMaya.MObject()
132  itemMatches = True
133  selListIter.getDagPath(dagPath, component)
134 
135  # Check for selected UV components
136  #
137  if itemMatches and (component.apiType() == OpenMaya.MFn.kMeshMapComponent):
138  if not found:
139  # The variable 'component' holds all selected components on the selected
140  # object, thus only a single call to MFnComponentListData::add() is needed
141  # to store the selected components for a given object.
142  #
143  compListFn.add(component)
144 
145  # Copy the component list created by MFnComponentListData into our local
146  # component list MObject member.
147  #
148  self.__fComponentList = compListFn.object()
149 
150  # Locally store the actual uvIds of the selected UVs so that this command
151  # can directly modify the mesh in the case when there is no history and
152  # history is turned off.
153  #
154  compFn = OpenMaya.MFnSingleIndexedComponent(component)
155  compFn.getElements(self.__fSelUVs)
156 
157  # Ensure that this DAG path will point to the shape of our object.
158  # Set the DAG path for the polyModifierCmd.
159  #
160  dagPath.extendToShape()
161  self._setMeshNode(dagPath)
162  found = True
163  else:
164  # Break once we have found a multiple object holding selected UVs, since
165  # we are not interested in how many multiple objects there are, only
166  # the fact that there are multiple objects.
167  #
168  foundMultiple = True
169  break
170 
171  selListIter.next()
172 
173  if foundMultiple:
174  self.displayWarning("Found more than one object with selected UVs - Only operating on first found object.")
175 
176  # Initialize the polyModifierCmd node type - mesh node already set
177  #
178  self._setModifierNodeType(kPluginNodeId)
179 
180  if found:
181  if self.__validateUVs():
182  # Now, pass control over to the polyModifierCmd._doModifyPoly() method
183  # to handle the operation.
184  #
185  try:
186  self._doModifyPoly()
187  except:
188  self.displayError("splitUV command failed!")
189  raise
190  else:
191  self.setResult("splitUV command succeeded!")
192  else:
193  self.displayError("splitUV command failed: Selected UVs are not splittable")
194  else:
195  self.displayError("splitUV command failed: Unable to find selected UVs")
196 
197 
198  def redoIt(self):
199  """
200  Implements redo for the scripted splitUV command.
201 
202  This method is called when the user has undone a command of this type
203  and then redoes it. No arguments are passed in as all of the necessary
204  information is cached by the doIt method.
205  """
206  try:
207  self._redoModifyPoly()
208  self.setResult("splitUV command succeeded!")
209  except:
210  self.displayError("splitUV command failed!")
211  raise
212 
213 
214  def undoIt(self):
215  """
216  implements undo for the scripted splitUV command.
217 
218  This method is called to undo a previous command of this type. The
219  system should be returned to the exact state that it was it previous
220  to this command being executed. That includes the selection state.
221  """
222  try:
223  self._undoModifyPoly()
224  self.setResult("splitUV undo succeeded!")
225  except:
226  self.displayError("splitUV undo failed!")
227  raise
228 
229 
230  def _initModifierNode(self, modifierNode):
231  # We need to tell the splitUV node which UVs to operate on. By overriding
232  # the polyModifierCmd._initModifierNode() method, we can insert our own
233  # modifierNode initialization code.
234  #
235  depNodeFn = OpenMaya.MFnDependencyNode(modifierNode)
236  uvListAttr = depNodeFn.attribute("inputComponents")
237 
238  # Pass the component list down to the splitUV node
239  #
240  uvListPlug = OpenMaya.MPlug(modifierNode, uvListAttr)
241  uvListPlug.setMObject(self.__fComponentList)
242 
243 
244  def _directModifier(self, mesh):
245  self.__fSplitUVFactory.setMesh(mesh)
246  self.__fSplitUVFactory.setUVIds(self.__fSelUVs)
247 
248  # Now, perform the splitUV
249  #
250  self.__fSplitUVFactory.doIt()
251 
252 
253  def __validateUVs(self):
254  """
255  Validate the UVs for the splitUV operation. UVs are valid only if they are shared
256  by more than one face. While the splitUVNode is smart enough to not process the
257  split if a UV is not splittable, a splitUV node is still created by the polyModifierCmd.
258  So call this method to validate the UVs before calling _doModifyPoly().
259 
260  validateUVs() will return true so long as there is at least one valid UV. It will
261  also prune out any invalid UVs from both the component list and UVId array.
262  """
263  # Get the mesh that we are operating on
264  #
265  dagPath = self._getMeshNode()
266  mesh = dagPath.node()
267 
268  # Get the number of faces sharing the selected UVs
269  #
270  meshFn = OpenMaya.MFnMesh(mesh)
271  polyIter = OpenMaya.MItMeshPolygon(mesh)
272  selUVFaceCountArray = OpenMaya.MIntArray()
273 
274  indexParam = OpenMaya.MScriptUtil(0)
275  indexPtr = indexParam.asIntPtr()
276 
277  count = 0
278  selUVsCount = self.__fSelUVs.length()
279  for i in range(selUVsCount):
280  while not polyIter.isDone():
281  if polyIter.hasUVs():
282  polyVertCount = polyIter.polygonVertexCount()
283 
284  for j in range(polyVertCount):
285  polyIter.getUVIndex(j, indexPtr)
286  UVIndex = indexParam.getInt(indexPtr)
287 
288  if UVIndex == self.__fSelUVs[i]:
289  count += 1
290  break
291  polyIter.next()
292  selUVFaceCountArray.append(count)
293 
294  # Now, check to make sure that at least one UV is being shared by more than one
295  # face. So long as we have one UV that we can operate on, we should proceed and let
296  # the splitUVNode ignore the UVs which are only shared by one face.
297  #
298  isValid = False
299  validUVIndices = OpenMaya.MIntArray()
300 
301  for i in range(selUVsCount):
302  if selUVFaceCountArray[i] > 1:
303  isValid = True
304  validUVIndices.append(i)
305 
306  if isValid:
307  self.__pruneUVs(validUVIndices)
308 
309  return isValid
310 
311 
312  def __pruneUVs(self, validUVIndices):
313  """
314  This method will remove any invalid UVIds from the component list and UVId array.
315  The benefit of this is to reduce the amount of extra processing that the node would
316  have to perform. It will result in less iterations through the mesh as there are
317  less UVs to search for.
318  """
319  validUVIds = OpenMaya.MIntArray()
320 
321  for i in range(validUVIndices.length()):
322  uvIndex = validUVIndices[i]
323  validUVIds.append(self.__fSelUVs[uvIndex])
324 
325  # Replace the local int array of UVIds
326  #
327  self.__fSelUVs.clear()
328  self.__fSelUVs = validUVIds
329 
330  # Build the list of valid components
331  #
332  compFn = OpenMaya.MFnSingleIndexedComponent()
333  try:
334  compFn.create(OpenMaya.MFn.kMeshMapComponent)
335  except:
336  statusError("compFn.create( MFn::kMeshMapComponent )")
337 
338  try:
339  compFn.addElements(validUVIds)
340  except:
341  statusError("compFn.addElements( validUVIds )")
342 
343  # Replace the component list
344  #
345  component = compFn.object()
346  compListFn = OpenMaya.MFnComponentListData()
347  compListFn.create()
348  try:
349  compListFn.add(component)
350  except:
351  statusError("compListFn.add( component )")
352 
353  self.__fComponentList = compListFn.object()
354 
355 
356 #####################################################################
357 ## FACTORY ##########################################################
358 #####################################################################
359 
360 # Overview:
361 #
362 # The splitUV factory implements the actual splitUV operation. It takes in
363 # only two parameters:
364 #
365 # 1) A polygonal mesh
366 # 2) An array of selected UV Ids
367 #
368 # The algorithm works as follows:
369 #
370 # 1) Parse the mesh for the selected UVs and collect:
371 #
372 # (a) Number of faces sharing each UV
373 # (stored as two arrays: face array, indexing/offset array)
374 # (b) Associated vertex Id
375 #
376 # 2) Create (N-1) new UVIds for each selected UV, where N represents the number of faces
377 # sharing the UV.
378 #
379 # 3) Set each of the new UVs to the same 2D location on the UVmap.
380 #
381 # 3) Arbitrarily let the last face in the list of faces sharing this UV to keep the original
382 # UV.
383 #
384 # 4) Assign each other face one of the new UVIds.
385 #
386 #
387 class splitUVFty(polyModifier.polyModifierFty):
388  def __init__(self):
389  polyModifier.polyModifierFty.__init__(self)
390  # Mesh Node
391  # Note: We only make use of this MObject during a single call of
392  # the splitUV plugin. It is never maintained and used between
393  # calls to the plugin as the MObject handle could be invalidated
394  # between calls to the plugin.
395  #
396  self.__fMesh = OpenMaya.MObject()
397  self.__fSelUVs = OpenMaya.MIntArray()
398  self.__fSelUVs.clear()
399 
400 
401  def setMesh(self, mesh):
402  self.__fMesh = mesh
403 
404 
405  def setUVIds(self, uvIds):
406  self.__fSelUVs = uvIds
407 
408 
409  def doIt(self):
410  """
411  Performs the actual splitUV operation on the given object and UVs
412  """
413  ####################################
414  # Declare our processing variables #
415  ####################################
416 
417  # Face Id and Face Offset map to the selected UVs
418  #
419  selUVFaceIdMap = OpenMaya.MIntArray()
420  selUVFaceOffsetMap = OpenMaya.MIntArray()
421 
422  # Local Vertex Index map to the selected UVs
423  #
424  selUVLocalVertIdMap = OpenMaya.MIntArray()
425 
426  #################################################
427  # Collect necessary information for the splitUV #
428  # #
429  # - uvSet #
430  # - faceIds / localVertIds per selected UV #
431  #################################################
432 
433  meshFn = OpenMaya.MFnMesh(self.__fMesh)
434  selUVSet = meshFn.currentUVSetName()
435 
436  indexParam = OpenMaya.MScriptUtil(0)
437  indexPtr = indexParam.asIntPtr()
438 
439  offset = 0
440  selUVsCount = self.__fSelUVs.length()
441  polyIter = OpenMaya.MItMeshPolygon(self.__fMesh)
442  for i in range(selUVsCount):
443  selUVFaceOffsetMap.append(offset)
444 
445  polyIter.reset()
446  while not polyIter.isDone():
447  if polyIter.hasUVs():
448  polyVertCount = polyIter.polygonVertexCount()
449 
450  for j in range(polyVertCount):
451  polyIter.getUVIndex(j, indexPtr)
452  UVIndex = indexParam.getInt(indexPtr)
453 
454  if UVIndex == self.__fSelUVs[i]:
455  selUVFaceIdMap.append(polyIter.index())
456  selUVLocalVertIdMap.append(j)
457  offset += 1
458  break
459 
460  polyIter.next()
461 
462  # Store total length of the faceId map in the last element of
463  # the offset map so that there is a way to get the number of faces
464  # sharing each of the selected UVs
465  #
466  selUVFaceOffsetMap.append(offset)
467 
468  ###############################
469  # Begin the splitUV operation #
470  ###############################
471 
472  currentUVCount = meshFn.numUVs(selUVSet)
473 
474  for i in range(selUVsCount):
475  # Get the current FaceId map offset
476  #
477  offset = selUVFaceOffsetMap[i]
478 
479  # Get the U and V values of the current UV
480  #
481  uvId = self.__fSelUVs[i]
482 
483  uParam = OpenMaya.MScriptUtil(0.0)
484  uPtr = uParam.asFloatPtr()
485  vParam = OpenMaya.MScriptUtil(0.0)
486  vPtr = vParam.asFloatPtr()
487  meshFn.getUV(uvId, uPtr, vPtr, selUVSet)
488  u = uParam.getFloat(uPtr)
489  v = vParam.getFloat(vPtr)
490 
491  # Get the number of faces sharing the current UV
492  #
493  faceCount = selUVFaceOffsetMap[i + 1] - selUVFaceOffsetMap[i]
494 
495  # Arbitrarily choose that the last faceId in the list of faces
496  # sharing this UV, will keep the original UV.
497  #
498  for j in range(faceCount-1):
499  meshFn.setUV(currentUVCount, u, v, selUVSet)
500 
501  localVertId = selUVLocalVertIdMap[offset]
502  faceId = selUVFaceIdMap[offset]
503 
504  meshFn.assignUV(faceId, localVertId, currentUVCount, selUVSet)
505 
506  currentUVCount += 1
507  offset += 1
508 
509 
510 #####################################################################
511 ## NODE #############################################################
512 #####################################################################
513 
514 class splitUVNode(polyModifier.polyModifierNode):
515  uvList = OpenMaya.MObject()
516 
517 
518  def __init__(self):
519  polyModifier.polyModifierNode.__init__(self)
520  self.fSplitUVFactory = splitUVFty()
521 
522 
523  def compute(self, plug, data):
524  """
525  Description:
526  This method computes the value of the given output plug based
527  on the values of the input attributes.
528 
529  Arguments:
530  plug - the plug to compute
531  data - object that provides access to the attributes for this node
532  """
533  stateData = 0
534  state = OpenMayaMPx.cvar.MPxNode_state
535  try:
536  stateData = data.outputValue(state)
537  except:
538  statusError("ERROR getting state")
539 
540  # Check for the HasNoEffect/PassThrough flag on the node.
541  #
542  # (stateData is an enumeration standard in all depend nodes - stored as short)
543  #
544  # (0 = Normal)
545  # (1 = HasNoEffect/PassThrough)
546  # (2 = Blocking)
547  # ...
548  #
549  if stateData.asShort() == 1:
550  try:
551  inputData = data.inputValue(splitUVNode.inMesh)
552  except:
553  statusError("ERROR getting inMesh")
554 
555  try:
556  outputData = data.outputValue(splitUVNode.outMesh)
557  except:
558  statusError("ERROR getting outMesh")
559 
560  # Simply redirect the inMesh to the outMesh for the PassThrough effect
561  #
562  outputData.setMObject(inputData.asMesh())
563  else:
564  # Check which output attribute we have been asked to
565  # compute. If this node doesn't know how to compute it,
566  # we must return MS::kUnknownParameter
567  #
568  if plug == splitUVNode.outMesh:
569  try:
570  inputData = data.inputValue(splitUVNode.inMesh)
571  except:
572  statusError("ERROR getting inMesh")
573 
574  try:
575  outputData = data.outputValue(splitUVNode.outMesh)
576  except:
577  statusError("ERROR getting outMesh")
578 
579  # Now, we get the value of the uvList and use it to perform
580  # the operation on this mesh
581  #
582  try:
583  inputUVs = data.inputValue(splitUVNode.uvList)
584  except:
585  statusError("ERROR getting uvList")
586 
587  # Copy the inMesh to the outMesh, and now you can
588  # perform operations in-place on the outMesh
589  #
590  outputData.setMObject(inputData.asMesh())
591  mesh = outputData.asMesh()
592 
593  # Retrieve the UV list from the component list.
594  #
595  # Note, we use a component list to store the components
596  # because it is more compact memory wise. (ie. comp[81:85]
597  # is smaller than comp[81], comp[82],...,comp[85])
598  #
599  compList = inputUVs.data()
600  compListFn = OpenMaya.MFnComponentListData(compList)
601 
602  uvIds = OpenMaya.MIntArray()
603  for i in range(compListFn.length()):
604  comp = compListFn[i]
605  if comp.apiType() == OpenMaya.MFn.kMeshMapComponent:
606  uvComp = OpenMaya.MFnSingleIndexedComponent(comp)
607  for j in range(uvComp.elementCount()):
608  uvId = uvComp.element(j)
609  uvIds.append(uvId)
610 
611  # Set the mesh object and uvList on the factory
612  #
613  self.fSplitUVFactory.setMesh(mesh)
614  self.fSplitUVFactory.setUVIds(uvIds)
615 
616  # Now, perform the splitUV
617  #
618  try:
619  self.fSplitUVFactory.doIt()
620  except:
621  statusError("ERROR in splitUVFty.doIt()")
622 
623  # Mark the output mesh as clean
624  #
625  outputData.setClean()
626  else:
627  return OpenMaya.kUnknownParameter
628 
629  return None
630 
631 
632 #####################################################################
633 ## REGISTRATION #####################################################
634 #####################################################################
635 
636 def cmdCreator():
637  return OpenMayaMPx.asMPxPtr(splitUV())
638 
639 
640 def nodeCreator():
641  return OpenMayaMPx.asMPxPtr(splitUVNode())
642 
643 
644 def nodeInitializer():
645  attrFn = OpenMaya.MFnTypedAttribute()
646 
647  splitUVNode.uvList = attrFn.create("inputComponents", "ics", OpenMaya.MFnComponentListData.kComponentList)
648  attrFn.setStorable(True) # To be stored during file-save
649 
650  splitUVNode.inMesh = attrFn.create("inMesh", "im", OpenMaya.MFnMeshData.kMesh)
651  attrFn.setStorable(True) # To be stored during file-save
652 
653  # Attribute is read-only because it is an output attribute
654  #
655  splitUVNode.outMesh = attrFn.create("outMesh", "om", OpenMaya.MFnMeshData.kMesh)
656  attrFn.setStorable(False)
657  attrFn.setWritable(False)
658 
659  # Add the attributes we have created to the node
660  #
661  splitUVNode.addAttribute(splitUVNode.uvList)
662  splitUVNode.addAttribute(splitUVNode.inMesh)
663  splitUVNode.addAttribute(splitUVNode.outMesh)
664 
665  # Set up a dependency between the input and the output. This will cause
666  # the output to be marked dirty when the input changes. The output will
667  # then be recomputed the next time the value of the output is requested.
668  #
669  splitUVNode.attributeAffects(splitUVNode.inMesh, splitUVNode.outMesh)
670  splitUVNode.attributeAffects(splitUVNode.uvList, splitUVNode.outMesh)
671 
672 
673 def initializePlugin(mobject):
674  mplugin = OpenMayaMPx.MFnPlugin(mobject, "Autodesk", "1.0", "Any")
675  try:
676  mplugin.registerCommand(kPluginCmdName, cmdCreator)
677  except:
678  sys.stderr.write( "Failed to register command: %s\n" % kPluginCmdName)
679  raise
680 
681  try:
682  mplugin.registerNode(kPluginNodeTypeName, kPluginNodeId, nodeCreator, nodeInitializer)
683  except:
684  sys.stderr.write( "Failed to register node: %s" % kPluginNodeTypeName)
685  raise
686 
687 
688 def uninitializePlugin(mobject):
689  mplugin = OpenMayaMPx.MFnPlugin(mobject)
690  try:
691  mplugin.deregisterCommand(kPluginCmdName)
692  except:
693  sys.stderr.write("Failed to unregister command: %s\n" % kPluginCmdName)
694  raise
695 
696  try:
697  mplugin.deregisterNode(kPluginNodeId)
698  except:
699  sys.stderr.write("Failed to deregister node: %s" % kPluginNodeTypeName)
700  raise