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

# Example: Voxelizer Node

Filename: voxelizerNode.py

Rendered Output: The following rendered image was obtained by voxelizing the mesh of a sphere. We applied a blue lambertian material to the original sphere, and a red surface shader to the voxelized mesh. The voxelized mesh was also assigned a Toon outline with a line width value of 0.223.

Program Summary: The plug-in code below creates a new MPxNode which accepts a mesh as input, as well as user-defined attributes to determine the voxel width and inter-voxel distance. This voxelizer node transforms the input mesh into a new mesh consisting of cubes, which fill the volume occupied by the original mesh. The voxelization process makes use of MFnMesh.allIntersections() to cast rays from inside the bounding box of the original mesh towards the camera. Assuming that the mesh is closed, if the ray intersects with an odd number points along the mesh's surface, then the origin of the ray is contained within the mesh.

NoteThis voxelizer was implemented as an MPxNode plug-in. It would not be possible to create such a voxelized mesh by deriving from MPxDeformerNode because a deformer cannot change the topology of the mesh. That is to say, the edge connections between the vertices cannot be modified within a deformer; a deformer can only modify the vertex positions.

    # voxelizerNode.py

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

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

    # Default input values.
    defaultVoxelWidth = 0.9       # The width of a cubic voxel.
    defaultVoxelDistance = 1.0    # The distance which separates the center of two adjacent voxels.

    ##########################################################
    # Plug-in
    ##########################################################
    class VoxelizerNode(OpenMayaMPx.MPxNode):
        # Static variables which will later be replaced by the node's attributes.
        voxelWidthAttribute = OpenMaya.MObject()
        voxelDistanceAttribute = OpenMaya.MObject()
        inputMeshAttribute = OpenMaya.MObject()
        outputMeshAttribute = OpenMaya.MObject()

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

        def compute(self, pPlug, pDataBlock):
            ''' Here, we will create a voxelized version of the input mesh. '''

            if( pPlug == VoxelizerNode.outputMeshAttribute ):

                # Get our custom input node attributes and values.
                voxelWidthHandle = pDataBlock.inputValue( VoxelizerNode.voxelWidthAttribute )
                voxelWidth = voxelWidthHandle.asFloat()

                voxelDistanceHandle = pDataBlock.inputValue( VoxelizerNode.voxelDistanceAttribute )
                voxelDistance = voxelDistanceHandle.asFloat()

                inputMeshHandle = pDataBlock.inputValue( VoxelizerNode.inputMeshAttribute )
                inputMeshObj = inputMeshHandle.asMesh()

                # Compute the bounding box around the mesh's vertices.
                boundingBox = self.getBoundingBox( inputMeshObj )

                # Determine which voxel centerpoints are contained within the mesh.
                voxels = self.getVoxels( voxelDistance, inputMeshObj, boundingBox )

                # Create a mesh data container, which will store our new voxelized mesh.
                meshDataFn = OpenMaya.MFnMeshData()
                newOutputMeshData = meshDataFn.create()

                # Create a cubic polygon for each voxel and populate the 'newOutputMeshData' MeshData object.
                self.createVoxelMesh( voxels, voxelWidth, newOutputMeshData)

                # Set the output data.
                outputMeshHandle = pDataBlock.outputValue( VoxelizerNode.outputMeshAttribute )
                outputMeshHandle.setMObject( newOutputMeshData )

            else:
                return OpenMaya.kUnknownParameter

        def getBoundingBox(self, pMeshObj):
            ''' 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 )

            return boundingBox

        def getVoxels(self, pVoxelDistance, pMeshObj, pBoundingBox):
            '''
            Obtain a list of voxels as a set of (x,y,z) coordinates in the mesh's local space.

            We obtain these voxels by casting rays from points separated pVoxelDistance apart within the
            mesh's bounding box, and test whether or not these points are contained within the mesh.

            A point is contained within a closed mesh if the ray shot from the point intersects an odd
            number of times with the surface of the mesh.
            '''
            # Initialize a list of voxels contained within the mesh.
            voxels = []

            # Get a reference to the MFnMesh function set, and use it on the given mesh object.
            meshFn = OpenMaya.MFnMesh( pMeshObj )

            # Compute an offset which we will apply to the min and max corners of the bounding box.
            halfVoxelDist = 0.5 * pVoxelDistance

            # Offset the position of the minimum point to account for the inter-voxel distance.
            minPoint = pBoundingBox.min()
            minPoint.x += halfVoxelDist
            minPoint.y += halfVoxelDist
            minPoint.z += halfVoxelDist

            # Offset the position of the maximum point to account for the inter-voxel distance.
            maxPoint = pBoundingBox.max()
            maxPoint.x += halfVoxelDist
            maxPoint.y += halfVoxelDist
            maxPoint.z += halfVoxelDist

            # Define an iterator which will allow us to step through the pVoxelDistance
            # point intervals contained within our bounding box. We use this iterator
            # in the for loops that follow to visit each voxel center in the bounding box.
            def floatIterator(start, stop, step):
                r = start
                while r < stop:
                    yield r
                    r += step

            # Iterate over every point in the bounding box, stepping by pVoxelDistance...
            for xCoord in floatIterator( minPoint.x, maxPoint.x, pVoxelDistance ):
                for yCoord in floatIterator( minPoint.y, maxPoint.y, pVoxelDistance ):
                    for zCoord in floatIterator( minPoint.z, maxPoint.z, pVoxelDistance ):

                        # 2D representation of a ray cast from the point within the bounding box:
                        #
                        #  (+) ^-----------------
                        #      |                |
                        #  y   |                |  - We are shooting the ray from the point: [*]
                        # axis | <======[*]     |  - The direction of the ray is parallel to the -Z axis.
                        #      |                |
                        #      |                |
                        #  (-) ------------------>
                        #     (-)    z axis     (+)
                        #
                        # If the ray intersects with an odd number of points along the surface of the mesh, the
                        # point is contained within the mesh (assuming a closed mesh).
                        raySource = OpenMaya.MFloatPoint( xCoord, yCoord, zCoord )
                        rayDirection = OpenMaya.MFloatVector( 0, 0, -1 )
                        intersectionPoints = OpenMaya.MFloatPointArray()
                        tolerance = 0.0001

                        meshFn.allIntersections( raySource,                  # raySource - where we are shooting the ray from.
                                                 rayDirection,               # rayDirection - the direction in which we are shooting the ray.
                                                 None,                       # faceIds - here, we do not care if specific faces are intersected)
                                                 None,                       # triIds - here, we do not care if specific tri's are intersected)
                                                 False,                      # idsSorted - here, we do not need to sort the faceId's or triId's indices.
                                                 OpenMaya.MSpace.kTransform, # coordinate space - the mesh's local coordinate space.
                                                 float(9999),                # the range of the ray.
                                                 False,                      # testBothDirections - we are not checking both directions from the raySource
                                                 None,                       # accelParams - this object is not applicable here.
                                                 False,                      # sortHits - we do not need to sort the intersection points along the ray.
                                                 intersectionPoints,         # hitPoints - the array of points which have been intersected.
                                                 None,                       # hitRayParams - we do not need any parametric distances of the points along the ray.
                                                 None,                       # hitFaces - we do not need the id's of the faces intersected.
                                                 None,                       # hitTriangles - we do not need the id's of the triangles intersected.
                                                 None,                       # hitBary1s - we do not need the barycentric coordinates of the points within the triangles.
                                                 None,                       # hitBary2s - we do not need the barycentric coordinates of the points within the triangles.
                                                 tolerance                   # tolerance - a numeric tolerance threshold which allow intersections to occur just outside the mesh.
                                                 )

                        # If there is an odd number of intersection points, then the point lies within the mesh. Otherwise,
                        # the point lies outside the mesh. We are only concerned with voxels whose centerpoint lies within the mesh
                        if( intersectionPoints.length() % 2 == 1 ):
                            voxels.append( raySource )

            # Return the list of voxel coordinates which lie within the mesh.
            return voxels

        def createVoxelMesh(self, pVoxelPositions, pVoxelWidth, pOutMeshData):
            ''' Create a mesh containing one cubic polygon for each voxel in the pVoxelPositions list. '''

            numVoxels = len( pVoxelPositions )

            numVerticesPerVoxel = 8 # a cube has eight vertices.
            numPolygonsPerVoxel = 6 # a cube has six faces.
            numVerticesPerPolygon = 4  # four vertices are required to define a face of a cube.
            numPolygonConnectsPerVoxel = numPolygonsPerVoxel * numVerticesPerPolygon # 24

            # Initialize the required arrays used to create the mesh in MFnMesh.create()
            totalVertices = numVoxels * numVerticesPerVoxel
            vertexArray = OpenMaya.MFloatPointArray()
            vertexArray.setLength( totalVertices )
            vertexIndexOffset = 0

            totalPolygons = numVoxels * numPolygonsPerVoxel
            polygonCounts = OpenMaya.MIntArray()
            polygonCounts.setLength( totalPolygons )
            polygonCountsIndexOffset = 0

            totalPolygonConnects = numVoxels * numPolygonConnectsPerVoxel
            polygonConnects = OpenMaya.MIntArray()
            polygonConnects.setLength( totalPolygonConnects )
            polygonConnectsIndexOffset = 0

            # Populate the required arrays used in MFnMesh.create()
            for i in range( 0, numVoxels ):
                voxelPosition = pVoxelPositions[i]

                # Add a new cube to the arrays.
                self.createCube( voxelPosition, pVoxelWidth,
                                 vertexArray, vertexIndexOffset, numVerticesPerVoxel,
                                 polygonCounts, polygonCountsIndexOffset, numPolygonsPerVoxel, numVerticesPerPolygon,
                                 polygonConnects, polygonConnectsIndexOffset )

                # Increment the respective index offsets.
                vertexIndexOffset += numVerticesPerVoxel
                polygonCountsIndexOffset += numPolygonsPerVoxel
                polygonConnectsIndexOffset += numPolygonConnectsPerVoxel

            # Create the mesh now that the arrays have been populated. The mesh is stored in pOutMeshData
            meshFn = OpenMaya.MFnMesh()
            meshFn.create( totalVertices, totalPolygons, vertexArray, polygonCounts, polygonConnects, pOutMeshData )

        def createCube(self, pVoxelPosition, pWidth,
                             pVertexArray, pVertexIndexOffset, pNumVerticesPerVoxel,
                             pPolygonCountArray, pPolygonCountIndexOffset, pNumPolygonsPerVoxel, pNumVerticesPerPolygon,
                             pPolygonConnectsArray, pPolygonConnectsIndexOffset ):
            ''' Add a cubic polygon to the specified arrays. '''

            # We are using half the given width to compute the vertices of the cube.
            halfWidth = float( pWidth / 2.0 )

            # Declare the eight corners of the cube. The cube is centered at pVoxelPosition.

            vertices = [OpenMaya.MFloatPoint(-halfWidth + pVoxelPosition.x, -halfWidth + pVoxelPosition.y, -halfWidth + pVoxelPosition.z), # 0
                        OpenMaya.MFloatPoint( halfWidth + pVoxelPosition.x, -halfWidth + pVoxelPosition.y, -halfWidth + pVoxelPosition.z), # 1
                        OpenMaya.MFloatPoint( halfWidth + pVoxelPosition.x, -halfWidth + pVoxelPosition.y,  halfWidth + pVoxelPosition.z), # 2
                        OpenMaya.MFloatPoint(-halfWidth + pVoxelPosition.x, -halfWidth + pVoxelPosition.y,  halfWidth + pVoxelPosition.z), # 3
                        OpenMaya.MFloatPoint(-halfWidth + pVoxelPosition.x,  halfWidth + pVoxelPosition.y, -halfWidth + pVoxelPosition.z), # 4
                        OpenMaya.MFloatPoint(-halfWidth + pVoxelPosition.x,  halfWidth + pVoxelPosition.y,  halfWidth + pVoxelPosition.z), # 5
                        OpenMaya.MFloatPoint( halfWidth + pVoxelPosition.x,  halfWidth + pVoxelPosition.y,  halfWidth + pVoxelPosition.z), # 6
                        OpenMaya.MFloatPoint( halfWidth + pVoxelPosition.x,  halfWidth + pVoxelPosition.y, -halfWidth + pVoxelPosition.z)] # 7

            # Declare the data structure which binds each vertex to a polygon corner
            polygonConnections = [ (0, 12, 16), # the vertex indexed at 0 corresponds to the polygon corners whose indexes are (0, 12, 16) in pPolygonConnectsArray.
                                   (1, 19, 20),
                                   (2,  9, 23),
                                   (3,  8, 13),
                                   (4, 15, 17),
                                   (5, 11, 14),
                                   (6, 10, 22),
                                   (7, 18, 21) ]

            # Store the eight corners of the cube in the vertex array.
            for i in range( 0, pNumVerticesPerVoxel ):
                # Store the vertex in the passed vertex array.
                pVertexArray.set( vertices[i], pVertexIndexOffset + i )

                # Assign the vertex in the pVertexArray to the relevant polygons.
                for polygonConnectionIndex in polygonConnections[i]:
                    pPolygonConnectsArray.set( pVertexIndexOffset + i, pPolygonConnectsIndexOffset + polygonConnectionIndex )

            # Declare the number of vertices for each face.
            for i in range( 0, pNumPolygonsPerVoxel ):
                 # Set the number of vertices for the polygon at the given index.
                 pPolygonCountArray.set( pNumVerticesPerPolygon, pPolygonCountIndexOffset + i )

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

    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()

        # This one allows us to create our input and output mesh attributes.
        typedAttributeFn = OpenMaya.MFnTypedAttribute()

        #==================================
        # INPUT NODE ATTRIBUTE(S)
        #==================================
        # We will need a voxel width.
        global defaultVoxelWidth
        VoxelizerNode.voxelWidthAttribute = numericAttributeFn.create( 'voxelWidth', 'vw',
                                                                        OpenMaya.MFnNumericData.kFloat, defaultVoxelWidth )
        numericAttributeFn.setWritable( True )
        numericAttributeFn.setReadable( False )
        numericAttributeFn.setStorable( True )
        numericAttributeFn.setHidden( False )
        numericAttributeFn.setMin( 0.1 )
        VoxelizerNode.addAttribute( VoxelizerNode.voxelWidthAttribute )

        # We will need a voxel distance value (higher values means we can see more voxels within the volume of the mesh).
        global defaultVoxelDistance
        VoxelizerNode.voxelDistanceAttribute = numericAttributeFn.create( 'voxelDistance', 'vd',
                                                                           OpenMaya.MFnNumericData.kFloat, defaultVoxelDistance )
        numericAttributeFn.setWritable( True )
        numericAttributeFn.setReadable( False )
        numericAttributeFn.setStorable( True )
        numericAttributeFn.setHidden( False )
        numericAttributeFn.setMin( 0.1 )
        VoxelizerNode.addAttribute( VoxelizerNode.voxelDistanceAttribute )

        # We will need an input mesh attribute.
        VoxelizerNode.inputMeshAttribute = typedAttributeFn.create( 'inputMesh', 'im',
                                                                    OpenMaya.MFnData.kMesh )
        typedAttributeFn.setWritable( True )
        typedAttributeFn.setReadable( False )
        typedAttributeFn.setStorable( False )
        typedAttributeFn.setHidden( False )
        VoxelizerNode.addAttribute( VoxelizerNode.inputMeshAttribute )

        #==================================
        # OUTPUT NODE ATTRIBUTE(S)
        #==================================
        VoxelizerNode.outputMeshAttribute = typedAttributeFn.create( 'outputMesh', 'om',
                                                                     OpenMaya.MFnData.kMesh )
        typedAttributeFn.setWritable( False )
        typedAttributeFn.setReadable( True )
        typedAttributeFn.setStorable( False )
        typedAttributeFn.setHidden( False )
        VoxelizerNode.addAttribute( VoxelizerNode.outputMeshAttribute )

        #==================================
        # NODE ATTRIBUTE DEPENDENCIES
        #==================================
        # If any of the inputs change, the output mesh will be recomputed.
        VoxelizerNode.attributeAffects( VoxelizerNode.voxelWidthAttribute, VoxelizerNode.outputMeshAttribute )
        VoxelizerNode.attributeAffects( VoxelizerNode.voxelDistanceAttribute, VoxelizerNode.outputMeshAttribute )
        VoxelizerNode.attributeAffects( VoxelizerNode.inputMeshAttribute, VoxelizerNode.outputMeshAttribute )

    def initializePlugin( mobject ):
        ''' Initialize the plug-in '''
        mplugin = OpenMayaMPx.MFnPlugin( mobject )
        try:
            mplugin.registerNode( kPluginNodeName, kPluginNodeId, nodeCreator, nodeInitializer )
        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.file( newFile=True, force=True )

    cmds.unloadPlugin( 'voxelizerNode.py' )
    cmds.loadPlugin( 'voxelizerNode.py' )

    # Create a sphere which will act as our input shape.
    cmds.polySphere( r=5.0, sx=20, sy=20, name='sphere1' )
    cmds.move( -20, 0, 0, 'sphere1' ) # move it over to the side.

    # Create the voxelization node.
    cmds.createNode( 'voxelizerNode', name='voxelizerNode1' )

    # Create a target shape.
    cmds.createNode( 'transform', name='target1' )
    cmds.createNode( 'mesh', name='target1Shape', parent='target1' )
    cmds.sets( 'target1Shape', add='initialShadingGroup' )

    # Connect the attributes.
    cmds.connectAttr( 'sphere1Shape.outMesh', 'voxelizerNode1.inputMesh' )
    cmds.connectAttr( 'voxelizerNode1.outputMesh', 'target1Shape.inMesh' )

    '''