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

# Example: Creating a Depth Shader

Filename: depthShader.py

Rendered Output:

Program Summary: The plug-in code below creates a new depth shader by inheriting from the OpenMayaMPx.MPxNode class. This shader can only be used by the Maya software shader. The compute() method is run for every rendered surface point, and determines the pixel's color based on the surface point's Z-axis distance from the camera.

To access the depth of each rendered surface point, we create the following numeric attribute in nodeInitializer():

    depthShader.surfacePointAttribute = numericAttributeFn.createPoint('pointCamera', 'p')

The name of this attribute ('pointCamera'), and its short name ('p'), are very important here, because they allow Maya to automatically connect our shader node to the samplerInfo node's pointCamera output attribute. The samplerInfo node provides vital information about the point being rendered. For a full list of its attributes, consult the Maya User Guide > Technical Documentation > Nodes > samplerInfo. You may also wish to consult the Appendix C: Rendering attributes in the API Documentation for a detailed list of other rendering attributes.

    # depthShader.py

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

    # Plug-in information:
    kPluginNodeName = 'myDepthShader'
    kPluginNodeClassify = 'utility/general'
    kPluginNodeId = OpenMaya.MTypeId(0x870FE)

    # Default attribute values
    defaultNearDistance = 20.0
    defaultFarDistance  = 70.0
    defaultNearColor    = (1.0, 1.0, 1.0) # (r,g,b) white
    defaultFarColor     = (0.0, 0.0, 0.0) # (r,g,b) black
    defaultGamma        = 1.0
    minGamma            = 0.1
    maxGamma            = 5.0

    ##########################################################
    # Plug-in
    ##########################################################
    class depthShader(OpenMayaMPx.MPxNode):
        ''' Creates a depth shader which renders in Maya's software renderer. '''
        # Define the static variables to which we will assign the node's attributes
        # in nodeInitializer() defined below.
        surfacePointAttribute = OpenMaya.MObject()
        nearDistanceAttribute = OpenMaya.MObject()
        farDistanceAttribute  = OpenMaya.MObject()
        nearColorAttribute    = OpenMaya.MObject()
        farColorAttribute     = OpenMaya.MObject()
        gammaAttribute        = OpenMaya.MObject()
        outColorAttribute     = OpenMaya.MObject()

        def __init__(self):
            ''' Constructor. '''
            OpenMayaMPx.MPxNode.__init__(self)

        def compute(self, pPlug, pDataBlock):
            '''
            Node computation method.
              - pDataBlock contains the data on which we will base our computations.
              - pPlug is a connection point related to one of our node attributes (either an input or an output).
            '''
            if ( pPlug == depthShader.outColorAttribute ):

                # Get the data handles corresponding to your attributes among the values in the data block.
                surfacePointDataHandle = pDataBlock.inputValue( depthShader.surfacePointAttribute )
                nearDistanceDataHandle = pDataBlock.inputValue( depthShader.nearDistanceAttribute )
                farDistanceDataHandle  = pDataBlock.inputValue( depthShader.farDistanceAttribute )
                nearColorDataHandle    = pDataBlock.inputValue( depthShader.nearColorAttribute )
                farColorDataHandle     = pDataBlock.inputValue( depthShader.farColorAttribute )
                gammaDataHandle        = pDataBlock.inputValue( depthShader.gammaAttribute )

                # Obtain the (x,y,z) location of the currently rendered point in camera coordinates.
                surfacePoint = surfacePointDataHandle.asFloatVector()

                # Since the camera is looking along its negative Z axis (the Y axis is
                # the up vector), we must take the absolute value of the Z coordinate
                # to obtain the point's depth.
                depth = abs(surfacePoint.z)

                # Get the actual near and far threshold values.
                nearValue = nearDistanceDataHandle.asFloat()
                farValue = farDistanceDataHandle.asFloat()

                # Find the proportion of depth between the near and far values.
                if( (farValue - nearValue) == 0):
                    # Avoid a division by zero if the near and far values somehow have the same value.
                    depthProportion = 0
                else:
                    depthProportion = (depth - nearValue) / (farValue - nearValue)

                # Clamp the depthProportion value in the interval [0.0, 1.0]
                depthProportion = max(0, min( depthProportion, 1.0 ) )

                # Modify the depth proportion using the gamma roll-off bias.
                gammaValue = gammaDataHandle.asFloat()
                depthProportion = math.pow(depthProportion, gammaValue)

                # Linearly interpolate the output color based on the depth proportion.
                outColor = OpenMaya.MFloatVector(0, 0, 0)
                nearColor = nearColorDataHandle.asFloatVector()
                farColor = farColorDataHandle.asFloatVector()

                outColor.x = nearColor.x + ( ( farColor.x - nearColor.x ) * depthProportion )
                outColor.y = nearColor.y + ( ( farColor.y - nearColor.y ) * depthProportion )
                outColor.z = nearColor.z + ( ( farColor.z - nearColor.z ) * depthProportion )

                # Write to the output data.
                outColorDataHandle = pDataBlock.outputValue( depthShader.outColorAttribute )
                outColorDataHandle.setMFloatVector( outColor )
                outColorDataHandle.setClean()
            else:
                return OpenMaya.kUnknownParameter

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

    def nodeInitializer():
        '''
        Defines the set of attributes for our node. The attributes
        declared in this function are assigned as static members to our
        depthShader class. Instances of depthShader will use these attributes
        to create plugs for use in the compute() method.
        '''
        # Create a numeric attribute function set, since our attributes will all be defined by numeric types.
        numericAttributeFn = OpenMaya.MFnNumericAttribute()

        #==================================
        # INPUT NODE ATTRIBUTE(S)
        #==================================
        # - The (x,y,z) point on the surface defined according to the camera's frame of reference.
        #   > (!) Important: the 'pointCamera' string relates to the samplerInfo maya node.
        #   > This value is supplied by the render sampler at computation time.
        depthShader.surfacePointAttribute = numericAttributeFn.createPoint('pointCamera', 'p')
        numericAttributeFn.setStorable(False)
        numericAttributeFn.setHidden(True)
        depthShader.addAttribute( depthShader.surfacePointAttribute )

        # - The 'near' distance, i.e. the minimum distance threshold from the camera after which the
        #   pixel's color is modified by the depth of the point.
        #   > This value can be defined by the user, and is storable.
        global defaultNearDistance, defaultFarDistance
        depthShader.nearDistanceAttribute = numericAttributeFn.create( 'nearDistance', 'nd',
                                                                       OpenMaya.MFnNumericData.kFloat, defaultNearDistance )
        numericAttributeFn.setStorable( True )
        numericAttributeFn.setMin( 0.0 )
        numericAttributeFn.setMax( defaultFarDistance )
        depthShader.addAttribute( depthShader.nearDistanceAttribute )

        # - The 'far' distance, i.e. the minimum distance threshold from the camera before which the
        #   pixel's color is modified by the depth of the point.
        #   > This value can be defined by the user, and is storable.
        depthShader.farDistanceAttribute = numericAttributeFn.create( 'farDistance', 'fd',
                                                                      OpenMaya.MFnNumericData.kFloat, defaultFarDistance )
        numericAttributeFn.setStorable( True )
        numericAttributeFn.setMin( defaultFarDistance + 0.1 ) # Add an epsilon value of 0.1 to avoid near distance overlap.
        numericAttributeFn.setMax( 3 * defaultFarDistance )
        depthShader.addAttribute( depthShader.farDistanceAttribute )

        # - The 'near' color.
        #   > This value can be defined by the user using a color picker, and is storable.
        global defaultNearColor
        depthShader.nearColorAttribute = numericAttributeFn.createColor( 'nearColor', 'nc' )
        numericAttributeFn.setStorable( True )
        numericAttributeFn.setDefault( defaultNearColor[0], defaultNearColor[1], defaultNearColor[2] )
        depthShader.addAttribute( depthShader.nearColorAttribute )

        # - The 'far' color.
        #   > This value can be defined by the user using a color picker, and is storable.
        global defaultFarColor
        depthShader.farColorAttribute = numericAttributeFn.createColor( 'farColor', 'fc' )
        numericAttributeFn.setStorable( True )
        numericAttributeFn.setDefault( defaultFarColor[0], defaultFarColor[1], defaultFarColor[2] )
        depthShader.addAttribute( depthShader.farColorAttribute )

        # - The gamma value, or roll-off bias, which will affect how the color is interpolated between
        #   the near and far colors.
        #   > This value can be defined by the user using a slider, and is storable.
        global defaultGamma, minGamma, maxGamma
        depthShader.gammaAttribute = numericAttributeFn.create( 'gamma', 'g',
                                                                OpenMaya.MFnNumericData.kFloat, defaultGamma )
        numericAttributeFn.setStorable( True )
        numericAttributeFn.setMin( minGamma )
        numericAttributeFn.setMax( maxGamma )
        depthShader.addAttribute( depthShader.gammaAttribute )

        #==================================
        # OUTPUT NODE ATTRIBUTE(S)
        #==================================
        # - The pixel color output.
        #   > This value is computed in our depthShader.compute() method, and should not be stored.
        depthShader.outColorAttribute = numericAttributeFn.createColor( 'outColor', 'oc')
        numericAttributeFn.setStorable( False )
        numericAttributeFn.setWritable( False )
        numericAttributeFn.setReadable( True )
        numericAttributeFn.setHidden( False )
        depthShader.addAttribute( depthShader.outColorAttribute )

        #==================================
        # NODE ATTRIBUTE DEPENDENCIES
        #==================================
        #  - All the input attributes affect the computation of the pixel color output (outColor).
        depthShader.attributeAffects( depthShader.surfacePointAttribute, depthShader.outColorAttribute )
        depthShader.attributeAffects( depthShader.nearDistanceAttribute, depthShader.outColorAttribute )
        depthShader.attributeAffects( depthShader.farDistanceAttribute,  depthShader.outColorAttribute )
        depthShader.attributeAffects( depthShader.nearColorAttribute,    depthShader.outColorAttribute )
        depthShader.attributeAffects( depthShader.farColorAttribute,     depthShader.outColorAttribute )
        depthShader.attributeAffects( depthShader.gammaAttribute,        depthShader.outColorAttribute )

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

    def uninitializePlugin( mobject ):
        ''' Unitializes 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
    import random

    # Load our plug-in.
    cmds.loadPlugin( 'depthShader.py' )

    # Clear the scene.
    cmds.file( new=True, force=True )

    # Create an instance of our depth shader.
    depthShaderName = cmds.shadingNode( 'myDepthShader', asUtility=True )
    cmds.setAttr( depthShaderName + '.gamma', 1.4 )
    cmds.setAttr( depthShaderName + '.farColor', 0, 0.43, 1, type='double3' )

    # Create a surface shader to which we will attach our depth shader.
    surfaceShaderName = cmds.shadingNode( 'surfaceShader', asShader=True )
    cmds.connectAttr( depthShaderName + '.outColor', surfaceShaderName + '.outColor', force=True )

    # Create a shading group to tie everything together.
    shadingGroupName = cmds.sets( name='surfaceShader1SG', empty=True, noSurfaceShader=True, renderable=True )
    cmds.connectAttr( surfaceShaderName + '.outColor', shadingGroupName + '.surfaceShader', force=True )

    # Create a bunch cubes pseudo-randomly placed according to a seeded random number generator.
    numCubes = 50
    positionRange = 14

    randomSeed = 5
    random.seed( randomSeed )

    for i in range( 0, numCubes ):
        # Create a new cube.
        newCubeName = cmds.polyCube( w=2, h=2, d=2 )

        # Move the cube somewhere near the grid.
        cmds.move( random.randint( -positionRange, positionRange ), # x
                   random.randint( -positionRange, positionRange ), # y
                   random.randint( -positionRange, positionRange ), # z
                   newCubeName )

        # Rotate the cube along all three axes.
        cmds.rotate( random.randint( 0, 360 ), # x axis
                     random.randint( 0, 360 ), # y axis
                     random.randint( 0, 360 ), # z axis
                     newCubeName )

        # Assign the cube we have created to our surface (depth) shader.
        cmds.sets( newCubeName, e=True, forceElement=shadingGroupName )

    # Render the current view.
    cmds.render()

    '''