scripted/instanceShape.py

scripted/instanceShape.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 ###############################################################################
40 ##
41 ## instanceShape.py
42 ##
43 ## Description:
44 ## Registers a new shape that acts like an instancer. The new shape
45 ## type is called "instanceShape".
46 ##
47 ## The shape will instance N copies of a shape connected via a message
48 ## attribute on the node. The sample will distribute these N copies
49 ## in the XZ plane.
50 ##
51 ## There are no output attributes for this shape.
52 ## The following input attributes define the type of shape to draw.
53 ##
54 ## radius : circle radius for instance object.
55 ## instanceShape : a connection to the shape to instance
56 ## count : number of instances to make.
57 ##
58 ## Additionally the instancing feature demonstrated in this code
59 ## only works for custom shapes. Non-custom shapes will not work.
60 ##
61 ################################################################################
62 
63 # Usage:
64 # import maya
65 # maya.cmds.loadPlugin("instanceShape.py")
66 # maya.cmds.loadPlugin("basicShape.py")
67 
68 # basicShape = maya.cmds.createNode("spBasicShape")
69 # instanceShape = maya.cmds.createNode("spInstanceShape")
70 # maya.cmds.connectAttr( basicShape + ".message", instanceShape + ".instanceShape" )
71 #
72 
73 import maya.OpenMaya as OpenMaya
74 import maya.OpenMayaMPx as OpenMayaMPx
75 import maya.OpenMayaRender as OpenMayaRender
76 import maya.OpenMayaUI as OpenMayaUI
77 
78 import math
79 import sys
80 
81 kPluginNodeTypeName = "spInstanceShape"
82 spInstanceShapeNodeId = OpenMaya.MTypeId(0x00080029)
83 
84 glRenderer = OpenMayaRender.MHardwareRenderer.theRenderer()
85 glFT = glRenderer.glFunctionTable()
86 
87 kLeadColor = 18 # green
88 kActiveColor = 15 # white
89 kActiveAffectedColor = 8 # purple
90 kDormantColor = 4 # blue
91 kHiliteColor = 17 # pale blue
92 
93 kDefaultRadius = 1.0
94 kDefaultCount = 10
95 
96 
97 #####################################################################
98 ##
99 ## Geometry class
100 ##
101 class instanceGeom:
102  def __init__(self):
103  self.radius = kDefaultRadius
104  self.count = kDefaultCount
105  self.instanceShape = None
106  self.drawQueueList = []
107 
108 #####################################################################
109 ##
110 ## Shape class - defines the non-UI part of a shape node
111 ##
112 class instanceShape(OpenMayaMPx.MPxSurfaceShape):
113  # class variables
114  aRadius = OpenMaya.MObject()
115  aCount = OpenMaya.MObject()
116  aInstanceShape = OpenMaya.MObject()
117 
118  def __init__(self):
119  OpenMayaMPx.MPxSurfaceShape.__init__(self)
120 
121  # geometry
122  self.__myGeometry = instanceGeom()
123 
124  # override
125  def postConstructor(self):
126  """
127  When instances of this node are created internally, the
128  MObject associated with the instance is not created until
129  after the constructor of this class is called. This means
130  that no member functions of MPxSurfaceShape can be called in
131  the constructor. The postConstructor solves this
132  problem. Maya will call this function after the internal
133  object has been created. As a general rule do all of your
134  initialization in the postConstructor.
135  """
136  self.setRenderable(True)
137 
138  # override
139  def getInternalValue(self, plug, datahandle):
140  """
141  Handle internal attributes.
142  In order to impose limits on our attribute values we
143  mark them internal and use the values in fGeometry instead.
144  """
145  if (plug == instanceShape.aRadius):
146  datahandle.setDouble(self.__myGeometry.radius)
147  elif (plug == instanceShape.aCount):
148  datahandle.setInt(self.__myGeometry.count)
149  else:
150  return OpenMayaMPx.MPxSurfaceShape.getInternalValue(self, plug, datahandle)
151 
152  return True
153 
154 
155  # override
156  def setInternalValue(self, plug, datahandle):
157  """
158  Handle internal attributes.
159  In order to impose limits on our attribute values we
160  mark them internal and use the values in fGeometry instead.
161  """
162 
163  # the minimum radius is 0
164  #
165  if (plug == instanceShape.aRadius):
166  radius = datahandle.asDouble()
167 
168  if (radius < 0):
169  radius = 0
170 
171  self.__myGeometry.radius = radius
172 
173  elif (plug == instanceShape.aCount):
174  count = datahandle.asInt()
175  if (count < 0):
176  count = 0
177  self.__myGeometry.count = count
178  else:
179  return OpenMayaMPx.MPxSurfaceShape.setInternalValue(self, plug, datahandle)
180  return True
181 
182 
183  # override
184  def isBounded(self):
185  return True
186 
187 
188  # override
189  def boundingBox(self):
190  """
191  Returns the bounding box for the shape.
192  In this case just use the radius and height attributes
193  to determine the bounding box.
194  """
195  result = OpenMaya.MBoundingBox()
196 
197  geom = self.geometry()
198 
199  # Include the instance shape bounding box
200  if geom.instanceShape:
201  fnDag = OpenMaya.MFnDagNode( geom.instanceShape )
202  result = fnDag.boundingBox()
203 
204  r = geom.radius
205  instanceBbox = OpenMaya.MBoundingBox( result )
206  for c in range( geom.count ):
207  percent = float(c)/float(geom.count)
208  rad = 2*math.pi * percent
209  p = (r*math.cos(rad), r*math.sin(rad),0.0)
210  newbbox = OpenMaya.MBoundingBox( instanceBbox )
212  vec = OpenMaya.MVector( p[0], p[1], p[2] )
213  trans.setTranslation( vec, OpenMaya.MSpace.kTransform )
214  mmatrix = trans.asMatrix();
215  newbbox.transformUsing( mmatrix )
216  result.expand( newbbox )
217 
218  return result
219 
220 
221  def geometry(self):
222  """
223  This function gets the values of all the attributes and
224  assigns them to the fGeometry. Calling MPlug::getValue
225  will ensure that the values are up-to-date.
226  """
227  # return self.__myGeometry
228 
229  this_object = self.thisMObject()
230 
231  plug = OpenMaya.MPlug(this_object, instanceShape.aRadius)
232  self.__myGeometry.radius = plug.asDouble()
233  plug = OpenMaya.MPlug(this_object, instanceShape.aCount)
234  self.__myGeometry.count = plug.asInt()
235 
236  plug = OpenMaya.MPlug(this_object, instanceShape.aInstanceShape)
237  plugArray = OpenMaya.MPlugArray()
238  plug.connectedTo( plugArray, True, False )
239  if ( plugArray.length() > 0 ):
240  node = plugArray[0].node()
241  dagNode = OpenMaya.MFnDagNode(node)
242  path = OpenMaya.MDagPath()
243  dagNode.getPath(path)
244  self.__myGeometry.instanceShape = path
245 
246  return self.__myGeometry
247 
248 #####################################################################
249 ##
250 ## UI class - defines the UI part of a shape node
251 ##
252 class instanceShapeUI(OpenMayaMPx.MPxSurfaceShapeUI):
253  # private enums
254  def __init__(self):
255  OpenMayaMPx.MPxSurfaceShapeUI.__init__(self)
256 
257  # override
258  def getDrawRequests(self, info, objectAndActiveOnly, queue):
259  """
260  The draw data is used to pass geometry through the
261  draw queue. The data should hold all the information
262  needed to draw the shape.
263  """
264  self.geometry = None
265 
266  # Custom instancer objects can instance other custom surface
267  # shapes. This is done by first getting the draw request
268  # data for the instancer shape
269  #
270  data = OpenMayaUI.MDrawData()
271  request = info.getPrototype(self)
272  shapeNode = self.surfaceShape()
273  path = info.multiPath()
274  view = info.view()
275  # We stored the instance object via a connection on the surface
276  # shape. Retrieve that value.
277  #
278  geom = shapeNode.geometry()
279  shadedMode = False
280  mainMaterial = None
281  if request.displayStyle() == OpenMayaUI.M3dView.kGouraudShaded:
282  shadedMode = True
283 
284  if geom.instanceShape:
285  # Find the MPxSurfaceShape for the object that we are instancing
286  #
287  shapeUI = OpenMayaMPx.MPxSurfaceShapeUI.surfaceShapeUI(geom.instanceShape)
288  if shapeUI:
289  mainMaterial = shapeUI.material( geom.instanceShape )
290  mainMaterial.evaluateMaterial( view, geom.instanceShape )
291  r = geom.radius
292  for a in range(geom.count):
293  myQueue = OpenMayaUI.MDrawRequestQueue()
294  percent = float(a)/float(geom.count)
295  rad = 2*math.pi * percent
296  position = (r*math.cos(rad), r*math.sin(rad),0.0)
297  # Construct a reference to MDrawInfo and modify it
298  # to point to the instance shape. If we do not do
299  # this in then the call to getDrawRequests will think
300  # that we are still the instancer shape and not the
301  # instance shape.
302  #
303  myinfo = OpenMayaUI.MDrawInfo( info )
304  myinfo.setMultiPath( geom.instanceShape )
305  shapeUI.getDrawRequests( myinfo,
306  objectAndActiveOnly, myQueue )
307  geom.drawQueueList.append( (myQueue, position) )
308 
309  info.setMultiPath( path )
310  # Finally we must supply a material back to the drawing code.
311  # We attempt to use the instance shape material; however, if
312  # that fails then we fall back to the default material
313  #
314  if shadedMode:
315  defaultMaterial = OpenMayaUI.MMaterial.defaultMaterial()
316  if not mainMaterial:
317  mainMaterial = defaultMaterial
318  try:
319  request.setMaterial( mainMaterial )
320  except:
321  request.setMaterial( defaultMaterial )
322 
323 
324  self.getDrawData(geom, data)
325  request.setDrawData(data)
326 
327  self.geometry = geom
328  queue.add(request)
329 
330  # override
331  def draw(self, request, view):
332  """
333  From the given draw request, get the draw data and determine
334  which basic shape to draw and with what values.
335  """
336 
337  data = request.drawData()
338  shapeNode = self.surfaceShape()
339  geom = self.geometry
340  glFT.glMatrixMode( OpenMayaRender.MGL_MODELVIEW )
341  if geom.instanceShape:
342  shapeUI = OpenMayaMPx.MPxSurfaceShapeUI.surfaceShapeUI(geom.instanceShape)
343  for (queue, pos) in geom.drawQueueList:
344  glFT.glPushMatrix();
345  glFT.glTranslatef( pos[0], pos[1], pos[2] )
346  while not queue.isEmpty():
347  request = queue.remove()
348  shapeUI.draw( request, view )
349  glFT.glPopMatrix()
350 
351  # Draw a shell area that shows where the instances are being
352  # drawn. This is nice to have if we don't have any instance
353  # shapes connected to this plugin.
354  #
355  glFT.glPushAttrib( OpenMayaRender.MGL_ALL_ATTRIB_BITS )
356  glFT.glPolygonMode(OpenMayaRender.MGL_FRONT_AND_BACK,
357  OpenMayaRender.MGL_LINE)
358  glFT.glBegin(OpenMayaRender.MGL_QUADS)
359  glFT.glVertex3f(-1*(geom.radius), -1*(geom.radius), 0.0)
360  glFT.glNormal3f(0, 0, 1.0)
361 
362  glFT.glVertex3f(-1*(geom.radius), (geom.radius), 0.0)
363  glFT.glNormal3f(0, 0, 1.0)
364 
365  glFT.glVertex3f((geom.radius), (geom.radius), 0.0)
366  glFT.glNormal3f(0, 0, 1.0)
367 
368  glFT.glVertex3f((geom.radius), -1*(geom.radius), 0.0)
369  glFT.glNormal3f(0, 0, 1.0)
370  glFT.glEnd()
371  glFT.glPopAttrib( )
372 
373  # override
374  def select(self, selectInfo, selectionList, worldSpaceSelectPts):
375  """
376  Select function. Gets called when the bbox for the object is
377  selected. This function just selects the object without
378  doing any intersection tests.
379  """
380 
381  priorityMask = OpenMaya.MSelectionMask(OpenMaya.MSelectionMask.kSelectObjectsMask)
382  item = OpenMaya.MSelectionList()
383  item.add(selectInfo.selectPath())
384  xformedPt = OpenMaya.MPoint()
385  selectInfo.addSelection(item, xformedPt, selectionList,
386  worldSpaceSelectPts, priorityMask, False)
387  return True
388 
389 
390 
391 
392 #####################################################################
393 
394 def nodeCreator():
395  return OpenMayaMPx.asMPxPtr( instanceShape() )
396 
397 
398 def uiCreator():
399  return OpenMayaMPx.asMPxPtr( instanceShapeUI() )
400 
401 
402 def nodeInitializer():
403  # utility func for numeric attrs
404  def setOptions(attr):
405  attr.setHidden(False)
406  attr.setKeyable(True)
407  attr.setInternal(True)
408 
409  messageAttr = OpenMaya.MFnMessageAttribute()
410  numericAttr = OpenMaya.MFnNumericAttribute()
411 
412  instanceShape.aInstanceShape = messageAttr.create("instanceShape", "is")
413  instanceShape.addAttribute(instanceShape.aInstanceShape)
414 
415  instanceShape.aRadius = numericAttr.create("radius", "r", OpenMaya.MFnNumericData.kDouble, kDefaultRadius)
416  setOptions(numericAttr)
417  instanceShape.addAttribute(instanceShape.aRadius)
418 
419  instanceShape.aCount = numericAttr.create("count", "ct", OpenMaya.MFnNumericData.kInt, kDefaultCount)
420  setOptions(numericAttr)
421  instanceShape.addAttribute(instanceShape.aCount)
422 
423 # initialize the script plug-in
424 def initializePlugin(mobject):
425  mplugin = OpenMayaMPx.MFnPlugin(mobject, "Autodesk", "2011", "Any")
426  try:
427  mplugin.registerShape( kPluginNodeTypeName, spInstanceShapeNodeId,
428  nodeCreator, nodeInitializer, uiCreator )
429  except:
430  sys.stderr.write( "Failed to register node: %s" % kPluginNodeTypeName )
431  raise
432 
433 
434 # uninitialize the script plug-in
435 def uninitializePlugin(mobject):
436  mplugin = OpenMayaMPx.MFnPlugin(mobject)
437  try:
438  mplugin.deregisterNode( spInstanceShapeNodeId )
439  except:
440  sys.stderr.write( "Failed to deregister node: %s" % kPluginNodeTypeName )
441  raise