[OpenMaya] :: MPxLocator Python 2.0 Plugin

Okay following the previous vector posts, I decided to plunge ahead and create a plugin that capitalizes on that knowledge.

Previously, in OpenMaya 1.0, the MPxLocator has been defined by using the draw method by using Open Graphics Library (OpenGL) functions. Maya’s architecture has updated a new method, and I used the OpenMaya.MUIDrawManager class method to do the drawings, making things straight forward. Here, I draw a circle, rectangle and a line; I spent way too much time figuring out the nuances of this plugin.

At first, finding out which MPxLocator examples file work right out of the box has been an issue, except finally, I found this one: uiDrawManager/uiDrawManager.cpp

In addition to finding out how the 2.0 plugins work, I also wanted to learn a bit more on reflection math, and I put that to use in this plugin:

R = 2(N * L) * N – L

Which using Maya’s Python code looks like this:

# define normal vector at origin
normal = OpenMaya.MVector(0.0, 1.0, 0.0)

# get opposing vector through double cross product
opposing_vector = normal * (2 * (normal * input_point))
opposing_vector -= input_point

# now multiply it by the scalar value
opposing_vector *= scale
if as_vector:
   return opposing_vector
else:
   return opposing_vector.x, opposing_vector.y, opposing_vector.z

Maya viewport handles drawing by using the DrawManager, like this:

A circle:

        radius = 2.0
        is_filled = True
        position = OpenMaya.MPoint(0, 0, 0)
        normal = OpenMaya.MVector(0, 1, 0)
        drawManager.beginDrawable()
        drawManager.beginDrawInXray()
        drawManager.setLineWidth(line_width)
        drawManager.setLineStyle(drawManager.kSolid)
        drawManager.setColor(OpenMaya.MColor(plane_color))
        drawManager.circle(position, normal, radius, is_filled)
        drawManager.endDrawInXray()
        drawManager.endDrawable()

A rectangle:

        rect_scale_x = 1.0
        rect_scale_y = 1.0
        is_filled = False
        position = OpenMaya.MPoint(0, 0, 0)
        normal = OpenMaya.MVector(0, 0, 1)
        up = OpenMaya.MVector(0, 1, 0)
        drawManager.beginDrawable()
        drawManager.setLineWidth(line_width)
        drawManager.setLineStyle(drawManager.kSolid)
        drawManager.setColor(OpenMaya.MColor(plane_color))
        # For 3d rectangle, the up vector should not be parallel with the normal vector.
        drawManager.rect(position, normal, up, rect_scale_x, rect_scale_y, is_filled)
        drawManager.endDrawable()

A line:

        drawManager.beginDrawable()
        drawManager.setLineWidth(line_width)
        drawManager.setLineStyle(drawManager.kSolid)
        drawManager.setColor(OpenMaya.MColor(plane_color))
        drawManager.line(OpenMaya.MPoint(0, -1, 0), OpenMaya.MPoint(0, 1, 0))
        drawManager.endDrawable()

The reason why I dived into Maya’s Viewport drawing is because I was following Chad Vernon’s excellent C++ series, and his MPxLocator example no longer works in the current Maya 2020 version. The full working code can be found at my GitHub page:

[OpenMaya] :: MFnNurbsCurve.create

Alright, so this one is also lots of fun. We are going to create a NurbsCurve using OpenMaya, with a leading degree of 2 (Quadratic). Remember in the previous post about how I calculated the vectors between the two locator positions? Well this time, we are going to do the same, but creating nurbsCurve. This is because each CV needs a position vector array:.

def get_point_array(points_array, equal_distance=False):
    """
    calculate the positional array object.

    :param points_array:
    :param equal_distance: <bool> calculate the equal distance of CV's
    :return:
    """
    m_array = OpenMaya.MPointArray()
    if equal_distance:
        array_length = len(points_array)
        for idx, point in enumerate(points_array):
            if idx == 0:
                m_array.append(OpenMaya.MPoint(*point))
                m_array.append(OpenMaya.MPoint(*point))
            elif idx >= 1 and idx != array_length - 1:
                prev_p, cur_p, next_p = list_scanner(points_array, idx)
                cur_v = math_utils.Vector(*cur_p)
                prev_v = math_utils.Vector(*prev_p)
                new_vec = math_utils.Vector(cur_v - prev_v)
                new_vec = math_utils.Vector(new_vec * 0.5)
                new_vec = math_utils.Vector(prev_v + new_vec)
                m_array.append(OpenMaya.MPoint(*new_vec.position))
            elif idx == array_length - 1:
                prev_p, cur_p, next_p = list_scanner(points_array, idx)
                prev_v = math_utils.Vector(*prev_p)
                next_v = math_utils.Vector(*next_p)
                new_vec = math_utils.Vector(next_v - prev_v)
                new_vec = math_utils.Vector(new_vec * 0.5)
                new_vec = math_utils.Vector(prev_v + new_vec)
                # add two points in the same spot
                m_array.append(OpenMaya.MPoint(*new_vec.position))
                m_array.append(OpenMaya.MPoint(*point))
    else:
        for idx, point in enumerate(points_array):
            if idx == 1:
                prev_p, cur_p, next_p = list_scanner(points_array, idx)
                cur_v = math_utils.Vector(*cur_p)
                prev_v = math_utils.Vector(*prev_p)
                new_vec = math_utils.Vector(cur_v - prev_v)
                new_vec = math_utils.Vector(new_vec * 0.5)
                new_vec = math_utils.Vector(prev_v + new_vec)
                m_array.append(OpenMaya.MPoint(*new_vec.position))
            elif idx == len(points_array) - 1:
                prev_p, cur_p, next_p = list_scanner(points_array, idx)
                prev_v = math_utils.Vector(*prev_p)
                next_v = math_utils.Vector(*next_p)
                new_vec = math_utils.Vector(next_v - prev_v)
                new_vec = math_utils.Vector(new_vec * 0.5)
                new_vec = math_utils.Vector(prev_v + new_vec)
                m_array.append(OpenMaya.MPoint(*new_vec.position))
            m_array.append(OpenMaya.MPoint(*point))
    return m_array

So above is just a point array collector that recalculates positions from an existing array of positions: Like selected locators or joints. Preferably at world-space co-ordinates. We then take these recalculated positional array into the OpenMaya.MFnNurbsCurve.create function. I wrote this create_curve_from_points function below that uses this:

def create_curve_from_points(points_array, degree=2, curve_name="", equal_cv_positions=False):
    """
    create a nurbs curve from points.
    :param points_array: <tuple> positional points array.
    :param degree: <int> curve degree.
    :param curve_name: <str> the name of the curve to create.
    :param equal_cv_positions: <bool> if True create CV's at equal positions.
    :return: <str> maya curve name.
    """
    knot_length = len(points_array)
    knot_array = get_knot_sequence(knot_length, degree)
    m_point_array = get_point_array(points_array, equal_distance=equal_cv_positions)

    # curve_data = OpenMaya.MFnNurbsCurveData().create()
    curve_fn = OpenMaya.MFnNurbsCurve()
    curve_fn.create(m_point_array, knot_array, degree,
                    OpenMaya.MFnNurbsCurve.kOpen,
                    False, False)
    m_path = OpenMaya.MDagPath()
    curve_fn.getPath(m_path)

    if curve_name:
        parent_obj = object_utils.get_parent_obj(m_path.partialPathName())[0]
        object_utils.rename_node(parent_obj, curve_name)
        return curve_name
    return curve_fn.name()

In the function above, there is a boolean parameter: equal_cv_positions. The default is False. The result of this is creating CV’s at their locator’s positions, like so:

And if the equal_cv_positions is set to True, this is the result:

As you can see, this utility tool is going to become immediately useful. You could already guess at plans use this already!

[OpenMaya] :: Iteration

As a tools developoer, it is in my best interest to make the code run fast, and if there is anything in Maya that makes things go fast, it’s OpenMaya:

Below is an example of how OpenMaya iterates a scene to find all the AnimCurve nodes connected to an object in Maya.:

# import maya modules
from maya import OpenMaya as om

def get_connected_nodes(object_name="", find_node_type=om.MFn.kAnimCurve): """ get connected nodes from node provided. :param object_name: <str> string object to use for searching from. :param find_node_type: <om.MFn> kObjectName type to find. """ node = get_m_obj(object_name) dag_iter = om.MItDependencyGraph( node, om.MItDependencyGraph.kUpstream, om.MItDependencyGraph.kPlugLevel) dag_iter.reset() found_nodes = [] while not dag_iter.isDone(): cur_item = dag_iter.currentItem() if cur_item.hasFn(find_node_type): found_nodes.append(cur_item) dag_iter.next() return found_nodes

Another way to go about doing this business is creating a generator object by introducing yield, in the same code, we just remove the return statement:

# import maya modules
from maya import OpenMaya as om

def get_connected_nodes_gen(object_name="", find_node_type=om.MFn.kAnimCurve): """ nodes generator. :param object_name: <str> string object to use for searching from. :param find_node_type: <om.MFn> kObjectName type to find. """ node = get_m_obj(object_name) dag_iter = om.MItDependencyGraph( node, om.MItDependencyGraph.kUpstream, om.MItDependencyGraph.kPlugLevel) dag_iter.reset() while not dag_iter.isDone(): cur_item = dag_iter.currentItem() if cur_item.hasFn(find_node_type): yield cur_item dag_iter.next()

We can test the speed of the code by utilizing the cProfile module:

# import maya modules
from maya import OpenMaya as om

# import local modules
import cProfile

# define variables
anim_key_nodes = object_utils.get_connected_nodes_gen('pCube1')

# run profiler
cProfile.run("for n in anim_key_nodes: print n")

<...>
<...>
<maya.OpenMaya.MObject; proxy of <Swig Object of type 'MObject *' at 0x000002776FB30B40> >
<maya.OpenMaya.MObject; proxy of <Swig Object of type 'MObject *' at 0x000002776FB306C0> >

213 function calls in 0.021 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
1 0.020 0.020 0.021 0.021 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 OpenMaya.py:1539(__init__)
1 0.000 0.000 0.000 0.000 OpenMaya.py:7370(__init__)
14 0.000 0.000 0.000 0.000 OpenMaya.py:84(_swig_repr)
1 0.000 0.000 0.000 0.000 OpenMaya.py:9666(__init__)
15 0.000 0.000 0.001 0.000 object_utils.py:60(get_connected_nodes_gen)
1 0.000 0.000 0.000 0.000 object_utils.py:80(get_m_obj)
42 0.000 0.000 0.000 0.000 {maya._OpenMaya.MItDependencyGraph_currentItem}
43 0.000 0.000 0.000 0.000 {maya._OpenMaya.MItDependencyGraph_isDone}
42 0.000 0.000 0.000 0.000 {maya._OpenMaya.MItDependencyGraph_next}
1 0.000 0.000 0.000 0.000 {maya._OpenMaya.MItDependencyGraph_reset}
1 0.000 0.000 0.000 0.000 {maya._OpenMaya.MItDependencyGraph_swiginit}
42 0.000 0.000 0.000 0.000 {maya._OpenMaya.MObject_hasFn}
1 0.000 0.000 0.000 0.000 {maya._OpenMaya.MObject_swiginit}
1 0.000 0.000 0.000 0.000 {maya._OpenMaya.MSelectionList_add}
1 0.000 0.000 0.000 0.000 {maya._OpenMaya.MSelectionList_getDependNode}
1 0.000 0.000 0.000 0.000 {maya._OpenMaya.MSelectionList_swiginit}
1 0.000 0.000 0.000 0.000 {maya._OpenMaya.new_MItDependencyGraph}
1 0.000 0.000 0.000 0.000 {maya._OpenMaya.new_MObject}
1 0.000 0.000 0.000 0.000 {maya._OpenMaya.new_MSelectionList}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}

And there we have it, we have covered OpenMaya iteration by traversing the node connections and finding the corresponding node (in this case the AnimCurve node), and a python generator to show the similarities between a loop and a generator. And yes, you can do the similar basic maya command, but it’s not as fun:

# import maya modules
from maya import cmds

def get_connected_anim(object_name=""):
    """
    get connected nodes from node provided.
    :param object_name: <str> string object to use for searching from.
    :param find_node_type: <om.MFn> kObjectName type to find.
    """
    anim_c = cmds.listConnections(object_name, s=1, d=0, type='animCurve')
    anim_b = cmds.listConnections(object_name, s=1, d=0, type='blendWeighted')
    anim_curves = []
    if not anim_c and anim_b:
        for blend_node in anim_b:
            anim_curves.extend(cmds.listConnections(blend_node, s=1, d=0, type='animCurve'))
        return anim_curves
    else:
        return anim_c

 ...
animCurveUL4
animCurveUL5
animCurveUL8
animCurveUL10
animCurveUL12

         2 function calls in 0.013 seconds
Ordered by: standard name
 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
         1    0.013    0.013    0.013    0.013 :1()
         1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects} 

Yes this seems like regular maya commands are faster, but you’ve got to remember, this is just a couple of nodes. What if you had to loop through a large set of vertices on a piece of geometry? I’ve created two functions: One loops through vertices by using standard maya cmds, the other using OpenMaya MItMeshVertex:

def get_mesh_points(object_name):
    """
    Mesh points iterator.
    :param object_name: <str> object name.
    :return: <list> vertex positions
    """
    mesh_fn, mesh_ob, mesh_dag = get_mesh_fn(object_name)
    mesh_it = om.MItMeshVertex(mesh_ob)
    mesh_vertexes = []
    print("[Number of Vertices] :: {}".format(mesh_fn.numVertices()))
    while not mesh_it.isDone():
        mesh_vertexes.append(mesh_it.position())
        mesh_it.next()
    return mesh_vertexes


def get_mesh_points_cmds(object_name):
    """
    Mesh points iterator.
    :param object_name:  <str> object name. 
    :return: <list> vertex positions 
    """
    mesh_vertices = cmds.ls(object_name + '.vtx[*]', flatten=1)
    print("[Number of Vertices] :: {}".format(len(mesh_vertices)))
    nums = []
    for i in mesh_vertices:
        nums.append(i)
    return nums

Now let’s see how cProfiler works on them both by iterating through 429510 vertices mesh:

 # run cmdsiterator 
cProfile.run("object_utils.get_mesh_points_cmds('Emmanuel_Guevarra_Ian_McKellen_medres:Group2')")
 [Number of Vertices] :: 429510
          429516 function calls in 1.629 seconds
 Ordered by: standard name
 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
         1    0.019    0.019    1.629    1.629 :1()
         1    0.038    0.038    1.609    1.609 object_utils.py:128(get_mesh_points_cmds)
         1    1.548    1.548    1.548    1.548 {built-in method ls}
         1    0.000    0.000    0.000    0.000 {len}
    429510    0.023    0.000    0.023    0.000 {method 'append' of 'list' objects}
         1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
         1    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
# run OpenMaya iterator
cProfile.run("object_utils.get_mesh_points('Emmanuel_Guevarra_Ian_McKellen_medres:Group2')")
 [Number of Vertices] :: 429510
          1718065 function calls in 0.860 seconds
 Ordered by: standard name
 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
         1    0.059    0.059    0.860    0.860 :1()
         1    0.000    0.000    0.000    0.000 OpenMaya.py:2790(init)
         1    0.000    0.000    0.000    0.000 OpenMaya.py:5304(init)
         1    0.000    0.000    0.000    0.000 OpenMaya.py:7701(init)
         1    0.000    0.000    0.000    0.000 OpenMaya.py:9666(init)
         1    0.313    0.313    0.801    0.801 object_utils.py:112(get_mesh_points)
         1    0.000    0.000    0.000    0.000 object_utils.py:142(get_mesh_fn)
         1    0.000    0.000    0.000    0.000 {isinstance}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MDagPath_extendToShapeDirectlyBelow}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MDagPath_node}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MDagPath_swiginit}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MFnMesh_numVertices}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MFnMesh_swiginit}
    429511    0.030    0.000    0.030    0.000 {maya._OpenMaya.MItMeshVertex_isDone}
    429510    0.035    0.000    0.035    0.000 {maya._OpenMaya.MItMeshVertex_next}
    429510    0.393    0.000    0.393    0.000 {maya._OpenMaya.MItMeshVertex_position}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MItMeshVertex_swiginit}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MObject_hasFn}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MSelectionList_add}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MSelectionList_getDagPath}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.MSelectionList_swiginit}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.new_MDagPath}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.new_MFnMesh}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.new_MItMeshVertex}
         1    0.000    0.000    0.000    0.000 {maya._OpenMaya.new_MSelectionList}
    429510    0.030    0.000    0.030    0.000 {method 'append' of 'list' objects}
         1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
         1    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}

for cmds and OpenMaya, 1.629 seconds and 0.860 seconds respectively.