C4D Python Party: Retiming Caches

Control time! Not space though, just time, with this script at least. If you're working with Mograph and get asked to make something just a tad slower here but quicker there to match the timings of something else in your animation, or maybe you'd like to ramp up the speed of a rigid body simulation, then this is the script for you - you can use it to retime your caches with an animation curve, similar to the retiming feature in After Effects. It also works with Alembic generators and the Point Cache Tag so it's a pretty versatile way to control time in your projects.

To install the script:
1 Download it here (it's a freebie)
2 Unzip it into your scripts folder (eg "C:\Program Files\MAXON\CINEMA 4D R18\library\scripts\" on Windows, or "/Applications/MAXON/CINEMA 4D R18/library/scripts/" on OSX)

To use the script:
1 Select the object whose cache you'd like to retime (this can be an Alembic generator, a polygon object with a Point Cache Tag, or any of the Mograph generators so long as you've given it a Mograph Cache Tag).
2 Run the script (click 'Script' > 'User Scripts' > 'cache_retimer').
3 Change the animation curve for the 'Frame' value in the new Python script tag that gets added to your object - flatter curves are slower, steeper curves are faster, and you can make your cache go backwards by setting the slope downwards.

Handy hints
While Point Cache and Mograph Cache Tags can interpolate frames, Alembic generators don't - so if you slow them down with this script they might judder along as each cache frame takes up multiple frames in your scene. To avoid this you can increase the 'Subframes' value in the Alembic Export Settings so there's some inbetween frame information to use when slowing down your Alembic.

The Cache Retimer script doesn't work on Dynamics Body Tag caches, but it does work on Mograph Cache Tags and Point Cache Tags. You can use rigid body dynamics via the Mograph objects (eg applied to a Cloner or Fracture object with your dynamic objects inside), cache the animation with a Mograph Cache Tag instead of within the dynamics tag, and then retime that. For soft body dynamics you can try adding a Point Cache Tag to retime (this will only work if you've made the object editable).

If you later remove the script make sure to set your cache's 'Offset' (for Mograph Cache and Point Cache Tags) or 'Start of Animation' (for Alembic generators) back to 0.



How it works
When you run the script it adds a Python tag to your object that takes control of the cache's offset or start parameters (depending on the cache type). The script that gets added is very simple, here's the one for Mograph Cache Tags:
27import c4d
28 def main():
29     obj = op.GetObject()
30     frame = op[c4d.ID_USERDATA,1]
31    
32     offset = c4d.BaseTime(((frame / doc.GetFps()) - doc.GetTime().Get()))
33     tag = obj.GetTag(1019337)
34     tag[c4d.MGCACHETAG_OFFSET] = c4d.BaseTime(-offset.Get())
Line 29 gets the object this Python tag is attached to; line 30 reads the Python tag's 'Frame' value from its Userdata parameters; line 32 calculates a time offset (based on the Frame value and the current document time); line 33 finds the object's Mograph Cache Tag ('1019337' is the type identifier for Mograph Cache Tags); and finally line 34 sets the Mograph Cache Tag's 'Offset' parameter to the offset we calculated on line 32.

It's that simple! But hang on how did that script get there? And how did that Frame userdata get animated? We sort all that in 'cache_retimer.py', the script you run from C4D's 'Script > User Scripts' menu. And how does that work? I'll break some of it down here:

Starting things off
125def main():
126     #the main action starts here, where we find the selected object
127     obj = doc.GetActiveObject()
128    
129     #find what cache type the object has
130     cachetype = find_cacheable_type(obj)
131     #if there isn't a valid cache type we display an error message and return
132     if cachetype == -1:
133         msg = "Please select an Alembic generator, an object with a Mograph Cache Tag or an object with a Point Cache Tag to apply the Cache Retimer script to."
134         print msg
135         gui.MessageDialog(msg)
136         return
137        
138     #there's a good cache type, so add the rig
139     new_rig(obj, cachetype)
This initial code gets the selected object (line 127), checks what type of cacheable thing the object has (by calling 'find_cacheable_type()' on line 130), then returns an error message if there's nothing we can work with (lines 132-136) or applies the retime rig if there is (line 139).

Checking the cacheable type
This is the function to get the type of cache object we're working with:
63def find_cacheable_type(obj):
64     if obj==None:
65         return -1
66     if obj.GetTag(mograph_cache_tag):
67         return mograph_cache_tag
68     if obj.GetTag(point_cache_tag):
69         return point_cache_tag
70     if(obj.GetType() == c4d.Oalembicgenerator):
71         return c4d.Oalembicgenerator
72     #no supported cache object found
73     return -1
Given any object (in our case, the selected object) this script will check if it's got a Mograph Cache Tag, a Point Cache Tag, or is an Alembic Generator, and return C4D's internal ID for those object types if it does. The checks for tags look like "object.GetTag(tagtype)" - which returns the first tag of that type attached to the object, or 'None' if there aren't any matching tags.

For example on line 66 we check if there's a 'mograph_cache_tag' (a variable I've defined further up in the script, on line 34, as 1019337 - C4D's numeric identifier for Mograph Cache Tags). We're only interested in whether one exists so we don't keep the result - a result of 'None' equates to 'False' for the 'if' statement so the code within it would be skipped. If there is a tag the 'if' statement succeeds and the next line is run, returning the identifier for Mograph Cache Tags.

If there's no object (eg if nothing is selected) we return -1, and if all the other tests fail we also return -1. A classier way of dealing with this would be to throw an exception so we don't have to check for the error code later on.

Where the action's at
Once we have a valid object, we get thrown into the 'new_rig' function, starting on line 76, where most of the good stuff happens.
76def new_rig(obj, cachetype):
77     #create a Python tag and attach it to the selected tag's object
78     pythontag = c4d.BaseTag(c4d.Tpython)
79    
80     #set the Python code inside the tag
81     pythontag[c4d.TPYTHON_CODE] = copyright + pythontagcode + pythontagcode_dict[cachetype]
First of all, we create a Python tag (on line 78), then set its code. Why three variables to make up the code? Each cache type needs slightly different code to work, but they also have some common elements. 'pythontagcode' is defined on lines 39-43 and includes all code common to each cache type. 'pythontagcode_dict' is a dictionary variable defined from line 47 to 59, and contains three different string variables (bits of text) associated with three different 'keys', which are the values we use to determine which of those three bits of text to extract from this variable to add to our new Python tag's code.

How do we determine which string to use? You might notice that the keys in the dictionary are the same as those we return from the 'find_cacheable_type' function. We pass the result of that function into our 'new_rig' function as the variable 'cachetype', which we use on line 81 in 'pythontagcode_dict[cachetype]', to get the text associated with that key. Finally, 'copyright' is on lines 1-27 (a fairly easygoing licence!)

In lines 84-87 we create a new parameter called 'Frame' and add it to the Python tag's Userdata. This is the value that the Python Tag script reads on line 30 ('frame = op[c4d.ID_USERDATA,1]').

Adding animation
Next up is the really fun bit, using Python to add animation to our new Userdata parameter. To get there we: create a track for the parameter; get the curve from the track; then create keyframes and add them to that curve.
90    track = c4d.CTrack(pythontag, c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA,c4d.DTYPE_SUBCONTAINER,0),c4d.DescLevel(1,c4d.DTYPE_REAL,0)))
91     pythontag.InsertTrackSorted(track)
Line 90 creates the track, and line 91 adds it to our Python Tag. Hang on what's all that DescID and DescLevel stuff?! The DescID is how we describe to C4D the exact thing we want to add our track to, and it looks confusing as heck. In our case it contains two 'DescLevels', the first one describes the container our parameter is in (the Userdata group in our case - this would be the same for any user data parameters), and the next refers to the actual parameter within that container we want to animate (in our case this is the user data we already added in lines 84-87).

Within each DescLevel the first value is the ID of the parameter, the second is the type, and the third is the owner, which I don't really understand but setting it to 0 seems to work. So our DescID looks for the container (DTYPE_SUBCONTAINER) with an ID of 'c4d.ID_USERDATA' (which happens to equate to 700), and within that container looks for the number parameter (DTYPE_REAL - for 'floating point' numbers, the ones that can have decimal points) with ID 1 (because this is the first userdata we added - if you add more than one user data you can increase this number to refer to the later ones, and you should make sure the type mentioned here matches the type of the parameter you added).

Next we get the track's curve:
93    curve = track.GetCurve()
and we add some keyframes to it:
95    key1 = c4d.CKey()
96     key1.SetTime(curve, doc.GetMinTime())
97     key1.SetValue(curve, doc.GetMinTime().Get() * doc.GetFps())
98     key1.SetTimeRight(curve, c4d.BaseTime(0.5))
To add the keyframes we: create it (line 95); set its time (line 96) - doc.GetMinTime() gives us the first frame in our project; set its value (line 97) - here we convert the first frame from a time value to a number value that we can put in our userdata; then we set the shape of the curve at this keyframe on line 98 by setting the spline control's 'Right' length. Later on we add this keyframe to our animation track's curve:
106    curve.InsertKey(key1)
We do the same stuff to create the second keyframe, modifying the values to put it at the end of the project's timeline and with the project's last frame as its value.

Wrapping up
On lines 110-113 we set the Python Tag's priority (what order its script should be run in relative to the scene as a whole). Then finally we add the Python tag to our selected object:
116    doc.StartUndo()
117     obj.InsertTag(pythontag)
118     doc.AddUndo(c4d.UNDOTYPE_NEW, pythontag)
119     doc.EndUndo()
The actual adding of the tag is done on line 117, the other three lines allow us to undo that (any AddUndos we add between a single pair of StartUndos and EndUndos will all be undone with a single undo - in our case we're only adding a single undoable thing, we don't have to worry about all the animation and code changes we just made to the Python Tag, because if we undo they'll all disappear as our tag is removed anyway).

On line 121 we call 'EventAdd' - this asks C4D to refresh the scene, if we forget this we won't see any new objects we add until the next scene refresh, usually after clicking the mouse.

The very last thing we do on line 123 is print a nice message to the script console to say we've succeeded in adding the retime rig. Woo!

Conclusion
That's it! Get in touch if you have any questions or suggestions.

Oh and here's a quick final tip - if there are any other cache types you'd like to support, this should be possible so long as they include an 'Offset' parameter or similar (which is why the script can't effect Dynamics Body Caches - they don't have an offset). You'd just need to extend the 'find_cacheable_type' function to return its type and add the specific code to the 'pythontagcode_dict' dictionary with that type as its key.
Posted June 20, 2017