PyMEL Resources

This aims to serve as a quick reference guide to common tasks and workflows when using PyMEL

PyMEL technical documentation

Using PyMEL

Importing and Testing PyMEL

# Import the package and print the current Maya version
import pymel.core as pm
print pm.about(version=True, query=True)

Creating Maya nodes

import pymel.core as pm

floor = pm.polyPlane()

print floor
# Result // [nt.Transform(u'pPlane1'), nt.PolyPlane(u'polyPlane1')]

# Creating a maya node returns a list object


# We usually only want the transform when manipulating the scene using PyMEL
# Get in the habit of appending a [0] to the end to only return the first element (the transform)
floor = pm.polyPlane()[0]

print floor
# Result // pPlane1

Using the "Quick Help" in the Maya Script Editor

When using the Maya Script Editor, you can double click on any blue highlighted word, then right click and go to "Quick Help". This will open a shortcut panel on the right that will list all the keyword arguments for than command, and the data type

This lets you see how you control the creation of Maya nodes. For example:

import pymel.core as pm
# Set the subdivisions and size
my_plane = pm.polyPlane(width=20, height=20, subdivisionsWidth=20, subdivisionsHeight=20)[0]

### Querying and Editing Maya Nodes
```python
import pymel.core as pm
floor = pm.polyPlane()[0]

# Print the number of vertices for the object
print len(floor.verts)

# Iterate over each vertex
for vert in floor.verts:
    print vert, vert.getPosition()
	

Moving Objects

# We can use the Maya command "move" to move an object. 
# This takes at least two parameters, the object to move, and the position to move it to
import pymel.core as pm
floor = pm.polyPlane(width=20, height=20, subdivisionsWidth=20, subdivisionsHeight=20)[0]
# This is moved in world space
pm.move(floor, [0, 5, 0])
# Now lets shift it 2 units in X **from where it is**
pm.move(floor, [0, 5, 0], relative=True)


# We can apply the same technique to components, such as vertices
import pymel.core as pm
import random

floor = pm.polyPlane(width=20, height=20, subdivisionsWidth=20, subdivisionsHeight=20)[0]

# Iterate over each vertex and move it by a random amount
for vert in floor.verts:
    pm.move(vert, [0, random.uniform(-0.5, 0.5), 0], relative=True)

Useful Functions and Samples

Simple Browser UI Using PyMEL and Contexts

This is an example of simple UI written in PyMEL, that uses the "with" contexts to make the code structure more readable In this simple but functional UI, we can browse to a directory on disk, and then run actions using this path

import pymel.core as pm
import os
    
   
class SimpleBrowserUI():
    def __init__(self):
        # Create a "handle" for our UI. Nobody will ever see it, but we want to control it so we can check for it
        self.handle = 'SimpleBrowser'
        
        # Delete the UI if it exists - basically ensure we only ever have one instance running
        if pm.window(self.handle, exists=True):
            pm.deleteUI(self.handle)
                    
        # Delete the prefs - this is useful while building out your UI. It can be removed for deployment though
        if pm.windowPref(self.handle, exists=True):
            pm.windowPref(self.handle, remove=True)

        # PyMEL allows us to build UIs using a python context
        # the "with" context wraps everything, and makes building UIs in Maya much simpler
        with pm.window(self.handle, title='Simple Directory Browser', width=400, height=100):
            
            # Create a basic layout
            with pm.columnLayout(rs=10):  
                # Now we want two columns, we do this using a rowLayout and defining the numberOfColumns
                with pm.rowLayout(nc=2):
                    # Make a "browse" button and link the command using a callback
                    pm.button(label='Browse', width=80, height=30, command=pm.Callback(self.browse))
                    # A textField is where can save and visualize a string (our dir path)
                    self.target_dir = pm.textField(width=320, height=30)
                # Create our main button and link the command again
                # This time we set the backgroundColor to a soft green
                pm.button(label='Go', width=400, height=50, backgroundColor=[0.46, 0.86, 0.46], command=pm.Callback(self.do_stuff))
                    
            # Don't forget to show the window!
            pm.showWindow()
            
            
    def browse(self):
        """Basic browse function to locate an existing directory on disk
        """
        # dialogStyle defines how the UI looks, fileMode determines what we browse for
        # See the docs for more info
        files = pm.fileDialog2(dialogStyle=2, fileMode=3)
        # If the user presses "Cancel", fileDialog2 returns None so check for this
        if files is not None:
            # fileDialog2 returns a list of files selected, but for fileMode=3 we only get one, so get the first element
            self.target_dir.setText(files[0])
            
            
    def do_stuff(self):
        """This function is called when you press the "Go" button
        """
        # If the user presses "Go" without entering a path, warn them and exit gracefully
        if self.target_dir.getText() == '':
            print 'No directory chosen yet'
            return
            
        # Now we can do stuff with our directory path from the UI
        print 'Doing stuff using directory : "%s"' %self.target_dir.getText()
        
        print 'Directory exists = %s' %os.path.exists(self.target_dir.getText())

Example usage:

SimpleBrowserUI()

Scan Assets in a Directory

This will recursively get every obj and fbx file in a given directory tree, load them and get some basic data, which is printed out to a CSV file.

This csv file can then be loaded in Excel

import pymel.core as pm
import os


def scan_assets_in_dir(root_directory):
    # Make the csv file appear in the chosen root directory
    csv_file = os.path.join(root_directory, '_assets.csv')
    
    # Define valid extensions in a list
    EXTENSIONS = ['.obj', '.fbx']
    
    # Open the CSV file write mode, and yield the file_handle
    with open(csv_file, 'w') as file_handle:
        # Write the top line of the CSV, so we have column headers
        file_handle.write('File, Mesh, TriCount, Density\n')
        # Recursively walk the directory tree
        for (dir, subdir, files) in os.walk(root_directory):
            for file_name in files :
                # Assemble the file path
                file_path = os.path.join(root_directory, file_name)
                
                # Check if the file extension is in our list (this is case sensitive)
                if os.path.splitext(file_path )[1] in EXTENSIONS:
                    print 'Opening %s' %file_path 
                    # Force open the scene
                    pm.openFile(file_path, force=True)
                    # Loop over every mesh
                    for mesh in pm.listTransforms(type='mesh'):
                        # Freeze the scale so the area calculation is accurate
                        pm.makeIdentity(mesh, apply=True, scale=1)
                        # Get some basic data about the mesh
                        tri_count = mesh.numTriangles()
                        surface_area = mesh.area()
                        # Calculate mesh density
                        density = tri_count / surface_area
                        # Write current mesh data to CSV
                        file_handle.write('%s,%s, %i, %.2f\n' %(pm.sceneName(), mesh, mesh.numTriangles(), density))

Example Usage:

scan_assets_in_dir(r"C:\TestAssets")

Get LOD Data

This will find all lodGroups in the active scene and print data about the meshes at each level

import pymel.core as pm

def get_lod_info():
    """Get LOD group info and print it
    """
    lod_groups = pm.ls(type='lodGroup')
    print 'Found %i LOD Groups in the current scene' %len(lod_groups)
    
    for lod_group in lod_groups:
        # Create a dictionary. We'll use the LOD level as the key
        lod_meshes = dict()
        for transform in lod_group.getChildren():
            # Loop through each child in the LOD group
            # Currently only one transform per LOD level is supported
            if transform.name().endswith('0'):
                lod_meshes[0] = transform.getChildren()[0]
                
            elif transform.name().endswith('1'):
                lod_meshes[1] = transform.getChildren()[0]
                
            elif transform.name().endswith('2'):
                lod_meshes[2] = transform.getChildren()[0]
                
        # Print the data per level
        print 'Number of LODs = %i' %len(lod_meshes)
        for lod in lod_meshes:
            print 'LOD %i\
            Mesh: %s\n\t\
            Tri Count = %i\n\t\
            Area = %.2f\n\t\
            Density = %.2f'\
                  %(lod, lod_meshes[lod], lod_meshes[lod].numTriangles(), lod_meshes[lod].area(), (lod_meshes[lod].numTriangles()/lod_meshes[lod].area()))
                  
                  
            

Find Materials Used on Selected Meshes

def get_materials_on_selected_meshes(mesh_list):
    """Returns a list of materials used on the given meshes
    """
    all_materials = list()
    
    for mesh in mesh_list:
        # There's no error checking to ensure that you have a valid mesh transform selected!!
        for shading_engine in mesh.getShape().listConnections(type='shadingEngine'):
            materials = pm.listConnections('%s.surfaceShader' %shading_engine)
            if materials is not None and len(materials) > 0:
                all_materials.extend(materials)
    
    # Swap to a set to force a single instance, then back to a list
    all_materials = list(set(all_materials))
    return all_materials

Example Usage:

print get_materials_on_selected_meshes(pm.ls(sl=True))

Find Meshes Using Material

import pymel.core as pm

def get_meshes_using_materials(material_list):
    meshes = list()
    
    for material in material_list:
        shading_engine = pm.listConnections(material, type='shadingEngine')[0]
        meshes.extend(shading_engine.listConnections(type='mesh'))
    
    # Swap to a set to force a single instance, then back to a list
    meshes = list(set(meshes))
    return meshes

Example Usage:

print get_meshes_using_materials(['blinn1'])

Create a Material Using a Supplied Color Image

def create_material(image_path):
    """
    Function to create a new material using the supplied image
    
    Args:
        image_path: path on disk to the image to use
    """
    
    # Create a file node and assign the texture
    file_node = pm.createNode("file")
    file_node.fileTextureName.set(image_path)
    
    # Create a blank blinn material
    material = pm.createNode("blinn")
    
    # Create a blank shading group
    sg = pm.sets(renderable=True, noSurfaceShader=True, empty=True)
    
    # Connect the output from the file node to the input on the material
    pm.connectAttr( (file_node + ".outColor"), (material + ".color"), force=True)
    
    # Connect the output of the material to the input on the shading group
    pm.connectAttr( (material + ".outColor"), (sg + ".surfaceShader"), force=True)
    
    # Return our new material instance
    return material

Arnold Render Wrapper

import pymel.core as pm

def arnold_render(render_path, x=1024, y=1024, cam=pm.PyNode('persp')):
    """Arnold render wrapper
    
    Args:
        render_path: absolute path on disk to the rendered image
        
    Keyword Args:
        x: int x resolution (default = 1024)
        y: int y resolution (default = 1024)
        cam: PyNode of the camera to render (default = persp)
    """
    # Grab the extension as we need to add it as an attribute
    extension = os.path.splitext(render_path)[1][1:].lower()
    
    # Arnold only likes the 'jpeg' extension, so lets ensure we have that
    if extension == 'jpg':
        extension = 'jpeg'
    
    print 'Rendering to %s' %render_path
    
    # Remove the extension - the Arnold command adds it's own extension
    render_path = os.path.splitext(render_path)[0]
    
    # Grab the arnold node and set the attributes
    arnold_driver = pm.PyNode('defaultArnoldDriver')
    arnold_driver.ai_translator.set(extension)
    arnold_driver.pre.set(render_path)
    
    # Trigger the render
    pm.arnoldRender(w=512, h=512, cam=cam.name(), origFileName=render_path)
    

Example Usage:

arnold_render(r'C:\temprenders\out.jpg')

arnold_render(r'C:\temprenders\out.jpg', x=2048, y=2048, cam=pm.ls(sl=True)[0])

Print Number of Vertices Split by Normal

This is useful for estimating the cost of a mesh when exporting to a game engine

import pymel.core as pm

def get_object_split_vertex_count(mesh_transform):
    """Get an estimate of the number of vertices an object may have once exported to a game engine
    """
    split_verts = 0
    for vert in mesh_transform.verts:
        # Get all the vertex normals
        all_normals = vert.getNormals()
        # Create a dictionary of normal variants 
        normal_dict = {str(normal):all_normals.count(normal) for normal in all_normals}
        split_verts += len(normal_dict)
    return split_verts

Example Usage:

import pymel.core as pm

for object in pm.ls(sl=True):
    split_verts = get_object_split_vertex_count(object)
    print object, split_verts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.