If you work in motion graphics or product visualization, you know the pain of stacking objects in Cinema 4D. You have two standard options: manually aligning coordinates (tedious) or using a Cloner (restrictive if you need individual control).

While there are many Cinema 4D Python scripts out there for stacking, most of them suffer from two major flaws:
- The “Bounding Box” Gap: When you rotate an object, standard scripts calculate the “Axis Aligned Bounding Box” (AABB), creating huge gaps between objects.
- The “Dirty Cache” Jitter: When you use Generators (like Extrudes or deformers), the script often loses track of the object during frame updates, causing the last object to flicker or snap back to zero.
Today, I’m sharing a robust C4D stacking tool that solves both problems using Matrix Math and smart priority management.

1. Matrix-Accurate Bounding Boxes
Instead of asking “how big is this box globally?”, the script looks at the object’s internal points and multiplies them by the Rotation Matrix. This means if you rotate a cube, the script calculates the exact new height mathematically. No gaps, no floating objects.
2. Generator Stability (The Anti-Jitter Fix)
The script uses a smart fallback system. It first checks for mathematically perfect Primitive data. If that fails, it looks for the internal Object Radius (GetRad), which remains stable even when the geometry cache is dirty or rebuilding. This ensures your objects stay pinned solid, even during heavy animations.
3. Priority Management
The script includes an auto-updater that forces the Python Tag to run at Priority: Generators 400. This ensures the calculation happens after the geometry is created but before the viewport draws, eliminating lag.
How to Use The Script
- Create a Null Object and place your objects inside it as children.
- Add a Python Tag to the Null Object.
- Paste the code below.
- (Optional) Add User Data to the Tag to control:
- Enable/Disable (Bool)
- Axis (Integer: 0=X, 1=Y, 2=Z)
- Gap (Float)
- Random Gap (Float)
- Seed (Integer)
- Center Others (Bool)
import c4d
from c4d import utils as u
import random
# ==============================================================================
# PERFECT STACKER (STABILITY EDITION)
# - Uses GetRad() for Generators to avoid Dirty Cache flickering.
# - Forces position update to prevent 'Snap to Initial'.
# ==============================================================================
def EnsurePriority(tag):
"""
Sets Priority to Generators 400.
"""
pd = tag[c4d.EXPRESSION_PRIORITY]
if pd is None: pd = c4d.PriorityData()
if (pd.GetPriorityValue(c4d.PRIORITYVALUE_MODE) != c4d.CYCLE_GENERATORS or
pd.GetPriorityValue(c4d.PRIORITYVALUE_PRIORITY) != 400):
pd.SetPriorityValue(c4d.PRIORITYVALUE_MODE, c4d.CYCLE_GENERATORS)
pd.SetPriorityValue(c4d.PRIORITYVALUE_PRIORITY, 400)
tag[c4d.EXPRESSION_PRIORITY] = pd
return True
return False
def GetBoundsFromVectors(rad, center, local_m, stack_axis):
"""
Helper to calculate rotated bounds from Radius and Center vectors.
"""
# Create the 8 corners of the box in local space
# rad is the distance from center to corner
min_v = center - rad
max_v = center + rad
corners = [
c4d.Vector(min_v.x, min_v.y, min_v.z),
c4d.Vector(min_v.x, min_v.y, max_v.z),
c4d.Vector(min_v.x, max_v.y, min_v.z),
c4d.Vector(min_v.x, max_v.y, max_v.z),
c4d.Vector(max_v.x, min_v.y, min_v.z),
c4d.Vector(max_v.x, min_v.y, max_v.z),
c4d.Vector(max_v.x, max_v.y, min_v.z),
c4d.Vector(max_v.x, max_v.y, max_v.z),
]
# Transform corners by the Rotation/Scale Matrix
transformed_corners = [local_m * p for p in corners]
# Find Min/Max on the stacking axis
min_val = transformed_corners[0][stack_axis]
max_val = transformed_corners[0][stack_axis]
for p in transformed_corners:
if p[stack_axis] < min_val: min_val = p[stack_axis]
if p[stack_axis] > max_val: max_val = p[stack_axis]
size = max_val - min_val
offset = -min_val
return size, offset
def GetSmartBounds(obj, stack_axis):
"""
Robust bounds calculation.
Priority:
1. Primitives (Math)
2. GetRad (Internal Bounding Box - Stable on Dirty Generators)
3. GetBBox (Geometry Cache - Fallback for Null hierarchies)
"""
# Prepare Local Matrix (Rotation/Scale only, No Position)
ml = obj.GetMl()
ml.off = c4d.Vector(0, 0, 0)
obj_type = obj.GetType()
# --- 1. PRIMITIVES (Mathematical Exactness) ---
# (Simplified for brevity, complex types handled by GetRad below)
if obj_type in [c4d.Ocube, c4d.Osphere, c4d.Oplane]:
# Reuse the logic if needed, but GetRad covers these perfectly too.
pass
# --- 2. GETRAD (Stable for Generators) ---
# This is the FIX. GetRad() returns the Object-Oriented Radius.
# It is often persistent even when the cache is dirty.
rad = obj.GetRad()
# If rad is not zero, use it!
# (We check length squared to avoid float errors)
if rad.GetLengthSquared() > 0.00001:
center = obj.GetMp() # Get Midpoint (Center offset)
return GetBoundsFromVectors(rad, center, ml, stack_axis)
# --- 3. CACHE FALLBACK (For Nulls with Children) ---
# Only if GetRad failed (e.g. a Null Object has 0 rad), we check children/cache.
geo = obj.GetDeformCache() or obj.GetCache() or obj
# u.GetBBox is slower and unstable on dirty generators, hence it is the last resort
center, rad = u.GetBBox(geo, ml)
# If this is also zero, the object is truly empty or loading
if rad.GetLengthSquared() < 0.00001:
return 0.0, 0.0 # Return 0 instead of None to maintain control
mn = center[stack_axis] - rad[stack_axis]
mx = center[stack_axis] + rad[stack_axis]
return (mx - mn), -mn
def main():
tag = op
obj = tag.GetObject()
if not obj: return
if EnsurePriority(tag): return
children = obj.GetChildren()
if not children: return
# --- USER DATA ---
ENABLE = True
AXIS = 1 # 0=X, 1=Y, 2=Z
GAP = 0.0
RANDOM_GAP = 0.0
SEED = 12345
CENTER_OTHERS = True
try:
if tag[c4d.ID_USERDATA, 1] is not None: ENABLE = tag[c4d.ID_USERDATA, 1]
if tag[c4d.ID_USERDATA, 2] is not None: AXIS = tag[c4d.ID_USERDATA, 2]
if tag[c4d.ID_USERDATA, 3] is not None: GAP = tag[c4d.ID_USERDATA, 3]
if tag[c4d.ID_USERDATA, 4] is not None: RANDOM_GAP = tag[c4d.ID_USERDATA, 4]
if tag[c4d.ID_USERDATA, 5] is not None: SEED = tag[c4d.ID_USERDATA, 5]
if tag[c4d.ID_USERDATA, 6] is not None: CENTER_OTHERS = tag[c4d.ID_USERDATA, 6]
except: pass
if not ENABLE: return
random.seed(SEED)
stack_axis = AXIS
cursor = 0.0
epsilon = 0.001
for child in children:
# Measure
size, shift_offset = GetSmartBounds(child, stack_axis)
# CRITICAL FIX: Never 'continue' here.
# Even if size is 0, we MUST set the position to pin the object.
# If we skip, the object snaps to its keyframe/initial position.
# Calculate target
target_val = cursor + shift_offset
# Get current
current_pos = child.GetRelPos()
# Build New Vector
new_pos = c4d.Vector(current_pos)
new_pos[stack_axis] = target_val
if CENTER_OTHERS:
for i in range(3):
if i != stack_axis:
new_pos[i] = 0.0
# DEADZONE CHECK
# We apply the move if it differs significantly.
diff = new_pos - current_pos
if diff.Dot(diff) > (epsilon * epsilon):
child.SetRelPos(new_pos)
# Move Cursor
rnd = random.uniform(0.0, RANDOM_GAP) if RANDOM_GAP > 0 else 0.0
cursor += size + GAP + rnd