Search

Week 26: How to Register "Repeat Last Command" and Orient To Face, Edge, or Vertex Normal in Maya

Week 26! That means there's been half of a year of active development! About half of it has been game development and the other half has been rigging/pipeline development with a bit of concept, modeling, and texturing tossed in there too. To celebrate this milestone, I'll share a few scripts on how to orient a transform to a face, edge, or vertex normal in Maya.

 

First, let's talk about the repeat last command in Maya. Every time an action is performed in Maya, it gets registered to the "repeat last" which lets you press "G" to repeat the last action you performed. Repeat last is a pretty hand feature and I never thought I used it much until I needed to do something and my action wasn't registered.


To register an action, we need a function. In this example, we'll be using the polyCube function, it simply makes a cube in the scene. We'll also need to know the function's module and name since the repeatLast command uses string formatting to register actions. Below we store the polyCube function into a function variable.

import pymel.core as pm

function = pm.polyCube

Note that pm.polyCube does not include the parenthesis at the end "()". Including the parenthesis would execute the function, instead we want to store the function at the variable called "function"

Now that we have the function stored, we can get some information about the function, such as which module it comes from, and its name as a string. To get the following we'll use the special variables "__module__" and "__name__".

module = function.__module__
function_name = function.__name__
full_function = module + '.' + function_name 

The repeatLast function takes in a MEL string, so in order to format to python, we'll use the module name and function names.

pm.repeatLast(addCommand='python("import {}; {}()")'.format(module, full_function), addCommandLabel=full_function)

Essentially, the command we register is:

import pymel.core.modeling; pymel.core.modeling.polyCube()

When we put it all together, we get the following:

 

Orienting a transform to a component's normal requires a bit of vector math and matrix composition. Once a transform is oriented to a component's normal, such as a face normal, orienting to another component's normal, such as an edge or vertex normal, is easy since the logic is pretty similar.

To orient, we'll compose a matrix. To compose a matrix we need three orthogonal direction vectors (the X, Y, Z axis of the transform) and its position vector. We'll start with the easy one, the position:

position = pm.xform(transform, q=True, ws=True, t=True)

Note, everything will be in world space, since its an easy consistent space to do all our calculations. Now we need a forward, up, and right vector. The forward is easy, that will be our normal vector. Pymel has the wonderful function on components called "getNormal".

forward = face.getNormal(space='world')

To figure out the up vector, we'll use the direction from the face to a random edge of the face. To get one of the face's edge, we get all of its edges, and use one of their edge indices to figure out the full name.

edges = face.getEdges()
edge = pm.PyNode('{}.e[{}]'.format(face.node().name(), str(edges[0])))

To get the edge's and face's position, we select the component and get the position of the move in world space.

pm.select(edge)
pm.setToolTo('Move')
edge_position = pm.dt.Vector(pm.manipMoveContext('Move', q=1, p=1))

Now that we have the edge's and face's position, we can figure out the direction with vector math. The direction is found by subtracting the face position from the edge position and normalizing the result.

up = (edge_position - face_position).normal()

We're finished with the tough part! Figuring out the right vector is easy because we already have two vectors, all we have to do is cross product.

right = forward.cross(up).normal()

After we have the right vector, we must correct the up vector to make sure it is fully orthogonal by doing another cross product.

up = forward.cross(right).normal()

Lastly, we compose the matrix. Note the order in which we place the vectors into the matrix list matters! Currently, the Z-axis will be facing forward (parallel to the component's normal).

matrix = pm.dt.Matrix(*[right.get(), up.get(), forward.get(), position])

And now that we have the matrix with the correct orientation, we can use the xform function to set the world matrix of our transform

pm.xform(transform, ws=True, m=matrix)
 

Below are functions for orienting a transform to a face, edge, or vertex normal.


So you may be wondering, wth is orienting a transform for a component useful for? Personally, I use it for orienting joints, but I could see it being applied in modeling as well. Below are the results for joints being created and oriented on all of a cube's components.

 

PS: This week I also got the animation exporter stood up with clip data. So several animations can be exported from one Maya file using the Clipper GUI. The Clipper can easily be expanded in the future to include more information than the current "start", and "end" frames, such as "hit" frame(s) or "attack" frame(s).