Pymunk is a easy-to-use pythonic 2D physics library built on Munk2D
—
Pymunk provides advanced geometry processing capabilities for automatic shape generation, convex hull calculation, polygon simplification, and bitmap-to-geometry conversion using marching squares algorithms.
The pymunk.autogeometry module contains functions for automatic geometry generation and processing, particularly useful for converting images or bitmap data into physics collision shapes.
Functions for processing and simplifying polylines (sequences of connected points).
def is_closed(polyline: list[tuple[float, float]]) -> bool:
"""
Test if a polyline is closed (first vertex equals last vertex).
Args:
polyline: List of (x, y) coordinate tuples
Returns:
True if polyline forms a closed loop
Example:
open_line = [(0, 0), (10, 0), (10, 10)]
closed_line = [(0, 0), (10, 0), (10, 10), (0, 0)]
print(is_closed(open_line)) # False
print(is_closed(closed_line)) # True
"""
def simplify_curves(polyline: list[tuple[float, float]], tolerance: float) -> list[Vec2d]:
"""
Simplify a polyline using the Douglas-Peucker algorithm.
Excellent for smooth or gently curved shapes, removes redundant vertices
while preserving overall shape within tolerance.
Args:
polyline: List of (x, y) coordinate tuples
tolerance: Maximum allowed deviation from original shape
Returns:
Simplified polyline as list of Vec2d points
Example:
# Smooth curve with many points
curve = [(i, math.sin(i * 0.1) * 10) for i in range(100)]
simplified = pymunk.autogeometry.simplify_curves(curve, tolerance=1.0)
print(f"Reduced from {len(curve)} to {len(simplified)} points")
"""
def simplify_vertexes(polyline: list[tuple[float, float]], tolerance: float) -> list[Vec2d]:
"""
Simplify a polyline by discarding "flat" vertices.
Excellent for straight-edged or angular shapes, removes vertices that
don't significantly change direction.
Args:
polyline: List of (x, y) coordinate tuples
tolerance: Maximum allowed angular deviation
Returns:
Simplified polyline with flat vertices removed
Example:
# Rectangle with extra points on edges
rect_with_extras = [
(0, 0), (5, 0), (10, 0), # Extra point at (5,0)
(10, 5), (10, 10), # Extra point at (10,5)
(5, 10), (0, 10), # Extra point at (5,10)
(0, 5), (0, 0) # Extra point at (0,5)
]
simplified = pymunk.autogeometry.simplify_vertexes(rect_with_extras, 0.1)
# Result: [(0,0), (10,0), (10,10), (0,10), (0,0)] - clean rectangle
"""def to_convex_hull(polyline: list[tuple[float, float]], tolerance: float) -> list[Vec2d]:
"""
Calculate the convex hull of a set of points.
Returns the smallest convex polygon that contains all input points.
Useful for creating simplified collision shapes from complex geometry.
Args:
polyline: List of (x, y) coordinate tuples
tolerance: Precision tolerance for hull calculation
Returns:
Convex hull as closed polyline (first and last points are same)
Example:
# Complex concave shape
points = [
(0, 0), (10, 0), (5, 5), # Triangle with internal point
(15, 2), (20, 0), (25, 5), # More complex boundary
(20, 10), (10, 8), (0, 10)
]
hull = pymunk.autogeometry.to_convex_hull(points, 0.1)
# Create physics shape from hull
hull_body = pymunk.Body(body_type=pymunk.Body.STATIC)
hull_shape = pymunk.Poly(hull_body, hull[:-1]) # Exclude duplicate last point
space.add(hull_body, hull_shape)
"""def convex_decomposition(polyline: list[tuple[float, float]], tolerance: float) -> list[list[Vec2d]]:
"""
Decompose a complex polygon into multiple convex polygons.
Breaks down concave shapes into simpler convex parts that can be
used directly as Pymunk collision shapes.
Args:
polyline: List of (x, y) coordinate tuples forming the polygon
tolerance: Approximation tolerance
Returns:
List of convex polygons, each as a list of Vec2d points
Warning:
Self-intersecting polygons may produce overly simplified results.
Example:
# L-shaped polygon (concave)
l_shape = [
(0, 0), (10, 0), (10, 5),
(5, 5), (5, 10), (0, 10), (0, 0)
]
convex_parts = pymunk.autogeometry.convex_decomposition(l_shape, 0.1)
# Create physics shapes for each convex part
body = pymunk.Body(body_type=pymunk.Body.STATIC)
for i, part in enumerate(convex_parts):
shape = pymunk.Poly(body, part)
shape.collision_type = i # Different collision types if needed
space.add(shape)
"""Convert bitmap data (images, heightmaps, etc.) into collision geometry using marching squares algorithms.
def march_soft(
bb: BB,
x_samples: int,
y_samples: int,
threshold: float,
sample_func: Callable[[tuple[float, float]], float]
) -> PolylineSet:
"""
Trace anti-aliased contours from bitmap data using marching squares.
Creates smooth contours by interpolating between sample points.
Excellent for organic shapes and smooth terrain generation.
Args:
bb: Bounding box of area to sample
x_samples: Number of sample points horizontally
y_samples: Number of sample points vertically
threshold: Value threshold for inside/outside determination
sample_func: Function that returns sample value at (x, y) coordinate
Returns:
PolylineSet containing traced contour polylines
Example:
# Convert circular bitmap to geometry
def circle_sample(point):
x, y = point
center = (50, 50)
radius = 20
distance = math.sqrt((x - center[0])**2 + (y - center[1])**2)
return 1.0 if distance <= radius else 0.0
bb = pymunk.BB(0, 0, 100, 100)
polylines = pymunk.autogeometry.march_soft(
bb, x_samples=50, y_samples=50, threshold=0.5, sample_func=circle_sample
)
# Create collision shapes from contours
for polyline in polylines:
if len(polyline) >= 3: # Need at least 3 points for polygon
body = space.static_body
shape = pymunk.Poly(body, polyline)
space.add(shape)
"""
def march_hard(
bb: BB,
x_samples: int,
y_samples: int,
threshold: float,
sample_func: Callable[[tuple[float, float]], float]
) -> PolylineSet:
"""
Trace aliased (pixelated) contours from bitmap data.
Creates hard-edged, pixelated contours following exact sample boundaries.
Better for pixel art, tile-based games, or when exact pixel alignment is needed.
Args:
bb: Bounding box of area to sample
x_samples: Number of sample points horizontally
y_samples: Number of sample points vertically
threshold: Value threshold for inside/outside determination
sample_func: Function that returns sample value at (x, y) coordinate
Returns:
PolylineSet containing traced contour polylines
Example:
# Convert pixel art to collision shapes
pixel_art = [
[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 1, 0, 1, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 0, 0]
]
def pixel_sample(point):
x, y = int(point[0]), int(point[1])
if 0 <= x < 5 and 0 <= y < 5:
return pixel_art[y][x]
return 0
bb = pymunk.BB(0, 0, 5, 5)
polylines = pymunk.autogeometry.march_hard(
bb, x_samples=5, y_samples=5, threshold=0.5, sample_func=pixel_sample
)
"""class PolylineSet:
"""
Collection of polylines returned by marching squares algorithms.
Provides sequence interface for easy iteration and processing.
"""
def __init__(self) -> None:
"""Create empty polyline set."""
def collect_segment(self, v0: tuple[float, float], v1: tuple[float, float]) -> None:
"""
Add line segment to appropriate polyline.
Automatically connects segments into continuous polylines.
Args:
v0: Start point of segment
v1: End point of segment
"""
# Sequence interface
def __len__(self) -> int:
"""Return number of polylines in set."""
def __getitem__(self, index: int) -> list[Vec2d]:
"""Get polyline at index."""
def __iter__(self):
"""Iterate over polylines."""The pymunk.util module contains legacy geometry utilities. These functions are deprecated in favor of autogeometry but remain for compatibility.
import pymunk.util
def is_clockwise(points: list[tuple[float, float]]) -> bool:
"""
Test if points form a clockwise polygon.
Args:
points: List of (x, y) coordinate tuples
Returns:
True if points are in clockwise order
Example:
cw_points = [(0, 0), (10, 0), (10, 10), (0, 10)] # Clockwise
ccw_points = [(0, 0), (0, 10), (10, 10), (10, 0)] # Counter-clockwise
print(pymunk.util.is_clockwise(cw_points)) # True
print(pymunk.util.is_clockwise(ccw_points)) # False
"""
def is_convex(points: list[tuple[float, float]]) -> bool:
"""
Test if polygon is convex.
Args:
points: List of at least 3 (x, y) coordinate tuples
Returns:
True if polygon is convex (no internal angles > 180°)
Example:
square = [(0, 0), (10, 0), (10, 10), (0, 10)]
l_shape = [(0, 0), (10, 0), (10, 5), (5, 5), (5, 10), (0, 10)]
print(pymunk.util.is_convex(square)) # True
print(pymunk.util.is_convex(l_shape)) # False
"""
def is_left(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float]) -> int:
"""
Test if point p2 is left, on, or right of line from p0 to p1.
Args:
p0: First point defining the line
p1: Second point defining the line
p2: Point to test
Returns:
> 0 if p2 is left of the line
= 0 if p2 is on the line
< 0 if p2 is right of the line
"""import pymunk.util
def calc_center(points: list[tuple[float, float]]) -> Vec2d:
"""
Calculate centroid (geometric center) of polygon.
Args:
points: Polygon vertices
Returns:
Center point as Vec2d
"""
def calc_area(points: list[tuple[float, float]]) -> float:
"""
Calculate signed area of polygon.
Returns:
Positive area for counter-clockwise polygons
Negative area for clockwise polygons
"""
def calc_perimeter(points: list[tuple[float, float]]) -> float:
"""Calculate perimeter (total edge length) of polygon."""
def convex_hull(points: list[tuple[float, float]]) -> list[Vec2d]:
"""
Calculate convex hull using Graham scan algorithm.
Returns:
Convex hull vertices in counter-clockwise order
"""
def triangulate(polygon: list[tuple[float, float]]) -> list[list[Vec2d]]:
"""
Triangulate polygon into triangles.
Args:
polygon: Simple polygon vertices
Returns:
List of triangles, each as 3-vertex list
"""import pymunk
import pymunk.autogeometry
import math
def generate_terrain_from_heightmap(heightmap, world_width, world_height):
"""Generate physics terrain from 2D heightmap array"""
height_samples = len(heightmap)
width_samples = len(heightmap[0]) if heightmap else 0
def sample_height(point):
x, y = point
# Normalize to heightmap coordinates
hx = int((x / world_width) * width_samples)
hy = int((y / world_height) * height_samples)
# Clamp to valid range
hx = max(0, min(width_samples - 1, hx))
hy = max(0, min(height_samples - 1, hy))
return heightmap[hy][hx]
# Define terrain bounding box
bb = pymunk.BB(0, 0, world_width, world_height)
# Generate contour lines at different heights
terrain_shapes = []
for height_level in [0.2, 0.4, 0.6, 0.8]:
polylines = pymunk.autogeometry.march_soft(
bb, x_samples=50, y_samples=50,
threshold=height_level, sample_func=sample_height
)
# Convert polylines to collision shapes
for polyline in polylines:
if len(polyline) >= 3:
# Simplify for performance
simplified = pymunk.autogeometry.simplify_curves(polyline, tolerance=2.0)
if len(simplified) >= 3:
body = space.static_body
shape = pymunk.Poly(body, simplified)
shape.friction = 0.7
terrain_shapes.append(shape)
return terrain_shapes
# Example heightmap (mountain shape)
size = 20
heightmap = []
for y in range(size):
row = []
for x in range(size):
# Create mountain shape
center_x, center_y = size // 2, size // 2
distance = math.sqrt((x - center_x)**2 + (y - center_y)**2)
height = max(0, 1.0 - (distance / (size // 2)))
row.append(height)
heightmap.append(row)
# Generate terrain
terrain_shapes = generate_terrain_from_heightmap(heightmap, 800, 600)
space.add(*terrain_shapes)import pymunk
import pymunk.autogeometry
from PIL import Image # Requires Pillow: pip install pillow
def create_collision_from_image(image_path, scale=1.0, threshold=128):
"""Create collision shapes from image alpha channel or brightness"""
# Load image
image = Image.open(image_path).convert('RGBA')
width, height = image.size
pixels = image.load()
def sample_image(point):
x, y = int(point[0] / scale), int(point[1] / scale)
# Clamp coordinates
x = max(0, min(width - 1, x))
y = max(0, min(height - 1, y))
# Sample alpha channel (or brightness for non-alpha images)
r, g, b, a = pixels[x, y]
return 1.0 if a > threshold else 0.0 # Use alpha for transparency
# Define sampling area
bb = pymunk.BB(0, 0, width * scale, height * scale)
# Generate contours
polylines = pymunk.autogeometry.march_soft(
bb, x_samples=width, y_samples=height,
threshold=0.5, sample_func=sample_image
)
# Convert to physics shapes
shapes = []
for polyline in polylines:
if len(polyline) >= 3:
# Simplify contour
simplified = pymunk.autogeometry.simplify_curves(polyline, tolerance=1.0)
if len(simplified) >= 3:
body = space.static_body
shape = pymunk.Poly(body, simplified)
shape.friction = 0.7
shapes.append(shape)
return shapes
# Usage
collision_shapes = create_collision_from_image("sprite.png", scale=2.0)
space.add(*collision_shapes)import pymunk
import pymunk.autogeometry
def create_complex_shape(vertices, body):
"""Create physics shapes for complex (possibly concave) polygon"""
# Check if shape is already convex
if pymunk.util.is_convex(vertices):
# Simple case - create single polygon
shape = pymunk.Poly(body, vertices)
return [shape]
else:
# Complex case - decompose into convex parts
convex_parts = pymunk.autogeometry.convex_decomposition(vertices, tolerance=1.0)
shapes = []
for i, part in enumerate(convex_parts):
if len(part) >= 3:
shape = pymunk.Poly(body, part)
shape.collision_type = i # Different collision types for each part
shapes.append(shape)
return shapes
# Example: L-shaped building
l_building = [
(0, 0), (100, 0), (100, 60), # Main rectangle
(60, 60), (60, 100), (0, 100), # Extension
(0, 0) # Close polygon
]
building_body = pymunk.Body(body_type=pymunk.Body.STATIC)
building_shapes = create_complex_shape(l_building, building_body)
space.add(building_body, *building_shapes)import pymunk
import pymunk.autogeometry
import random
import math
def generate_cave_system(width, height, complexity=0.1):
"""Generate cave system using noise-based sampling"""
def cave_sample(point):
x, y = point
# Multiple octaves of noise for complexity
noise = 0
amplitude = 1
frequency = complexity
for octave in range(4):
# Simple noise approximation (replace with proper noise library)
nx = x * frequency + octave * 100
ny = y * frequency + octave * 100
octave_noise = (math.sin(nx * 0.02) + math.cos(ny * 0.03)) * 0.5
noise += octave_noise * amplitude
amplitude *= 0.5
frequency *= 2
# Add some randomness
noise += (random.random() - 0.5) * 0.2
# Threshold for cave walls
return 1.0 if noise > 0.1 else 0.0
# Generate cave geometry
bb = pymunk.BB(0, 0, width, height)
polylines = pymunk.autogeometry.march_hard(
bb, x_samples=width//10, y_samples=height//10,
threshold=0.5, sample_func=cave_sample
)
# Create collision shapes
cave_shapes = []
for polyline in polylines:
# Simplify cave walls
simplified = pymunk.autogeometry.simplify_vertexes(polyline, tolerance=5.0)
if len(simplified) >= 3:
body = space.static_body
shape = pymunk.Poly(body, simplified)
shape.friction = 0.8
shape.collision_type = 1 # Cave walls
cave_shapes.append(shape)
return cave_shapes
# Generate procedural cave level
cave_walls = generate_cave_system(800, 600, complexity=0.05)
space.add(*cave_walls)import pymunk
import pymunk.autogeometry
def optimize_polygon_for_physics(vertices, target_vertex_count=8):
"""Optimize polygon for physics simulation performance"""
# Step 1: Remove flat vertices
simplified_vertices = pymunk.autogeometry.simplify_vertexes(vertices, tolerance=1.0)
# Step 2: If still too complex, use curve simplification
if len(simplified_vertices) > target_vertex_count:
simplified_vertices = pymunk.autogeometry.simplify_curves(
simplified_vertices, tolerance=2.0
)
# Step 3: If still too complex, use convex hull
if len(simplified_vertices) > target_vertex_count:
simplified_vertices = pymunk.autogeometry.to_convex_hull(
simplified_vertices, tolerance=1.0
)
return simplified_vertices
# Example: Optimize complex shape for physics
complex_shape = [(i, math.sin(i * 0.1) * 20) for i in range(100)] # 100 vertices
optimized_shape = optimize_polygon_for_physics(complex_shape, target_vertex_count=6)
print(f"Reduced from {len(complex_shape)} to {len(optimized_shape)} vertices")
# Create optimized physics shape
body = pymunk.Body(10, pymunk.moment_for_poly(10, optimized_shape))
shape = pymunk.Poly(body, optimized_shape)
space.add(body, shape)Pymunk's geometry processing capabilities provide powerful tools for converting complex artwork, images, and procedural data into efficient physics collision shapes, enabling sophisticated level generation and automatic collision boundary creation for games and simulations.
Install with Tessl CLI
npx tessl i tessl/pypi-pymunk