Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions crates/processing_ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3694,14 +3694,6 @@ pub extern "C" fn processing_particles_kernel_impulse() -> u64 {
.unwrap_or(0)
}

#[unsafe(no_mangle)]
pub extern "C" fn processing_particles_kernel_flock() -> u64 {
error::clear_error();
error::check(particles_kernel_flock)
.map(|e| e.to_bits())
.unwrap_or(0)
}

#[unsafe(no_mangle)]
pub extern "C" fn processing_particles_kernel_orient() -> u64 {
error::clear_error();
Expand Down
2 changes: 1 addition & 1 deletion crates/processing_pyo3/examples/flocking_duck.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Flocking inside a duck: the GPU boids from flocking_gpu.py, seeded from
# Flocking inside a duck: GPU boids seeded from
# the vertices of the Duck glTF mesh. Each boid remembers its spawn vertex
# in a `home` attribute; a homing force that is negligible near home but
# grows quadratically with distance lets the boids swirl and flock locally
Expand Down
76 changes: 44 additions & 32 deletions crates/processing_pyo3/examples/flocking_gpu.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
# GPU flocking: the boids from flocking.py, moved entirely onto the GPU.
# Positions and velocities live in particle attribute buffers, two compute
# kernels update them each frame, and the flock renders instanced — nothing
# is ever read back to the CPU. Brute-force O(N²) neighbor search is trivial
# for a GPU at this scale; a spatial hash grid is the next step past ~100k.
# GPU flocking: the boids from flocking.py, built from generic particle ops
# over each boid's neighbors. Nothing is read back to the CPU.
from mewnala import *
from math import cos, sin
from random import uniform

BOID_COUNT = 10000
BOID_COUNT = 100000
BOUND = 30.0 # half-extent of the wrapping box
NEIGHBOR_DIST = 5.0
SEPARATION_DIST = 2.5
MAX_SPEED = 10.0 # units per second
MAX_FORCE = 6.0 # units per second²
DT = 1.0 / 60.0

# Pass 1: every boid reads the whole flock's state and writes only its
# steering force. Splitting the read from the write mirrors the CPU
# example's two loops — no boid sees a half-updated neighbor.

# Pass 2: integrate the steering force, wrap at the box edges, and point
# each instanced boid along its velocity via the rotation quaternion.
# Separation steers away from the summed offsets, so its speed is negative.
RULES = [
("separation", -MAX_SPEED, 1.5, "close"),
("alignment", MAX_SPEED, 1.0, "near"),
("cohesion", MAX_SPEED, 1.0, "near"),
]

p = None
boid = None
Expand Down Expand Up @@ -60,15 +57,7 @@ def setup():

directional_light((0.95, 0.9, 0.85), 800.0)

p = create_particles(
BOID_COUNT,
attributes=[
Attribute.position(),
Attribute.rotation(),
Attribute.color(),
Attribute.velocity(),
],
)
p = create_particles(BOID_COUNT)

positions = []
velocities = []
Expand Down Expand Up @@ -98,6 +87,38 @@ def setup():
)


# Reynolds steering. `mask` zeroes it for boids with no neighbors in range,
# which would otherwise brake.
def steer(desired, speed, mask):
p.apply(MAP, desired, op=NORMALIZE, length=speed)
p.apply(COMBINE, desired, "velocity", op=SUB)
p.apply(MAP, desired, op=LIMIT, max_length=MAX_FORCE * DT)
p.apply(COMBINE, desired, mask, op=MUL)


def flock():
p.apply(FIND_NEIGHBORS, grid=grid, radius=NEIGHBOR_DIST)
# what each rule steers toward, summed over the neighbors
p.apply(NEIGHBOR, "position", out="separation", op=SUM,
relative=True, radius=SEPARATION_DIST, falloff=INVERSE_SQUARE)
p.apply(NEIGHBOR, "velocity", out="alignment", op=SUM)
p.apply(NEIGHBOR, "position", out="cohesion", op=SUM, relative=True)
# which boids have any neighbors for each rule
p.apply(NEIGHBOR, out="close", op=COUNT, radius=SEPARATION_DIST)
p.apply(NEIGHBOR, out="near", op=COUNT)
p.apply(MAP, "close", op=GREATER, threshold=0)
p.apply(MAP, "near", op=GREATER, threshold=0)

for i, (rule, speed, weight, mask) in enumerate(RULES):
steer(rule, speed, mask)
if i == 0:
p.apply(MAP, rule, out="force", op=AFFINE, scale=weight)
else:
p.apply(COMBINE, "force", rule, op="add", b_scale=weight)
p.apply(COMBINE, "velocity", "force", op="add")
p.apply(MAP, "velocity", op=LIMIT, min_length=MAX_SPEED * 0.25, max_length=MAX_SPEED)


def draw():
global title_last_time, title_last_frame

Expand All @@ -114,20 +135,11 @@ def draw():
camera_look_at(0.0, 0.0, 0.0)
background(10, 12, 18)

flock()

material(mat)
particles(p, boid)

p.flock(
grid,
sep_distance=SEPARATION_DIST,
neighbor_distance=NEIGHBOR_DIST,
weight_separation=1.5,
weight_alignment=1.0,
weight_cohesion=1.0,
max_speed=MAX_SPEED,
max_force=MAX_FORCE * DT,
min_speed=MAX_SPEED * 0.25,
)
p.apply(INTEGRATE, dt=DT)
p.apply(BOUNDS_BOX, aabb_min=[-BOUND] * 3, aabb_max=[BOUND] * 3, mode=2)
p.apply(ORIENT, forward=[0.0, 0.0, 1.0], up=[0.0, 1.0, 0.0])
Expand Down
30 changes: 0 additions & 30 deletions crates/processing_pyo3/src/particles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,18 +150,6 @@ impl Drop for Grid {
}
}

static FLOCK_COMPUTE: std::sync::Mutex<Option<Entity>> = std::sync::Mutex::new(None);

fn flock_compute() -> PyResult<Entity> {
let mut guard = FLOCK_COMPUTE.lock().unwrap();
if let Some(e) = *guard {
return Ok(e);
}
let e = particles_kernel_flock().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
*guard = Some(e);
Ok(e)
}

static PHYSICS_COMPUTES: std::sync::Mutex<Option<HashMap<String, Entity>>> =
std::sync::Mutex::new(None);

Expand Down Expand Up @@ -1113,24 +1101,6 @@ impl Particles {
Ok(Grid { entity })
}

#[pyo3(signature = (grid, **kwargs))]
pub fn flock(&self, grid: &Grid, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> {
let flock = flock_compute()?;
if let Some(kwargs) = kwargs {
crate::compute::set_compute_kwargs(flock, kwargs)?;
}
let cell = grid.cell_size()?;
let neighbor_distance = kw_f32(kwargs, "neighbor_distance", cell)?.min(cell);
compute_set(
flock,
"neighbor_distance",
shader_value::ShaderValue::Float(neighbor_distance),
)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
particles_flock(self.entity, flock, grid.entity)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

#[staticmethod]
pub fn orient() -> PyResult<Compute> {
let entity =
Expand Down
4 changes: 2 additions & 2 deletions crates/processing_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ pub use particles::{
particles_apply, particles_attribute_add, particles_attributes, particles_buffer,
particles_capacity, particles_connectivity_indirect, particles_create,
particles_create_from_geometry, particles_destroy, particles_emit, particles_emit_gpu,
particles_ensure_attribute, particles_flock, particles_gather, particles_kernel_age,
particles_ensure_attribute, particles_gather, particles_kernel_age,
particles_kernel_attr_combine, particles_kernel_attr_linear, particles_kernel_attr_lookup1d,
particles_kernel_attr_lookup2d, particles_kernel_attr_mix, particles_kernel_attract,
particles_kernel_bounds_box, particles_kernel_bounds_geometry, particles_kernel_bounds_sphere,
particles_kernel_drag, particles_kernel_field, particles_kernel_flock, particles_kernel_force,
particles_kernel_drag, particles_kernel_field, particles_kernel_force,
particles_kernel_impulse, particles_kernel_integrate, particles_kernel_noise,
particles_kernel_orient, particles_kernel_transform, particles_kernel_vortex,
particles_reset_indices, particles_scatter_create, particles_scatter_volume_create,
Expand Down
28 changes: 0 additions & 28 deletions crates/processing_render/src/particles/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,34 +143,6 @@ pub fn particles_emit(
})
}

pub fn particles_flock(
particles_entity: Entity,
flock_entity: Entity,
grid: Entity,
) -> error::Result<()> {
let position = app_mut(|app| {
let world = app.world();
let field = world
.get::<Particles>(particles_entity)
.ok_or(error::ProcessingError::ParticlesNotFound)?;
for (&attr_entity, &buf_entity) in &field.buffers {
let attr = world
.get::<geometry::Attribute>(attr_entity)
.ok_or(error::ProcessingError::InvalidEntity)?;
if attr.name == "position" {
return Ok(buf_entity);
}
}
Err(error::ProcessingError::InvalidArgument(
"particles_flock requires a `position` attribute".to_string(),
))
})?;

grid_build(grid, position)?;
compute_set(flock_entity, "grid", ShaderValue::Grid(grid))?;
particles_apply(particles_entity, flock_entity)
}

static NEIGHBOR_COMPUTE: Mutex<Option<Entity>> = Mutex::new(None);

fn neighbor_compute() -> error::Result<Entity> {
Expand Down
125 changes: 0 additions & 125 deletions crates/processing_render/src/particles/kernels/flock.wgsl

This file was deleted.

16 changes: 0 additions & 16 deletions crates/processing_render/src/particles/kernels/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ impl Plugin for ParticlesKernelsPlugin {
embedded_asset!(app, "bounds_sphere.wgsl");
embedded_asset!(app, "bounds_box.wgsl");
embedded_asset!(app, "impulse.wgsl");
embedded_asset!(app, "flock.wgsl");
embedded_asset!(app, "orient.wgsl");
embedded_asset!(app, "field.wgsl");
embedded_asset!(app, "attr_linear.wgsl");
Expand Down Expand Up @@ -255,21 +254,6 @@ pub fn particles_kernel_impulse() -> error::Result<Entity> {
Ok(entity)
}

pub fn particles_kernel_flock() -> error::Result<Entity> {
let shader = shader_load("embedded://processing_render/particles/kernels/flock.wgsl")?;
let entity = compute_create(shader)?;
set_requires(entity, &["position", "velocity"])?;
compute_set(entity, "sep_distance", ShaderValue::Float(1.2))?;
compute_set(entity, "neighbor_distance", ShaderValue::Float(2.5))?;
compute_set(entity, "weight_separation", ShaderValue::Float(1.5))?;
compute_set(entity, "weight_alignment", ShaderValue::Float(1.0))?;
compute_set(entity, "weight_cohesion", ShaderValue::Float(1.0))?;
compute_set(entity, "max_speed", ShaderValue::Float(0.1))?;
compute_set(entity, "max_force", ShaderValue::Float(0.003))?;
compute_set(entity, "min_speed", ShaderValue::Float(0.02))?;
Ok(entity)
}

pub fn particles_kernel_orient() -> error::Result<Entity> {
let shader = shader_load("embedded://processing_render/particles/kernels/orient.wgsl")?;
let entity = compute_create(shader)?;
Expand Down
Loading
Loading