Instancing Objects With Particles [maya, python]

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!

leaf_algebra

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'

leaf_code2a

#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.

  1. How many verts are there?
  2. How many faces are there?
  3. What are the vertex positions?
  4. How many verts does each face have?
  5. 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[0].__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).

leaf_code2b

#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() + [1] 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 ''

leaf_code2c

#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

Conclusion:

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!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s