
The script is designed to generate procedural book pages inside Cinema 4D. Instead of manually modeling each page, the generator builds them automatically based on user-defined parameters. It handles:
- Page count: Define how many pages your book or catalog should have.
- Dimensions: Control width, height, and thickness of each page.
- Animation: Animate page turns with bias, overlap, and timing controls.
- Geometry details: Add rounding, bending, and gaps for realistic page curvature.
- UV mapping: Automatically assigns UV coordinates for textures.
- Selection tags: Creates “Front,” “Back,” and “Edge” polygon selections for easy material assignment.
⚙️ How It Works
The script leverages Cinema 4D’s Python API to construct polygon objects step by step:
- User Data Parameters: Designers can adjust values like page width, height, rounding, and animation percentage directly from the object’s attributes.
- Procedural Geometry: Pages are built using loops that calculate corner rounding, bending offsets, and stacking order.
- Animation Mode: Pages can flip from left to right (-180° to 0°) with overlap control, simulating realistic book-turning.
- Manual Mode: Designers can interpolate between custom start and end angles for static or stylized layouts.
- Selection Tags: Automatically generated tags allow quick material assignments (e.g., different textures for front and back of pages).

For both Front and Back textures, simply multiply page width by page count from left to right. Here, our page width is 200 so the document width is 800.
🧩 Tips for Usage
- Keep rounding values moderate for natural curvature.
- Use bias to control acceleration of page flips (e.g., slow start, fast end).
- Experiment with overlap for cascading page animations.
- Apply textures via UV tags for realistic printed designs.
- Combine with effectors or deformers for advanced stylization.

Conclusion
This Cinema 4D Python Generator script is more than just a technical tool—it’s a creative accelerator. Automating page-turn geometry empowers motion designers to focus on storytelling, design, and animation rather than repetitive modeling. Whether you’re creating a product showcase or a stylized motion sequence, this generator can become a staple in your toolkit.
import c4d
import math
from c4d import Vector, Matrix
# Enums for Selection Tags
TYPE_EDGE = 0
TYPE_FRONT = 1
TYPE_BACK = 2
def main():
if not op: return c4d.BaseObject(c4d.Onull)
# ==========================================
# --- PARAMETERS & USER DATA ---
# ==========================================
def get_ud(id_val, default_val):
try:
val = op[c4d.ID_USERDATA, id_val]
if val is None: return default_val
return val
except:
return default_val
# Standard Params
page_count = max(1, int(get_ud(1, 20)))
p_width = float(get_ud(2, 200.0))
p_height = float(get_ud(3, 300.0))
angle_first_deg = float(get_ud(4, 0.0))
angle_last_deg = float(get_ud(5, 0.0))
spine_offset = float(get_ud(6, 2.0))
p_thick = float(get_ud(7, 2.0))
bias = float(get_ud(8, 1.0))
if bias < 0.001: bias = 0.001
bend_amt = float(get_ud(9, 0.0))
rounding = float(get_ud(10, 0.0))
gap = float(get_ud(11, 0.0))
# Animation Params
anim_mode = bool(get_ud(12, False))
anim_pct = float(get_ud(13, 0.0))
if anim_pct > 1.0: anim_pct /= 100.0
anim_pct = max(0.0, min(1.0, anim_pct))
overlap = float(get_ud(14, 0.0))
if overlap > 1.0: overlap /= 100.0
overlap = max(0.0, min(1.0, overlap))
# --- PRE-CALCULATION & CLAMPING ---
eff_width = p_width - gap
if eff_width < 0.1: eff_width = 0.1
max_r = min(eff_width, p_height) * 0.49
if rounding > max_r: rounding = max_r
if rounding < 0.0: rounding = 0.0
ang_a = c4d.utils.Rad(angle_first_deg)
ang_b = c4d.utils.Rad(angle_last_deg)
points = []
polys = []
uvw_list = []
bs_front = c4d.BaseSelect()
bs_back = c4d.BaseSelect()
bs_edge = c4d.BaseSelect()
poly_counter = 0
# ==========================================
# --- GEOMETRY STEPS GENERATION ---
# ==========================================
x_steps = []
corner_res = 6
mid_res = 2
# 1. Spine Corner
if rounding > 0.001:
for k in range(corner_res + 1):
val = (k / float(corner_res)) * rounding
x_steps.append(val)
else:
x_steps.append(0.0)
# 2. Middle Section
start_mid = rounding
end_mid = eff_width - rounding
if end_mid > start_mid + 0.001:
if abs(x_steps[-1] - start_mid) > 0.001:
x_steps.append(start_mid)
for k in range(1, mid_res + 1):
val = start_mid + (k / float(mid_res)) * (end_mid - start_mid)
x_steps.append(val)
# 3. Tip Corner
if rounding > 0.001:
start_tip = eff_width - rounding
if abs(x_steps[-1] - start_tip) > 0.001:
x_steps.append(start_tip)
for k in range(1, corner_res + 1):
val = start_tip + (k / float(corner_res)) * rounding
x_steps.append(val)
else:
if abs(x_steps[-1] - eff_width) > 0.001:
x_steps.append(eff_width)
# Helpers
def get_bend_y(x_pos):
nx = x_pos / eff_width
return math.sin(nx * math.pi) * bend_amt
def get_z_inset(x_pos):
if rounding < 0.001: return 0.0
if x_pos < rounding:
d = rounding - x_pos
return rounding - math.sqrt(max(0, rounding**2 - d**2))
elif x_pos > (eff_width - rounding):
d = x_pos - (eff_width - rounding)
return rounding - math.sqrt(max(0, rounding**2 - d**2))
return 0.0
hh = p_height / 2.0
ht = p_thick / 2.0
# ==========================================
# --- ANIMATION TIMING ---
# ==========================================
seg_duration = 1.0
seg_step = 0.0
if anim_mode:
if page_count > 1:
denom = 1.0 + (float(page_count) - 1.0) * (1.0 - overlap)
seg_duration = 1.0 / denom
seg_step = seg_duration * (1.0 - overlap)
else:
seg_duration = 1.0
seg_step = 0.0
# ==========================================
# --- MAIN LOOP ---
# ==========================================
for i in range(page_count):
current_angle = 0.0
# --- 1. Determine Angle ---
if anim_mode:
# ANIMATION MODE: Left (-180) to Right (0)
s_start = i * seg_step
s_end = s_start + seg_duration
local_p = 0.0
if anim_pct <= s_start: local_p = 0.0
elif anim_pct >= s_end: local_p = 1.0
else: local_p = (anim_pct - s_start) / (s_end - s_start)
local_p = pow(local_p, bias)
target_start = c4d.utils.Rad(-180)
target_end = c4d.utils.Rad(0)
current_angle = target_start + (target_end - target_start) * local_p
else:
# MANUAL MODE: Interpolate between First and Last User Data
if page_count > 1: raw_t = i / float(page_count - 1)
else: raw_t = 0.0
t = pow(raw_t, bias)
current_angle = ang_a + (ang_b - ang_a) * t
# --- 2. Calculate Matrix & DYNAMIC STACKING ---
m_page = c4d.utils.MatrixRotZ(current_angle)
# Calculate Stacking Heights based on page index
# Left Stack Height: Page 0 is Top (Max Height), Page N is Bottom (0)
h_left = (page_count - 1 - i) * spine_offset
# Right Stack Height: Page 0 is Bottom (0), Page N is Top (Max Height)
h_right = i * spine_offset
# Determine blend factor based on current angle
# -180 (Left) => blend 0.0
# 0 (Right) => blend 1.0
# Normalize Angle (-PI to 0) -> 0 to 1
# Clamp to ensure we don't overshoot if manual angles are crazy
pi = math.pi
norm_angle = (current_angle - (-pi)) / pi
# Clamp
if norm_angle < 0.0: norm_angle = 0.0
if norm_angle > 1.0: norm_angle = 1.0
# Interpolate Y height
# If angle is Left (0.0), we use h_left. If Right (1.0), use h_right.
# This ensures pages physically swap vertical order as they turn.
current_y = h_left * (1.0 - norm_angle) + h_right * norm_angle
m_page.off = Vector(gap, current_y, 0)
# --- 3. UV Info ---
u_page_start = i / float(page_count)
u_page_width = 1.0 / float(page_count)
# --- 4. Generate Geometry ---
page_indices = []
for x_val in x_steps:
bend_y = get_bend_y(x_val)
z_inset = get_z_inset(x_val)
curr_hh = max(0.0, hh - z_inset)
real_x = gap + x_val
pts_local = [
Vector(real_x, ht + bend_y, curr_hh), # Top Back
Vector(real_x, ht + bend_y, -curr_hh), # Top Front
Vector(real_x, -ht + bend_y, -curr_hh), # Bot Front
Vector(real_x, -ht + bend_y, curr_hh) # Bot Back
]
current_ring = []
for pt in pts_local:
final_pt = m_page.Mul(pt)
points.append(final_pt)
current_ring.append(len(points)-1)
page_indices.append(current_ring)
# --- 5. Stitch Polygons ---
def get_v(z_local): return 1.0 - ((z_local + hh) / (2.0 * hh))
for k in range(len(x_steps) - 1):
r_curr = page_indices[k]
r_next = page_indices[k+1]
u_local_1 = x_steps[k] / eff_width
u_local_2 = x_steps[k+1] / eff_width
u1 = u_page_start + u_local_1 * u_page_width
u2 = u_page_start + u_local_2 * u_page_width
z_inset_1 = get_z_inset(x_steps[k])
z_inset_2 = get_z_inset(x_steps[k+1])
z_back_1 = hh - z_inset_1; z_front_1 = -(hh - z_inset_1)
z_back_2 = hh - z_inset_2; z_front_2 = -(hh - z_inset_2)
# Top Face
polys.append(c4d.CPolygon(r_curr[0], r_curr[1], r_next[1], r_next[0]))
bs_front.Select(poly_counter)
uv_a = Vector(u1, get_v(z_back_1), 0); uv_b = Vector(u1, get_v(z_front_1), 0)
uv_c = Vector(u2, get_v(z_front_2), 0); uv_d = Vector(u2, get_v(z_back_2), 0)
uvw_list.append((uv_a, uv_b, uv_c, uv_d))
poly_counter += 1
# Front Edge
polys.append(c4d.CPolygon(r_curr[1], r_curr[2], r_next[2], r_next[1]))
bs_edge.Select(poly_counter)
uvw_list.append((Vector(0), Vector(0), Vector(0), Vector(0)))
poly_counter += 1
# Bottom Face
polys.append(c4d.CPolygon(r_curr[2], r_next[2], r_next[3], r_curr[3]))
bs_back.Select(poly_counter)
uv_a = Vector(u1, get_v(z_front_1), 0); uv_b = Vector(u2, get_v(z_front_2), 0)
uv_c = Vector(u2, get_v(z_back_2), 0); uv_d = Vector(u1, get_v(z_back_1), 0)
uvw_list.append((uv_a, uv_b, uv_c, uv_d))
poly_counter += 1
# Back Edge
polys.append(c4d.CPolygon(r_curr[3], r_curr[0], r_next[0], r_next[3]))
bs_edge.Select(poly_counter)
uvw_list.append((Vector(0), Vector(0), Vector(0), Vector(0)))
poly_counter += 1
# --- 6. Caps ---
def create_cap(ring, reverse):
nonlocal poly_counter
p0, p1, p2, p3 = ring
if reverse: polys.append(c4d.CPolygon(p0, p3, p2, p1))
else: polys.append(c4d.CPolygon(p0, p1, p2, p3))
bs_edge.Select(poly_counter)
uvw_list.append((Vector(0), Vector(0), Vector(0), Vector(0)))
poly_counter += 1
create_cap(page_indices[0], True) # Spine
create_cap(page_indices[-1], False) # Tip
# ==========================================
# --- OUTPUT ---
# ==========================================
res_obj = c4d.PolygonObject(len(points), len(polys))
res_obj.SetAllPoints(points)
for i, p in enumerate(polys): res_obj.SetPolygon(i, p)
uv_tag = c4d.UVWTag(len(polys))
for i, u in enumerate(uvw_list): uv_tag.SetSlow(i, u[0], u[1], u[2], u[3])
res_obj.InsertTag(uv_tag)
def make_tag(name, sel):
t = c4d.SelectionTag(c4d.Tpolygonselection)
t.SetName(name)
t.GetBaseSelect().Merge(sel)
res_obj.InsertTag(t)
make_tag("Front", bs_front)
make_tag("Back", bs_back)
make_tag("Edge", bs_edge)
phong = c4d.BaseTag(c4d.Tphong)
phong[c4d.PHONGTAG_PHONG_ANGLE] = c4d.utils.Rad(80)
res_obj.InsertTag(phong)
res_obj.Message(c4d.MSG_UPDATE)
return res_obj