Motion designers often need precise control when aligning objects in 3D space. Whether it’s stacking elements, creating modular layouts, or ensuring clean spacing between animated assets, manual alignment can be time‑consuming and error‑prone. This Python Tag script for Cinema 4D 2025 introduces a smart bounding box alignment system that automates the process.
What the Script Does
The script calculates the true bounding box of an object, including:
- Its children
- Generator caches
- Deformers
It then aligns the current object relative to a linked target object based on user‑defined parameters:
- Link: The object to align to
- Position: A cycle menu (Right, Left, Top, Bottom, Front, Back, Center)
- Gap: A float value in meters to control spacing
By combining bounding box calculations with global/local matrix transformations, the script ensures that objects are positioned exactly where you expect them — even when their axis isn’t centered on their geometry.
Why It’s Useful
For motion designers, this script solves several common pain points:
- Automated alignment: No more eyeballing positions or manually nudging objects.
- Geometry‑aware placement: Works with complex hierarchies, nulls, and cached generators.
- Consistent spacing: The
Gapparameter ensures uniform offsets, perfect for grids, stacks, or modular setups. - Non‑destructive workflow: The script respects existing rotations and scales, only adjusting position.
- Efficiency boost: Ideal for repetitive layout tasks in broadcast design, product visualization, or motion graphics sequences.
How to Use It
- Add a Python Tag to the object you want to align.
- Create the required User Data fields:
- Link (Baselist Link)
- Position (Cycle with options Right, Left, Top, Bottom, Front, Back, Center)
- Gap (Float, unit: meters)
- Paste the script into the Python Tag.
- Drag your target object into the Link field.
- Choose the alignment direction from the Position dropdown.
- Adjust the Gap value to control spacing.
The object will automatically reposition itself relative to the target’s bounding box, maintaining clean and predictable layouts.
Practical Applications
- Title sequences: Align text blocks or logos to animated backgrounds.
- Product showcases: Position multiple items around a hero object with consistent spacing.
- UI/UX mockups in 3D: Stack panels or screens with precise offsets.
- Modular motion graphics: Build grids or arrays where each element snaps into place automatically.
import c4d
# --- 1. SMART BOUNDS FUNCTION ---
# Calculates the true bounding box of an object, including children/caches
def get_smart_bounds(obj):
if not obj: return c4d.Vector(0), c4d.Vector(0), c4d.Matrix()
# Get basic properties
mg = obj.GetMg() # Global Matrix
rad = obj.GetRad() # Radius (Bounding Box half-size)
mp = obj.GetMp() # Midpoint (Center offset from Axis)
# Check for Deformers/Generators Cache
cache = obj.GetCache()
if cache:
# If cache exists, use its matrix and radius
return cache.GetMg() * cache.GetMp(), cache.GetRad(), obj.GetMg()
# If radius is tiny (e.g. a Null), check children to find total size
if rad.x < 0.001:
children = obj.GetChildren()
if children:
min_v = c4d.Vector(1e15)
max_v = c4d.Vector(-1e15)
has_child = False
for child in children:
cmg = child.GetMg()
crad = child.GetRad()
# Check child cache if child is a generator
if crad.x < 0.001 and child.GetCache():
crad = child.GetCache().GetRad()
# Calculate 8 corners of the child's bounding box
for i in range(8):
vx = crad.x if (i&1) else -crad.x
vy = crad.y if (i&2) else -crad.y
vz = crad.z if (i&4) else -crad.z
# Convert local box corner to global space
p_global = cmg * (child.GetMp() + c4d.Vector(vx,vy,vz))
# Convert back to Parent Local space for Min/Max calc
p_local = ~mg * p_global
min_v.x = min(min_v.x, p_local.x)
max_v.x = max(max_v.x, p_local.x)
min_v.y = min(min_v.y, p_local.y)
max_v.y = max(max_v.y, p_local.y)
min_v.z = min(min_v.z, p_local.z)
max_v.z = max(max_v.z, p_local.z)
has_child = True
if has_child:
mp = (min_v + max_v) * 0.5
rad = (max_v - min_v) * 0.5
# Returns: Global Center, Radius, Global Matrix
return mg * mp, rad, mg
def main():
# The object this tag is attached to
my_obj = op.GetObject()
if not my_obj: return
# --- 2. GET USER DATA ---
try:
link_obj = op[c4d.ID_USERDATA, 1] # Link
pos_mode = int(op[c4d.ID_USERDATA, 2]) # Cycle (Right, Left, Top, etc)
gap = op[c4d.ID_USERDATA, 3] # Float
except:
return # User data not set up yet
# Validation
if not link_obj: return
if link_obj.GetGUID() == my_obj.GetGUID(): return # Don't align to self
# --- 3. CALCULATE BOUNDS ---
# Get bounds of the TARGET (Link)
target_center, target_rad, target_mg = get_smart_bounds(link_obj)
# Get bounds of MYSELF
# We only need the Radius and Midpoint to calculate the offset
_, my_rad, _ = get_smart_bounds(my_obj)
# --- 4. DETERMINE OFFSET VECTOR ---
# This calculates the shift needed in the Target's Local Space
offset_vec = c4d.Vector(0)
# X Axis
if pos_mode == 0: # Right (+X)
offset_vec = c4d.Vector(target_rad.x + my_rad.x + gap, 0, 0)
elif pos_mode == 1: # Left (-X)
offset_vec = c4d.Vector(-(target_rad.x + my_rad.x + gap), 0, 0)
# Y Axis
elif pos_mode == 2: # Top (+Y)
offset_vec = c4d.Vector(0, target_rad.y + my_rad.y + gap, 0)
elif pos_mode == 3: # Bottom (-Y)
offset_vec = c4d.Vector(0, -(target_rad.y + my_rad.y + gap), 0)
# Z Axis
elif pos_mode == 4: # Front (+Z)
offset_vec = c4d.Vector(0, 0, target_rad.z + my_rad.z + gap)
elif pos_mode == 5: # Back (-Z)
offset_vec = c4d.Vector(0, 0, -(target_rad.z + my_rad.z + gap))
# Center
elif pos_mode == 6: # Same position
offset_vec = c4d.Vector(0, 0, 0)
# --- 5. APPLY POSITION ---
# 1. Take the Target's Global Center
# 2. Add the Offset Vector (rotated by the Target's rotation)
final_global_pos = target_center + target_mg.MulV(offset_vec)
# 3. Adjust for my own axis offset (if my axis isn't in the center of my geometry)
my_mg = my_obj.GetMg()
my_mp_global_offset = my_mg.MulV(my_obj.GetMp()) # My midpoint offset in global space
# We want the GEOMETRY center to be at final_global_pos
# So we place the Axis at: FinalPos - MidpointOffset
# However, we must keep our own rotation, so we construct a new matrix
new_mg = c4d.Matrix(my_mg) # Copy current rotation/scale
new_mg.off = final_global_pos - new_mg.MulV(my_obj.GetMp())
# Set the matrix only if changed (prevents unnecessary updates)
if (my_obj.GetMg().off - new_mg.off).GetLength() > 0.001:
my_obj.SetMg(new_mg)