1- # GPU flocking: the boids from flocking.py, moved entirely onto the GPU.
2- # Positions and velocities live in particle attribute buffers, two compute
3- # kernels update them each frame, and the flock renders instanced — nothing
4- # is ever read back to the CPU. Brute-force O(N²) neighbor search is trivial
5- # for a GPU at this scale; a spatial hash grid is the next step past ~100k.
1+ # GPU flocking: the boids from flocking.py, built from generic particle ops
2+ # over each boid's neighbors. Nothing is read back to the CPU.
63from mewnala import *
74from math import cos , sin
85from random import uniform
96
10- BOID_COUNT = 10000
7+ BOID_COUNT = 100000
118BOUND = 30.0 # half-extent of the wrapping box
129NEIGHBOR_DIST = 5.0
1310SEPARATION_DIST = 2.5
1411MAX_SPEED = 10.0 # units per second
1512MAX_FORCE = 6.0 # units per second²
1613DT = 1.0 / 60.0
1714
18- # Pass 1: every boid reads the whole flock's state and writes only its
19- # steering force. Splitting the read from the write mirrors the CPU
20- # example's two loops — no boid sees a half-updated neighbor.
21-
22- # Pass 2: integrate the steering force, wrap at the box edges, and point
23- # each instanced boid along its velocity via the rotation quaternion.
15+ # Separation steers away from the summed offsets, so its speed is negative.
16+ RULES = [
17+ ( "separation" , - MAX_SPEED , 1.5 , "close" ),
18+ ( "alignment" , MAX_SPEED , 1.0 , "near" ),
19+ ( "cohesion" , MAX_SPEED , 1.0 , "near" ),
20+ ]
2421
2522p = None
2623boid = None
@@ -60,15 +57,7 @@ def setup():
6057
6158 directional_light ((0.95 , 0.9 , 0.85 ), 800.0 )
6259
63- p = create_particles (
64- BOID_COUNT ,
65- attributes = [
66- Attribute .position (),
67- Attribute .rotation (),
68- Attribute .color (),
69- Attribute .velocity (),
70- ],
71- )
60+ p = create_particles (BOID_COUNT )
7261
7362 positions = []
7463 velocities = []
@@ -98,6 +87,38 @@ def setup():
9887 )
9988
10089
90+ # Reynolds steering. `mask` zeroes it for boids with no neighbors in range,
91+ # which would otherwise brake.
92+ def steer (desired , speed , mask ):
93+ p .apply (MAP , desired , op = NORMALIZE , length = speed )
94+ p .apply (COMBINE , desired , "velocity" , op = SUB )
95+ p .apply (MAP , desired , op = LIMIT , max_length = MAX_FORCE * DT )
96+ p .apply (COMBINE , desired , mask , op = MUL )
97+
98+
99+ def flock ():
100+ p .apply (FIND_NEIGHBORS , grid = grid , radius = NEIGHBOR_DIST )
101+ # what each rule steers toward, summed over the neighbors
102+ p .apply (NEIGHBOR , "position" , out = "separation" , op = SUM ,
103+ relative = True , radius = SEPARATION_DIST , falloff = INVERSE_SQUARE )
104+ p .apply (NEIGHBOR , "velocity" , out = "alignment" , op = SUM )
105+ p .apply (NEIGHBOR , "position" , out = "cohesion" , op = SUM , relative = True )
106+ # which boids have any neighbors for each rule
107+ p .apply (NEIGHBOR , out = "close" , op = COUNT , radius = SEPARATION_DIST )
108+ p .apply (NEIGHBOR , out = "near" , op = COUNT )
109+ p .apply (MAP , "close" , op = GREATER , threshold = 0 )
110+ p .apply (MAP , "near" , op = GREATER , threshold = 0 )
111+
112+ for i , (rule , speed , weight , mask ) in enumerate (RULES ):
113+ steer (rule , speed , mask )
114+ if i == 0 :
115+ p .apply (MAP , rule , out = "force" , op = AFFINE , scale = weight )
116+ else :
117+ p .apply (COMBINE , "force" , rule , op = "add" , b_scale = weight )
118+ p .apply (COMBINE , "velocity" , "force" , op = "add" )
119+ p .apply (MAP , "velocity" , op = LIMIT , min_length = MAX_SPEED * 0.25 , max_length = MAX_SPEED )
120+
121+
101122def draw ():
102123 global title_last_time , title_last_frame
103124
@@ -114,20 +135,11 @@ def draw():
114135 camera_look_at (0.0 , 0.0 , 0.0 )
115136 background (10 , 12 , 18 )
116137
138+ flock ()
139+
117140 material (mat )
118141 particles (p , boid )
119142
120- p .flock (
121- grid ,
122- sep_distance = SEPARATION_DIST ,
123- neighbor_distance = NEIGHBOR_DIST ,
124- weight_separation = 1.5 ,
125- weight_alignment = 1.0 ,
126- weight_cohesion = 1.0 ,
127- max_speed = MAX_SPEED ,
128- max_force = MAX_FORCE * DT ,
129- min_speed = MAX_SPEED * 0.25 ,
130- )
131143 p .apply (INTEGRATE , dt = DT )
132144 p .apply (BOUNDS_BOX , aabb_min = [- BOUND ] * 3 , aabb_max = [BOUND ] * 3 , mode = 2 )
133145 p .apply (ORIENT , forward = [0.0 , 0.0 , 1.0 ], up = [0.0 , 1.0 , 0.0 ])
0 commit comments