Results 1 to 19 of 19

Thread: Looking to improve the speed of one of my PyMEL functions

  1. #1
    Jr. Technical Artist Nightshade's Avatar
    Join Date
    Sep 2012
    Location
    Stockholm, Sweden
    Posts
    226

    Default Looking to improve the speed of one of my PyMEL functions



    I have a PyMEL-function for converting a uv selection into a list containing each individual uv shell; it is a general-purpose function that I use for all sorts of things.
    I have noticed that it's not very fast though, so when there are too many uv shells around this function will stall - making other functions (further down the line) having to wait for it. Im hoping that maybe someone here on the forums could provide a little bit of feedback - maybe even a solution to this problem.

    Code:
    import pymel.core as pm
    
    # Returns list with each shell in it
    def convertToShell():
        
        listFinal = []
        
        # Store coords and check count
        selCoords = pm.filterExpand(selectionMask=35) # Poly UV's
        uvRemain = uvCount = len(selCoords)
        
        # Progress window
        pm.progressWindow(
            isInterruptable=True,  
            maxValue=uvCount, 
            progress=0, 
            status="Pre-processing UV shells", 
            title="Pre-processing UV shells" 
        )
        
        # Do for every shell
        while uvRemain != 0:
            
            # Break if cancelled by user
            if pm.progressWindow(query=True, isCancelled=True) == True:
                pm.warning("Interupted by user")
                break
            
            # Edit the progress window
            pm.progressWindow(
                edit=True, 
                    progress=(uvCount - uvRemain),
                    status=("Pre-processing shells.\n%s UVs left")%uvRemain
            )
        
            # Select the first and current coord of selCoords
            pm.select(selCoords[0], replace=True)
            
            # Expand selection to entire shell and store
            pm.polySelectConstraint(type=0x0010, shell=True, border=False, mode=2)
            pm.polySelectConstraint(shell=False, border=False, mode=0)
            selShell = pm.ls(selection=True, flatten=True)
            
            # Go back to original single-UV selection (replace)
            pm.select(selCoords, replace=True)
            
            # Reduce that selection by deselecting previous shell
            pm.select(selShell, deselect=True)
            selCoords = pm.ls(selection=True, flatten=True)
            
            # Recalculate number of UVs left
            uvRemain = len(selCoords)
            
            # Add shell to list
            listFinal.append(selShell)
            pm.select(clear=True)
            
        # Close progress window
        pm.progressWindow(endProgress=True)
        
        # Return shell list
        return listFinal
    I know what is taking so long: selections. I've been told by more experienced programmers that the select-command is slow, and that it's always better to work on the raw data instead of working with selections and reading data on the fly.
    But I know of no other way for collecting UV shells than this. There's probably some super-speedy Maya API way of getting all the UV shells but C++ isn't in my skills-toolbox :(

    Any advice?
    "There he goes. One of God's own prototypes. A high-powered mutant of some kind never even considered for mass production. Too weird to live, and too rare to die."
    -- Hunter S. Thompson

  2. #2
    while loop
    Join Date
    Feb 2010
    Posts
    179

    Default

    Code:
    import pymel.all as pm
    
    def get_selected_uv_shells():
        sel = pm.selected(o = True)[0]
        selected_uvs = set(uv for uv in pm.selected() if isinstance(uv, pm.MeshUV))
        # getUvShellsIds returns a list of shell_ids, and the number of shells
        uv_shell_ids, num_shells = sel.getUvShellsIds()
        # You still need to actually get the MeshUVs yourself    
        # Using sets instead of sublists because checking if an item is in a set is much faster
        all_shells = [set() for i in xrange(num_shells)]
        for uv_index, uv_shell_id in enumerate(uv_shell_ids):
            all_shells[uv_shell_id].add(sel.uvs[uv_index])
        # If you've not selected any UVs, I'm assuming you want them all.
        # Converting your selection to uvs before getting selected_uvs would probably be more accurate
        if not selected_uvs:
            return all_shells
        selected_shells = []
        # This goes through and finds shells that contain one of the selected uvs
        while selected_uvs:
            test_uv = selected_uvs.pop()
            for shell in all_shells:
                if test_uv in shell:
                    break
            selected_shells.append(shell)
            # No need to check against that shell again
            all_shells.remove(shell)
            # Also we can get rid of all the uvs from that shell
            # This way we don't end up iterating through all the UVs
            selected_uvs.difference_update(shell)
        return selected_shells
    get_selected_uv_shells()
    Hopefully this makes sense, but basically I'm speeding the whole thing up by relying on the getUvShellIds method, that returns a list of shell ids for every uv.
    Because it is called from the mesh though, you still need to go through and filter out the uvs that you don't currently have selected.

    This can still be slow with dense meshes, just due to the Pymels speed issues with creating a whole lot of component objects.
    At which point it might be faster to move everything down to API calls.

  3. #3
    Technical Artist
    Join Date
    Jul 2008
    Location
    Austin, TX
    Posts
    676

    Default

    Here a function I wrote to retrieve UVs ordered by UV Shell, maybe it can help you get started with working with the API.
    The best performance outside of learning C++ is to skip PyMel and use OpenMaya directly.


    Code:
    def getUVShells(shape, uvSet=None):
        
        uvSets = cmds.polyUVSet(shape, q=True, allUVSets=True)
        if not uvSet or uvSet not in uvSets:
            uvSet = cmds.polyUVSet(shape, q=True, cuv=True)[0]
    
        selectionList = om.MSelectionList()
        selectionList.add(shape)
    
        mDag = om.MDagPath()
        selectionList.getDagPath(0, mDag)
        
        meshFn = om.MFnMesh(mDag)
        
        uvShellArray = om.MIntArray()
        shells = om.MScriptUtil()
        shellsPtr = shells.asUintPtr()
        
        meshFn.getUvShellsIds(uvShellArray, shellsPtr, uvSet)
        
        uvShells = defaultdict(list)
        for i, shellId in enumerate(uvShellArray):
            uvShells[shellId].append('{0}.map[{1}]'.format(shape, i))
        
        return uvShells.values()

  4. #4
    Jr. Technical Artist Nightshade's Avatar
    Join Date
    Sep 2012
    Location
    Stockholm, Sweden
    Posts
    226

    Default

    Thanks for your replies guys, I appreciate it!
    I'll digest that code later tomorrow :)

    rgkovach123:
    If you have any good resources/guides on getting started with OpenMaya it would be greatly appreciated. I did have a look at some kind of intro to the Maya API -coding in the book "Maya Python for Games and Films" but found the texts there to be really lacking (really poor descriptions of what was going on. Those chapters felt like they were written by a totally different author (and not a very pedagogic one).
    "There he goes. One of God's own prototypes. A high-powered mutant of some kind never even considered for mass production. Too weird to live, and too rare to die."
    -- Hunter S. Thompson

  5. #5
    Technical Artist
    Join Date
    Jul 2008
    Location
    Austin, TX
    Posts
    676

    Default

    the toughest part of the C++ api is understanding when, and how, to use MScriptUtil. I found the Maya Python for Games and Film to be helpful, especially the chapter on how to translate C++ syntax.

    Maya comes will a lot of examples, both C++ and Python. Maya's own documentation is handy as well.

    http://docs.autodesk.com/MAYAUL/2014...mber=d30e34315

  6. #6
    Jr. Technical Artist Nightshade's Avatar
    Join Date
    Sep 2012
    Location
    Stockholm, Sweden
    Posts
    226

    Default

    Quote Originally Posted by rgkovach123 View Post
    the toughest part of the C++ api is understanding when, and how, to use MScriptUtil. I found the Maya Python for Games and Film to be helpful, especially the chapter on how to translate C++ syntax.

    Maya comes will a lot of examples, both C++ and Python. Maya's own documentation is handy as well.

    http://docs.autodesk.com/MAYAUL/2014...mber=d30e34315
    From my point of view, the big hurdle is knowing how to deconstruct a command (or set of actions) so that I can start looking at reverse-engineering it with openMaya. I can read what's going on in your code but I do not fully understand/grasp the structure of the objects that you present: It's like looking at a lego construction, being unable to identify the shape, form and color of the individual lego pieces - and why certain pieces have to be together with certain other pieces.

    Also, the code doesn't run: I get a NameError on the defaultdict -object (which is undefined).
    Last edited by Nightshade; 09-07-2014 at 10:14 AM.
    "There he goes. One of God's own prototypes. A high-powered mutant of some kind never even considered for mass production. Too weird to live, and too rare to die."
    -- Hunter S. Thompson

  7. #7
    Technical Artist
    Join Date
    Jul 2008
    Location
    Austin, TX
    Posts
    676

    Default

    Wrapping your head around the c++ classes is pretty daunting, but once you get over the initial hurdle, it's not too bad.

    I forgot to include an import statement in my example. Try adding:

    Code:
    from collections import defaultdict

  8. #8
    Technical Artist
    Join Date
    Jul 2008
    Location
    Austin, TX
    Posts
    676

    Default

    I'll try to explain what is happening. Anyone who understands C++ or the API will probably be horrified by my explanation.

    First we need to take a string, our object name, and use to create a Class of MDagObject. This is one of the most basic classes. Once we have a MDagObject, we can use it to create a MfnMesh Class, which is a class that has a lot of polygon specific functions.

    The MfnMesh class has a function that will return the UV Shell ID of a UV coordinate. For example, the first and second polygons of a mesh are different shells, it would look like this:

    0, 0, 0, 1, 1, 1

    This is telling us that .map[0], .map[1], .map[2] define a UV Shell and .map[3], .map[4], .map[5] define a second shell.
    If our mesh only had two triangles, this would point back to:

    .vtx[0], .vtx[1], vtx[2] belong to a UV Shell and .vtx[0], .vtx[2], vtx[3] define a second UV Shell.

    The Shell ID itself is not that useful to use, but it is useful as a grouping mechanism, since every UV coordinate in a shell will have the same ID. We can use this later in combination with the defaultdict.

    So to get this list of shell IDs we have to deal with the API. The getUvShellIDs function doesn't return a value, it needs to be passed a variable to dump the results. If you have written shaders in HLSL or GLSL, its sort of like using inout.

    We have to create ahead of time a variable to hold the results, but we have to create a very specific type of variable, to do that we use MScriptUtils. (This class is a crutch to cobble together python and API, and it still gives me headaches).

    Now that we have the prerequisites, we can ask for the Shell IDs, this is an ordered list of the UV coordinates and which shell they belong to. If I use the Shell ID as a dictionary key, then I can iterate through the list of IDs and sort them into lists. This is where the defaultdict comes in handy. It eliminates the need to test if a key already exists buy assuming it does, as long as you tell the defaultdict what the default value is if the key doesn't already exist.

    Since I know my mesh name and I know the value of i maps to a UV coordinate, I can use string formatting to setup my return value.

  9. #9
    Jr. Technical Artist Nightshade's Avatar
    Join Date
    Sep 2012
    Location
    Stockholm, Sweden
    Posts
    226

    Default

    rgkovach123:

    I've tried getting this to work today but the code doesn't execute fully.
    Code:
    import maya.api.OpenMaya as om
    import maya.cmds as cmds
    from collections import defaultdict
    
    def getUVShells(shape, uvSet=None):
        
        uvSets = cmds.polyUVSet(shape, q=True, allUVSets=True)
        if not uvSet or uvSet not in uvSets:
            uvSet = cmds.polyUVSet(shape, q=True, cuv=True)[0]
    
        selectionList = om.MSelectionList()
        selectionList.add(shape)
    
        mDag = om.MDagPath()
        selectionList.getDagPath(0, mDag)
        
        meshFn = om.MFnMesh(mDag)
        
        uvShellArray = om.MIntArray()
        shells = om.MScriptUtil()
        shellsPtr = shells.asUintPtr()
        
        meshFn.getUvShellsIds(uvShellArray, shellsPtr, uvSet)
        
        uvShells = defaultdict(list)
        for i, shellId in enumerate(uvShellArray):
            uvShells[shellId].append('{0}.map[{1}]'.format(shape, i))
        
        return uvShells.values()
        
    test = getUVShells("pSphere1")
    print test
    # TypeError: getDagPath() takes exactly one argument (2 given) #

    Also I need to run it on a UV selection so the shape has to be retrieved from the components. Went with this:
    Code:
    shapes = pm.listRelatives(fullPath=True, parent=True, type="mesh")[0]
    Not sure though if that's the proper way though as I can't really test the code without it halting :(

    I tried removing the name from getDagPath, and while the code does execute a few rows more, I get another error 3 rows below:
    # AttributeError: 'module' object has no attribute 'MScriptUtil' #
    "There he goes. One of God's own prototypes. A high-powered mutant of some kind never even considered for mass production. Too weird to live, and too rare to die."
    -- Hunter S. Thompson

  10. #10
    Technical Artist
    Join Date
    Jul 2008
    Location
    Austin, TX
    Posts
    676

    Default

    maya.OpenMaya and maya.api.OpenMaya are not the same thing. Try using the maya.OpenMaya instead.

    MScriptUtil is obsolete in API 2.0.


    Maya Python API 2.0 Reference
    http://help.autodesk.com/cloudhelp/2...ref/index.html


    getting uv shells for only the selected components is do-able with a few slight tweaks. instead of adding the shape onto the MSelectionList, we can add the components.

    Code:
     # API 1.0 example, may be slightly different in API 2.0.
        selectionList = om.MSelectionList()
        for face in selectedFaces:
            selectionList.add(face)
    Last edited by rgkovach123; 04-21-2015 at 02:32 PM.

  11. #11

    Default

    Here's a quick rewrite to Maya api 2.0 plus some extra notes/comments and additions.
    Maybe that helps?

    Code:
    import maya.api.OpenMaya as om2
    import maya.cmds as cmds
    from collections import defaultdict
    
    def getUVShells(mesh, uvSet=None):
        """ Return UVs per shell ID for a given mesh.
        
        :return: list(list)
        :rtype: Return a list of UVs per shell.
        """
        
        # define which uvSet to process
        uvSets = cmds.polyUVSet(mesh, q=True, allUVSets=True)
        if not uvSet or uvSet not in uvSets:
            uvSet = cmds.polyUVSet(mesh, q=True, cuv=True)[0]
    
        # get the api dag path
        sel = om2.MSelectionList()
        sel.add(mesh)
        dagPath = sel.getDagPath(0)
        dagPath.extendToShape()
        
        # Note:
        # Maya returns the .map[id] on the transform instead of shape if you select it.
        # For convenience for now let's do the same (whether you want this is up to you)
        mesh = cmds.ls(dagPath.fullPathName(), long=True, o=True)[0]
        mesh = mesh.rsplit('|', 1)[0] # get the parent transform
        
        # get shell uv ids
        fnMesh = om2.MFnMesh(dagPath)
        uvCount, uvShellArray = fnMesh.getUvShellsIds(uvSet)
        
        # convert to a format we like it (per shell)
        uvShells = defaultdict(list)
        for i, shellId in enumerate(uvShellArray):
            uv = '{0}.map[{1}]'.format(mesh, i)
            uvShells[shellId].append(uv)
        
        # return a list per shell
        return uvShells.values()
        
    if __name__ == '__main__':
        
        # create test
        sphere = cmds.polySphere()[0]
        cmds.polyAutoProjection()
        cmds.select('{0}.map[1]'.format(sphere), r=1)
        sel = cmds.ls(sl=1)
        shells = getUVShells(sel[0])
        
        # test: print each shell
        for shell in shells:
            print shell
        
        # test: convert each selected uv component to uv shell
        sel = cmds.ls(sl=1, flatten=True, long=True)
        selected_shells = []  
        for shell in shells:
            for component in sel:
                if component in shell:
                    selected_shells.extend(shell)
                    
        cmds.select(selected_shells, r=1)
    Also for retrieving the shape from the selection can be done in multiple ways.
    For one we know that the part in front of the dot is the node, after it are the components.
    The tricky thing though is that Maya will return selected components as if its on the parent transform node; though I've found this to not be the case for 100% of the scenarios!
    So relying on it is prone to break at a point.
    Anyway, here are some more examples. You could also use the objects argument on the ls command.

    Code:
    sel = cmds.ls(sl=1)[0]
    print sel
    print cmds.listRelatives(sel, fullPath=True, parent=True, type="mesh")[0]
    print cmds.listRelatives(fullPath=True, parent=True, type="mesh")[0]
    print cmds.ls(sel,  o=True)[0]
    print cmds.ls(sl=1, o=True)[0]
    
    # this won't work since it returns the transform node in most cases
    print sel.rsplit(".", 1)[0]
    Last edited by BigRoyNL; 04-22-2015 at 05:45 AM.

  12. #12
    Jr. Technical Artist Nightshade's Avatar
    Join Date
    Sep 2012
    Location
    Stockholm, Sweden
    Posts
    226

    Default

    Sorry for the exceptionally late answer. I do appreciate your help even though I havenīt looked at this further for a couple months.

    I only have two questions:
    Say that I am developing in PyMEL and I do not want to have to import another module (Maya.cmds) - what changes needs to be done to the code?
    Both your code Robert - and yours BigRoyNL - runs on Maya.cmds, so the ls command always returns a string. But in PyMEL ls returns an object.
    The code breaks on the row where the selection is added into om.MSelectionList(). My first idea here is to just turn the class object into a string and feed it to the function, but that just feels awfully bad - even stupid!

    My second question is in what Maya version API 2.0 was implemented? Royīs code does not run in Maya 2012 so I assume 2013 was the first one with API 2.0. I have not tested Robertīs code in 2012.
    "There he goes. One of God's own prototypes. A high-powered mutant of some kind never even considered for mass production. Too weird to live, and too rare to die."
    -- Hunter S. Thompson

  13. #13
    struct
    Join Date
    Mar 2011
    Location
    San Francisco
    Posts
    348

    Default

    If you're going between PyMel and the API classes, you're going to have to deal with the type ugliness that you mentioned. Either cast it to a string or use maya.cmds (or pm.cmds) for anything you want to pass to an API class.

    The other option is to use the available PyMel attributes for accessing the underlying API classes for objects. PyMel stores (or allows easy access to) the underlying API classes for its objects. You can access the MDagPath of a PyNode with the method __apimdagpath__()

    In the docs: http://download.autodesk.com/us/maya...e-counterparts

    You can access these by using the special methods __apimobject__, __apihandle__, __apimdagpath__, __apimplug__, and __apimfn__. ( Be aware that this is still considered internal magic, and the names of these methods are subject to change ):

  14. #14
    Jr. Technical Artist Nightshade's Avatar
    Join Date
    Sep 2012
    Location
    Stockholm, Sweden
    Posts
    226

    Default

    Quote Originally Posted by capper View Post
    If you're going between PyMel and the API classes, you're going to have to deal with the type ugliness that you mentioned. Either cast it to a string or use maya.cmds (or pm.cmds) for anything you want to pass to an API class.

    The other option is to use the available PyMel attributes for accessing the underlying API classes for objects. PyMel stores (or allows easy access to) the underlying API classes for its objects. You can access the MDagPath of a PyNode with the method __apimdagpath__()

    In the docs: http://download.autodesk.com/us/maya...e-counterparts
    Thanks for the information.
    I didn't know that the Maya.cmds module was integrated into PyMEL! Handy!
    "There he goes. One of God's own prototypes. A high-powered mutant of some kind never even considered for mass production. Too weird to live, and too rare to die."
    -- Hunter S. Thompson

  15. #15
    Jr. Technical Artist Nightshade's Avatar
    Join Date
    Sep 2012
    Location
    Stockholm, Sweden
    Posts
    226

    Default

    I just realized that if I only want the shells which are active in my current UV selection, then I would need some additional code.
    Like...
    Code:
    def getShells():
        selUV = pm.ls(selection=True, flatten=True)
        
        # code which gets all_shells - based of R.White's example
        
        shellList = []
        for shell in all_shells:
            for uv in sel:
                if uv in shell:
                    if shell not in shellList:
                        shellList.append(shell)
        
        return shellList
    shells = getShells()
    print shells
    But this is really slow. In fact, running those for-loops increases the processing time by over 50% compared to just returning all_shells
    It would be so much easier if I could just iterate through the MeshUVs, ask them what shellID they belong to and then just get that shellID -set and place it into the shellList[].
    But the MeshUV class doesn't have any methods at all it seems.

    NOTE: I want the return to be a list of the UV-shells so I can easily do pm.select(shells[0]) for example (or list of sets if Maya is clever enough to get all components inside a set() )
    Last edited by Nightshade; 06-16-2015 at 02:52 PM. Reason: Added important stuff
    "There he goes. One of God's own prototypes. A high-powered mutant of some kind never even considered for mass production. Too weird to live, and too rare to die."
    -- Hunter S. Thompson

  16. #16
    struct
    Join Date
    Mar 2011
    Location
    San Francisco
    Posts
    348

    Default

    The getUvShellsIds function returns a flat list that contains a uv shell id for each uv index. Get the indices of the currently selected uvs, then look up the shell id for each using that flat list.

    You still need the code to determine which uv ids comprise each shell, but this is probably the approach I'd try first for figuring out which shell each selected uv is in

    I can't test it right now but the general idea is:
    Code:
    uv_shell_ids = mfnMesh.getUvShellIds()
    for item in pm.cmds.ls(sl=True, flatten=True): # cmds is faster for looping through large attribute selections
        shell_id = uv_shell_ids[int(item.split('[')[-1].split(']')[0])]
    Last edited by capper; 06-16-2015 at 05:50 PM.

  17. #17
    Jr. Technical Artist Nightshade's Avatar
    Join Date
    Sep 2012
    Location
    Stockholm, Sweden
    Posts
    226

    Default

    I appreciate all your help!

    Final code:
    Code:
    import pymel.core as pm
    
    # Converts UV selection to list of UV shells
    # Returns list
    def convertToShells():
    
        sel = pm.cmds.ls(selection=True, flatten=True)
        selObj = pm.ls(selection=True, objectsOnly=True)[0]
    
        # Create uvShellIds -array
        uvShellIDs, uvShellArray = selObj.getUvShellsIds()
        
        selShellsIDs = set() # Set with selected shells' IDs
    
        # Get shell IDs for each UV in selection
        for uv in sel:
            i = int( uv.split('[')[-1].split(']')[0] ) # Get UV index
            selShellsIDs.add(uvShellIDs[i])
    
        # Get selected shells' UVs as a dictionary
        uvShells = defaultdict(list)
        for i, shellID in enumerate(uvShellIDs):
            if shellID not in selShellsIDs:
                continue
            else:
                uv = "%s.map[%s]"%(selObj, i)
                uvShells[shellID].append(uv)
        
        # Return as list
        return uvShells.values()
    
    shells = convertToShells()
    Off-topic:
    Is there any good reason to use delimiter-separated words compared to letter-case separated words or is it just a matter of taste?
    "There he goes. One of God's own prototypes. A high-powered mutant of some kind never even considered for mass production. Too weird to live, and too rare to die."
    -- Hunter S. Thompson

  18. #18
    program Theodox's Avatar
    Join Date
    Mar 2012
    Location
    Seattle
    Posts
    1,107

    Default

    Python style is usually to prefer under_score_names to camelCaseNames, but to use PascalCaseNames for classes

    BTW.: You may get some minor perf benefits by keeping all your dictionary as integer > set(integer) mappings and using a generator to get the .map values back out, since any set actions you do will be a bit quicker if the sets contain integers instead of strings. It will vary depending on what use you make of the information, though, so if the above works for you that's great.

  19. #19
    Jr. Technical Artist Nightshade's Avatar
    Join Date
    Sep 2012
    Location
    Stockholm, Sweden
    Posts
    226

    Default

    Quote Originally Posted by Theodox View Post
    Python style is usually to prefer under_score_names to camelCaseNames, but to use PascalCaseNames for classes

    BTW.: You may get some minor perf benefits by keeping all your dictionary as integer > set(integer) mappings and using a generator to get the .map values back out, since any set actions you do will be a bit quicker if the sets contain integers instead of strings. It will vary depending on what use you make of the information, though, so if the above works for you that's great.
    I see.
    I will have a look at those suggestions tomorrow. But for now I am kinda happy: The new function is approx 200 times faster than my old one.
    "There he goes. One of God's own prototypes. A high-powered mutant of some kind never even considered for mass production. Too weird to live, and too rare to die."
    -- Hunter S. Thompson

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •