I’m putting this in the ‘solved’ category, but here’s one last update on the matrices problem I’ve been tackling! It’s been a great learning tool, as there are a million ways to do it wrong, but they all involve learning how to write better Python, or go deeper into the Maya API.
Here’s a pic of one of the textbook examples that helped get me started with the matrix stuff. My eyes lit up when I saw the words “vertices” and… Actually, no other words excited me. You can see the large letter N on the left though, scaled/sheared a bit. That’s the stuff we’re doing in Maya!
Alright, onto the code!
#1 Import Your Business Most of this script is pure Python/PyMel, and everything else that’s been imported is for a specific use. OpenMaya is for the matrix math, because it was much faster than the PyMel counterpart when there are 1000’s of matrix operations. Maya’s particles are accessed through OpenMayaFX, and the rest of the imports are all one line uses; picking some ‘random’ verts, accessing the constant Pi, and timing the whole script with DateTime.
1 import maya.OpenMaya as om 2 import maya.OpenMayaFX as omfx 3 import pymel.core as pm 4 import random 5 import math 6 from datetime import datetime
#8 Stay Classy This first class is basically the whole script. We open with the doc string, then declare some class variables that any instance of this class would have access to, and announce that they’re to be treated as constant (why they’re placed in that location and sans ‘self.’, and why they’re in ALL_CAPS, respectively). In other languages, these are called static constant variables.
8 class ShellsToInstances(): 9 """Converts the shells of a mesh to nParticle instance objects.""" 10 MAX_INVERSE_TRIES = 20 11 INSTANCE_NAME = 'base_instance' 12 ORIG_MESH_NAME = 'original_mesh'
#14 Instant Variables Lines 16-19 aren’t necessary for the code to run, and could’ve been omitted, but these are the instance variables that the whole class will have access to, and I’m ‘announcing’ them here just so everyone knows what they are.
14 def __init__(self): 15 16 self.large_mesh = None # MeshInfo class object 17 self.instance_mesh = None # MeshInfo class object 18 self.shell_transform_matrices =  # MTransformationMatrix list 19 self.verts_4 =  # Four ints; the 4 vert indices for matrix stuff. 20 self.DoIt()
- ‘large_mesh’ is the mesh we’re acting on,
- ‘instance_mesh’ is the shell/shape we’ll be instancing around with nParticles.
- The third variable is going to be a GIGANTIC LIST of transform matrices that we’ll use to position/orient the particle instance objects.
- ‘verts_4’, is just a short list of the vertex indices that we’ll use across all the shells.
- Line 20 is where we actually set things into motion!
#22 Do It The class method “DoIt”, called in the last line of __init__, calls every other method and basically runs the script. I’ve been trying speed this thing up, so it opens with a stopwatch to start the clock on line 24. If everything goes smoothly, the clock stops on line 34, and the results are printed. This is a quick ‘n dirty way to test speed, and isn’t a replacement for cProfile (which has been a trick to garner useful info from for me).
22 def DoIt(self): 23 24 time_start = datetime.now() 25 26 try: 27 self.large_mesh = MeshInfo(self._FindMeshToActOn()) 28 self.instance_mesh = MeshInfo(self._CreateBaseInstance(self.large_mesh)) 29 base_inverse_matrix, self.verts_4 = self._FindInverseMatrixOfShape(self.instance_mesh.shape) 30 self.shell_transform_matrices = self._FindTransformMatricesOfShells(base_inverse_matrix, self.large_mesh, self.verts_4) 31 self._CreateInstanceShells() 32 self._CleanUp() 33 34 time_end = datetime.now() 35 print '\n#---------------------------------------------------#' 36 print 'total time: ' + str(time_end - time_start) 37 38 except Exception, e: 39 print e
- #27 Get The Big Mesh Now we’re actually doing some work! First thing, we’re finding the largest mesh in the scene to act on. If we find it, we return its shape, then cast/instantiate to a helper struct called MeshInfo, that reads things like # of vertices, shells, and some other info we’ll want later on. That class/struct is at the bottom of the script if you want to take a look. It almost isn’t necessary, but did reduce some duplicate code.
- #28 Duplicate A Shell Using info from the first shell of the mesh we just found, create a new object, move it to the origin, then cast it to another MeshInfo object.
- #29 Find Neo Using the mesh we just created, create a data matrix of 4 vertex positions (x,y,z). But! Make sure the resulting matrix is invertible so we can use it later for some math. Return that inverse matrix, and the four verts we used.
- #30 Giant Loop Using the inverse matrix we just found, use it to compare the postion/orientation of the base object to that of every shell in the large_mesh. If we treat that base mesh as ‘zeroed out’, the resulting matrices we find will represent how each shells is positioned/rotated relative to it.
- #31 Create ‘Em Now that we know the transforms of each shell in the large_mesh, we can create the instance objects (with nParticle object instancing), and transform them so they ‘replace’ each original shell.
- #32 Clean We’ve done the work, now just clean the scene up a bit.
#49 Except… A few of the methods are peppered with try/except/raise statements. The ‘raise’ keyword is a great way to send a signal that things aren’t going well, and to abort the current operation. Instead of returning a value at the end of a function, an exception is ‘raised’ up to be caught and dealt with. I’m using them here to deal with the scene not having an appropriate mesh to act on. You can see in the DoIt method that all the exceptions are pushed up into that try/except block. I really like this way of doing things, as you don’t have to adjust your return statements to communicate that something’s wrong, .i.e. return a_value, a_message_indicating_success_or_failure. I could have returned ‘None’ instead of raising an exception, which would make this method more versatile, but less… fun?
42 def _FindMeshToActOn(self): 43 44 pm.select(clear = True) 45 geos = pm.ls(type = 'mesh') 46 try: 47 mesh_shape = geos[-1] 48 except: 49 raise Exception('''Can't find a mesh to act on...''') 50 51 for geo in geos: 52 if geo.numVertices() > mesh_shape.numVertices(): 53 mesh_shape = geo 54 mesh_info_obj = MeshInfo(mesh_shape) 55 if mesh_info_obj.num_shells < 2: 56 raise Exception('''This mesh doesn't have enough shells...''') 57 58 return mesh_shape
#60 Make The Base If we found a good mesh to act on in the previous method, now we can duplicate one of its shells as a new object, then move it to the origin. (This new object is the shape we’ll be instancing around). There are a million ways to do this, with basic Maya commands like polySeparate and maybe polyExtract. But, I’m not using those! They’re too easy! Aaaand, one requirement of this problem is that original mesh can’t be modified, which all of those commands will do. So what’s the solution? How do you duplicate one shell of the large mesh, without actually affecting it? It turns out you can read all of its mesh data, then use it to create a new object from scratch with some cumbersome OpenMaya commands.
There are just 5 things we need to create a mesh from scratch, and that’s what this method is about.
- How many verts are there?
- How many faces are there?
- What are the vertex positions?
- How many verts does each face have?
- Which faces have which verts?
Once you gather all that info, you can call one command to create a new mesh from scratch, as on line 87. This method uses some shortcuts for interacting with the Maya API. The PyMel command to get MFn and MIt objects are much shorter ways to go! Have a mesh? A face? Just call __apimobject__, __apimit__, or __apimfn__ as needed!
60 def _CreateBaseInstance(self, large_mesh): 61 62 mesh_mobj = large_mesh.shape.__apimobject__() 63 mesh_fn = om.MFnMesh(mesh_mobj) 64 65 face_iter = large_mesh.shape.f.__apimit__() 66 face_iter.reset(mesh_mobj) 67 68 face_vert_counts = om.MIntArray() 69 face_verts = om.MIntArray() 70 while (face_iter.index() < large_mesh.faces_per_shell) and not face_iter.isDone(): 71 face_vert_counts.append(face_iter.polygonVertexCount()) 72 vert_indicies_on_one_face = om.MIntArray() 73 face_iter.getVertices(vert_indicies_on_one_face) 74 for i in range(vert_indicies_on_one_face.length()): 75 face_verts.append(vert_indicies_on_one_face[i]) 76 face_iter.next() 77 78 position_array = om.MFloatPointArray() 79 for i in range(large_mesh.verts_per_shell): 80 temp_point = om.MPoint() 81 temp_float_point = om.MFloatPoint() 82 mesh_fn.getPoint(i, temp_point) 83 temp_float_point.setCast(temp_point) 84 position_array.append(temp_float_point) 85 86 mesh2_fn = om.MFnMesh() 87 mesh2_trans = pm.PyNode(mesh2_fn.create(large_mesh.verts_per_shell, 88 large_mesh.faces_per_shell, 89 position_array, 90 face_vert_counts, 91 face_verts)) 92 pm.xform(mesh2_trans, cp = True) 93 pm.move(0,0,0, mesh2_trans, rpr = True) 94 pm.makeIdentity(mesh2_trans, apply = True) 95 96 return mesh2_trans.getShape()
#98 Find the Matrix I went over this method in more detail in the previous post, so check that out. I added a few try/except/raise statements though, and they’re used for two different purposes here: There’s a problem and the program should gracefully stop or, There is no problem, but I need to call a method that will probably throw an error until I adjust its input correctly, or give up trying (line 124).
#134 Find All The Matrix I think this is the slowest method in the script, purely because of the matrix stuff. Here we’re finding the transformation matrices of each shell in the large_mesh. After testing a bit, I found the PyMel matrix stuff was pretty slow compared to OpenMaya, so the method has been updated to reflect that. I switch between MMatrix and MTransformationMatrix because they each have different methods that are necessary. MMatrix has a transpose() method and supports multiplication, while MTransformationMatrix doesn’t.
134 def _FindTransformMatricesOfShells(self, vert_matrix_orig_inverse, large_mesh, verts_4): 135 136 shell_ids = range(0, large_mesh.num_verts, large_mesh.verts_per_shell) 137 138 trans_matrices = [None]*len(shell_ids) 139 all_positions = large_mesh.shape.getPoints(space = 'world') 140 141 for i, shell_id in enumerate(shell_ids): 142 143 matrix_list = [all_positions[shell_id + verts_4[j]].tolist() +  for j in range(4)] 144 matrix_list = [item for sublist in matrix_list for item in sublist] 145 vert_matrix_new = om.MMatrix() 146 om.MScriptUtil_createMatrixFromList(matrix_list, vert_matrix_new) 147 vert_matrix_new = vert_matrix_new.transpose() 148 149 trans_matrix = vert_matrix_new * vert_matrix_orig_inverse 150 trans_matrices[i] = om.MTransformationMatrix(trans_matrix.transpose()) 151 152 return trans_matrices
#138 Big List Since the list of matrices is going to be HUGE, I want to avoid changing/interacting with the list as much as possible, so I declare its initial size here, and just populate it with Nones.
#154 nParticles!! Here’s where we actually create and postion/rotate the instance objects! Why use particles, to that end? Good question! It turns out you can affect the position/rotation of each instance object in just ONE CALL with particles. This is much better than my previous approach of looping through each object and explicitly setting their transforms. The only catch, obviously, is that you need to compile all that transform data into a usable format. That’s why there’s so much list comprehension here and elsewhere in the script – just moving the same numbers around so they’ll play nice with the methods we need to use.
154 def _CreateInstanceShells(self): 155 156 # p_ means particle. The variable names are so long! 157 p_translates = [self.shell_transform_matrices[i].getTranslation(om.MSpace.kWorld) for i in range(len(self.shell_transform_matrices))] 158 p_translates_simple = [(i.x, i.y, i.z) for i in p_translates] 159 160 161 nparticles_transform, nparticles_shape = pm.nParticle(position = p_translates_simple) 162 p_instancer = pm.PyNode(pm.particleInstancer( 163 nparticles_shape, addObject=True, object=self.instance_mesh.shape, 164 cycle='None', cycleStep=1, cycleStepUnits='Frames', 165 levelOfDetail='Geometry', rotationUnits='Degrees', 166 rotationOrder='XYZ', position='worldPosition', age='age')) 167 168 pm.setAttr('nucleus1.gravity', 0.0) 169 nparticles_shape.computeRotation.set(True) 170 pm.addAttr(nparticles_shape, ln = 'rotationPP', dt = 'vectorArray') 171 pm.addAttr(nparticles_shape, ln = 'rotationPP0', dt = 'vectorArray') 172 pm.particleInstancer(nparticles_shape, name = p_instancer, edit = True, rotation = "rotationPP") 173 174 p_rotates = [self.shell_transform_matrices[i].rotation().asEulerRotation() for i in range(len(self.shell_transform_matrices))] 175 p_rotates_simple = [[i.x/(math.pi) *180, i.y/(math.pi) *180, i.z/(math.pi) *180] for i in p_rotates] 176 p_rotates_simple = [item for sublist in p_rotates_simple for item in sublist] 177 178 vector_array = om.MVectorArray() 179 for i in range(0, len(p_rotates_simple), 3): 180 vector_array.append(om.MVector(p_rotates_simple[i], p_rotates_simple[i+1], p_rotates_simple[i+2])) 181 182 particle_fn = omfx.MFnParticleSystem(nparticles_shape.__apimobject__()) 183 particle_fn.setPerParticleAttribute('rotationPP', vector_array) 184 particle_fn.setPerParticleAttribute('rotationPP0', vector_array)
#161 LOOK! Look how easy it is to create a bajillion particles, in the exact locations you want! One command, and the only argument is the list of positions! You don’t even need to say how many particles – it does that for you with the positions.
pm.nParticle(position = p_translates_simple)
#162 Object Instancing Here, in one command, we’re placing a copy of that mesh we created earlier at every particle location.
pm.particleInstancer(nparticles_shape, addObject=True, object=self.instance_mesh.shape...
#169 Rotation Harder The rest of the method is devoted to getting the particles oriented correctly. It’s a bit tricky, as they need new attributes for their starting rotation (rotationPP0), and general rotation (rotationPP). You can get away with only one of these attributes, but things will go pear-shaped if you scrub the timeline at all.
#186 Almost Done The real work is complete, now we just need to clean things up a bit, and as a final gesture, select the four verts from the instance object we created early. Because it’s been instanced around with our particle commands above, you should see those same verts highlighted on EVERY INSTANCE OBJECT. Pretty sweet, huh?
#196 __repr__ I overwrote the default __repr__ method, only because I didn’t like the memory address of the script object being printed out every time it’s run. Maybe I could make it something cooler.
def __repr__(self): return ''
#201 MeshInfo Lastly, here’s that helper struct I mentioned earlier. Not totally necessary, but actually made things easier/cleaner overall.
201 class MeshInfo(): 202 """Simple struct to hold relevant mesh data.""" 203 def __init__(self, shape = None): 204 self.shape = shape 205 self.trans = None 206 self.num_shells = None 207 self.num_verts = None 208 self.verts_per_shell = None 209 self.faces_per_shell = None 210 211 self.populate_info() 212 213 def populate_info(self): 214 if self.shape is None: 215 return 216 self.trans = self.shape.getParent() 217 self.num_shells = pm.polyEvaluate(self.shape, shell = True) 218 self.num_verts = self.shape.numVertices() 219 num_faces = self.shape.numFaces() 220 self.verts_per_shell = self.num_verts / self.num_shells 221 self.faces_per_shell = num_faces / self.num_shells
That’s it! I’m done with this! It’s still not as fast as I want, and I think could take about 15 minutes to run on a million objects, but it’s been a fun experiment to play with. I swear I’m done!