🎯 Custom Cinema 4D Python Effector: Pivot-Based Sequential Scaling with Anchor Control
Introduction
In this post, we’ll dive into a powerful Cinema 4D Python Effector that enables precise sequential scaling of clones with pivot-based anchor control. This is ideal for motion designers who want exact control over how and when clones scale—especially when scaling from specific anchor points like bottom, top, left, or center.
We’ll cover:
- What the effector does
- Why pivot-based correction matters
- How to use it in your C4D projects
- The full commented code
- Download & usage instructions
🔧 What This Python Effector Does
This custom effector lets you:
✅ Scale clones from any pivot point (e.g. Bottom Left, Top Center, Middle Right, etc.)
✅ Delay the animation per clone for a ripple or sequence effect
✅ Control scaling duration frame-by-frame
✅ Automatically detect clone dimensions, or use fallback sizes
✅ Ensure scaling maintains visual anchor alignment by applying real-time matrix corrections
📐 Why Pivot Anchoring Matters in MoGraph
By default, MoGraph clones scale from their geometric center. But if you want a clone to grow from the bottom up (as seen in bar charts or UI transitions), you’ll need to calculate the correct offset to fix the anchor in place while scaling.
This effector solves that problem dynamically using pivot correction matrices, so you never have to fudge positions manually again.
🧪 User Data Parameters
To use this effector, add these fields via User Data on your Python Effector object:
Name | Type | Description |
---|---|---|
pivot | Integer (Cycle) | 0–8 options: Bottom Left to Top Right |
delay_per_clone | Integer | Frame delay between clones (e.g. 2) |
scale_duration | Integer | How many frames the scale lasts |
assumed_clone_width | Float | Fallback width if auto-detect fails |
assumed_clone_height | Float | Fallback height |
assumed_clone_depth | Float | Fallback depth |
initial_scale | Vector | Start scale (e.g. 1,1,1) |
end_scale | Vector | Final scale (e.g. 1,3,1) |
Pivot Index Mapping:
0 = Bottom Left 1 = Bottom Center 2 = Bottom Right
3 = Middle Left 4 = Middle Center 5 = Middle Right
6 = Top Left 7 = Top Center 8 = Top Right
🧠 How It Works (In Simple Terms)
- Get MoGraph Data: Access clone matrices, count, and timing.
- Detect Clone Size: Automatically get size from Cloner or MoText source, or fallback to user-provided size.
- Calculate Pivot Offset: Based on user-chosen pivot (e.g. bottom-left), determine correction offset.
- Apply Time-based Scaling: Each clone animates based on its index and delay.
- Correct Clone Matrix: The pivot offset is used to keep the visual anchor in place during scale.
📜 The Code
import c4d
import math
"""
User Data Setup (create these via the User Data interface on the Effector):
---------------------------------------------------
1: pivot (Integer, Cycle) - Options (0-indexed):
0 = Bottom Left
1 = Bottom Center
2 = Bottom Right
3 = Middle Left
4 = Middle Center
5 = Middle Right
6 = Top Left
7 = Top Center
8 = Top Right
Default: 4 (Middle Center)
2: delay_per_clone (Integer) - Range: [0, 120]. Default: 2.
3: scale_duration (Integer) - Range: [1, 300]. Default: 30.
4: assumed_clone_width (Float) - Fallback width if auto-detect fails. Default: 100.0.
5: assumed_clone_height (Float) - Fallback height if auto-detect fails. Default: 100.0.
6: assumed_clone_depth (Float) - Fallback depth if auto-detect fails. Default: 100.0.
7: initial_scale (Vector) - Initial scale for the clone as a vector (X, Y, Z).
Default: (1, 1, 1).
8: end_scale (Vector) - End scale for the clone as a vector (X, Y, Z).
Default: (1, 3, 1).
---------------------------------------------------
IMPORTANT:
MoGraph clones are generated with their local origin at the center.
For proper scaling about an anchor (e.g. bottom), we must offset the clone so that the desired anchor remains fixed.
For example, for a cube of 200 in height with a bottom pivot, the bottom (100 units below the center) must remain at its original global position.
We achieve this by applying an extra correction offset.
"""
# Cache for the calculated clone dimensions to avoid recalculating every frame.
cached_clone_dimensions = None # Dictionary with keys: "width", "height", "depth"
cache_attempted = False
def get_pivot_offset(pivot_value, clone_width, clone_height):
"""
Returns a c4d.Vector for the desired pivot position relative to a model with its origin at the center.
Mapping for vertical (Y):
Bottom: desired local position = (0, -clone_height/2)
Middle: (0, 0)
Top: (0, clone_height/2)
Horizontal (X) mapping:
Left: -clone_width/2, Center: 0, Right: clone_width/2.
"""
# Ensure pivot_value is an integer from 0 to 8, default to 4 if out of bounds.
if not isinstance(pivot_value, int) or pivot_value < 0 or pivot_value > 8:
pivot_value = 4
row = pivot_value // 3 # 0: bottom, 1: middle, 2: top.
col = pivot_value % 3 # 0: left, 1: center, 2: right.
# Vertical offset assuming object local origin is at center.
if row == 0: # Bottom: desired local position is -clone_height/2.
offset_y = -clone_height / 2.0
elif row == 1: # Middle: at 0.
offset_y = 0.0
else: # Top: at clone_height/2.
offset_y = clone_height / 2.0
# Horizontal offset.
if col == 0: # Left: -clone_width/2.
offset_x = -clone_width / 2.0
elif col == 1: # Center: 0.
offset_x = 0.0
else: # Right: clone_width/2.
offset_x = clone_width / 2.0
return c4d.Vector(offset_x, offset_y, 0.0)
def main() -> bool:
"""
Sequential Scale From Axis Effector
This effector scales clones from an initial scale to an end scale.
Scaling occurs sequentially (with a delay per clone and a defined scale duration).
A pivot transform is applied to ensure the chosen anchor remains fixed.
NOTE:
Since MoGraph clones are generated with their local origin at the center,
if you select a pivot different from Middle Center, the clone must be repositioned.
For example, for a bottom pivot the clone is shifted upward by half its height so that
its bottom remains at the intended global position.
"""
global cached_clone_dimensions, cache_attempted
# --- Access User Data parameters ---
# Instead of using "or" (which fails when a valid value is 0), we check explicitly.
pivot = op[c4d.ID_USERDATA, 1]
if pivot is None:
pivot = 4
delay_per_clone = op[c4d.ID_USERDATA, 2]
if delay_per_clone is None:
delay_per_clone = 2
scale_duration = op[c4d.ID_USERDATA, 3]
if scale_duration is None:
scale_duration = 30
assumed_width = op[c4d.ID_USERDATA, 4]
if assumed_width is None:
assumed_width = 100.0
assumed_height = op[c4d.ID_USERDATA, 5]
if assumed_height is None:
assumed_height = 100.0
assumed_depth = op[c4d.ID_USERDATA, 6]
if assumed_depth is None:
assumed_depth = 100.0
initial_scale = op[c4d.ID_USERDATA, 7]
if initial_scale is None:
initial_scale = c4d.Vector(1, 1, 1)
end_scale = op[c4d.ID_USERDATA, 8]
if end_scale is None:
end_scale = c4d.Vector(1, 3, 1)
# --- Get MoGraph Data ---
data = c4d.modules.mograph.GeGetMoData(op)
if data is None:
return True # Exit if no MoGraph data.
matrices = data.GetArray(c4d.MODATA_MATRIX)
count = data.GetCount()
if count == 0:
return True # Exit if no clones.
frame = doc.GetTime().GetFrame(doc.GetFps())
# --- Determine Clone Dimensions (Attempt automatic detection once) ---
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() # half-dimensions.
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"]
# --- Compute Correction Offset to Account for Clone's Center Origin ---
# The correction offset is the negative of the desired pivot offset.
# For example, for Bottom Left: desired pivot (relative to center) is (-clone_width/2, -clone_height/2),
# so correction offset becomes (clone_width/2, clone_height/2).
correction = -get_pivot_offset(pivot, clone_width, clone_height)
# --- Process Clones ---
for i in range(count):
original_matrix = matrices[i]
# Calculate animation timing for each clone.
start_frame_for_clone = i * delay_per_clone
effective_frame = frame - start_frame_for_clone
progress = 0.0
if effective_frame >= 0 and scale_duration > 0:
progress = min(1.0, float(effective_frame) / scale_duration)
elif effective_frame >= 0 and scale_duration == 0:
progress = 1.0
# Interpolate per-axis scale between the initial and end scales.
current_scale_x = initial_scale.x + (end_scale.x - initial_scale.x) * progress
current_scale_y = initial_scale.y + (end_scale.y - initial_scale.y) * progress
current_scale_z = initial_scale.z + (end_scale.z - initial_scale.z) * progress
# --- Apply Correction and Pivot-Based Scaling ---
# First, offset the clone so its desired anchor becomes the new origin.
base_matrix = original_matrix * c4d.utils.MatrixMove(correction)
# Then, perform scaling about that fixed anchor using:
# T(-correction) * Scale * T(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))
final_matrix = base_matrix * pivot_scale_transform
matrices[i] = final_matrix
# --- Update MoGraph Data ---
data.SetArray(c4d.MODATA_MATRIX, matrices, op[c4d.FIELDS].HasContent())
return True
🎬 How to Use
- Create a Cloner with objects inside (e.g. cubes, MoText).
- Add a Python Effector and paste the full script below.
- Create all required User Data parameters.
- Adjust settings such as pivot point, delay, and scaling vector.
- Animate your timeline!

🚀 Example Use Cases
- Bar chart or graph animations
- Sequential unfolding UIs
- “Growing” clone elements from the floor
- Creative text reveals using MoText
- Logo animation breakdowns
Hashtags/Tags:#cinema4d
, #mograph
, #pythonEffector
, #c4dtutorial
, #customEffector
, #motiondesign
, #pivotscale
, #c4dpython
, #sequentialanimation
📁 Download
You can download the .c4d
example project file here: