0
# Geometry Processing and Generation
1
2
Pymunk provides advanced geometry processing capabilities for automatic shape generation, convex hull calculation, polygon simplification, and bitmap-to-geometry conversion using marching squares algorithms.
3
4
## Autogeometry Module
5
6
The `pymunk.autogeometry` module contains functions for automatic geometry generation and processing, particularly useful for converting images or bitmap data into physics collision shapes.
7
8
### Polyline Processing
9
10
Functions for processing and simplifying polylines (sequences of connected points).
11
12
```python { .api }
13
def is_closed(polyline: list[tuple[float, float]]) -> bool:
14
"""
15
Test if a polyline is closed (first vertex equals last vertex).
16
17
Args:
18
polyline: List of (x, y) coordinate tuples
19
20
Returns:
21
True if polyline forms a closed loop
22
23
Example:
24
open_line = [(0, 0), (10, 0), (10, 10)]
25
closed_line = [(0, 0), (10, 0), (10, 10), (0, 0)]
26
27
print(is_closed(open_line)) # False
28
print(is_closed(closed_line)) # True
29
"""
30
31
def simplify_curves(polyline: list[tuple[float, float]], tolerance: float) -> list[Vec2d]:
32
"""
33
Simplify a polyline using the Douglas-Peucker algorithm.
34
35
Excellent for smooth or gently curved shapes, removes redundant vertices
36
while preserving overall shape within tolerance.
37
38
Args:
39
polyline: List of (x, y) coordinate tuples
40
tolerance: Maximum allowed deviation from original shape
41
42
Returns:
43
Simplified polyline as list of Vec2d points
44
45
Example:
46
# Smooth curve with many points
47
curve = [(i, math.sin(i * 0.1) * 10) for i in range(100)]
48
simplified = pymunk.autogeometry.simplify_curves(curve, tolerance=1.0)
49
print(f"Reduced from {len(curve)} to {len(simplified)} points")
50
"""
51
52
def simplify_vertexes(polyline: list[tuple[float, float]], tolerance: float) -> list[Vec2d]:
53
"""
54
Simplify a polyline by discarding "flat" vertices.
55
56
Excellent for straight-edged or angular shapes, removes vertices that
57
don't significantly change direction.
58
59
Args:
60
polyline: List of (x, y) coordinate tuples
61
tolerance: Maximum allowed angular deviation
62
63
Returns:
64
Simplified polyline with flat vertices removed
65
66
Example:
67
# Rectangle with extra points on edges
68
rect_with_extras = [
69
(0, 0), (5, 0), (10, 0), # Extra point at (5,0)
70
(10, 5), (10, 10), # Extra point at (10,5)
71
(5, 10), (0, 10), # Extra point at (5,10)
72
(0, 5), (0, 0) # Extra point at (0,5)
73
]
74
simplified = pymunk.autogeometry.simplify_vertexes(rect_with_extras, 0.1)
75
# Result: [(0,0), (10,0), (10,10), (0,10), (0,0)] - clean rectangle
76
"""
77
```
78
79
### Convex Hull Generation
80
81
```python { .api }
82
def to_convex_hull(polyline: list[tuple[float, float]], tolerance: float) -> list[Vec2d]:
83
"""
84
Calculate the convex hull of a set of points.
85
86
Returns the smallest convex polygon that contains all input points.
87
Useful for creating simplified collision shapes from complex geometry.
88
89
Args:
90
polyline: List of (x, y) coordinate tuples
91
tolerance: Precision tolerance for hull calculation
92
93
Returns:
94
Convex hull as closed polyline (first and last points are same)
95
96
Example:
97
# Complex concave shape
98
points = [
99
(0, 0), (10, 0), (5, 5), # Triangle with internal point
100
(15, 2), (20, 0), (25, 5), # More complex boundary
101
(20, 10), (10, 8), (0, 10)
102
]
103
104
hull = pymunk.autogeometry.to_convex_hull(points, 0.1)
105
106
# Create physics shape from hull
107
hull_body = pymunk.Body(body_type=pymunk.Body.STATIC)
108
hull_shape = pymunk.Poly(hull_body, hull[:-1]) # Exclude duplicate last point
109
space.add(hull_body, hull_shape)
110
"""
111
```
112
113
### Convex Decomposition
114
115
```python { .api }
116
def convex_decomposition(polyline: list[tuple[float, float]], tolerance: float) -> list[list[Vec2d]]:
117
"""
118
Decompose a complex polygon into multiple convex polygons.
119
120
Breaks down concave shapes into simpler convex parts that can be
121
used directly as Pymunk collision shapes.
122
123
Args:
124
polyline: List of (x, y) coordinate tuples forming the polygon
125
tolerance: Approximation tolerance
126
127
Returns:
128
List of convex polygons, each as a list of Vec2d points
129
130
Warning:
131
Self-intersecting polygons may produce overly simplified results.
132
133
Example:
134
# L-shaped polygon (concave)
135
l_shape = [
136
(0, 0), (10, 0), (10, 5),
137
(5, 5), (5, 10), (0, 10), (0, 0)
138
]
139
140
convex_parts = pymunk.autogeometry.convex_decomposition(l_shape, 0.1)
141
142
# Create physics shapes for each convex part
143
body = pymunk.Body(body_type=pymunk.Body.STATIC)
144
for i, part in enumerate(convex_parts):
145
shape = pymunk.Poly(body, part)
146
shape.collision_type = i # Different collision types if needed
147
space.add(shape)
148
"""
149
```
150
151
### Marching Squares - Bitmap to Geometry
152
153
Convert bitmap data (images, heightmaps, etc.) into collision geometry using marching squares algorithms.
154
155
```python { .api }
156
def march_soft(
157
bb: BB,
158
x_samples: int,
159
y_samples: int,
160
threshold: float,
161
sample_func: Callable[[tuple[float, float]], float]
162
) -> PolylineSet:
163
"""
164
Trace anti-aliased contours from bitmap data using marching squares.
165
166
Creates smooth contours by interpolating between sample points.
167
Excellent for organic shapes and smooth terrain generation.
168
169
Args:
170
bb: Bounding box of area to sample
171
x_samples: Number of sample points horizontally
172
y_samples: Number of sample points vertically
173
threshold: Value threshold for inside/outside determination
174
sample_func: Function that returns sample value at (x, y) coordinate
175
176
Returns:
177
PolylineSet containing traced contour polylines
178
179
Example:
180
# Convert circular bitmap to geometry
181
def circle_sample(point):
182
x, y = point
183
center = (50, 50)
184
radius = 20
185
distance = math.sqrt((x - center[0])**2 + (y - center[1])**2)
186
return 1.0 if distance <= radius else 0.0
187
188
bb = pymunk.BB(0, 0, 100, 100)
189
polylines = pymunk.autogeometry.march_soft(
190
bb, x_samples=50, y_samples=50, threshold=0.5, sample_func=circle_sample
191
)
192
193
# Create collision shapes from contours
194
for polyline in polylines:
195
if len(polyline) >= 3: # Need at least 3 points for polygon
196
body = space.static_body
197
shape = pymunk.Poly(body, polyline)
198
space.add(shape)
199
"""
200
201
def march_hard(
202
bb: BB,
203
x_samples: int,
204
y_samples: int,
205
threshold: float,
206
sample_func: Callable[[tuple[float, float]], float]
207
) -> PolylineSet:
208
"""
209
Trace aliased (pixelated) contours from bitmap data.
210
211
Creates hard-edged, pixelated contours following exact sample boundaries.
212
Better for pixel art, tile-based games, or when exact pixel alignment is needed.
213
214
Args:
215
bb: Bounding box of area to sample
216
x_samples: Number of sample points horizontally
217
y_samples: Number of sample points vertically
218
threshold: Value threshold for inside/outside determination
219
sample_func: Function that returns sample value at (x, y) coordinate
220
221
Returns:
222
PolylineSet containing traced contour polylines
223
224
Example:
225
# Convert pixel art to collision shapes
226
pixel_art = [
227
[0, 0, 0, 0, 0],
228
[0, 1, 1, 1, 0],
229
[0, 1, 0, 1, 0],
230
[0, 1, 1, 1, 0],
231
[0, 0, 0, 0, 0]
232
]
233
234
def pixel_sample(point):
235
x, y = int(point[0]), int(point[1])
236
if 0 <= x < 5 and 0 <= y < 5:
237
return pixel_art[y][x]
238
return 0
239
240
bb = pymunk.BB(0, 0, 5, 5)
241
polylines = pymunk.autogeometry.march_hard(
242
bb, x_samples=5, y_samples=5, threshold=0.5, sample_func=pixel_sample
243
)
244
"""
245
```
246
247
### PolylineSet Class
248
249
```python { .api }
250
class PolylineSet:
251
"""
252
Collection of polylines returned by marching squares algorithms.
253
254
Provides sequence interface for easy iteration and processing.
255
"""
256
257
def __init__(self) -> None:
258
"""Create empty polyline set."""
259
260
def collect_segment(self, v0: tuple[float, float], v1: tuple[float, float]) -> None:
261
"""
262
Add line segment to appropriate polyline.
263
264
Automatically connects segments into continuous polylines.
265
266
Args:
267
v0: Start point of segment
268
v1: End point of segment
269
"""
270
271
# Sequence interface
272
def __len__(self) -> int:
273
"""Return number of polylines in set."""
274
275
def __getitem__(self, index: int) -> list[Vec2d]:
276
"""Get polyline at index."""
277
278
def __iter__(self):
279
"""Iterate over polylines."""
280
```
281
282
## Utility Module (Deprecated)
283
284
The `pymunk.util` module contains legacy geometry utilities. These functions are deprecated in favor of `autogeometry` but remain for compatibility.
285
286
### Polygon Analysis
287
288
```python { .api }
289
import pymunk.util
290
291
def is_clockwise(points: list[tuple[float, float]]) -> bool:
292
"""
293
Test if points form a clockwise polygon.
294
295
Args:
296
points: List of (x, y) coordinate tuples
297
298
Returns:
299
True if points are in clockwise order
300
301
Example:
302
cw_points = [(0, 0), (10, 0), (10, 10), (0, 10)] # Clockwise
303
ccw_points = [(0, 0), (0, 10), (10, 10), (10, 0)] # Counter-clockwise
304
305
print(pymunk.util.is_clockwise(cw_points)) # True
306
print(pymunk.util.is_clockwise(ccw_points)) # False
307
"""
308
309
def is_convex(points: list[tuple[float, float]]) -> bool:
310
"""
311
Test if polygon is convex.
312
313
Args:
314
points: List of at least 3 (x, y) coordinate tuples
315
316
Returns:
317
True if polygon is convex (no internal angles > 180°)
318
319
Example:
320
square = [(0, 0), (10, 0), (10, 10), (0, 10)]
321
l_shape = [(0, 0), (10, 0), (10, 5), (5, 5), (5, 10), (0, 10)]
322
323
print(pymunk.util.is_convex(square)) # True
324
print(pymunk.util.is_convex(l_shape)) # False
325
"""
326
327
def is_left(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float]) -> int:
328
"""
329
Test if point p2 is left, on, or right of line from p0 to p1.
330
331
Args:
332
p0: First point defining the line
333
p1: Second point defining the line
334
p2: Point to test
335
336
Returns:
337
> 0 if p2 is left of the line
338
= 0 if p2 is on the line
339
< 0 if p2 is right of the line
340
"""
341
```
342
343
### Geometric Calculations
344
345
```python { .api }
346
import pymunk.util
347
348
def calc_center(points: list[tuple[float, float]]) -> Vec2d:
349
"""
350
Calculate centroid (geometric center) of polygon.
351
352
Args:
353
points: Polygon vertices
354
355
Returns:
356
Center point as Vec2d
357
"""
358
359
def calc_area(points: list[tuple[float, float]]) -> float:
360
"""
361
Calculate signed area of polygon.
362
363
Returns:
364
Positive area for counter-clockwise polygons
365
Negative area for clockwise polygons
366
"""
367
368
def calc_perimeter(points: list[tuple[float, float]]) -> float:
369
"""Calculate perimeter (total edge length) of polygon."""
370
371
def convex_hull(points: list[tuple[float, float]]) -> list[Vec2d]:
372
"""
373
Calculate convex hull using Graham scan algorithm.
374
375
Returns:
376
Convex hull vertices in counter-clockwise order
377
"""
378
379
def triangulate(polygon: list[tuple[float, float]]) -> list[list[Vec2d]]:
380
"""
381
Triangulate polygon into triangles.
382
383
Args:
384
polygon: Simple polygon vertices
385
386
Returns:
387
List of triangles, each as 3-vertex list
388
"""
389
```
390
391
## Usage Examples
392
393
### Terrain Generation from Heightmap
394
395
```python { .api }
396
import pymunk
397
import pymunk.autogeometry
398
import math
399
400
def generate_terrain_from_heightmap(heightmap, world_width, world_height):
401
"""Generate physics terrain from 2D heightmap array"""
402
403
height_samples = len(heightmap)
404
width_samples = len(heightmap[0]) if heightmap else 0
405
406
def sample_height(point):
407
x, y = point
408
# Normalize to heightmap coordinates
409
hx = int((x / world_width) * width_samples)
410
hy = int((y / world_height) * height_samples)
411
412
# Clamp to valid range
413
hx = max(0, min(width_samples - 1, hx))
414
hy = max(0, min(height_samples - 1, hy))
415
416
return heightmap[hy][hx]
417
418
# Define terrain bounding box
419
bb = pymunk.BB(0, 0, world_width, world_height)
420
421
# Generate contour lines at different heights
422
terrain_shapes = []
423
for height_level in [0.2, 0.4, 0.6, 0.8]:
424
polylines = pymunk.autogeometry.march_soft(
425
bb, x_samples=50, y_samples=50,
426
threshold=height_level, sample_func=sample_height
427
)
428
429
# Convert polylines to collision shapes
430
for polyline in polylines:
431
if len(polyline) >= 3:
432
# Simplify for performance
433
simplified = pymunk.autogeometry.simplify_curves(polyline, tolerance=2.0)
434
if len(simplified) >= 3:
435
body = space.static_body
436
shape = pymunk.Poly(body, simplified)
437
shape.friction = 0.7
438
terrain_shapes.append(shape)
439
440
return terrain_shapes
441
442
# Example heightmap (mountain shape)
443
size = 20
444
heightmap = []
445
for y in range(size):
446
row = []
447
for x in range(size):
448
# Create mountain shape
449
center_x, center_y = size // 2, size // 2
450
distance = math.sqrt((x - center_x)**2 + (y - center_y)**2)
451
height = max(0, 1.0 - (distance / (size // 2)))
452
row.append(height)
453
heightmap.append(row)
454
455
# Generate terrain
456
terrain_shapes = generate_terrain_from_heightmap(heightmap, 800, 600)
457
space.add(*terrain_shapes)
458
```
459
460
### Image-Based Collision Generation
461
462
```python { .api }
463
import pymunk
464
import pymunk.autogeometry
465
from PIL import Image # Requires Pillow: pip install pillow
466
467
def create_collision_from_image(image_path, scale=1.0, threshold=128):
468
"""Create collision shapes from image alpha channel or brightness"""
469
470
# Load image
471
image = Image.open(image_path).convert('RGBA')
472
width, height = image.size
473
pixels = image.load()
474
475
def sample_image(point):
476
x, y = int(point[0] / scale), int(point[1] / scale)
477
478
# Clamp coordinates
479
x = max(0, min(width - 1, x))
480
y = max(0, min(height - 1, y))
481
482
# Sample alpha channel (or brightness for non-alpha images)
483
r, g, b, a = pixels[x, y]
484
return 1.0 if a > threshold else 0.0 # Use alpha for transparency
485
486
# Define sampling area
487
bb = pymunk.BB(0, 0, width * scale, height * scale)
488
489
# Generate contours
490
polylines = pymunk.autogeometry.march_soft(
491
bb, x_samples=width, y_samples=height,
492
threshold=0.5, sample_func=sample_image
493
)
494
495
# Convert to physics shapes
496
shapes = []
497
for polyline in polylines:
498
if len(polyline) >= 3:
499
# Simplify contour
500
simplified = pymunk.autogeometry.simplify_curves(polyline, tolerance=1.0)
501
502
if len(simplified) >= 3:
503
body = space.static_body
504
shape = pymunk.Poly(body, simplified)
505
shape.friction = 0.7
506
shapes.append(shape)
507
508
return shapes
509
510
# Usage
511
collision_shapes = create_collision_from_image("sprite.png", scale=2.0)
512
space.add(*collision_shapes)
513
```
514
515
### Complex Shape Decomposition
516
517
```python { .api }
518
import pymunk
519
import pymunk.autogeometry
520
521
def create_complex_shape(vertices, body):
522
"""Create physics shapes for complex (possibly concave) polygon"""
523
524
# Check if shape is already convex
525
if pymunk.util.is_convex(vertices):
526
# Simple case - create single polygon
527
shape = pymunk.Poly(body, vertices)
528
return [shape]
529
else:
530
# Complex case - decompose into convex parts
531
convex_parts = pymunk.autogeometry.convex_decomposition(vertices, tolerance=1.0)
532
533
shapes = []
534
for i, part in enumerate(convex_parts):
535
if len(part) >= 3:
536
shape = pymunk.Poly(body, part)
537
shape.collision_type = i # Different collision types for each part
538
shapes.append(shape)
539
540
return shapes
541
542
# Example: L-shaped building
543
l_building = [
544
(0, 0), (100, 0), (100, 60), # Main rectangle
545
(60, 60), (60, 100), (0, 100), # Extension
546
(0, 0) # Close polygon
547
]
548
549
building_body = pymunk.Body(body_type=pymunk.Body.STATIC)
550
building_shapes = create_complex_shape(l_building, building_body)
551
552
space.add(building_body, *building_shapes)
553
```
554
555
### Procedural Level Generation
556
557
```python { .api }
558
import pymunk
559
import pymunk.autogeometry
560
import random
561
import math
562
563
def generate_cave_system(width, height, complexity=0.1):
564
"""Generate cave system using noise-based sampling"""
565
566
def cave_sample(point):
567
x, y = point
568
569
# Multiple octaves of noise for complexity
570
noise = 0
571
amplitude = 1
572
frequency = complexity
573
574
for octave in range(4):
575
# Simple noise approximation (replace with proper noise library)
576
nx = x * frequency + octave * 100
577
ny = y * frequency + octave * 100
578
octave_noise = (math.sin(nx * 0.02) + math.cos(ny * 0.03)) * 0.5
579
580
noise += octave_noise * amplitude
581
amplitude *= 0.5
582
frequency *= 2
583
584
# Add some randomness
585
noise += (random.random() - 0.5) * 0.2
586
587
# Threshold for cave walls
588
return 1.0 if noise > 0.1 else 0.0
589
590
# Generate cave geometry
591
bb = pymunk.BB(0, 0, width, height)
592
polylines = pymunk.autogeometry.march_hard(
593
bb, x_samples=width//10, y_samples=height//10,
594
threshold=0.5, sample_func=cave_sample
595
)
596
597
# Create collision shapes
598
cave_shapes = []
599
for polyline in polylines:
600
# Simplify cave walls
601
simplified = pymunk.autogeometry.simplify_vertexes(polyline, tolerance=5.0)
602
603
if len(simplified) >= 3:
604
body = space.static_body
605
shape = pymunk.Poly(body, simplified)
606
shape.friction = 0.8
607
shape.collision_type = 1 # Cave walls
608
cave_shapes.append(shape)
609
610
return cave_shapes
611
612
# Generate procedural cave level
613
cave_walls = generate_cave_system(800, 600, complexity=0.05)
614
space.add(*cave_walls)
615
```
616
617
### Polygon Optimization Pipeline
618
619
```python { .api }
620
import pymunk
621
import pymunk.autogeometry
622
623
def optimize_polygon_for_physics(vertices, target_vertex_count=8):
624
"""Optimize polygon for physics simulation performance"""
625
626
# Step 1: Remove flat vertices
627
simplified_vertices = pymunk.autogeometry.simplify_vertexes(vertices, tolerance=1.0)
628
629
# Step 2: If still too complex, use curve simplification
630
if len(simplified_vertices) > target_vertex_count:
631
simplified_vertices = pymunk.autogeometry.simplify_curves(
632
simplified_vertices, tolerance=2.0
633
)
634
635
# Step 3: If still too complex, use convex hull
636
if len(simplified_vertices) > target_vertex_count:
637
simplified_vertices = pymunk.autogeometry.to_convex_hull(
638
simplified_vertices, tolerance=1.0
639
)
640
641
return simplified_vertices
642
643
# Example: Optimize complex shape for physics
644
complex_shape = [(i, math.sin(i * 0.1) * 20) for i in range(100)] # 100 vertices
645
optimized_shape = optimize_polygon_for_physics(complex_shape, target_vertex_count=6)
646
647
print(f"Reduced from {len(complex_shape)} to {len(optimized_shape)} vertices")
648
649
# Create optimized physics shape
650
body = pymunk.Body(10, pymunk.moment_for_poly(10, optimized_shape))
651
shape = pymunk.Poly(body, optimized_shape)
652
space.add(body, shape)
653
```
654
655
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.