
Mosaic Tile Extruder: A Python Generator Script for Cinema 4D
The Mosaic Tile Extruder script for Cinema 4D is a powerful Python generator tool designed specifically for motion designers and 3D artists. It converts images into stunning 3D mosaic tiles by dividing the image into grid tiles and extruding each tile based on its brightness. This functionality allows for dynamic, visually appealing 3D effects that can be customized to fit any creative vision.
How the Script Works
The Mosaic Tile Extruder connects with an image source and processes it into a grid defined by the user. Each tile’s brightness determines its extrusion height, enabling you to create a 3D representation of the image. The script supports various tile shapes, colors, and extrusion settings for maximum creative control.
Key Features
- Dynamic Image Input: Use any image as the base for tile generation.
- Custom Grid Settings: Adjust the number of tiles horizontally and vertically (up to 500×500).
- Multiple Tile Shapes: Choose from Pyramid, Dome, Box, and Beveled Box shapes.
- Brightness-Based Extrusion: Height of tiles adjusts dynamically based on brightness.
- Customizable Gaps and Sizes: Fine-tune tile spacing (gaps) and base dimensions.
- Subdivision Support: Create smoother dome and pyramid shapes with subdivisions.
- Vertex Colors: Apply color from the image to the tiles with customizable intensity.
- Sampling Modes: Choose how brightness is sampled (Average, Center, Max, or Min).
How to Use the Script
Step 1: User Data Setup
To make this script work, specific user data parameters need to be added. Use a separate script or manually add the following user data fields to the Python Generator object:
- image_link: Texture input for the image source.
- grid_x: Horizontal tile count (default: 20).
- grid_y: Vertical tile count (default: 20).
- tile_shape: Tile shape selector (0=Pyramid, 1=Dome, 2=Box, 3=Beveled).
- tile_size: Base tile size in meters (default: 10).
- tile_gap: Gap between tiles (default: 0.5).
- extrude_height: Maximum extrusion height (default: 50).
- extrude_min: Minimum extrusion height (default: 2).
- invert_brightness: Inverts brightness values (default: False).
- sampling_mode: Sampling mode selector (0=Average, 1=Center, 2=Max, 3=Min).
- subdiv: Subdivision level for dome/pyramid tiles (default: 1).
- use_vertex_color: Apply vertex colors (default: True).
- color_intensity: Multiplier for vertex color intensity (default: 1.0).

Step 2: Add the Script
Paste the script into the Python Generator in Cinema 4D.
import c4d
from c4d import Vector, utils
# Mosaic Tile Extruder Python Generator
# Divides bitmap into grid tiles and extrudes based on brightness
"""
USER DATA REQUIREMENTS (Use the separate "Add User Data Script" to add these automatically):
-----------------------------------------------------------------------------------------
1. image_link - Texture - Image source
2. grid_x - Real - Horizontal tile count (1-500, default: 20)
3. grid_y - Real - Vertical tile count (1-500, default: 20)
4. tile_shape - Long - Cycle: 0=Pyramid, 1=Dome, 2=Box, 3=Beveled
5. tile_size - Real - Base tile size in meters (0.1-1000, default: 10)
6. tile_gap - Real - Gap between tiles (0-100, default: 0.5)
7. extrude_height - Real - Maximum extrusion height (0-1000, default: 50)
8. extrude_min - Real - Minimum extrusion height (0-1000, default: 2)
9. invert_brightness- Bool - Invert brightness values (default: False)
10. sampling_mode - Long - Cycle: 0=Average, 1=Center, 2=Max, 3=Min
11. subdiv - Long - Subdivision for dome/pyramid (1-20, default: 1)
12. use_vertex_color - Bool - Apply vertex colors (default: True)
13. color_intensity - Real - Color intensity multiplier (0-2, default: 1.0)
"""
def get_bitmap_pixel(bitmap, x, y, width, height):
"""Sample a pixel from bitmap with bounds checking"""
x = max(0, min(int(x), width - 1))
y = max(0, min(int(y), height - 1))
# Get pixel color - returns [r, g, b] list with values 0-255
pixel = bitmap.GetPixel(x, y)
# Convert to 0-1 range Vector
return Vector(pixel[0] / 255.0, pixel[1] / 255.0, pixel[2] / 255.0)
def calculate_brightness(color):
"""Calculate perceptual brightness from RGB (color is a Vector)"""
# Perceptual brightness formula
return 0.299 * color.x + 0.587 * color.y + 0.114 * color.z
def sample_tile_region(bitmap, start_x, start_y, end_x, end_y, width, height, mode):
"""Sample a tile region and return brightness and average color"""
colors = []
brightnesses = []
if mode == 1: # Center sampling
cx = int((start_x + end_x) / 2)
cy = int((start_y + end_y) / 2)
color = get_bitmap_pixel(bitmap, cx, cy, width, height)
return calculate_brightness(color), color
# For other modes, sample multiple points
samples_x = max(1, int(end_x - start_x))
samples_y = max(1, int(end_y - start_y))
# Limit sampling for performance
step_x = max(1, samples_x // 4)
step_y = max(1, samples_y // 4)
for y in range(int(start_y), int(end_y), step_y):
for x in range(int(start_x), int(end_x), step_x):
color = get_bitmap_pixel(bitmap, x, y, width, height)
colors.append(color)
brightnesses.append(calculate_brightness(color))
if not brightnesses:
return 0.0, Vector(0.5, 0.5, 0.5)
# Calculate average color
avg_color = Vector(
sum(c.x for c in colors) / len(colors),
sum(c.y for c in colors) / len(colors),
sum(c.z for c in colors) / len(colors)
)
# Return brightness based on mode
if mode == 0: # Average
brightness = sum(brightnesses) / len(brightnesses)
elif mode == 2: # Max
brightness = max(brightnesses)
elif mode == 3: # Min
brightness = min(brightnesses)
else:
brightness = sum(brightnesses) / len(brightnesses)
return brightness, avg_color
def create_pyramid(width, height, subdiv):
"""Create a pyramid mesh"""
points = []
polygons = []
# Base vertices
hw = width / 2.0
points.append(Vector(-hw, 0, -hw)) # 0
points.append(Vector(hw, 0, -hw)) # 1
points.append(Vector(hw, 0, hw)) # 2
points.append(Vector(-hw, 0, hw)) # 3
# Apex
apex_idx = len(points)
points.append(Vector(0, height, 0)) # 4
# Base
polygons.append(c4d.CPolygon(0, 3, 2, 1))
# Sides
polygons.append(c4d.CPolygon(0, 1, apex_idx, apex_idx))
polygons.append(c4d.CPolygon(1, 2, apex_idx, apex_idx))
polygons.append(c4d.CPolygon(2, 3, apex_idx, apex_idx))
polygons.append(c4d.CPolygon(3, 0, apex_idx, apex_idx))
return points, polygons
def create_dome(width, height, subdiv):
"""Create a dome/hemisphere mesh"""
points = []
polygons = []
hw = width / 2.0
rings = max(2, subdiv)
segments = max(4, subdiv * 2)
# Base vertices
for i in range(segments):
angle = (i / float(segments)) * 2.0 * 3.14159265359
x = hw * utils.FCos(angle)
z = hw * utils.FSin(angle)
points.append(Vector(x, 0, z))
# Ring vertices
for ring in range(1, rings):
t = ring / float(rings)
ring_height = height * utils.FSin(t * 3.14159265359 / 2.0)
ring_radius = hw * utils.FCos(t * 3.14159265359 / 2.0)
for seg in range(segments):
angle = (seg / float(segments)) * 2.0 * 3.14159265359
x = ring_radius * utils.FCos(angle)
z = ring_radius * utils.FSin(angle)
points.append(Vector(x, ring_height, z))
# Apex
apex_idx = len(points)
points.append(Vector(0, height, 0))
# Bottom face (fan triangulation)
for i in range(segments - 2):
polygons.append(c4d.CPolygon(0, i + 2, i + 1, i + 1))
# Side polygons between base and first ring
for i in range(segments):
next_i = (i + 1) % segments
ring_start = segments
polygons.append(c4d.CPolygon(i, next_i, ring_start + next_i, ring_start + i))
# Polygons between rings
for ring in range(1, rings - 1):
ring_start = segments + (ring - 1) * segments
next_ring_start = segments + ring * segments
for seg in range(segments):
next_seg = (seg + 1) % segments
polygons.append(c4d.CPolygon(
ring_start + seg,
ring_start + next_seg,
next_ring_start + next_seg,
next_ring_start + seg
))
# Top cap to apex
last_ring_start = segments + (rings - 2) * segments
for seg in range(segments):
next_seg = (seg + 1) % segments
polygons.append(c4d.CPolygon(
last_ring_start + seg,
last_ring_start + next_seg,
apex_idx,
apex_idx
))
return points, polygons
def create_box(width, height):
"""Create a simple box"""
points = []
polygons = []
hw = width / 2.0
# Bottom
points.append(Vector(-hw, 0, -hw))
points.append(Vector(hw, 0, -hw))
points.append(Vector(hw, 0, hw))
points.append(Vector(-hw, 0, hw))
# Top
points.append(Vector(-hw, height, -hw))
points.append(Vector(hw, height, -hw))
points.append(Vector(hw, height, hw))
points.append(Vector(-hw, height, hw))
# Polygons
polygons.append(c4d.CPolygon(0, 1, 2, 3)) # Bottom
polygons.append(c4d.CPolygon(4, 7, 6, 5)) # Top
polygons.append(c4d.CPolygon(0, 4, 5, 1)) # Front
polygons.append(c4d.CPolygon(1, 5, 6, 2)) # Right
polygons.append(c4d.CPolygon(2, 6, 7, 3)) # Back
polygons.append(c4d.CPolygon(3, 7, 4, 0)) # Left
return points, polygons
def create_beveled_box(width, height, subdiv):
"""Create a beveled box"""
points = []
polygons = []
hw = width / 2.0
bevel = width * 0.15 # 15% bevel
# Bottom corners
points.extend([
Vector(-hw + bevel, 0, -hw + bevel),
Vector(hw - bevel, 0, -hw + bevel),
Vector(hw - bevel, 0, hw - bevel),
Vector(-hw + bevel, 0, hw - bevel),
])
# Top corners
top_bevel = bevel * 0.5
points.extend([
Vector(-hw + top_bevel, height, -hw + top_bevel),
Vector(hw - top_bevel, height, -hw + top_bevel),
Vector(hw - top_bevel, height, hw - top_bevel),
Vector(-hw + top_bevel, height, hw - top_bevel),
])
# Bottom and top faces
polygons.append(c4d.CPolygon(0, 1, 2, 3))
polygons.append(c4d.CPolygon(4, 7, 6, 5))
# Side faces
polygons.append(c4d.CPolygon(0, 4, 5, 1))
polygons.append(c4d.CPolygon(1, 5, 6, 2))
polygons.append(c4d.CPolygon(2, 6, 7, 3))
polygons.append(c4d.CPolygon(3, 7, 4, 0))
return points, polygons
def main():
# Get texture from user data
texture_data = op[c4d.ID_USERDATA, 1] # image_link (Texture type)
if not texture_data:
return None
# Get bitmap from texture
bitmap = None
# Method 1: Direct string path (if texture is a filepath)
if isinstance(texture_data, str):
bitmap = c4d.bitmaps.BaseBitmap()
if bitmap.InitWith(texture_data)[0] != c4d.IMAGERESULT_OK:
bitmap = None
# Method 2: Check if it's already a BaseBitmap
elif isinstance(texture_data, c4d.bitmaps.BaseBitmap):
bitmap = texture_data
# Method 3: If it's a shader
elif isinstance(texture_data, c4d.BaseShader):
shader = texture_data
doc = c4d.documents.GetActiveDocument()
if doc:
shader.InitRender(doc)
bitmap = shader.GetBitmap()
# Method 4: Try to get filename and load
else:
if hasattr(texture_data, 'GetFilename'):
filename = texture_data.GetFilename()
if filename:
bitmap = c4d.bitmaps.BaseBitmap()
if bitmap.InitWith(filename)[0] == c4d.IMAGERESULT_OK:
pass
if not bitmap:
return None
# Get parameters from user data (with fallback defaults)
grid_x = int(op[c4d.ID_USERDATA, 2]) if op[c4d.ID_USERDATA, 2] else 20
grid_y = int(op[c4d.ID_USERDATA, 3]) if op[c4d.ID_USERDATA, 3] else 20
tile_shape = op[c4d.ID_USERDATA, 4] if op[c4d.ID_USERDATA, 4] is not None else 0
tile_size = op[c4d.ID_USERDATA, 5] if op[c4d.ID_USERDATA, 5] else 10.0
tile_gap = op[c4d.ID_USERDATA, 6] if op[c4d.ID_USERDATA, 6] is not None else 0.5
extrude_height = op[c4d.ID_USERDATA, 7] if op[c4d.ID_USERDATA, 7] else 50.0
extrude_min = op[c4d.ID_USERDATA, 8] if op[c4d.ID_USERDATA, 8] is not None else 2.0
invert_brightness = op[c4d.ID_USERDATA, 9] if op[c4d.ID_USERDATA, 9] is not None else False
sampling_mode = op[c4d.ID_USERDATA, 10] if op[c4d.ID_USERDATA, 10] is not None else 0
subdiv = op[c4d.ID_USERDATA, 11] if op[c4d.ID_USERDATA, 11] else 1
use_vertex_color = op[c4d.ID_USERDATA, 12] if op[c4d.ID_USERDATA, 12] is not None else True
color_intensity = op[c4d.ID_USERDATA, 13] if op[c4d.ID_USERDATA, 13] else 1.0
# Get bitmap dimensions
width = bitmap.GetBw()
height = bitmap.GetBh()
if width <= 0 or height <= 0:
return None
# Create polygon object
result = c4d.BaseObject(c4d.Opolygon)
all_points = []
all_polygons = []
all_colors = []
# Calculate tile dimensions in image space
tile_w = width / float(grid_x)
tile_h = height / float(grid_y)
# Generate tiles
for iy in range(grid_y):
for ix in range(grid_x):
# Calculate image region (flip X axis to match bitmap orientation)
flipped_ix = grid_x - 1 - ix
img_x1 = flipped_ix * tile_w
img_y1 = iy * tile_h
img_x2 = (flipped_ix + 1) * tile_w
img_y2 = (iy + 1) * tile_h
# Sample tile
brightness, avg_color = sample_tile_region(
bitmap, img_x1, img_y1, img_x2, img_y2,
width, height, sampling_mode
)
if invert_brightness:
brightness = 1.0 - brightness
# Calculate extrusion
tile_height = extrude_min + brightness * (extrude_height - extrude_min)
# Calculate tile position
effective_size = tile_size - tile_gap
pos_x = (ix - grid_x / 2.0) * tile_size
pos_z = (iy - grid_y / 2.0) * tile_size
# Generate tile geometry
if tile_shape == 0: # Pyramid
points, polys = create_pyramid(effective_size, tile_height, subdiv)
elif tile_shape == 1: # Dome
points, polys = create_dome(effective_size, tile_height, subdiv)
elif tile_shape == 2: # Box
points, polys = create_box(effective_size, tile_height)
else: # Beveled
points, polys = create_beveled_box(effective_size, tile_height, subdiv)
# Offset points
point_offset = len(all_points)
for pt in points:
all_points.append(Vector(pt.x + pos_x, pt.y, pt.z + pos_z))
if use_vertex_color:
col = avg_color * color_intensity
all_colors.append(Vector(
max(0, min(1, col.x)),
max(0, min(1, col.y)),
max(0, min(1, col.z))
))
# Offset polygon indices
for poly in polys:
all_polygons.append(c4d.CPolygon(
poly.a + point_offset,
poly.b + point_offset,
poly.c + point_offset,
poly.d + point_offset
))
# Set geometry
result.ResizeObject(len(all_points), len(all_polygons))
for i, pt in enumerate(all_points):
result.SetPoint(i, pt)
for i, poly in enumerate(all_polygons):
result.SetPolygon(i, poly)
# Set vertex colors if enabled
if use_vertex_color and all_colors:
vtag = c4d.VertexColorTag(len(all_points))
if vtag:
data = vtag.GetDataAddressW()
for i, col in enumerate(all_colors):
c4d.VertexColorTag.SetColor(data, None, None, i, col)
result.InsertTag(vtag)
result.Message(c4d.MSG_UPDATE)
return resultStep 3: Customize Parameters
Adjust the user data fields to experiment with different tile layouts, shapes, and extrusion settings.
Creative Use Cases
- 3D Data Visualization: Transform grayscale images into 3D height maps.
- Architectural Concepts: Generate tile-based facade designs.
- Motion Graphics: Create dynamic animations of tiles extruding and retracting.
- Abstract Art: Use vibrant images to create colorful, extruded 3D mosaics.
- Product Design: Develop intricate textures or mockups.
Technical Overview of the Script
Tile Sampling and Brightness Calculation
The script includes a robust sampling function to determine brightness from the image. Based on the user-selected sampling mode (average, center, max, or min), the brightness values are calculated and used to define the extrusion height of each tile.
def calculate_brightness(color):
return 0.299 * color.x + 0.587 * color.y + 0.114 * color.z
Shape Generation
Custom functions are included to generate different tile shapes. For example, the create_pyramid function generates a pyramid mesh with appropriate vertices and polygons.
def create_pyramid(width, height, subdiv):
# Pyramid mesh generator
Color Application
Vertex colors extracted from the image are applied to each tile for a vibrant and accurate representation.
if use_vertex_color:
col = avg_color * color_intensity
all_colors.append(Vector(col.x, col.y, col.z))
Tips for Motion Designers
- Experiment with Subdivisions: Higher subdivision levels for dome and pyramid tiles can lead to smoother, more organic results.
- Animate Parameters: Create dynamic animations by keyframing grid size, extrusion height, or tile gaps.
- Combine with Effectors: Use effectors like Random or Shader to add secondary motion to tiles.
- Play with Brightness Inversion: Use the invert brightness option to create inverse effects for dramatic impact.
- Optimize for Performance: Large grids can be computationally intensive; start with smaller grids when testing.
Feel free to download scene file from the link below:
Download “Mosaic Tile Extruder Cinema 4D Python Generator” Mosaic-Tile-Extruder-Python-Generator.zip – Downloaded 0 times – 197.69 KB