
When animating in Cinema 4D, perfectly aligned keyframes can sometimes feel too mechanical. Adding subtle randomness to timing often makes motion more organic, lively, and visually appealing. This Python script introduces a Randomize Timing Options dialog that lets you offset selected keyframes by random amounts, giving your animation a natural, less predictable rhythm.
What the Script Does
The script opens a dialog where you can set:
- Minimum offset (frames): The smallest number of frames a keyframe can be shifted.
- Maximum offset (frames): The largest number of frames a keyframe can be shifted.
- Restriction option: Decide whether to apply randomization only to objects selected in the Object Manager, or scan the entire scene for selected keys.
- OK/Cancel buttons: Confirm or discard changes safely, with undo support.
Why It’s Useful
This tool is especially handy for motion designers working with:
- Crowd simulations: Randomizing timing makes multiple objects feel less synchronized and more natural.
- Text or logo reveals: Offset letters or elements for dynamic staggered animation.
- Particle systems or clones: Introduce variation without manually adjusting each keyframe.
- Organic motion: Break the robotic feel of perfectly timed sequences.
Instead of manually nudging keyframes, the script applies a random offset within the range you specify. Each object gets its own random value, ensuring variety across the scene.
How It Works Behind the Scenes
- The script scans objects for selected keyframes.
- It calculates a random frame offset between the minimum and maximum values.
- It applies this offset to each selected keyframe’s time, shifting them forward or backward.
- Undo support ensures you can experiment freely without losing your original animation.
By iterating backwards through keyframes, the script avoids index issues that can occur when re‑sorting happens after time changes. This makes it robust and reliable for complex timelines.
import c4d
from c4d import gui
import random
# --- ID Constants for the Dialog ---
DLG_GRP_MAIN = 1000
DLG_LBL_MIN = 1001
DLG_VAL_MIN = 1002
DLG_LBL_MAX = 1003
DLG_VAL_MAX = 1004
DLG_CHK_SEL = 1007 # Checkbox for restriction
DLG_BTN_OK = 1005
DLG_BTN_CANCEL = 1006
class RandomizeOptionsDlg(gui.GeDialog):
def __init__(self):
self.min_val = -10
self.max_val = 10
self.restrict_selection = False # Default to False so it works on all selected keys
self.ok_pressed = False
def CreateLayout(self):
self.SetTitle("Randomize Timing Options")
self.GroupBegin(DLG_GRP_MAIN, c4d.BFH_SCALEFIT, 2, 0)
# Min Value
self.AddStaticText(DLG_LBL_MIN, c4d.BFH_LEFT, name="Min Offset (Frames):")
self.AddEditNumberArrows(DLG_VAL_MIN, c4d.BFH_SCALEFIT)
# Max Value
self.AddStaticText(DLG_LBL_MAX, c4d.BFH_LEFT, name="Max Offset (Frames):")
self.AddEditNumberArrows(DLG_VAL_MAX, c4d.BFH_SCALEFIT)
self.GroupEnd()
# Checkbox option
self.GroupBegin(0, c4d.BFH_SCALEFIT, 1, 0)
self.AddCheckbox(DLG_CHK_SEL, c4d.BFH_LEFT, 0, 0, name="Only Affect Objects Selected in Object Manager")
self.GroupEnd()
# Buttons
self.GroupBegin(0, c4d.BFH_CENTER, 2, 0)
self.AddButton(DLG_BTN_OK, c4d.BFH_SCALE, name="OK")
self.AddButton(DLG_BTN_CANCEL, c4d.BFH_SCALE, name="Cancel")
self.GroupEnd()
return True
def InitValues(self):
self.SetInt32(DLG_VAL_MIN, self.min_val)
self.SetInt32(DLG_VAL_MAX, self.max_val)
self.SetBool(DLG_CHK_SEL, self.restrict_selection)
return True
def Command(self, id, msg):
if id == DLG_BTN_OK:
self.min_val = self.GetInt32(DLG_VAL_MIN)
self.max_val = self.GetInt32(DLG_VAL_MAX)
self.restrict_selection = self.GetBool(DLG_CHK_SEL)
self.ok_pressed = True
self.Close()
elif id == DLG_BTN_CANCEL:
self.Close()
return True
# Helper to iterate entire hierarchy
def GetNextObject(op):
if not op: return None
if op.GetDown(): return op.GetDown()
while not op.GetNext() and op.GetUp():
op = op.GetUp()
return op.GetNext()
# Helper to check if an object has ANY selected keys
def HasSelectedKeys(op):
tracks = op.GetCTracks()
if not tracks: return False
for track in tracks:
curve = track.GetCurve()
if not curve: continue
cnt = curve.GetKeyCount()
for i in range(cnt):
key = curve.GetKey(i)
# Check selection in any Timeline window
if (key.GetNBit(c4d.NBIT_TL1_SELECT) or
key.GetNBit(c4d.NBIT_TL2_SELECT) or
key.GetNBit(c4d.NBIT_TL3_SELECT) or
key.GetNBit(c4d.NBIT_TL4_SELECT)):
return True
return False
def main():
# 1. Open Dialog first
dlg = RandomizeOptionsDlg()
dlg.Open(c4d.DLG_TYPE_MODAL, defaultw=350, defaulth=150)
if not dlg.ok_pressed: return
min_offset = dlg.min_val
max_offset = dlg.max_val
restrict_to_om = dlg.restrict_selection
fps = doc.GetFps()
# 2. Determine which objects to process
objects_to_process = []
if restrict_to_om:
# Old method: Only use objects selected in Object Manager
objects_to_process = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
else:
# New method: Scan EVERYTHING for selected keys
op = doc.GetFirstObject()
while op:
if HasSelectedKeys(op):
objects_to_process.append(op)
op = GetNextObject(op)
if not objects_to_process:
gui.MessageDialog("No objects with selected keys found.")
return
# 3. Apply Randomization
doc.StartUndo()
updates_made = False
for obj in objects_to_process:
# Calculate one random offset for this specific object
actual_min = min(min_offset, max_offset)
actual_max = max(min_offset, max_offset)
random_frames = random.randint(actual_min, actual_max)
# If offset is 0, skip to save resources
if random_frames == 0: continue
offset_time = c4d.BaseTime(random_frames, fps)
doc.AddUndo(c4d.UNDOTYPE_CHANGE, obj)
tracks = obj.GetCTracks()
for track in tracks:
curve = track.GetCurve()
if not curve: continue
cnt = curve.GetKeyCount()
# Iterate backwards to avoid index issues if re-sorting happens
for i in range(cnt - 1, -1, -1):
key = curve.GetKey(i)
# Check if key is selected
if (key.GetNBit(c4d.NBIT_TL1_SELECT) or
key.GetNBit(c4d.NBIT_TL2_SELECT) or
key.GetNBit(c4d.NBIT_TL3_SELECT) or
key.GetNBit(c4d.NBIT_TL4_SELECT)):
new_time = key.GetTime() + offset_time
key.SetTime(curve, new_time)
updates_made = True
doc.EndUndo()
if updates_made:
c4d.EventAdd()
if __name__ == '__main__':
main()