22 May, 2012

No-pop implementation challenges

One of the key features of the constraint system I am developing is auto correction of "pop effect" when animation changes. This problem is quite complex due to a lot of different scenarios involved and based on how Maya architecture is designed.

It is very easy to calculate offset and store it when a constraint switches between different spaces to avoid pop. That's the general idea, however maintaining this offset when animation is constantly changing is difficult due to some of the reasons mentioned below:

  • User can update keys in the graph editor at any time value, this does not cause the compute since you might be on a frame that is not related to the changes. Hence we have to detect this as animation callback.
  • Maya's animation callbacks are not much useful in giving details about which keyframe was deleted so in this case we have to update all the key offsets from the beginning to make sure we resolve pop. Because deleting a driver key means that space will change and hence the offset at the next nearest keyframe.  
  • A space can be animated not only by its own transform but any parent in the hierarchy or a constraint. The constraint driver(master) itself might be animated by a transform up in the hierarchy.
  • This means that when animation for a space changes in the scene we have to first detect if the animation affects the constraint. In addition to using generic animation callbacks we also have to look at the input/output graph and also the hierarchy to detect if our constraint is affected. 
  • What if drivers are not keyed exactly on the switching keyframes?  This means that if I update animation curve handles it will update the driver transform at the switch and hence creating a pop.
  • One change in the earlier keyframe can cause update of offsets in all the subsequent keyframes to remove pop effect. To remove the pop we need to update drivers by either updating offset or adding extra transform on the affected switching keys, however that could mean that the offset/positioning in next switching frame is invalidated and will create a pop. This can lead to a cascading effect, creating a pop in each next frame when fixing the current one. This is true in my case especially since all the driven nodes are free to move while constrained to the driver of the system. I have some ideas to avoid this cascading effect, but I still need to test them first. 
  • To remove the pop we can match the current driver and previous drivers of the system at switching keyframe, but there are few tricky situations to look for:
    • What if both, current and previous, drivers are updated at the switch? Which one should be matched to the other one?
    • What if a driver has incoming connections and its transform cannot be keyed directly?

Below is a simple example:
We have two objects A(bluish) and B(reddish). They are animated as shown in the image below.

Think of the following scenarios after switching at frame 3:
  1. If B, the current driver, moves at frame 3
    This means that A will move as well since B is the current driver
  2. If A, the previous driver, moves at frame 3
    In this case B does not move since A is not the current driver

As for the status of GroupConstraint, the basics work quite nice with the animation. The problem arises when the animation changes creating a pop. The constraint handles pop for certain cases, but not all. I have finished writing logic for detecting keyframe changes and checking if it affects the constraint output or not based on graph connections and also DAG hierarchy. I also have all the functions ready to update offset information at any switch keyframe. The last (hopefully last) part now is to make sure that the auto-pop correction works when changing the existing animation in different cases. For me, I have to look at few more scenarios since I am also working hard to make switching possible with only one keyframe instead of two consecutive keyframes. And the possibility of animating the followers independently while driven by the driver of the system also adds to the difficulty.


  1. Yes your right - theres definately an 'order' of fixing the offset. Well you need to ask yourself some questions & statements:

    1. When a key is made changed etc, the offset is invalidated.
    2. Only transforms that don't have the right offset at the key time are 'bad' and need to be fixed.

    So what your looking for is a method to query the nodes in the system - check if there offset are valid and fix. To check your could test the value of the key - i.e check if it's the correct offset for the transform.

    From testing order then goes out of the way, but increases iteration if your not clever. To check how many cycles would be to check the interdependency of the switches first and then process in that order.

    Crucially you want isomorphism in systems that act as a whole and ones that are dynamic like mine.

    1. Mathematically and logically it's quite clear what should be done, the problems are arising due to working in the limits of Maya API and how Maya works.

      For example, we can not get information about which key was deleted, especially when listening to general key changes. If we are looking at single animation node it is possible to store the key indexes and compare updates against that to detect deleted keys. However, that's not practical when you consider that a driver can be keyed either by other constraint connected to it or by keying any node in its upper hierarchy.

      I am already doing the check by comparing previous and current driver to check if they match. However, issue is how do you actually match them if you find they are off. You have to update one driver previous/current so that they match, changing only offset value is not really helpful here (at least in my case since followers also move independently). And again, you don't know if previous driver is to match to current or the other way round.

  2. Hello charles, Maulik.
    Just a quick idea, your space switching is done on 1 key frame basis( not across an interval like you could with a constraint node weight ?) I assume you can use a short attribute to drive who the current space driver is? if animated you use a stepped interpolation?
    How about querying the world matrix attribute of your driver with a different time context ( like getAttr mydriverNode.worldMatrix -time (currentDriverTimeWhenChanged -1) in mel )?

  3. Hi Cedric,

    Yes, the driver is animated by using an enum attribtue. This gives stepped as default curve type. This enum attribute can be added to any node. Later I plan to write a command for this node if time permits, which will allow to quickly configure the node. I am currently using MDGContext to query plugs at different time when offset needs to be updated.

    I think I have figured out the final issue, need to implement in code and test it.

  4. Sweet, I will give a try myself just to see if it can be done with only custom spaceswitch node that output a matrix or a translate/rotate attribute

  5. Hello Maulik,
    Just for fun I have draft a spaceswitcher node in python: rename spaceswitcher.doc to spaceswitcher.zip

    Simple node that drives the parent of a transform node with the an offset computed when we change a driver index. It is funny to see that the rotate/translate value of this transform node don't trigger a cyclic error when we drive its parent transform. At this time it is pretty much broken( the offset method is not good--> i think it needs some more matrix multiplication in order to reconstruct the worldmatrix of our driven object and than converting it in the space of the current driver )

    1. Hi Cedric,

      Didn't get a chance to try you plug-in but I went through your code. It's a different approach than what GroupConstraint does. What I was thinking was that you have all the offsets initial values ready for you to compute on a given frame. So in the compute method you query the current driver and also get the necessary offsets ready to compute the constraint output. So updating necessary attributes for calculating final output is done in keying events or switching events. I will try your plug-in, you are very quick to whip up an example!

  6. Hello Maulik: thanks, I was trying a plain a simple space switch but in the end, I remove the driven object out of the equation: what is important is to drive an offset group and I just finished another python node to do that: i am eager to publish an article on my blog to talk about it. it does offset computation on live manipulation, read animCurve valueetc you can edit value before after, offset key, jump back and forth the time line' the most diffuclt situation ) manipulate driver in the viewport... the full monthy ( I hope so , I might use your brain and charles one too to see If my solution cover all ground ).

    I my case I didnt need any animation callbacks plug with offset timed dgcontext did the trick for me( ok a dirty solution but it works and is stable ).

    1. So you calculate offsets at switching point during compute method I guess. how do you randomly jump between frames and calculate the correct final output? Do you have to go back only one time offset or multiple frames in history?
      With the callbacks my aim was to make compute as minimal as possible so that when animators scrub the timeline they get the best speed.

      Actually I found a better way to stop the pop effect, but I will add that later as I am very close to finish in my current implementation.

      Looking forward to your post.

  7. You are right in your guess:

    I reconstruct the switch history until I found the current time range: you only need to compute an offset at the switching frame number then for the rest of time range( between stepped parent index animation ) this offset is valid. didnt test out on full blown production rig the impact on performance( with 50 switch key on 600 frame i have no real slowdown at this time, ( I start with the first key time value and request the plug matrix for the old and new parent and simply update an internal offset attribute )