Advanced Custom Python Effector for Cinema 4D

In the world of 3D motion design, flexibility and control over your animations can make or break a project. This custom Python Effector for Maxon Cinema 4D delivers a powerful and highly customizable animation system that blends deterministic control with randomized variation and easing.
Whether you’re working on MoText animations, clone-based sequences, or dynamic transitions, this script is built to enhance your procedural toolkit with features that are normally only available in advanced setups.
🔧 Key Features
🎛 User Data-Driven Controls
The effector reads values from the user data manager, allowing you to easily configure parameters like:
- Pivot alignment
- Delay per clone
- Scale duration
- Initial and end transform values (position, scale, rotation)
- Randomized transformations
- Easing types
- Color transitions
- Global frame offsets
- Playback control with ping-pong and reverse
🎨 Smooth Easing Functions
Over 20 easing types are supported including:
- Linear
- In/Out/Expo variants
- Bounce
- Spring
- Back
- Custom cubic/quartic interpolations
This allows for natural-feeling motion curves that can mimic real-world acceleration or exaggerated stylized transitions.
🔄 Sequence Modes & Animation Timing
With built-in support for sequence mode, frame-based timing, and custom time scaling, you gain absolute control over when and how each clone animates, including features like:
- Clone delay offset
- Random progress blending
- Ping-pong looping and reversed animations
🎲 Randomization Controls
Control random deviations on a per-clone basis with input vectors for:
- Position
- Rotation
- Scale
This enables subtle organic motions or chaotic explosion-like effects with full control.
🎯 Pivot Customization
Support for 9 pivot options (corners, center, sides) enables precise animation origins, crucial for proper scaling, rotation, and motion relative to anchor points.
🧠 Smart Caching for Efficiency
To avoid unnecessary computation, the effector caches clone dimensions after analyzing MoText or Cloner source object bounding boxes. This allows accurate transformations without heavy runtime recalculations.
💡 Practical Use Cases
- Text animation with bounce and springy movement
- Complex clone array reveals using easing and ping-pong timing
- Randomized object disintegration or build-up sequences
- Color blending over time per clone
📦 How to Use
- Add user data controls as specified in the script (or use a preset UI).
- Paste the code into the Python Effector’s script editor.
- Assign to a Cloner, MoText, or other MoGraph object.
- Animate using frame timing or sequence offset.
🔍 Code Highlights
Ease Functions
pythonCopyEditdef ease_out_bounce(p):
if p < 1 / 2.75:
return 7.5625 * p * p
...
Clone-Specific Animation Timing
pythonCopyEditprogress = (frame - global_start_frame - i * delay_per_clone) / scale_duration
Pivot Offset
pythonCopyEditdef get_pivot_offset(pivot_value, clone_width, clone_height):
...
return c4d.Vector(offset_x, offset_y, 0.0)
Randomized Parameters
pythonCopyEditrandomized_position = random_position * (random.random() - 0.5) * 2
import c4d, math, random
doc = c4d.documents.GetActiveDocument()
cached_clone_dimensions = None
cache_attempted = False
def get_userdata(param_index, default):
# For parameter 25 (sequence mode) we return default immediately if accessing fails.
if param_index == 25:
return default
try:
value = op[c4d.ID_USERDATA, param_index]
if value is None:
return default
if isinstance(default, c4d.Vector) and not isinstance(value, c4d.Vector):
try:
return c4d.Vector(value[0], value[1], value[2])
except Exception as e:
print("Error extracting vector at parameter", param_index, ":", e)
return default
return value
except Exception as e:
print("Error accessing parameter", param_index, ":", e)
return default
def get_pivot_offset(pivot_value, clone_width, clone_height):
if not isinstance(pivot_value, int) or pivot_value < 0 or pivot_value > 8:
pivot_value = 4
row = pivot_value // 3
col = pivot_value % 3
offset_y = -clone_height / 2.0 if row == 0 else (clone_height / 2.0 if row == 2 else 0.0)
offset_x = -clone_width / 2.0 if col == 0 else (clone_width / 2.0 if col == 2 else 0.0)
return c4d.Vector(offset_x, offset_y, 0.0)
def ease_linear(p): return p
def ease_in_quad(p): return p * p
def ease_out_quad(p): return p * (2 - p)
def ease_in_out_quad(p): return 2 * p * p if p < 0.5 else -1 + (4 - 2 * p) * p
def ease_in_cubic(p): return p ** 3
def ease_out_cubic(p): return 1 - (1 - p) ** 3
def ease_in_out_cubic(p): return 4 * p ** 3 if p < 0.5 else 1 - (-2 * p + 2) ** 3 / 2.0
def ease_in_quart(p): return p ** 4
def ease_out_quart(p): return 1 - (1 - p) ** 4
def ease_in_out_quart(p): return 8 * p ** 4 if p < 0.5 else 1 - 8 * (1 - p) ** 4
def ease_out_bounce(p):
if p < 1 / 2.75:
return 7.5625 * p * p
elif p < 2 / 2.75:
p -= 1.5 / 2.75; return 7.5625 * p * p + 0.75
elif p < 2.5 / 2.75:
p -= 2.25 / 2.75; return 7.5625 * p * p + 0.9375
else:
p -= 2.625 / 2.75; return 7.5625 * p * p + 0.984375
def ease_in_bounce(p): return 1 - ease_out_bounce(1 - p)
def ease_in_out_bounce(p):
if p < 0.5:
return ease_in_bounce(2 * p) * 0.5
else:
return ease_out_bounce(2 * p - 1) * 0.5 + 0.5
def ease_spring(p): return 1 - (math.cos(p * math.pi * (0.2 + p ** 3 * 2.5)) * math.exp(-p * 6))
def ease_in_back(p):
c1 = 1.70158; return p * p * ((c1 + 1) * p - c1)
def ease_out_back(p):
c1 = 1.70158; p = p - 1; return 1 + p * p * ((c1 + 1) * p + c1)
def ease_in_out_back(p):
c1 = 1.70158; c2 = c1 * 1.525
if p < 0.5:
return (2 * p) ** 2 * ((c2 + 1) * 2 * p - c2) / 2
else:
p = 2 * p - 2; return (p * p * ((c2 + 1) * p + c2) + 2) / 2
def ease_in_expo(p): return 0 if p == 0 else math.pow(2, 10 * (p - 1))
def ease_out_expo(p): return 1 if p == 1 else 1 - math.pow(2, -10 * p)
def ease_in_out_expo(p):
if p == 0: return 0
if p == 1: return 1
if p < 0.5:
return math.pow(2, 20 * p - 10) / 2
else:
return (2 - math.pow(2, -20 * p + 10)) / 2
def apply_easing(progress, easing_type):
if easing_type == 0: return ease_linear(progress)
elif easing_type == 1: return ease_in_quad(progress)
elif easing_type == 2: return ease_out_quad(progress)
elif easing_type == 3: return ease_in_out_quad(progress)
elif easing_type == 4: return ease_in_cubic(progress)
elif easing_type == 5: return ease_out_cubic(progress)
elif easing_type == 6: return ease_in_out_cubic(progress)
elif easing_type == 7: return ease_in_quart(progress)
elif easing_type == 8: return ease_out_quart(progress)
elif easing_type == 9: return ease_in_out_quart(progress)
elif easing_type == 10: return ease_out_bounce(progress)
elif easing_type == 11: return ease_in_bounce(progress)
elif easing_type == 12: return ease_in_out_bounce(progress)
elif easing_type == 13: return ease_spring(progress)
elif easing_type == 14: return ease_in_back(progress)
elif easing_type == 15: return ease_out_back(progress)
elif easing_type == 16: return ease_in_out_back(progress)
elif easing_type == 17: return ease_in_expo(progress)
elif easing_type == 18: return ease_out_expo(progress)
elif easing_type == 19: return ease_in_out_expo(progress)
else: return progress
def lerp_color(color_a, color_b, t):
return color_a + (color_b - color_a) * t
def main() -> bool:
global cached_clone_dimensions, cache_attempted
pivot = get_userdata(1, 4)
delay_per_clone = get_userdata(2, 2)
scale_duration = get_userdata(3, 30)
assumed_width = get_userdata(4, 100.0)
assumed_height = get_userdata(5, 100.0)
assumed_depth = get_userdata(6, 100.0)
initial_scale = get_userdata(7, c4d.Vector(1, 1, 1))
end_scale = get_userdata(8, c4d.Vector(1, 3, 1))
initial_position = get_userdata(9, c4d.Vector(0, 0, 0))
end_position = get_userdata(10, c4d.Vector(0, 0, 0))
initial_rotation = get_userdata(11, c4d.Vector(0, 0, 0))
end_rotation = get_userdata(12, c4d.Vector(0, 0, 0))
easing_type = get_userdata(13, 0)
initial_color = get_userdata(15, c4d.Vector(1, 1, 1))
end_color = get_userdata(16, c4d.Vector(0, 0, 0))
global_start_frame = get_userdata(22, 0)
time_scale = get_userdata(23, 1.0)
randomization_prog = get_userdata(14, 0.0)
reverse_mode = get_userdata(17, False)
pingpong_mode = get_userdata(18, False)
random_position = get_userdata(19, c4d.Vector(0, 0, 0))
random_rotation = get_userdata(20, c4d.Vector(0, 0, 0))
random_scale = get_userdata(21, c4d.Vector(0, 0, 0))
# Parameter 25 (sequence mode) might be missing; our function returns the default value.
sequence_mode = get_userdata(25, 0)
op[c4d.ID_USERDATA, 2] = delay_per_clone
op[c4d.ID_USERDATA, 3] = scale_duration
op[c4d.ID_USERDATA, 8] = end_scale
op[c4d.ID_USERDATA, 14] = randomization_prog
op[c4d.ID_USERDATA, 19] = random_position
op[c4d.ID_USERDATA, 20] = random_rotation
op[c4d.ID_USERDATA, 23] = time_scale
op[c4d.ID_USERDATA, 21] = random_scale
data = c4d.modules.mograph.GeGetMoData(op)
if data is None:
return True
matrices = data.GetArray(c4d.MODATA_MATRIX)
count = data.GetCount()
if count == 0:
return True
frame = doc.GetTime().GetFrame(doc.GetFps())
clone_width = assumed_width
clone_height = assumed_height
clone_depth = assumed_depth
if not cache_attempted:
cache_attempted = True
try:
generator = data.GetGenerator()
source_object = None
if generator is not None:
type_name = generator.GetTypeName().lower()
if "motext" in type_name:
if generator.GetParameter(c4d.MOTEXTOBJECT_MODE) == c4d.MOTEXTOBJECT_MODE_LINE:
source_object = generator.GetDown()
else:
source_object = generator.GetParameter(c4d.MOTEXTOBJECT_LINK)
elif "cloner" in type_name:
source_object = generator.GetDown()
if source_object:
rad = source_object.GetRad()
if rad.x > 1e-5 and rad.y > 1e-5 and rad.z > 1e-5:
clone_width = rad.x * 2.0
clone_height = rad.y * 2.0
clone_depth = rad.z * 2.0
cached_clone_dimensions = {"width": clone_width, "height": clone_height, "depth": clone_depth}
except Exception as e:
print("Could not auto-detect clone dimensions:", e)
if cached_clone_dimensions is None:
cached_clone_dimensions = {"width": assumed_width, "height": assumed_height, "depth": assumed_depth}
if cached_clone_dimensions is not None:
clone_width = cached_clone_dimensions["width"]
clone_height = cached_clone_dimensions["height"]
clone_depth = cached_clone_dimensions["depth"]
correction = -get_pivot_offset(pivot, clone_width, clone_height)
color_array = [None] * count
order_values = [0.0] * count
for i in range(count):
pos = matrices[i].off
if sequence_mode == 0:
order_values[i] = float(i)
elif sequence_mode == 1:
order_values[i] = pos.x
elif sequence_mode == 2:
order_values[i] = -pos.x
elif sequence_mode == 3:
order_values[i] = -pos.y
elif sequence_mode == 4:
order_values[i] = pos.y
elif sequence_mode == 5:
order_values[i] = pos.z
elif sequence_mode == 6:
order_values[i] = -pos.z
elif sequence_mode == 7:
order_values[i] = pos.GetLength()
elif sequence_mode == 8:
order_values[i] = -pos.GetLength()
else:
order_values[i] = float(i)
indices = list(range(count))
sorted_indices = sorted(indices, key=lambda i: (order_values[i], i))
rank_map = [0] * count
for rank, idx in enumerate(sorted_indices):
rank_map[idx] = rank
normalized_orders = [rank_map[i] / (count - 1) if count > 1 else 0 for i in range(count)]
for i in range(count):
original_matrix = matrices[i]
order_norm = normalized_orders[i]
start_frame_for_clone = order_norm * ((count - 1) * delay_per_clone)
effective_frame = (frame - start_frame_for_clone - global_start_frame) * time_scale
progress = effective_frame / scale_duration if scale_duration > 0 else 1.0
if pingpong_mode:
cycle = (effective_frame % (scale_duration * 2)) / float(scale_duration)
progress = cycle if cycle <= 1.0 else 2 - cycle
else:
progress = min(1.0, progress)
if reverse_mode:
progress = 1 - progress
eased_progress = apply_easing(progress, easing_type)
random.seed(i)
rand_progress = eased_progress + random.uniform(-randomization_prog, randomization_prog)
rand_progress = max(0.0, min(1.0, rand_progress))
scale_factor_x = initial_scale.x + (end_scale.x - initial_scale.x) * rand_progress
scale_factor_y = initial_scale.y + (end_scale.y - initial_scale.y) * rand_progress
scale_factor_z = initial_scale.z + (end_scale.z - initial_scale.z) * rand_progress
scale_rand_x = 1.0 + random.uniform(-random_scale.x, random_scale.x) * (1 - rand_progress)
scale_rand_y = 1.0 + random.uniform(-random_scale.y, random_scale.y) * (1 - rand_progress)
scale_rand_z = 1.0 + random.uniform(-random_scale.z, random_scale.z) * (1 - rand_progress)
current_scale_x = scale_factor_x * scale_rand_x
current_scale_y = scale_factor_y * scale_rand_y
current_scale_z = scale_factor_z * scale_rand_z
base_position = initial_position + (end_position - initial_position) * rand_progress
pos_rand = c4d.Vector(random.uniform(-random_position.x, random_position.x),
random.uniform(-random_position.y, random_position.y),
random.uniform(-random_position.z, random_position.z)) * (1 - rand_progress)
current_position = base_position + pos_rand
base_rot = c4d.Vector(initial_rotation.x + (end_rotation.x - initial_rotation.x) * rand_progress,
initial_rotation.y + (end_rotation.y - initial_rotation.y) * rand_progress,
initial_rotation.z + (end_rotation.z - initial_rotation.z) * rand_progress)
rot_rand = c4d.Vector(random.uniform(-random_rotation.x, random_rotation.x),
random.uniform(-random_rotation.y, random_rotation.y),
random.uniform(-random_rotation.z, random_rotation.z)) * (1 - rand_progress)
current_rot = base_rot + rot_rand
current_rot = c4d.Vector(math.radians(current_rot.x),
math.radians(current_rot.y),
math.radians(current_rot.z))
rot_matrix = c4d.utils.HPBToMatrix(current_rot)
base_matrix = original_matrix * c4d.utils.MatrixMove(correction)
pivot_scale_transform = (c4d.utils.MatrixMove(-correction) *
c4d.utils.MatrixScale(c4d.Vector(current_scale_x, current_scale_y, current_scale_z)) *
c4d.utils.MatrixMove(correction))
scaled_matrix = base_matrix * pivot_scale_transform
trans_matrix = c4d.utils.MatrixMove(current_position)
final_matrix = trans_matrix * rot_matrix * scaled_matrix
matrices[i] = final_matrix
current_color = lerp_color(initial_color, end_color, rand_progress)
color_array[i] = current_color
data.SetArray(c4d.MODATA_MATRIX, matrices, op[c4d.FIELDS].HasContent())
data.SetArray(c4d.MODATA_COLOR, color_array, op[c4d.FIELDS].HasContent())
return True
if __name__ == '__main__':
main()
Also you can download and experiment with the C4D file from the link below:
Download “Scale Position Rotation Python Effector for Cinema 4D” Scale_Position_Rotation_Effector7.zip – Downloaded 0 times – 84.27 KB