API Guide > Maya Python Plug-in Learning Path > Dependency Graph Plug-in Basics >

# Example: Bounding Box Deformer

Filename: boundingBoxDeformer.py

Sample Output: In the following output, we assigned a green phong material to the deformed sphere. By increasing the "Mesh Inflation" attribute, the mesh will attempt to fill its bounding box even more.

Program Summary: The plug-in code below defines a new deformer whose behavior is to "inflate" the mesh so that it expands within the boundaries of its bounding box. The size of this bounding box can be scaled via the "Bounding Box Scale" attribute so that the mesh has room to expand before hitting the boundaries of the box. The mesh's inflation is controlled via the "Mesh Inflation" attribute. The plug-in class inherits from MPxDeformerNode, and overrides the MPxDeformer.deform() function to specify its behavior. The following points are useful to keep in mind when creating a deformer node in Python:

- Built-in Deformer Node Attributes - By inheriting from MPxDeformerNode, your class will have access to the input and output mesh attributes, available via OpenMayaMPx.cvar.MPxDeformerNode_inputGeom and OpenMayaMPx.cvar.MPxDeformerNode_outputGeom respectively. These variables are generated by the SWIG tool during the C++ header file conversion to Python. By the same process, your deformer also has access to the built-in "envelope" attribute, available via OpenMayaMPx.cvar.MPxDeformerNode_envelope. This envelope attribute is used to specify the amount of influence your deformer has on the mesh.
- Obtaining the Input Geometry in deform() - The geometry iterator (MItGeometry) passed into the deform() function only provides you with the position of each vertex in the mesh. To obtain the normals of each vertex, you will require a reference to the actual mesh object. We define a helper function called getDeformerInputGeometry() which returns the input mesh object. The returned mesh can thereafter be used with the MFnMesh function set to retrieve the list of vertex normals, via MFnMesh.getVertexNormals().

    # boundingBoxDeformer.py

    import sys
    import maya.OpenMayaMPx as OpenMayaMPx
    import maya.OpenMaya as OpenMaya

    # Plug-in information:
    kPluginNodeName = 'boundingBoxDeformer'     # The name of the node.
    kPluginNodeId = OpenMaya.MTypeId( 0xBEEF8 ) # A unique ID associated to this node type.

    # This determines how fast the vertices of the mesh will "expand" towards the boundary of the bounding box.
    vertexIncrement = 0.1

    ##########################################################
    # Plug-in
    ##########################################################
    class MyDeformerNode(OpenMayaMPx.MPxDeformerNode):

        # Static variable(s) which will later be replaced by the node's attribute(s).
        boundingBoxScaleAttribute = OpenMaya.MObject()
        meshInflationAttribute    = OpenMaya.MObject()

        def __init__(self):
            ''' Constructor. '''
            # (!) Make sure you call the base class's constructor.
            OpenMayaMPx.MPxDeformerNode.__init__(self)

        def deform(self, pDataBlock, pGeometryIterator, pLocalToWorldMatrix, pGeometryIndex):
            ''' Deform each vertex using the geometry iterator. '''

            # The envelope determines the overall weight of the deformer on the mesh.
            # The envelope is obtained via the OpenMayaMPx.cvar.MPxDeformerNode_envelope variable.
            # This variable and others like it are generated by SWIG to expose variables or constants declared in C++ header files.
            envelopeAttribute = OpenMayaMPx.cvar.MPxDeformerNode_envelope
            envelopeValue = pDataBlock.inputValue( envelopeAttribute ).asFloat()

            # Get the value of the mesh inflation node attribute.
            meshInflationHandle = pDataBlock.inputValue( MyDeformerNode.meshInflationAttribute )
            meshInflation = meshInflationHandle.asDouble()

            # Get the value of the bounding box scale node attribute.
            boundingBoxScaleHandle = pDataBlock.inputValue( MyDeformerNode.boundingBoxScaleAttribute )
            boundingBoxScale = boundingBoxScaleHandle.asDouble()

            # Get the input mesh from the datablock using our getDeformerInputGeometry() helper function.
            inputGeometryObject = self.getDeformerInputGeometry(pDataBlock, pGeometryIndex)

            # Compute the bounding box using the input the mesh.
            boundingBox = self.getBoundingBox( inputGeometryObject, boundingBoxScale )

            # Obtain the list of normals for each vertex in the mesh.
            normals = OpenMaya.MFloatVectorArray()
            meshFn = OpenMaya.MFnMesh( inputGeometryObject )
            meshFn.getVertexNormals( True, normals, OpenMaya.MSpace.kTransform )

            # Iterate over the vertices to move them.
            global vertexIncrement
            while not pGeometryIterator.isDone():

                # Obtain the vertex normal of the geometry. This normal is the vertex's averaged normal value if that
                # vertex is shared among several polygons.
                vertexIndex = pGeometryIterator.index()
                normal = OpenMaya.MVector( normals[vertexIndex] ) # Cast the MFloatVector into a simple vector.

                # Increment the point along the vertex normal.
                point = pGeometryIterator.position()
                newPoint = point + ( normal * vertexIncrement * meshInflation * envelopeValue )

                # Clamp the new point within the bounding box.
                self.clampPointInBoundingBox( newPoint, boundingBox )

                # Set the position of the current vertex to the new point.
                pGeometryIterator.setPosition( newPoint )

                # Jump to the next vertex.
                pGeometryIterator.next()

        def getDeformerInputGeometry(self, pDataBlock, pGeometryIndex):
            '''
            Obtain a reference to the input mesh. This mesh will be used to compute our bounding box, and we will also require its normals.

            We use MDataBlock.outputArrayValue() to avoid having to recompute the mesh and propagate this recomputation throughout the
            Dependency Graph.

            OpenMayaMPx.cvar.MPxDeformerNode_input and OpenMayaMPx.cvar.MPxDeformerNode_inputGeom are SWIG-generated
            variables which respectively contain references to the deformer's 'input' attribute and 'inputGeom' attribute.
            '''
            inputAttribute = OpenMayaMPx.cvar.MPxDeformerNode_input
            inputGeometryAttribute = OpenMayaMPx.cvar.MPxDeformerNode_inputGeom

            inputHandle = pDataBlock.outputArrayValue( inputAttribute )
            inputHandle.jumpToElement( pGeometryIndex )
            inputGeometryObject = inputHandle.outputValue().child( inputGeometryAttribute ).asMesh()

            return inputGeometryObject

        def getBoundingBox(self, pMeshObj, pBoundingBoxScale):
            ''' Calculate a bounding box around the mesh's vertices. '''

            # Create the bounding box object we will populate with the points of the mesh.
            boundingBox = OpenMaya.MBoundingBox()
            meshFn = OpenMaya.MFnMesh( pMeshObj )
            pointArray = OpenMaya.MPointArray()

            # Get the points of the mesh in its local coordinate space.
            meshFn.getPoints( pointArray, OpenMaya.MSpace.kTransform )

            for i in range( 0, pointArray.length() ):
                point = pointArray[i]
                boundingBox.expand( point )

            # Expand the bounding box according to the scaling factor.
            newMinPoint = boundingBox.min() * pBoundingBoxScale
            newMaxPoint = boundingBox.max() * pBoundingBoxScale
            boundingBox.expand( newMinPoint )
            boundingBox.expand( newMaxPoint )

            return boundingBox

        def clampPointInBoundingBox(self, pPoint, pBoundingBox):
            ''' Ensure that the given point is contained within the bounding box's min/max coordinates. '''

            # Define a quick clamping function for internal use within this method body.
            def clamp(pValue, pMin, pMax):
                return max( pMin, min( pValue, pMax ) )

            pPoint.x = clamp( pPoint.x, pBoundingBox.min().x, pBoundingBox.max().x )
            pPoint.y = clamp( pPoint.y, pBoundingBox.min().y, pBoundingBox.max().y )
            pPoint.z = clamp( pPoint.z, pBoundingBox.min().z, pBoundingBox.max().z )

    ##########################################################
    # Plug-in initialization.
    ##########################################################
    def nodeCreator():
        ''' Creates an instance of our node class and delivers it to Maya as a pointer. '''
        return OpenMayaMPx.asMPxPtr( MyDeformerNode() )

    def nodeInitializer():
        ''' Defines the input and output attributes as static variables in our plug-in class. '''
        # The following MFnNumericAttribute function set will allow us to create our attributes.
        numericAttributeFn = OpenMaya.MFnNumericAttribute()

        #==================================
        # INPUT NODE ATTRIBUTE(S)
        #==================================
        # Define the scaling factor attribute of the bounding box around the mesh.
        MyDeformerNode.boundingBoxScaleAttribute = numericAttributeFn.create( 'boundingBoxScale', 'bs', OpenMaya.MFnNumericData.kDouble, 1.5 )
        numericAttributeFn.setMin( 1.0 )
        numericAttributeFn.setMax( 3.0 )
        numericAttributeFn.setStorable( True )
        numericAttributeFn.setWritable( True )
        numericAttributeFn.setReadable( False )
        MyDeformerNode.addAttribute( MyDeformerNode.boundingBoxScaleAttribute )

        # Define a mesh inflation attribute, responsible for actually moving the vertices in the direction of their normals.
        MyDeformerNode.meshInflationAttribute = numericAttributeFn.create( 'meshInflation', 'mi', OpenMaya.MFnNumericData.kDouble, 10.0 )
        numericAttributeFn.setMin( 1.0 )
        numericAttributeFn.setMax( 50.0 )
        numericAttributeFn.setStorable( True )
        numericAttributeFn.setWritable( True )
        numericAttributeFn.setReadable( False )
        MyDeformerNode.addAttribute( MyDeformerNode.meshInflationAttribute )

        ''' The input geometry node attribute is already declared in OpenMayaMPx.cvar.MPxDeformerNode_inputGeom '''

        #==================================
        # OUTPUT NODE ATTRIBUTE(S)
        #==================================

        ''' The output geometry node attribute is already declared in OpenMayaMPx.cvar.MPxDeformerNode_outputGeom '''

        #==================================
        # NODE ATTRIBUTE DEPENDENCIES
        #==================================
        # If any of the inputs change, the output mesh will be recomputed.
        MyDeformerNode.attributeAffects( MyDeformerNode.boundingBoxScaleAttribute, OpenMayaMPx.cvar.MPxDeformerNode_outputGeom )
        MyDeformerNode.attributeAffects( MyDeformerNode.meshInflationAttribute, OpenMayaMPx.cvar.MPxDeformerNode_outputGeom )

    def initializePlugin( mobject ):
        ''' Initialize the plug-in '''
        mplugin = OpenMayaMPx.MFnPlugin( mobject )
        try:
            mplugin.registerNode( kPluginNodeName, kPluginNodeId, nodeCreator,
                                  nodeInitializer, OpenMayaMPx.MPxNode.kDeformerNode )
        except:
            sys.stderr.write( 'Failed to register node: ' + kPluginNodeName )
            raise

    def uninitializePlugin( mobject ):
        ''' Uninitializes the plug-in '''
        mplugin = OpenMayaMPx.MFnPlugin( mobject )
        try:
            mplugin.deregisterNode( kPluginNodeId )
        except:
            sys.stderr.write( 'Failed to deregister node: ' + kPluginNodeName )
            raise

    ##########################################################
    # Sample usage.
    ##########################################################
    '''
    # Copy the following lines and run them in Maya's Python Script Editor:

    import maya.cmds as cmds
    cmds.loadPlugin( 'boundingBoxDeformer.py' )
    cmds.polySphere()
    cmds.deformer( type='boundingBoxDeformer' )

    '''