Archive for May, 2016

Wed, May 4th, 2016
posted by jjburton 09:05 PM

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
  • 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
#==============================================================================================