[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] :: MVector

I love math. Everything in life can change — your interests, your job, outside influences, but not math. Math never changes and I love about that very much.

Today, let’s go over why Maya’s MVector class object is so much fun: it’s a point in space (with a direction); we can add, subtract and multiply it against another MVector or a scalar value.

Right now, let’s deal with multiplying MVectors against scalar values.

Here we have two locators in space. Let’s have some fun with these two locators. First we will collect and manipulate information about these vectors using some Maya Python scripting. First, let’s show some code:

from maya.OpenMaya import MVector
from maya import cmds

class Vector(MVector):
    RESULT = ()

    def __init__(self, *args):
        super(Vector, self).__init__(*args)

    def do_division(self, amount=2.0):
        """
        divide the vector into sections.
        :param amount: <int> divide the vector by scalar amount.
        :return: <tuple> section vector.
        """
        self.RESULT = self.x / amount, self.y / amount, self.z / amount,
        return self.RESULT

    def do_multiply(self, amount=2.0):
        """
        multiply the vector by the amount.
        :param amount: <int> multiply the vector by scalar amount.
        :return: <tuple> section vector.
        """
        self.RESULT = self.x * amount, self.y * amount, self.z * amount,
        return self.RESULT

    def get_position(self):
        self.RESULT = self.x, self.y, self.z,
        return self.RESULT

    @property
    def result(self):
        return self.RESULT

    @property
    def position(self):
        return self.get_position()

def get_vector_position_2_points(position_1, position_2, divisions=2.0):
    """
    calculates the world space vector between the two positions.
    :param position_1: <tuple> list vector
    :param position_2: <tuple> list vector
    :param divisions: <int> calculate the vector by divisions.
    :return: <tuple> vector
    """
    positions = ()
    for i in xrange(1, divisions):
        vec_1 = Vector(*position_1)
        vec_2 = Vector(*position_2)
        new_vec = Vector(vec_1 - vec_2)
        div_vec = Vector(new_vec * (float(i) / float(divisions)))
        result_vec = Vector(*div_vec.position)
        positions += Vector(result_vec + vec_2).position,
    return positions

def get_vector_positon_2_objects(object_1, object_2, divisions=2):
    """
    calculates the world space vector between the two points.
    :return: <tuple> vector positions.
    """
    vector_1 = cmds.xform(object_1, ws=1, t=1)
    vector_2 = cmds.xform(object_2, ws=1, t=1) 
    return get_vector_position_2_points(vector_1, vector_2, divisions)

So this is a module I created for getting point positions between the two vectors. So let’s go through this step by step, in the get_vector_position_2_points function. Ignoring everything else but the math:

1.) we define the two vector positions.

2.) we subtract the first vector from the second to create a third vector at the origin.

3.) we loop through the number of divisions, dividing each number by the total number of divisions to give us the fraction that we can use to multiply with. (1/4, 2/4, 3/4, 4/4)

4.) we add the resultant origin vector by the second vector to place it relative to the second vector’s position.

5.) finally, we use this vector point to place our locators using the code below:

We are going to divide the space between the locators into 4 sections (divisions = 4). Let’s go into Maya and load up the script editor and paste this code there:

from maya_utils import math_utils
import maya.cmds as cmds
reload(math_utils)

positions = math_utils.get_vector_positon_2_objects('locator1', 'locator2', divisions=4)

for v_pos in positions:
    locator = cmds.createNode('locator')
    cmds.xform(object_utils.get_parent_name(locator), t=v_pos)

As we can see, between the locators, we have created four equal divisions, and have created the locators with the calculated positional vectors between the two original locators. This is useful in many of my rigging work, like creating springs, wires and folding wings.

It is important to be precise when creating any useful tool. So that we can eliminate any uncertainty in our work.