We’re pleased to announce our first on demand class with Rigging Dojo – Intro to Metadata. This is our first class of this type in general and we hope folks find it helpful. Click on the pic above or here….
This class was created with two purposes in mind:
- To share some of the many lessons learned over the past several years working with red9’s great code base
- To provide a basic foundation of knowledge for those wanting to delve into Morpheus Rig 2’s continued development.
Some might wonder what reason you might want to use red9’s code base or what benefits in particular you might find. The easiest way to give a quick example would be to provide a code example of a typical rigging task but with and without meta. Let’s look at something one does pretty regularly while rigging – do some stuff on a given joint chain.
Note — this exercise was painful to write as I’d forgotten most of the standard calls and ways to do stuff as so much is just built in now…
First, open up maya and make an amazing joint chain. If it’s not amazing, that’s okay – start over and do it again.
Here’s some standard code based on a selected joint chain:
def jointStuff_standard(): l_joints = mc.ls(sl = 1) for jnt in l_joints:#Validation loop before doing stuff... if not mc.objectType(jnt) == 'joint': raise ValueError,"Not a joint: {0}".format(jnt) for i,jnt in enumerate(l_joints): #First we're gonna create a curve at each joint. Name, parent and snap it ... jnt = mc.rename(jnt,"ourChain_{0}_jnt".format(i))#...gotta catch stuff when you rename it str_crv = mc.circle(normal = [1,0,0], ch = 0)[0] str_crv = mc.parent(str_crv,jnt)[0]#...gotta catch stuff when you parent it str_crv = mc.rename(str_crv, '{0}_crv'.format(jnt))#...everytime it changes mc.delete(mc.parentConstraint(jnt, str_crv, maintainOffset = False)) #Now we wanna add a locator at each joint - matching, position,orientation,rotation order loc = mc.spaceLocator(n = "{0}_loc".format(jnt))[0] ro = mc.getAttr('{0}.rotateOrder'.format(jnt)) mc.setAttr('{0}.rotateOrder'.format(loc),ro) mc.delete(mc.parentConstraint(jnt, loc, maintainOffset = False)) #Now if we wanna store data on each object one to another... mc.addAttr (jnt, ln='curveObject', at= 'message') mc.connectAttr ((str_crv+".message"),(jnt+'.curveObject')) mc.addAttr (str_crv, ln='targetJoint', at= 'message') mc.connectAttr ((jnt+".message"),(str_crv+'.targetJoint')) mc.addAttr (loc, ln='source', at= 'message') mc.connectAttr ((jnt+".message"),(loc+'.source')) #...the contains none of the built in checking and verifying built in to metaData and if you tried to #...run this on message attributes that existed or were locked or 15 other scenerios, it would fail
Here’s meta code. Simpler. Clearer. Much faster to write.
def jointStuff_meta(): ml_joints = cgmMeta.validateObjListArg(mc.ls(sl=1), mayaType = 'joint')#...gets us a validated meta data list of our selection for i,mJnt in enumerate(ml_joints): mJnt.rename("ourChain_{0}_jnt".format(i)) mi_crv = r9Meta.MetaClass( mc.circle(normal = [1,0,0], ch = 0)[0]) mc.parent(mi_crv.mNode, mJnt.mNode) mi_crv.rename('{0}_crv'.format(mJnt.p_nameBase))#...p_nameBase property cgmMeta only mc.delete(mc.parentConstraint(mJnt.mNode, mi_crv.mNode, maintainOffset = False)) #...same data storage mJnt.connectChildNode(mi_crv,'curveObject','targetJoint') mJnt.doLoc()#..doLoc cgmMeta only
If this looks like something you’d like to delve into, check out the class. I wish there was a class like this out there when I started with the meta stuff 4 years ago. Hope you find it helpful:)
j@cgm
As I was prepping Morpheus Rig for public dev release I found some pretty awful slowdowns in our code base. As I’m also working on an Intro to Meta course for Rigging Dojo, it seemed like a good time to resolve some of those issues.
So that was most of this week.
Before digging in, a little foundation. Our code base is a meta data system that relies heavily on red9’s MetaClass and caching in order to function. So when I dug into issues I needed to find if they were on our end or optimizations that could happen in red9 itself.
How does one start to analyze where the slow downs are and fixing them? I’m sure there are more intelligent and efficient ways but being mostly a self taught coder I decided to lean on my junior high science lesson of using the scientific method – namely devising questions and seeking to answer them with simple direct tests. So to start I came up with some questions I wanted to answer.
General:
- Does scene size have an effect on certain calls?
- Does cache size have an effect?
- Are there things that when iterated on increase the speed at which the next exact same call happen?
- Are there ways to make failed metaclass nodes fail sooner, with fewer errors and clearer ones to boot?
Process
- Unit tests in our code base made speed checking and function breaking much easier than not having that
- Simple setup for iteration tests where I could easily change what was being called and then being able to check speed differentials between functions based on a given scene size of objects or iterating new objects every round
Here’s a sample test call (warning – it’s a bit messy):
def speedTest_substantiation(*args, **kws): """ Test for seeing how substantiation speeds are affected """ _d_build = {'network':'network'} class fncWrap(cgmGeneral.cgmFuncCls): def __init__(self,*args, **kws): super(fncWrap, self).__init__(*args, **kws) self._str_funcName = 'speedTest_substantiation' self._b_reportTimes = 1 #..we always want this on so we're gonna set it on self._b_autoProgressBar = True self._l_ARGS_KWS_DEFAULTS = [{'kw':'targetCount',"default":10,"argType":'int','help':"How many objects to create"}, {'kw':'build',"default":'network',"argType":'string','help':"What kind of base node to build to test"}, ] self.__dataBind__(*args, **kws) #Now we're gonna register some steps for our function... self.l_funcSteps = [{'step':'Validating Args','call':self._validate_}, {'step':'Build stuffs','call':self._buildStuff_}, {'step':'Iterate','call':self._iterate_}, {'step':'Report','call':self._reportHowMayaIsStupid_}] def _validate_(self): mc.file(new=True,f=True) #self.int_iterations = int(cgmValid.valueArg(self.d_kws['iterations'],noneValid=False)) self.int_targetCount = int(cgmValid.valueArg(self.d_kws['targetCount'],noneValid=False)) self.str_nodeType = self.d_kws['build'] #self.l_valueBuffer = [i for i in range(self.int_iterations)] #self.log_debug("Debug in _validate_") #For each of our test values, we're gonna create a transform and store it #self.md_rootToChildren = {} self.l_times_1 = [] self.l_times_2 = [] self.l_times_3 = [] self.l_times_4 = [] self.l_roots_1 = [] self.l_roots_2 = [] self.l_roots_3 = [] self.l_roots_4 = [] self.l_objects = [] def test1_func(self,string): return r9Meta.MetaClass(string) def test2_func(self,string): #return cgmMeta_optimize.cgmNode2(string) #return cgmMeta.cgmNode(string) #return cgmMeta.validateObjArgOLD(string,'cgmObject',setClass = True) return cgmMeta.cgmNode(string) def test22_func(self,string): pass #return cgmMeta.cgmObject(string) def call2_func(self):pass def test3_func(self,string): #return string #cgmMeta_optimize #return string #return cgmMeta.cgmObject(string) #return cgmMeta_optimize.cgmNode2(string) return cgmMeta.validateObjArg(string,'cgmNode',setClass = False) #return cgmMeta.validateObjArg(string) def _buildStuff_(self): for i in range(self.int_targetCount): self.progressBar_set(status = ("Creating obj %i"%i), progress = i, maxValue = self.int_targetCount) #self.l_objects.append(mc.createNode( self.str_nodeType, n = "obj_{0}".format(i) )) _jnt = mc.joint(n = "obj_{0}".format(i) ) mc.select(cl=True) self.l_objects.append(_jnt) def _iterate_(self): self.call2_func = self.test2_func """if self.str_nodeType == 'network': self.call2_func = self.test2_func else: self.call2_func = self.test22_func""" for i in range(self.int_targetCount): self.progressBar_set(status = ("Pass 1: Substantiating Call %i"%i), progress = i, maxValue = self.int_targetCount) t1 = time.clock() self.test1_func(self.l_objects[i]) t2 = time.clock() self.l_times_1.append(t2-t1) t1 = time.clock() self.call2_func(self.l_objects[i]) t2 = time.clock() self.l_times_2.append(t2-t1) t1 = time.clock() self.test3_func(self.l_objects[i]) t2 = time.clock() self.l_times_3.append(t2-t1) def _reportHowMayaIsStupid_(self): _m1_time = sum(self.l_times_1) _m2_time = sum(self.l_times_2) _m3_time = sum(self.l_times_3) #cgmGeneral.report_enviornment() for i,t in enumerate(self.l_times_1): self.progressBar_set(status = ("Pass 1: Reporting %i"%i), progress = i, maxValue = len(self.l_times_1)) _dif1 = t - self.l_times_2[i] _dif2 = t - self.l_times_3[i] self.log_info("Step {0} | MetaClass: {1}| cgmNode: {2}(d{4}) | validate: {3}(d{5})".format(i,"%0.4f"%t, "%0.4f"%self.l_times_2[i], "%0.4f"%self.l_times_3[i], "%0.4f"%_dif1, "%0.4f"%_dif2, )) self.log_info(cgmGeneral._str_headerDiv + " Times " + cgmGeneral._str_headerDiv + cgmGeneral._str_subLine) self.log_info("Count: {0} | MetaClass: {1} | cgmNode: {2} | validate: {3}".format(self.int_targetCount, "%0.4f"%_m1_time, "%0.4f"%_m2_time, "%0.4f"%_m3_time)) self.log_info("Method 1 | Start: {0} | End: {1} | Difference: {2} | Total: {3} ".format("%0.4f"%self.l_times_1[0], "%0.4f"%self.l_times_1[-1], "%0.4f"%(self.l_times_1[-1] - self.l_times_1[0]), "%0.4f"%_m1_time)) self.log_info("Method 2 | Start: {0} | End: {1} | Difference: {2} | Total: {3} ".format("%0.4f"%self.l_times_2[0], "%0.4f"%self.l_times_2[-1], "%0.4f"%(self.l_times_2[-1] - self.l_times_2[0]), "%0.4f"%_m2_time)) self.log_info("Method 3 | Start: {0} | End: {1} | Difference: {2} | Total: {3} ".format("%0.4f"%self.l_times_3[0], "%0.4f"%self.l_times_3[-1], "%0.4f"%(self.l_times_3[-1] - self.l_times_3[0]), "%0.4f"%_m3_time)) self.log_info("Compare 2:1| Dif: {0} | Dif: {1} | Total: {2} ".format("%0.4f"%(self.l_times_1[0] - self.l_times_2[0]), "%0.4f"%(self.l_times_1[-1] - self.l_times_2[-1]), "%0.4f"%(_m1_time - _m2_time))) self.log_info("Compare 3:1| Dif: {0} | Dif: {1} | Total: {2} ".format("%0.4f"%(self.l_times_1[0] - self.l_times_3[0]), "%0.4f"%(self.l_times_1[-1] - self.l_times_3[-1]), "%0.4f"%(_m1_time - _m3_time))) return fncWrap(*args, **kws).go()
Here’s the output…
... speedTest_substantiation >> Step 998 | MetaClass: 0.0019| cgmNode: 0.0008(d0.0011) | validate: 0.0015(d0.0004) speedTest_substantiation >> Step 999 | MetaClass: 0.0019| cgmNode: 0.0008(d0.0011) | validate: 0.0013(d0.0006) speedTest_substantiation >> /// Times ///---------------------------------------------------------------------------------------------------- speedTest_substantiation >> Count: 1000 | MetaClass: 2.0867 | cgmNode: 0.8801 | validate: 1.5791 speedTest_substantiation >> Method 1 | Start: 0.0022 | End: 0.0019 | Difference: -0.0003 | Total: 2.0867 speedTest_substantiation >> Method 2 | Start: 0.0010 | End: 0.0008 | Difference: -0.0002 | Total: 0.8801 speedTest_substantiation >> Method 3 | Start: 0.0017 | End: 0.0013 | Difference: -0.0003 | Total: 1.5791 speedTest_substantiation >> Compare 2:1| Dif: 0.0012 | Dif: 0.0011 | Total: 1.2066 speedTest_substantiation >> Compare 3:1| Dif: 0.0006 | Dif: 0.0006 | Total: 0.5076 speedTest_substantiation >> [TIME] -- Step: 'Report' >> 5.392 speedTest_substantiation >> /// Times ///---------------------------------------------------------------------------------------------------- # Warning: cgm.core.cgm_General : speedTest_substantiation >> /// Total : 10.829 sec ///---------------------------------------------------------------------------------------------------- #
Issues and Solutions
- General
- It doesn’t appear to be the iterating itself that is causing the slowdown but some other process
- Reloading meta resets the slowdown to base (after the file new/open fix)
- cgm
- cgmNode was much slower than a MetaClass node
- Short version – I had a list.extend() when I should have had a if a not in list:list.append()
- Long Version – Tracked down an issue where everytime cgmNode was called ( a lot), it was micrscopically increasing the speed of the next call. On a subclass to r9Meta.MetaClass I was extending the UNMANAGED class list with some attributes on my root subclass’s __init__ doing so was adding duplicate attributes to that list any time my subclass was substantiated after initial reload of Meta. That fact caused some of the subfunctions to add that number of steps everytime they called. So long story short, every time my subclass substantiated after a meta reload it got minisculely slower. However, when that call happens tens/hundreds of thousands of times, it added up.
- Also was curious if having properties or too many functions would cause a slow down in substantiation speeds and the answer is, not really.
- I was was also concerned that use of a function class I’d been experimenting with was causing slow down and I didn’t come to a full answer on this one yet.
- autofill flag – There is a flag in MetaClass for autofilling attrs for auto completion to work. Turns out it’s a pretty big hit. Changed our autofill to off and it’s considerably faster than MetaClass.
- 1000 joint test – red9.MetaClass(autofilldefault) – 2.0699s | cgmNode – .8944s | validateObjArg – 1.5777s
- 1000 joint test – red9.MetaClass(autofill – False) – 1.s | cgmNode – .8944s | validateObjArg – 1.5777s
- validateObjArg was dog slow
- Completely rewrote this
- Decided to go at it a different way and found some nice savings
- for meta node conversion — Post rewrite – 1000 node conversion test – red9 – 238.129s | cgm – 8.965s
- cgmNode was much slower than a MetaClass node
- red9
- Reloading red9 introduced an appended file new/open check everytime. This a growing list of errors in the script editor and increased file new/open times.
- Code change suggested to red9
- 3 issues in one – 1) A single meta node that had been deleted generated up to 6 errors on an empty scene. This of course grows the bigger the scene is. and 2)error messages were non specific in nature providing no insight to what errors were happening . 3) a corrupted node can made the cache break when called
- Proposed two additional MetaClass attrs to store _LastDagPath and _lastUUID – these are displayed when a node fails to know what failed
- Proposed allowing failed nodes to attempt to auto remove themselves from the cache when they fail
- Proposed some changes that immediately raise an exception rather than keeping processing to get to a failed node state as quickly as possible
- convertMClassType gets slower the denser the scene
- rewrote cgmMeta.valiateObjArg. Will talk to Mark on this one.
- Hierarchical depth has a direct influence on substantiation speeds
- Created test where for each iteration a new joint is created and parented to the last so at the end you have a 1000 joint chain
- Base results- red9.MetaClass – start :.001s | end: .018s | total: 8.837s
- Oddly enough, if you pass shortNames of the children joints on call instead of the .mNode strings (long name), it cuts the end per time from .018 to .010 for a total of 5.571s
- Talking to Mark on this one.
- Reloading red9 introduced an appended file new/open check everytime. This a growing list of errors in the script editor and increased file new/open times.
Why should you care?
The end result of this pass is that a crazy 5 hour rig build anomaly for Morpheus was parred down to 40 minutes after the cgmNode fixes and 31 minutes after the cgmValidateObjArg rewrite. This is in 2011. Never versions of maya are more efficient and it will get better still as we more through optimizing more.
Note, none my optimizations are in red9’s core yet. Mark is on vacation and most of those fixes wouldn’t help anyone but a coder.
j@cgm
To anyone who’s worked with coding blendshape stuff it can be tedious especially when you bring in inbetweens. Thankfully, Autodesk is fixing a lot of that with 2016 extension 2 if you missed that update but there are still folks using older versions and it doesn’t resolve everything. We have to deal with them a good bit on Morpheus 2 and so we wrote a metaclass to deal with them.
Initial features of the cgmBlendshape metaclass that you can’t easily do with normal api or mc/cmd calls:
- Most functions work off of index/weight or shape/selection format
- Easy alias naming
- Replacing shapes — change out shapes in place keeping inbetweens and connections intact
- Extract shapes — extract shapes from index/weight calls and supporting multipliers to the delta difference
- Shape restoration — replace deleted shapes on the fly. Recreate a shape from delta and base information and plug it back in for further editing
- Subclass to cgmNode to all those functions carry over as well
- Tested in 2011 and 2016
- NOTE – this is wip metaclass and will undergo lots of changes
Before we get into the the specifics of the metaclass, here’s some general lessons learned on blendshapes working through this.
- A blendshape target has several bits of important information
- Index — this is it’s index in the blendshape node. Note – not necessarily sequential.
- Weight — this is the value at which this shape is ‘on’. Usually it is 1.0. Inbetween shapes are between 0 and 1.0.
- Shape — this is the shape that drives the blendshape channel
- Dag — the dag node for the shape
- Alias — the attribute corresponding to its index in the weight list. Typically it is the name of the dag node.
- Plug — the actual raw attribute of the shape on the node. ‘BSNODE.w[index]’
- Weight Index — follows a maya formula of index = wt * 1000 + 5000. So a 1.0 weight is a weight index of 6000.
- The way maya stores info
- Blendshape data is stored in these arrays in real time so that if you query the data and your base mesh isn’t zeroed out, the transformation happening is baked into that
- The caveat to that is that targets that have their base geo deleted are ‘locked’ in to their respective data channels at the point they were when deleted. Their delta information is frozen.
- BlendshapeNode.inputTarget[0].inputTargetGroup[index].inputTargetItem[weightIndex]
- inputTarget — this is most often 0.
- inputTargetGroup — information for a particular shape index
- inputTargetItem — information for a particular weight index
- Sub items at that index
- inputPointsTarget — the is the differential data of the point positions being transformed by a given shape target. It is indexed to the inputComponentsTarget array
- inputComponentsTarget — these are the compents that are being affected by a given shape
- inputGeomTarget — this is the geo affecting a particular target shape
- Replacing blendshapes – you can 1) use a copy geo function if the point count is exact to change the shape to what you want or 2) make a function to do it yourself. There’s not a great way to replace a shape except to rebuild that whole index or the node itself. We made a function to do that
- Once a blendshape node is created with targets, the individual targets are no longer needed and just take up space. Especially when you have the easy ability to extract shapes.
- Getting a base for calculating delta information. As the blendshapes are stored as delta off of the base, the best way I could find to get that delta was to turn off all the deformers on the base object, query that and then return on/connect the envelopes. I’m sure there’s more elegant solutions but I was unsuccessful in finding one.
- Once you have that creating a new mesh from a an existing one is as simple as:
- Taking base data
- For components that are affected on a given index/weight: add the delta to base
- duplicating the base and xform(t=vPos, absolute = True) each of the verts will give you a duplicate shape
- Once you have that creating a new mesh from a an existing one is as simple as:
- Aliasing weight attributes – mc.aliasAttr(‘NEWNAME’, ‘BSNODE.w[index]’)
Here’s a dummy file I used for testing:
https://www.dropbox.com/s/k4i8oo8qyiv3fd6/cgmBlendshape_test.mb?dl=0
Here’s some code to play with the first iteration. You’ll need to grab the MorpheusDev branch on bitbucket if you wanna play with it till I push it to the main branch.
""" ------------------------------------------ cgm.core.examples Author: Josh Burton email: jjburton@gmail.com Website : http://www.cgmonks.com ------------------------------------------ Help for learning the basis of cgmMeta.cgmBlendshape ================================================================ """ from cgm.core import cgm_Meta as cgmMeta cgm.core._reload()#...this is the core reloader #============================================================================================== #>> cgmMeta.cgmBlendshape #============================================================================================== import maya.cmds as mc #You MUST have the demo file to work though this exercise though you could probably glean the gist without it with your own setup #>>Starting off ========================================================================= bs1 = cgmMeta.cgmBlendShape('pSphere1_bsNode')#...let's initialize our blendshape bs1._MFN #...here you'll find the api blendshape deformer call should you be inclined to use it #>>bsShape Functions ========================================================================= #We're referring to the shapes that drive a blendshape nodeds base object here and the functions relating to them #Doing this first will make the blendshape wide functions make more sense on the queries and what not. bs1.bsShape_add('base1_add')#...we're gonna add a new shape to our node. Since no index is specified, it just chooses the next available bs1.bsShape_add('base1_add', 8)#...let's specify an index #...hmm, our add throws an error because that name is taken. let's fix it bs1.bsShape_nameWeightAlias('HeyThere',8)#...nice! bs1.bsShape_add('base1_tween', 0, weight = .5)#...we're gonna add a new inbetween shape by it's geo, index, and weight #============================================================================================== #Replace functions... #...replacing is not something easily done in basic maya calls bs1.bsShape_replace('base1_replace','base1_target')#...replace with a "from to"" call. bs1.bsShape_replace('base1_target','base1_replace')#...and back #...Note - the inbetween is intact as is the driver connection bs1.bsShape_replace('base1_replace',0)#...indice calls also work for most calls bs1.bsShape_replace('base1_target',0) #============================================================================================== #Indexing... #An index for use with working with blendshapes needs to have an index and weight in order to know what you're working with bs1.bsShape_index('base1_target')#...this will return a list of the indices and weights which this target affects in [[index,weight],...] format bs1.bsShape_index('base1_add')#...this will return a list of the indices and weights which this target affects in [[index,weight],...] format #============================================================================================== #Query... bs1.bsShape_getTargetArgs('base1_target')#...this returns data for a target in the format excpected by mc.blendshape for easier use in nested list format bs1.is_bsShape('base1_target')#...yup bs1.is_bsShape('bakeTo')#...nope #============================================================================================== #>>Blendshape node wide functions ========================================================================= bs1.get_targetWeightsDict()#...this is a handy call for just getting the data on a blendshape in {index:{weight:{data}}} format bs1.get_indices()#...get the indices in use on the blendshape from the api in a list format bs1.bsShapes_get()#...get our blendshape shapes that drive our blendshape bs1.get_baseObjects()#...get the base shapes of the blendshape or the object(s) the blendshape is driving bs1.get_weight_attrs()#...get the attributes on the bsNode which drive our indices bs1.bsShapes_get()#...get our shapes #============================================================================================== #>>Arg validation ========================================================================= bs1.bsShape_validateShapeArg()#...no target specified, error bs1.bsShape_validateShapeArg(0)#...more than one entry, error bs1.bsShape_validateShapeArg(0, .5)#...there we go bs1.bsShape_validateShapeArg('base1_target') #============================================================================================== #Generating geo... #Sometimes you wanna extract shapes from a blendShape node. Let's try some of that bs1.bsShape_createGeoFromIndex(0)#...will create the a new piece of geo matching the 1.0 weight at 1.0 bs1.bsShape_createGeoFromIndex(0,.5)#...will get you the inbetween bs1.bsShape_createGeoFromIndex(3)#...will get you squat because nothing is there bs1.bsShape_createGeoFromIndex(0, multiplier = 2.0)#...you can also generate factored targets bs1.bsShape_createGeoFromIndex(0, multiplier = .5)#... bs1.bsShapes_delete()#...delete all the targets for your blendshape. #...ah geeze I didn't mean to do that. No worries! bs1.bsShapes_restore()#...rebuilds the targets and plugs them back in #==============================================================================================
So, finally wrapped up my work for Morpheus 2 in regards to the wrap setups. As you can see from the last two trips on the rabbit trail(Step 1, Step 2), this wasn’t exactly a simple process.
The point of all of this is to be able to bake blendshapes reliably to nonconforming geo while affecting only the regions we want without having to go in and tweak things by hand. This will prove more and more useful as customization option expand. Why bother with this? Wrap deformers are exceedingly slow. Being able to replace them with skinning data and copying blendshapes between meshes will make your animations play faster and feel more interactive. The final solution was to create proximity geo which is localized to the area I want to affect the nonconforming target mesh. The proximesh is wrapped to the base driver and the target is wrapped to the proximesh.
target –[wraps to]—>> proximesh –[wraps to]–>> base
Here’s a general breakdown of the baking function:
- Given a source mesh that has the blendshapes and a nonconforming target mesh we want them on…
- Go through all blendshape channels on the source mesh and…
- Get their connections/values
- Break all connections/zero values so we have clean channels to push our specific shapes to our target mesh
- Generate a proximesh of the source with the area we want influencing our nonconforming mesh
- Generate a duplicate target mesh so we’re not messing with that mesh
- Wrap the proximesh to the source mesh
- Wrap the duplicate target mesh to the base mesh
- Go through all the blendshape channels on the source mesh and…
- Turn a channel on
- Duplicate our wrapped target mesh to create a new mesh with the blendshape data on it pushed through by the wrap
- If we’re going to cull no change shapes – check each generated shape against the target mesh to figure out when are not moving any verts and delete those offenders
- Go through all the original blendshape channels again and rewire them as they were before our function
- Delete the wraps and temporary geo
- If desired, create a new blendshape node with our final list of baked targets on our nonconforming base mesh
- If desired wire the new blendshape node to match the original one we baked from so the channels follow one another.
Easy peasy:)
Functions created while working through it:
- cgm.lib.deformers
- proximityWrapObject — This was the solution in the end to getting rid of movement in the mesh in areas I didn’t want affected.
- influenceWrapObject — See step one above. Dead end but might prove useful in the future
- bakeBlendShapeNodesToTargetObject — Greatly expanded this during this little journey
- Added wrapMethod — influence wrap and proximity wrap and associated flags
- Added cullNoChangeGeo — removes baked targets that don’t move the base mesh within the given tolerance
- cgm.core.lib.geo_Utils
- is_equivalent — Function comparing points of to pieces of geometry to see if their components match in object space. Useful for culling out empty blendshape targets that have been baked. Supports tolerance in checking as well
- get_proximityGeo — In depth function for returning geo within range of a source/target object setup. Search by boundingbox and raycasting to find geo within the source. Can return objects,faces,edges,verts or proximity geo which is new geo from the targets that corresponds to the search return
Lessons Learned for wraps in general
- The maya command call to create a node is mc.CreateWrap (in 2011 at least). I hope later versions made it easier as
- The object you wrap your target two gets two attributes (dropoff and smoothness) that dicatate how the wrap on your target is affected. No idea why it took me this long in maya to notice that in the docs.
- Simply using Maya wrapDeformer to wrap an object to another when the object to be wrapped doesn’t conform to the target geo is a bad idea. You’ll get movement in your wrap geo where you don’t want it.
Long story short. The wrap problem is resolved for Morpheus 2.0.
For now. 🙂
So the rabbit trail from over the weekend proved to not be the answer to my original problem as hoped. Namely I was still getting geo movement from far off regions when baking blendshapes to non-similar geo (think a sphere placed on a body).
As such, my next plan to resolve this was to create a specific conforming geo piece to wrap to then wrap my nonconforming object to. To do this, I need a way to find the geo closest to my geo I wanted to transfer the blendshapes too and so wrote a new function for this that would:
- Search target geo against a source geo piece to find geo from each target within the source by two methods:
- boundingBox.contains – by vert
- rayCasting – by the compass vectors to make sure it is completely within the sourceObject
- Translate that data to verts,edges, faces
- Have a method to expand that data:
- selection traversing
- softSelection radius
Lessons learned:
- bounding box checking is much much faster so use that mode unless you just have to have a more precise idea of what verts are inside the mesh.
- Not a lot of specific info I could find on some of these concepts and so wanted to share to save someone else time and deadends
Here’s part one of this issue which is housed at cgm.core.lib.geo_Utils.get_contained. There are too many dependencies to include them all but you can get this gist from the code.
def get_contained(sourceObj= None, targets = None, mode = 0, returnMode = 0, selectReturn = True, expandBy = None, expandAmount = 0): """ Method for checking targets componeents or entirty are within a source object. :parameters: sourceObj(str): Object to check against targets(list): list of objects to check mode(int):search by 0: rayCast interior 1: bounding box -- THIS IS MUCH FASTER returnMode(int):Data to return 0:obj 1:face 2:edge 3:verts/cv selectReturn(bool): whether to select the return or not expandBy(str): None expandSelection: uses polyTraverse to grow selection softSelect: use softSelection with linear falloff by the expandAmount Distance expandAmount(float/int): amount to expand :returns list items matching conditions :acknowledgements http://forums.cgsociety.org/archive/index.php?t-904223.html http://maya-tricks.blogspot.com/2009/04/python.html -- idea of raycasting to resolve if in interior of object http://forums.cgsociety.org/archive/index.php?t-1065459.html :TODO Only works with mesh currently """ __l_returnModes = ['obj/transform','face','edge/span','vert/cv'] __l_modes = ['raycast interior','bounding box'] __l_expandBy = [None, 'expandSelection','softSelect'] result = [] _mode = cgmValid.valueArg(mode, inRange=[0,1], noneValid=False, calledFrom = 'get_contained') log.info("mode: {0}".format(_mode)) _returnMode = cgmValid.valueArg(returnMode, inRange=[0,3], noneValid=False, calledFrom = 'get_contained') log.info("returnMode: {0}".format(_returnMode)) _selectReturn = cgmValid.boolArg(selectReturn, calledFrom='get_contained') _expandBy = None if expandBy is not None: if expandBy in __l_expandBy: _expandBy = expandBy else: raise ValueError,"'{0}' expandBy arg not found in : {1}".format(expandBy, __l_expandBy) #Get our objects if we don't have them if sourceObj is None and targets is None: _sel = mc.ls(sl=True) sourceObj = _sel[0] targets = _sel[1:] targets = cgmValid.listArg(targets)#...Validate our targets as a list l_targetCounts = [] for o in targets: _d = cgmValid.MeshDict(o) l_targetCounts.append(_d['pointCountPerShape'][0]) sel = OM.MSelectionList()#..make selection list for i,o in enumerate([sourceObj] + targets): try: sel.add(o)#...add objs except Exception,err: raise Exception,"{0} fail. {1}".format(o,err) _dagPath = OM.MDagPath()#...mesh path holder matching = []#...our match holder _l_found = OM.MSelectionList()#...new list for found matches guiFactory.doProgressWindow(winName='get_contained', statusMessage='Progress...', startingProgress=1, interruptableState=True) if _mode is 1: log.info('bounding box mode...') try:#Get our source bb info sel.getDagPath(0,_dagPath) fnMesh_source = OM.MFnMesh(_dagPath) matrix_source = OM.MMatrix(_dagPath.inclusiveMatrix()) bb_source = fnMesh_source.boundingBox() bb_source.transformUsing(matrix_source) sel.remove(0)#...remove the source except Exception,err: raise Exception,"Source validation fail | {0}".format(err) for i in xrange(sel.length()): _tar = targets[i] _vtxCount = l_targetCounts[i] log.info("Checking '{0}'".format(_tar)) guiFactory.doUpdateProgressWindow("Checking {0}".format(_tar), i, sel.length(), reportItem=False) sel.getDagPath(i, _dagPath)#...get the target fnMesh_target = OM.MFnMesh(_dagPath)#...get the FnMesh for the target fnMesh_target.setObject(_dagPath) pArray_target = OM.MPointArray()#...data array fnMesh_target.getPoints(pArray_target)#...get comparing data matrix_target = OM.MMatrix(_dagPath.inclusiveMatrix()) fnMesh_target = OM.MFnMesh(_dagPath) bb_target = fnMesh_source.boundingBox() bb_target.transformUsing(matrix_target) if bb_source.contains( cgmOM.Point(mc.xform(_tar, q=True, ws=True, t=True))) or bb_source.intersects(bb_target): if _returnMode is 0:#...object _l_found.add(_dagPath) continue iter = OM.MItGeometry(_dagPath) while not iter.isDone(): vert = iter.position(OM.MSpace.kWorld) if bb_source.contains(vert): _l_found.add(_dagPath, iter.currentItem()) iter.next() elif _mode is 0: log.info('Ray cast Mode...') sel.remove(0)#...remove the source for i in xrange(sel.length()): _tar = targets[i] _vtxCount = l_targetCounts[i] log.info("Checking '{0}'".format(_tar)) sel.getDagPath(i, _dagPath)#...get the target fnMesh_target = OM.MFnMesh(_dagPath)#...get the FnMesh for the target fnMesh_target.setObject(_dagPath) guiFactory.doUpdateProgressWindow("Checking {0}".format(_tar), i, sel.length(), reportItem=False) iter = OM.MItGeometry(_dagPath) _cnt = 0 if _returnMode is 0:#...if the object intersects _found = False while not iter.isDone(): guiFactory.doUpdateProgressWindow("Checking vtx[{0}]".format(_cnt), _cnt, _vtxCount, reportItem=False) _cnt +=1 vert = iter.position(OM.MSpace.kWorld) _inside = True for v in cgmValid.d_stringToVector.itervalues(): d_return = cgmRAYS.findMeshIntersection(sourceObj, vert, v, maxDistance=10000, tolerance=.1) if not d_return.get('hit'):#...if we miss once, it's not inside _inside = False if _inside: _l_found.add(_dagPath) _found = True iter.next() else:#...vert/edge/face mode... while not iter.isDone(): guiFactory.doUpdateProgressWindow("Checking vtx[{0}]".format(_cnt), _cnt, _vtxCount, reportItem=False) _cnt +=1 vert = iter.position(OM.MSpace.kWorld) _good = True p = cgmOM.Point(vert) for v in cgmValid.d_stringToVector.itervalues(): d_return = cgmRAYS.findMeshIntersection(sourceObj, vert, v, maxDistance=10000, tolerance=.1) if not d_return.get('hit'):#...if we miss once, it's not inside _good = False if _good: _l_found.add(_dagPath, iter.currentItem()) iter.next() guiFactory.doCloseProgressWindow() #Post processing ============================================================================= _l_found.getSelectionStrings(matching)#...push data to strings log.info("Found {0} vers contained...".format(len(matching))) #Expand ======================================================================================== if _expandBy is not None and returnMode > 0: log.info("Expanding result by '{0}'...".format(_expandBy)) _sel = mc.ls(sl=True) or [] _expandFactor = int(expandAmount) mc.select(matching) if _expandBy is 'expandSelection': for i in range(_expandFactor): mel.eval("PolySelectTraverse 1;") matching = mc.ls(sl = True) if _expandBy is 'softSelect': mc.softSelect(softSelectEnabled=True,ssc='1,0,0,0,1,2', softSelectFalloff = 1, softSelectDistance = _expandFactor) matching = cgmSELECT.get_softSelectionItems() mc.softSelect(softSelectEnabled=False) #if _sel:mc.select(_sel) if _returnMode > 0 and _returnMode is not 3 and matching:#...need to convert log.info("Return conversion necessary") if _returnMode is 1:#...face matching = mc.polyListComponentConversion(matching, fv=True, tf=True, internal = True) elif _returnMode is 2:#...edge matching = mc.polyListComponentConversion(matching, fv=True, te=True, internal = True ) if _selectReturn and matching: mc.select(matching) return matching
Up next is setting up a new wrap setup with this data. Will post when that’s done.
On yet another rabbit trail of problem solving on Morpheus 2.0, I came across an issue where wrap deformers weren’t working as needed. Namely transferring blendshapes from one mesh to another when the shape of the target mesh wasn’t like the original. Even geo changes in regions no where near the ‘to bake’ geo were affecting it.
So did some googling and came across a concept I’d not used before – namely using a mesh to deform another with a skinCluster.
Neat, so how do we do it?
- Get your target and source mesh ready
- Create a joint and skinCluster your target mesh to it
- Add the driving mesh to the skinCluster with the useGeometry flag ( sample code for this line below).
- polySmoothness flag. This controls the smoothness of the mesh deformation of the target mesh.
- A polySmoothness of 0 is the closest to a standard wrap deformer
- In my initial testing I found that this flag could only be set consistently on creation. Editing the flag could push the smoothness up but not down (in 2011 at least).
- Make sure the useComponents attribute on the skinCluster is set to True. If you don’t see the deformation doing anything this is the likely culprit.
mc.skinCluster(_cluster, e = True, addInfluence = sourceObject, wt = 100.0, useGeometry = 1, polySmoothness = 10)
I wrote a script to set this up but it’s still wip. It’s a function found here: cgm.lib.deformers.influenceWrapObject. Because of the issue noted in step 3.2, I added the polySmoothness as a creation flag.
This method of wrapping is much more localized than wrap deformers when the mesh isn’t close AND provides an easy way to paint weights for the deformation.
Acknowledgements:
This post is to remind us how to do this. J had a computer issue and had to reinstall Windows:)
- Follow this tutorial – https://confluence.atlassian.com/bitbucket/set-up-ssh-for-mercurial-728138122.html
- Make some coffee, you earned it.
If you get the “server’s host key is not cached…” error when attempting to push from SourceTree:
- Command prompt
- Go to Program Files (x86)/PuTTY
- “plink -agent bitbucket.org”
- Hit ‘y’
Sometimes you need to get stuff back to earlier versions of Maya and ignore version just doesn’t work. I needed to get a load of 2016 geo back to 2011. Here’s what worked:
- Export geo to a clean file in your current version of Maya
- If you can’t export it as an .ma file, run scene optimize on the mb file making sure to remove unknown nodes
- Save as an .ma
- In a text editor change the 2016(or whatever version you’re going from) to 2011(or whatever version you’re going to). Save it
- Open in the earlier version of Maya
-j@cgm
Released a build of Morpheus 2 this week and immediately ran into some issues with the marking menu and hot keys. I’d been using zooToolbox’s setup for years for hot keys but it didn’t work with 2016 so I dug in.
Maya 2016 has a pretty neat new editor but it’s still probably more steps than most of our users could reliably follow so wanted to get the button push setup back.
There a few things to remember when working with hot keys and in this order…
- runTimeCommand– This is the code that gets run. It can be python or mel
- nameCommand — This is required for a hot key to be setup properly
- hotkeySet — This is something new with 2016 and needs to be set to a new set to be able to add a new hot key because the default set is unchangable
- savePrefs — after setting up your hotkey, you must save the prefs or the newly created hotkeys go away (not sure if this is new to 2016 or not)
Lessons learned:
- hotkeySets — were added in 2016. Any hotkey work you do post 2016 needs to account for them. I ended up having my stuff use the existing set if it wasn’t the default and create a new one if the default is the current one
- hotkey -shiftModifier flag — this was added in 2016
- Pushing dicts into mc/cmds calls — In general, something like mc.command(**_d) works with _d being your dict. However on mc.hotkey I found that the keyShortcut flag needed to be in the dict and at the start of the call to work: mc.hotkey(_k, **_d).
I ended up writing a handler and gui to set stuff up. I’ll swing back and talk about it another time if there’s interest.
Back to Morpheus…
Because google doesn’t always get them the first go…
- 2011 – http://download.autodesk.com/us/maya/2011help/index.html
- 2012 – http://download.autodesk.com/global/docs/maya2012/en_us/index.html
- 2013 – http://download.autodesk.com/global/docs/maya2013/en_us/
- 2014 – http://download.autodesk.com/global/docs/maya2014/en_us/index.html
- 2015 – http://help.autodesk.com/view/MAYAUL/2015/ENU/
- 2016 – http://help.autodesk.com/view/MAYAUL/2016/ENU/

