🎬 Aligning Textures to Polygons in Cinema 4D with Python
If you’ve ever tried to manually align a texture to a specific polygon in Cinema 4D, you know the pain: fiddling with UVs, guessing projection scales, and praying for pixel-perfect alignment. This Python script changes the game. Designed with motion designers in mind, it automates the creation of a perfectly aligned plane on any selected polygon and maps your image texture with precision—no UVs, no tiling headaches.
Let’s break down why this script is a gem for artists who want control, speed, and accuracy.
🧠 What Does the Script Do?
In short: it lets you select a polygon on any mesh, and it will automatically:
- Create a plane that matches the polygon’s size and orientation
- Ask you to choose an image file
- Apply the image as a material with flat projection
- Scale the texture to match the image’s aspect ratio
- Disable tiling for pixel-perfect placement
All with one click. No manual tweaking. No UV unwrapping.
🔍 Smart Polygon Selection
The script uses a robust three-tiered approach to detect which polygon you’ve selected:
- Polygon Selection Tag – Checks if you’ve tagged a polygon.
- Live Selection – Reads your current selection in the viewport.
- Document Mode Scan – If all else fails, it scans for selected polygons in polygon mode.
If nothing is selected, it defaults to the first polygon. That’s a thoughtful fallback that avoids script failure.
🧱 Plane Creation That Just Works
Once a polygon is identified, the script:
- Extracts the polygon’s vertices in world space
- Calculates its center and orientation
- Determines whether it’s a triangle or quad
- Builds a plane with matching dimensions and orientation using Cinema 4D’s matrix math
This means your plane is not just slapped on top—it’s geometrically aligned to the polygon’s surface. Perfect for projection mapping, decals, or stylized overlays.
🎨 Texture Mapping Without the Headache
After selecting your image, the script:
- Reads the image’s pixel dimensions
- Calculates its aspect ratio
- Scales the texture to fit proportionally on the plane
- Applies a flat projection (no UVs needed!)
- Disables tiling to prevent unwanted repetition
And here’s the kicker: it scales the texture using a critical fix—dividing by 100—to match Cinema 4D’s flat projection expectations. That’s the kind of insider knowledge that saves hours.
🧵 Undo-Friendly and Artist-Safe
The script wraps all operations in an undo chain, so you can revert any step. It also includes detailed debug logging, which is perfect for learning or troubleshooting.
💡 Why Motion Designers Should Care
This script is a motion designer’s ally because:
- It removes technical friction from creative workflows
- It’s intuitive—select a polygon, choose an image, done
- It’s precise—no guesswork, no manual scaling
- It’s flexible—works on any polygonal object
- It’s safe—undo support and error dialogs prevent surprises
Whether you’re projecting UI elements onto geometry, creating stylized transitions, or building modular assets, this tool gives you control and speed.
🛠️ Final Thoughts
This Cinema 4D Python script is a masterclass in artist-friendly automation. It bridges the gap between technical precision and creative freedom, making it ideal for motion designers who want to focus on visuals—not math.
If you’re looking to expand your toolkit with smart, scalable scripting, this is a perfect starting point. And if you’re already deep into Python for C4D, this script is a great template for building more modular, user-centric tools.
import c4d
from c4d import gui, documents
DEBUG = True
def debug(msg):
if DEBUG:
print("[DEBUG]", msg)
def get_selected_polygon_index(obj):
"""
Try multiple methods to get selected polygon, with detailed debugging
"""
debug(f"Total polygons in object: {obj.GetPolygonCount()}")
# Method 1: Check polygon selection tag
sel_tag = obj.GetTag(c4d.Tpolygonselection)
if sel_tag:
debug("Found polygon selection tag")
bs = sel_tag.GetBaseSelect()
selected = [i for i in range(obj.GetPolygonCount()) if bs.IsSelected(i)]
if selected:
debug(f"Selected polygon from tag: {selected[0]}")
return selected[0]
else:
debug("Selection tag exists but no polygons selected in it")
else:
debug("No polygon selection tag found")
# Method 2: Check live polygon selection
sel = obj.GetPolygonS()
if sel:
debug("Found live polygon selection")
selected = [i for i in range(obj.GetPolygonCount()) if sel.IsSelected(i)]
if selected:
debug(f"Selected polygon from live selection: {selected[0]}")
return selected[0]
else:
debug("Live selection exists but no polygons selected")
else:
debug("No live polygon selection found")
# Method 3: Check document mode and active mode selection
doc = documents.GetActiveDocument()
mode = doc.GetMode()
debug(f"Current document mode: {mode}")
if mode == c4d.Mpolygons:
debug("Document is in polygon mode")
bs = obj.GetPolygonS()
if bs:
for i in range(obj.GetPolygonCount()):
if bs.IsSelected(i):
debug(f"Found selected polygon via range scan: {i}")
return i
debug("WARNING: No polygon selected, using first polygon (index 0)")
return 0 if obj.GetPolygonCount() > 0 else None
def create_plane_for_polygon(obj, poly_index):
"""
Create a plane object aligned to the selected polygon
"""
poly = obj.GetPolygon(poly_index)
if poly is None:
debug("ERROR: Invalid polygon index")
gui.MessageDialog("Invalid polygon index.")
return None
debug(f"Polygon indices: a={poly.a}, b={poly.b}, c={poly.c}, d={poly.d}")
# Get object's global matrix
mg = obj.GetMg()
debug(f"Object global matrix: {mg}")
# Get polygon points in world space
p0 = mg.Mul(obj.GetPoint(poly.a))
p1 = mg.Mul(obj.GetPoint(poly.b))
p2 = mg.Mul(obj.GetPoint(poly.c))
# Check if quad or triangle
is_quad = (poly.c != poly.d)
p3 = mg.Mul(obj.GetPoint(poly.d)) if is_quad else p2
debug(f"Point 0 (world): {p0}")
debug(f"Point 1 (world): {p1}")
debug(f"Point 2 (world): {p2}")
debug(f"Point 3 (world): {p3}")
debug(f"Is quad: {is_quad}")
# Calculate center
if is_quad:
center = (p0 + p1 + p2 + p3) * 0.25
else:
center = (p0 + p1 + p2) / 3.0
debug(f"Calculated center: {center}")
# Calculate plane axes from polygon edges
v1 = p1 - p0
v2 = p3 - p0 if is_quad else p2 - p0
debug(f"Edge vector v1 (width): {v1}, length: {v1.GetLength()}")
debug(f"Edge vector v2 (height): {v2}, length: {v2.GetLength()}")
# Calculate width and height
width = v1.GetLength()
height = v2.GetLength()
debug(f"Plane width: {width}, height: {height}")
# Build orthonormal basis
v1_norm = v1.GetNormalized() # tangent (X axis)
normal = v1.Cross(v2).GetNormalized() # normal (Z axis)
v2_norm = normal.Cross(v1_norm).GetNormalized() # bitangent (Y axis)
debug(f"Normalized tangent (X): {v1_norm}")
debug(f"Normalized bitangent (Y): {v2_norm}")
debug(f"Normalized normal (Z): {normal}")
# Create plane matrix with proper orientation
plane_matrix = c4d.Matrix(center, v1_norm, v2_norm, normal)
debug(f"Plane matrix: {plane_matrix}")
# Create plane primitive
plane = c4d.BaseObject(c4d.Oplane)
plane[c4d.PRIM_PLANE_WIDTH] = width if width > 0 else 1.0
plane[c4d.PRIM_PLANE_HEIGHT] = height if height > 0 else 1.0
# Set axis to center
try:
plane[c4d.PRIM_AXIS] = c4d.PRIM_AXIS_CENTER
debug("Set plane axis to center (modern API)")
except:
plane[c4d.PRIM_AXIS] = 4 # fallback
debug("Set plane axis to center (fallback value 4)")
# Apply the matrix to position and orient the plane
plane.SetMg(plane_matrix)
plane.SetName(f"PolygonPlane_{poly_index}")
debug("✓ Plane created successfully")
return plane, width, height
def create_standard_material_with_bitmap(path):
"""
Create a standard Cinema 4D material with bitmap texture
"""
mat = c4d.BaseMaterial(c4d.Mmaterial)
mat.SetName("Plane_Material")
# Create bitmap shader
shader = c4d.BaseList2D(c4d.Xbitmap)
shader[c4d.BITMAPSHADER_FILENAME] = path
# Insert shader into material
mat.InsertShader(shader)
mat[c4d.MATERIAL_COLOR_SHADER] = shader
mat[c4d.MATERIAL_USE_COLOR] = True
# Update material
mat.Message(c4d.MSG_UPDATE)
mat.Update(True, True)
debug(f"✓ Created material with bitmap: {path}")
return mat
def get_image_size(path):
"""
Get pixel dimensions of image file
"""
bmp = c4d.bitmaps.BaseBitmap()
result = bmp.InitWith(path)
if result[0] == c4d.IMAGERESULT_OK:
width, height = bmp.GetSize()
debug(f"Image dimensions: {width}x{height} pixels")
return (width, height)
else:
debug(f"WARNING: Could not load image, using default 1x1")
return (1, 1)
def main():
doc = documents.GetActiveDocument()
obj = doc.GetActiveObject()
debug("="*60)
debug("STARTING POLYGON PLANE CREATION")
debug("="*60)
if obj is None:
gui.MessageDialog("No object selected. Please select a polygon object.")
return
if obj.GetType() != c4d.Opolygon:
gui.MessageDialog(f"Selected object is not a polygon object (type: {obj.GetType()})")
return
debug(f"Selected object: {obj.GetName()}")
# Get selected polygon
poly_index = get_selected_polygon_index(obj)
if poly_index is None:
gui.MessageDialog("Object has no polygons.")
return
debug(f"Using polygon index: {poly_index}")
# Create aligned plane
plane_data = create_plane_for_polygon(obj, poly_index)
if not plane_data:
gui.MessageDialog("Could not create plane from polygon.")
return
plane, plane_w, plane_h = plane_data
# Ask user for image
bmp_path = c4d.storage.LoadDialog(
title="Select an image for the plane",
flags=c4d.FILESELECT_LOAD,
force_suffix="png;jpg;jpeg;tif;tiff;bmp;exr"
)
if not bmp_path:
debug("User cancelled image selection")
return
debug(f"Selected image: {bmp_path}")
# Get image dimensions
img_w, img_h = get_image_size(bmp_path)
img_aspect = img_w / img_h
plane_aspect = plane_w / plane_h
debug(f"Image aspect: {img_aspect:.3f}, Plane aspect: {plane_aspect:.3f}")
# Calculate texture size in world units to fit image proportionally
if img_aspect > plane_aspect:
# Image is wider - fit to plane width
tex_size_x = plane_w
tex_size_y = plane_w / img_aspect
else:
# Image is taller - fit to plane height
tex_size_x = plane_h * img_aspect
tex_size_y = plane_h
debug(f"Texture size (world units): {tex_size_x} x {tex_size_y}")
# *** CRITICAL FIX: Divide by 100 for correct scale ***
# Cinema 4D FLAT projection expects values scaled down by 100
tex_len_x = tex_size_x / 100.0
tex_len_y = tex_size_y / 100.0
debug(f"Texture length (scaled for C4D): {tex_len_x} x {tex_len_y}")
debug(f"This will display as: {tex_len_x * 100:.1f}% x {tex_len_y * 100:.1f}%")
# Start undo chain
doc.StartUndo()
# Insert plane into document
doc.InsertObject(plane)
doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, plane)
debug("✓ Plane inserted into document")
# Create and insert material
mat = create_standard_material_with_bitmap(bmp_path)
doc.InsertMaterial(mat)
doc.AddUndo(c4d.UNDOTYPE_NEW, mat)
debug("✓ Material inserted into document")
# Create texture tag with FLAT projection (no tiling issues!)
tex_tag = c4d.TextureTag()
tex_tag.SetMaterial(mat)
# Use FLAT projection instead of UVW
tex_tag[c4d.TEXTURETAG_PROJECTION] = c4d.TEXTURETAG_PROJECTION_FLAT
# Set texture size (divided by 100 for correct scale)
tex_tag[c4d.TEXTURETAG_LENGTHX] = tex_len_x
tex_tag[c4d.TEXTURETAG_LENGTHY] = tex_len_y
# No offset needed (centered by default with flat projection on centered plane)
tex_tag[c4d.TEXTURETAG_OFFSETX] = 0.0
tex_tag[c4d.TEXTURETAG_OFFSETY] = 0.0
# Disable tiling/seamless
tex_tag[c4d.TEXTURETAG_TILE] = False # This is the key parameter!
debug(f"Texture tag settings:")
debug(f" Projection: FLAT (no tiling)")
debug(f" Length X: {tex_len_x} (displays as {tex_len_x * 100:.1f}%)")
debug(f" Length Y: {tex_len_y} (displays as {tex_len_y * 100:.1f}%)")
debug(f" Tile: False")
plane.InsertTag(tex_tag)
doc.AddUndo(c4d.UNDOTYPE_NEW, tex_tag)
debug("✓ Texture tag created and applied")
# End undo and update
doc.EndUndo()
c4d.EventAdd()
debug("="*60)
debug("✓✓✓ PLANE CREATION COMPLETE ✓✓✓")
debug("="*60)
gui.MessageDialog(
"Plane created successfully!\n\n"
f"Polygon: #{poly_index}\n"
f"Plane: {plane_w:.1f} × {plane_h:.1f} cm\n"
f"Image: {img_w} × {img_h} px\n"
f"Texture Length: {tex_len_x * 100:.1f}% × {tex_len_y * 100:.1f}%\n"
f"Projection: Flat (no tiling)"
)
if __name__ == "__main__":
main()