Wednesday 21 December 2016

Matrix fun!

Here's a few snippets I wrote when learning how to interact with matrices via Maya's Python API.

Note: You can get the local and world matrices directly from attributes on a dag node. You can also use the xform command.

In the example image, pCube1 is a child of pSphere1. If the position of both objects in the scene are random, we can find the local and world matrices for pCube1 and the world-space and local-space positions (translate, rotate, scale) of pCube1 with the example bits of code below. The channel box will only give us local or relative values to the objects parent.




Finding a world matrix from a local matrix


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import maya.OpenMaya as om
import math

transformFn = om.MFnTransform() # Create empty transform function set

mSel = om.MSelectionList()
mSel.add('pSphere1')
mSel.add('pCube1')

dagPath = om.MDagPath()

mSel.getDagPath(0, dagPath) # Get the dagPath for pSphere1
transformFn.setObject(dagPath) # Set the function set to operate on pSphere1 dagPath
transformationMatrix = transformFn.transformation() # Get a MTransformationMatrix object
matrixA = transformationMatrix.asMatrix() # Get a MMatrix object representing to local matrix of pSphere1

mSel.getDagPath(1, dagPath)
transformFn.setObject(dagPath)
transformationMatrix = transformFn.transformation()
matrixB = transformationMatrix.asMatrix() # Get a MMatrix object representing to local matrix of pCube1

# Multiply local matrices up from child to parent
worldMatrixB = matrixB * matrixA

translation = om.MTransformationMatrix(worldMatrixB).getTranslation(om.MSpace.kWorld) # <-- Not sure on this parameter. All I care about is the 4th row in the matrix.
rotation = om.MTransformationMatrix(worldMatrixB).eulerRotation()

print '\n'
print 'pCube1.translate ->', translation.x, translation.y, translation.z
print 'pCube1.rotate ->', math.degrees(rotation.x), math.degrees(rotation.y), math.degrees(rotation.z)

We could just call the inclusiveMatrix() method on a dagPath object to retrieve the world matrix (as is done below) but lets just say that for umm...some unknown reason you only had the local matrix to work with.


Finding a local matrix from a world matrix


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import maya.OpenMaya as om
import math

transformFn = om.MFnTransform() # Create empty transform function set

mSel = om.MSelectionList()
mSel.add('pSphere1')
mSel.add('pCube1')

dagPath = om.MDagPath()

mSel.getDagPath(0, dagPath) # get dagPath for pSphere1
worldMatrixInverseA = dagPath.inclusiveMatrixInverse() # get the world inverse matrix for pCube1 parent (pSphere1)

mSel.getDagPath(1, dagPath)
worldMatrixB = dagPath.inclusiveMatrix() # Get the world matrix for pCube1

localMatrixB = worldMatrixB * worldMatrixInverseA # Multiply pCube1's world matrix by it's parents world inverse matrix

translation = om.MTransformationMatrix(localMatrixB).getTranslation(om.MSpace.kWorld) # <-- Not sure on this parameter. All I care about is the 4th row in the matrix.
rotation = om.MTransformationMatrix(localMatrixB).eulerRotation()

print '\n'
print 'pCube1.translate ->', translation.x, translation.y, translation.z
print 'pCube1.rotate ->', math.degrees(rotation.x), math.degrees(rotation.y), math.degrees(rotation.z)


Thursday 15 December 2016

Bounding Box fun!

I was recently working with a colleague on a custom deformer which required access to bounding box information for input meshes. I looked into different ways of getting that data via the API and we ended up trying a few different methods which I thought might be worth sharing.

Since I only had access to kMeshData in the deformer, I couldn't create a dagPath (will talk a bit more about that later) but in terms of writing a utility script or testing the examples in the script editor, these will work just fine.

Method 1 - Using the boundingBox method of a MFnDagNode function set


So both a transform and a mesh have bounding box information. I suppose it makes sense..but I've never really thought about it.

The bounding box of a transform relates to its children, and the bounding box of the mesh relates to its components. Since a mesh needs a transform in order to live in the scene, it can be confusing which one relates to what exactly but all we need to really know is that the values displayed in the attribute editor are in object space.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import maya.cmds as cmds
import maya.OpenMaya as om

transform = 'pCube1'

mSel = om.MSelectionList()
dagPath = om.MDagPath()
mSel.add( transform )
mSel.getDagPath(0, dagPath)

dagFn = om.MFnDagNode(dagPath)

# Returns the bounding box for the dag node in object space.
boundingBox = dagFn.boundingBox()

# There's a few useful methods available, including min and max
# which will return the min/max values represented in the attribute editor.

min = boundingBox.min()
max = boundingBox.max()


Since the mesh is a child of the transform, the bounding box co-ordinates represented on the transform will inform us of the bounding box of the mesh.
However, as I mentioned above, this will be in object space. So if the transform is parented to another transform and the object is arbitrarily positioned in the world, these values may not be of much use depending on the circumstance.

Group the polyCube, then move that group somewhere in the world. If you check the min/max values you will notice they don't change.


Method 2 - Building a bounding box from points


We can build a bounding box using the expand method and the points of the mesh. All we have to do is find the positions of each vertex in world space and add them to the bounding box object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Using the dagPath from previous example, lets create a vertex iterator.
iter = om.MItMeshVertex( dagPath )
# Note: The iterator is clever enough to know we want to work on the mesh, 
# even though we are passing in a dagPath for a transform

boundingBox = om.MBoundingBox()
while not iter.isDone():
    
    # Get the position of the current vertex (in world space)
    position = iter.position(om.MSpace.kWorld) 
    boundingBox.expand(position) # Expand the bounding box
    iter.next()

min = boundingBox.min()
max = boundingBox.max()

If we again group the polyCube and move the group somewhere in the world, this time we will have the world space co-ordinates for the min and max.


Method 3 - Creating world-space bounding box using min/max point matrix multiplication


Using the expand method may be quite costly on a mesh with lots of vertices so here's a quicker way of doing it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
dagPath.extendToShape() # Set the dagPath object to the mesh itself
dagFn.setObject(dagPath) # Update the dag function set with the mesh object
boundingBox = dagFn.boundingBox() # Grab the bounding box (object space)

min = boundingBox.min() * dagPath.inclusiveMatrix()
max = boundingBox.max() * dagPath.inclusiveMatrix()

# Create new bounding box, initializing with new world-space points, min and max
boundingBox = om.MBoundingBox( min, max )

min = boundingBox.min()
max = boundingBox.max()

The important part is on line 5 and line 6 where we multiply the object-space bounding box min and max points by it's world matrix (inclusive) which transforms the points to world-space.

Method 4 - transformUsing method of a MBoundingBox object


We can reduce the amount of steps further by making use of the transformUsing method.  It takes a MMatrix and transforms the points for us.

1
2
3
4
5
boundingBox = dagFn.boundingBox()
boundingBox.transformUsing( dagPath.inclusiveMatrix() )

min = boundingBox.min()
max = boundingBox.max()





Bonus - Testing intersection


We can use the intersects method to check for collision with another bounding box.

1
2
3
4
5
6
7
8
9
anotherTransform = 'pCube2'
mSel.add( anotherTransform )
mSel.getDagPath(1, dagPath)
dagFn.setObject( dagPath )

anotherBoundingBox = dagFn.boundingBox()

# Returns a true or false
print boundingBox.intersects( anotherBoundingBox )


To make sure the intersect results are correct, you would need to make sure you build a world space bounding box object as well..

I'll leave that up to you :)