Simulation

Struct Simulation 

Source
pub struct Simulation<P: ParticleTrait> { /* private fields */ }
Expand description

A particle simulation builder.

Simulation uses the builder pattern to configure all aspects of a particle simulation before running it. The generic parameter P is your particle type, which must derive Particle.

§Type Parameter

  • P: Your particle struct, must implement ParticleTrait (via #[derive(Particle)])

§Builder Methods

MethodRequiredDescription
with_particle_countNoNumber of particles (default: 10,000)
with_boundsNoSimulation cube half-size (default: 1.0)
with_particle_sizeNoBase particle render size (default: 0.015)
with_spawnerYesFunction to create each particle
with_ruleNoAdd behavior rules (can call multiple times)
with_spatial_configConditionalRequired if using neighbor rules

§Example

use rdpe::prelude::*;

#[derive(Particle, Clone)]
struct Boid {
    position: Vec3,
    velocity: Vec3,
}

Simulation::<Boid>::new()
    .with_particle_count(5000)
    .with_bounds(1.0)
    .with_spatial_config(0.1, 32)  // Needed for Separate/Cohere/Align
    .with_spawner(|ctx| Boid {
        position: ctx.random_in_bounds(),
        velocity: ctx.random_direction() * 0.5,
    })
    .with_rule(Rule::Separate { radius: 0.05, strength: 2.0 })
    .with_rule(Rule::Cohere { radius: 0.2, strength: 0.5 })
    .with_rule(Rule::Align { radius: 0.1, strength: 1.0 })
    .with_rule(Rule::SpeedLimit { min: 0.1, max: 1.5 })
    .with_rule(Rule::BounceWalls { restitution: 1.0 })
    .run();

Implementations§

Source§

impl<P: ParticleTrait + 'static> Simulation<P>

Source

pub fn new() -> Self

Create a new simulation with default settings.

§Defaults
  • Particle count: 10,000
  • Bounds: 1.0 (cube from -1.0 to +1.0)
  • No rules (particles won’t move)
  • No spawner (must be set before calling .run())
§Example
let sim = Simulation::<MyParticle>::new();
Source

pub fn with_particle_count(self, count: u32) -> Self

Set the number of particles.

§Arguments
  • count - Total number of particles to simulate
§Performance

Modern GPUs can handle millions of particles, but neighbor-based rules (Separate, Cohere, Align, etc.) add significant per-particle cost. Start with 10,000-50,000 for neighbor-heavy simulations.

§Example
Simulation::<Ball>::new()
    .with_particle_count(100_000)
    // ...
Source

pub fn with_bounds(self, bounds: f32) -> Self

Set the bounding box half-size.

Creates a cube from -bounds to +bounds on all axes. This defines the simulation space that Rule::BounceWalls and Rule::WrapWalls use.

§Arguments
  • bounds - Half-size of the cube (default: 1.0)
§Example
// Create a larger simulation space
Simulation::<Ball>::new()
    .with_bounds(5.0)  // Cube from -5 to +5
    // ...
Source

pub fn with_particle_size(self, size: f32) -> Self

Set the base particle render size.

This is the base size for rendering particles. Each particle’s scale field multiplies this base size, so:

final_size = particle_size * particle.scale

§Arguments
  • size - Base size in clip space (default: 0.015)
§Example
// Larger particles
Simulation::<Ball>::new()
    .with_particle_size(0.03)  // 2x default size
    // ...

// Tiny dots
Simulation::<Ball>::new()
    .with_particle_size(0.005)  // Small points
    // ...
§Note

For per-particle size variation, set the scale field in your spawner or modify it in custom rules. Scale of 1.0 = base size.

Source

pub fn with_spawner<F>(self, spawner: F) -> Self
where F: Fn(&mut SpawnContext) -> P + Send + Sync + 'static,

Set the particle spawner function.

The spawner is called once for each particle at simulation startup. It receives a SpawnContext with helper methods for common spawn patterns like random positions, colors, and structured layouts.

§Arguments
  • spawner - Function (&mut SpawnContext) -> P
§Required

This method must be called before .run(), or the simulation will panic.

§SpawnContext Helpers

The context provides many useful methods:

  • ctx.index, ctx.count, ctx.progress() - spawn info
  • ctx.random_in_sphere(r), ctx.random_on_sphere(r) - positions
  • ctx.random_in_cube(size), ctx.random_in_bounds() - box positions
  • ctx.random_direction() - unit vectors
  • ctx.random_hue(s, v), ctx.rainbow(s, v) - colors
  • ctx.grid_position(cols, rows, layers) - structured layouts
  • ctx.tangent_velocity(pos, speed) - orbital motion
§Examples
§Random sphere distribution
.with_spawner(|ctx| Ball {
    position: ctx.random_in_sphere(0.8),
    velocity: Vec3::ZERO,
})
§Colorful swirl
.with_spawner(|ctx| {
    let pos = ctx.random_in_sphere(0.6);
    Spark {
        position: pos,
        velocity: ctx.tangent_velocity(pos, 0.3),
        color: ctx.rainbow(0.9, 1.0),
    }
})
§Grid layout
.with_spawner(|ctx| Ball {
    position: ctx.grid_position(10, 10, 10),
    velocity: Vec3::ZERO,
})
§Type-based initialization
.with_spawner(|ctx| {
    let is_predator = ctx.index < 50;
    Creature {
        position: ctx.random_in_bounds(),
        velocity: ctx.random_direction() * 0.1,
        particle_type: if is_predator { 1 } else { 0 },
    }
})
Source

pub fn with_rule(self, rule: impl Into<Rule>) -> Self

Add a rule to the simulation.

Rules define particle behavior. They are executed in order every frame, so the sequence matters. Common patterns:

  1. Forces first: Gravity, attraction, neighbor interactions
  2. Constraints: Speed limits, drag
  3. Boundaries last: BounceWalls, WrapWalls
§Arguments
  • rule - The Rule to add
§Multiple rules

Call this method multiple times to add multiple rules:

Simulation::<Ball>::new()
    .with_rule(Rule::Gravity(9.8))
    .with_rule(Rule::Drag(0.5))
    .with_rule(Rule::BounceWalls { restitution: 1.0 })
    // ...
§See Also

See Rule for all available rules and their parameters.

Source

pub fn with_spatial_config(self, cell_size: f32, grid_resolution: u32) -> Self

Configure spatial hashing for neighbor queries.

Required when using neighbor-based rules: Separate, Cohere, Align, Collide, Chase, Evade, Convert, or Typed wrappers around these.

Spatial hashing divides space into a 3D grid to efficiently find nearby particles. Without it, checking all pairs would be O(n²).

§Arguments
  • cell_size - Size of each grid cell. Should be >= your largest interaction radius for best performance.
  • grid_resolution - Number of cells per axis. Must be a power of 2 (8, 16, 32, 64). Higher = more precision but more memory.
§Guidelines
Scenariocell_sizegrid_resolution
Small interactions (radius < 0.1)0.132
Medium interactions (radius 0.1-0.3)0.2-0.332
Large bounds (> 1.0)bounds / 1632 or 64
§Example
Simulation::<Boid>::new()
    .with_spatial_config(0.15, 32)  // Cell size 0.15, 32x32x32 grid
    .with_rule(Rule::Separate { radius: 0.1, strength: 2.0 })
    // ...
§Panics

Will panic if grid_resolution is not a power of 2.

Source

pub fn with_max_neighbors(self, max: u32) -> Self

Set maximum neighbors to process per particle.

This can significantly improve performance in dense simulations by limiting the number of neighbor interactions per particle. Use values like 32-64 for boids-style simulations.

§Arguments
  • max - Maximum neighbors to process (0 = unlimited, default)
§Example
Simulation::<Boid>::new()
    .with_spatial_config(0.1, 32)
    .with_max_neighbors(48)  // Process at most 48 neighbors
    .with_rule(Rule::Separate { radius: 0.05, strength: 2.0 })
Source

pub fn with_adjacency(self, max_neighbors: u32, radius: f32) -> Self

Enable adjacency buffer for graph-based particle operations.

When enabled, the simulation pre-computes and stores neighbor relationships in a GPU buffer. Each particle stores up to max_neighbors indices of nearby particles within the specified radius.

This is useful for:

  • Information propagation along particle connections
  • Constraint-based physics (springs between connected particles)
  • Network analysis (degree counting, clustering)
  • Graph-based algorithms that need stable neighbor lists
§Arguments
  • max_neighbors - Maximum neighbors to store per particle (e.g., 32)
  • radius - Distance threshold for neighbor detection
§WGSL Functions

When adjacency is enabled, these functions become available in custom rules:

  • adjacency_count(particle_idx) - Number of neighbors for a particle
  • adjacency_neighbor(particle_idx, n) - Get the nth neighbor’s index
§Example
Simulation::<Particle>::new()
    .with_spatial_config(0.1, 32)
    .with_adjacency(32, 0.1)  // Store up to 32 neighbors within radius 0.1
    .with_rule(Rule::Custom(r#"
        // Count neighbors and adjust behavior
        let neighbor_count = adjacency_count(index);
        if neighbor_count > 10u {
            p.color = vec3<f32>(1.0, 0.0, 0.0);  // Red if crowded
        }
    "#.into()))
    .run();
§Note

The adjacency buffer is computed after spatial hashing each frame, so it reflects the current neighbor state. This adds some GPU overhead but provides stable neighbor indices for the entire frame.

Source

pub fn with_inbox(self) -> Self

Enable particle-to-particle communication via inbox buffers.

When enabled, particles can send values to other particles’ “inbox” during neighbor iteration. Each particle has 4 inbox channels (vec4). Values are accumulated atomically and cleared each frame.

§WGSL Functions
  • inbox_send(target_idx, channel, value) - Send float to particle’s inbox
  • inbox_receive_at(index, channel) - Read inbox channel for particle (returns f32)
§Example
Simulation::<Particle>::new()
    .with_inbox()
    .with_spatial_config(0.2, 32)
    .with_rule(Rule::NeighborCustom(r#"
        // Transfer 10% of energy to neighbor
        if neighbor_dist < 0.1 {
            inbox_send(other_idx, 0u, p.energy * 0.1);
            p.energy *= 0.9;
        }
    "#.into()))
    .with_rule(Rule::Custom(r#"
        // Receive accumulated energy
        p.energy += inbox_receive_at(index, 0u);
    "#.into()))
    .run();
§Technical Details

The inbox uses atomic i32 operations with fixed-point encoding (16.16 format). This provides ~0.00001 precision in the range ±32768.

Source

pub fn with_field(self, name: impl Into<String>, config: FieldConfig) -> Self

Add a 3D spatial field for particle-environment interaction.

Fields are persistent 3D grids that particles can read from and write to. Unlike the inbox system (particle-to-particle), fields provide spatially indexed data that persists independently of particles.

§Arguments
  • name - Name for the field (for documentation; access by index in shaders)
  • config - Field configuration (resolution, extent, decay, blur)
§WGSL Functions

In Rule::Custom, you can use:

  • field_write(field_idx, position, value) - Deposit a value at a position
  • field_read(field_idx, position) - Sample field value (trilinear interpolation)
  • field_gradient(field_idx, position, epsilon) - Get gradient vector
§Example
Simulation::<Agent>::new()
    .with_field("pheromone", FieldConfig::new(64).with_decay(0.98).with_blur(0.1))
    .with_rule(Rule::Custom(r#"
        // Deposit pheromone at current position
        field_write(0u, p.position, 0.1);

        // Steer toward higher concentrations
        let gradient = field_gradient(0u, p.position, 0.05);
        p.velocity += normalize(gradient) * 0.5;
    "#.into()))
    .run();
§Use Cases
  • Pheromone trails: Particles deposit chemicals, others follow gradients
  • Density fields: Accumulate particle presence for fluid-like behavior
  • Temperature: Particles emit/absorb heat from spatial field
  • Reaction-diffusion: Classic pattern formation (Gray-Scott, Turing)
Source

pub fn with_volume_render(self, config: VolumeConfig) -> Self

Enable volume rendering for a field.

Volume rendering visualizes a 3D field as volumetric fog or glow using ray marching. This allows you to see the field data directly, not just the particles that interact with it.

Requires at least one field to be registered with with_field.

§Example
use rdpe::prelude::*;

Simulation::<Agent>::new()
    .with_field("pheromone", FieldConfig::new(64).with_decay(0.98))
    .with_volume_render(VolumeConfig::new()
        .with_palette(Palette::Inferno)
        .with_density_scale(5.0)
        .with_steps(64))
    .with_rule(Rule::Custom(r#"
        field_write(0u, p.position, 0.1);
    "#.into()))
    .run();
Source

pub fn with_fragment_shader(self, wgsl_code: &str) -> Self

Set a custom fragment shader for particle rendering.

The custom shader code replaces the default fragment shader body. Your code has access to:

  • in.uv - UV coordinates on the particle quad (-1 to 1 range)
  • in.color - The computed color for this particle (vec3)
  • uniforms.time - Current simulation time (f32)

Your code must return a vec4<f32> (RGBA color output).

§Example
Simulation::<Ball>::new()
    .with_fragment_shader(r#"
        // Glowing particles with pulsing animation
        let dist = length(in.uv);
        let pulse = sin(uniforms.time * 3.0) * 0.2 + 0.8;
        let glow = 1.0 / (dist * dist * 4.0 + 0.1) * pulse;
        let color = in.color * glow;
        return vec4<f32>(color, glow * 0.5);
    "#)
    .run();
§Effects you can create
  • Glow: Use 1.0 / (dist * dist + epsilon) for radial glow
  • Rings: Use smoothstep on distance to create ring shapes
  • Animation: Use uniforms.time for pulsing, rotation, etc.
  • Custom shapes: Discard fragments with discard; to cut out shapes
Source

pub fn with_vertex_shader(self, wgsl_code: &str) -> Self

Set a custom vertex shader for particle rendering.

The custom shader code replaces the default vertex shader body. Your code has access to:

Inputs:

  • vertex_index: u32 - Which vertex of the quad (0-5)
  • instance_index: u32 - Which particle (0 to particle_count-1)
  • particle_pos: vec3<f32> - Particle world position
  • particle_color: vec3<f32> - Particle color (if color field exists)
  • scale: f32 - Per-particle scale multiplier
  • quad_pos: vec2<f32> - Quad vertex offset (-1 to 1)
  • base_size: f32 - Base particle size from config
  • particle_size: f32 - Computed size (base_size * scale)
  • uniforms.view_proj - View-projection matrix
  • uniforms.time - Current simulation time
  • uniforms.delta_time - Time since last frame

Must set:

  • out.clip_position: vec4<f32> - Final clip-space position
  • out.color: vec3<f32> - Color to pass to fragment shader
  • out.uv: vec2<f32> - UV coordinates for fragment shader
§Example
Simulation::<Ball>::new()
    .with_vertex_shader(r#"
        // Wobbling particles
        let wobble = sin(uniforms.time * 5.0 + f32(instance_index) * 0.1) * 0.05;
        let offset_pos = particle_pos + vec3<f32>(wobble, 0.0, 0.0);

        let world_pos = vec4<f32>(offset_pos, 1.0);
        var clip_pos = uniforms.view_proj * world_pos;
        clip_pos.x += quad_pos.x * particle_size * clip_pos.w;
        clip_pos.y += quad_pos.y * particle_size * clip_pos.w;

        out.clip_position = clip_pos;
        out.color = particle_color;
        out.uv = quad_pos;
    "#)
    .run();
§Effects you can create
  • Wobble/Wave: Offset position with sin(time + index)
  • Rotation: Rotate quad_pos before applying to clip position
  • Size pulsing: Multiply particle_size by time-based factor
  • Billboarding variants: Custom billboard orientation
  • Screen-space effects: Modify clip position directly
Source

pub fn with_vertex_effect(self, effect: VertexEffect) -> Self

Add a pre-built vertex effect.

Vertex effects are composable transformations applied to particle rendering. Multiple effects can be stacked and are applied in order.

§Example
Simulation::<Ball>::new()
    .with_vertex_effect(VertexEffect::Rotate { speed: 2.0 })
    .with_vertex_effect(VertexEffect::Wobble {
        frequency: 3.0,
        amplitude: 0.05,
    })
    .with_vertex_effect(VertexEffect::Pulse {
        frequency: 4.0,
        amplitude: 0.3,
    })
    .run();
§Available Effects
§Note

If both with_vertex_effect() and with_vertex_shader() are used, vertex effects are ignored and the custom shader takes precedence.

Source

pub fn with_emitter(self, emitter: Emitter) -> Self

Add a particle emitter for runtime spawning.

Emitters respawn dead particles at a configurable rate. Use with Rule::Age and Rule::Lifetime to create continuous particle effects.

§Arguments
  • emitter - The Emitter configuration
§Example
Simulation::<Spark>::new()
    .with_particle_count(10_000)
    .with_emitter(Emitter::Point {
        position: Vec3::ZERO,
        rate: 500.0,  // 500 particles per second
    })
    .with_rule(Rule::Age)
    .with_rule(Rule::Lifetime(2.0))
    .with_rule(Rule::Gravity(9.8))
    .run();
§Note

When using emitters, particles should start dead (will be spawned by emitter) or use a spawner that sets some particles alive initially.

Source

pub fn with_lifecycle<F>(self, configure: F) -> Self
where F: FnOnce(Lifecycle) -> Lifecycle,

Configure particle lifecycle with a builder.

Lifecycle configuration handles aging, death, visual effects (fade, shrink), and respawning via emitters. This is the ergonomic way to set up particle systems with birth/death cycles.

§Hidden Lifecycle Fields

Every particle automatically has these fields (injected by derive macro):

  • age: f32 - time since spawn (seconds)
  • alive: u32 - 0 = dead, 1 = alive
  • scale: f32 - visual size multiplier
§Example: Custom Configuration
.with_lifecycle(|l| {
    l.lifetime(2.0)
     .fade_out()
     .shrink_out()
     .emitter(Emitter::Cone {
         position: Vec3::ZERO,
         direction: Vec3::Y,
         speed: 2.0,
         spread: 0.3,
         rate: 500.0,
     })
})
§Example: Using Presets
.with_lifecycle(Lifecycle::fire(Vec3::ZERO, 1000.0))
.with_lifecycle(Lifecycle::fountain(Vec3::new(0.0, -0.5, 0.0), 800.0))
.with_lifecycle(Lifecycle::explosion(Vec3::ZERO, 500))
§What This Does

The lifecycle builder automatically adds:

  • Rule::Age - increment particle age each frame
  • Rule::Lifetime(duration) - kill particles after duration
  • Rule::FadeOut(duration) - dim color over lifetime (if enabled)
  • Rule::ShrinkOut(duration) - shrink scale over lifetime (if enabled)
  • Rule::ColorOverLife { ... } - color gradient (if enabled)
  • Emitters for respawning dead particles
Source

pub fn with_lifecycle_preset(self, lifecycle: Lifecycle) -> Self

Configure particle lifecycle using a preset.

Convenience method for using lifecycle presets directly.

§Example
.with_lifecycle_preset(Lifecycle::fire(Vec3::ZERO, 1000.0))
Source

pub fn with_sub_emitter(self, sub_emitter: SubEmitter) -> Self

Add a sub-emitter that spawns child particles when parents die.

Sub-emitters enable chain reactions, fireworks, explosions with debris, and biological reproduction effects.

§How It Works
  1. When a particle of parent_type dies (via Rule::Lifetime or kill_particle())
  2. The death event is recorded with position, velocity, and color
  3. A secondary compute pass spawns count children at the death location
  4. Children inherit some parent velocity and spread outward
§Example: Fireworks
#[derive(ParticleType)]
enum Firework { Rocket, Spark }

Simulation::<Particle>::new()
    .with_lifecycle(|l| l.lifetime(2.0))
    .with_sub_emitter(
        SubEmitter::new(Firework::Rocket.into(), Firework::Spark.into())
            .count(50)
            .speed(1.0..3.0)
            .spread(std::f32::consts::PI)
    )
    .run();
§Chaining Sub-Emitters
// Rockets → Sparks → Embers
.with_sub_emitter(SubEmitter::new(Rocket, Spark).count(30))
.with_sub_emitter(SubEmitter::new(Spark, Ember).count(5))
Source

pub fn with_interactions<F>(self, configure: F) -> Self
where F: FnOnce(&mut InteractionMatrix),

Set up type-based interactions using an interaction matrix.

The interaction matrix defines attraction and repulsion forces between particle types. This is the foundation of “particle life” simulations.

The closure receives an InteractionMatrix which you configure with set(), attract(), repel(), or set_symmetric() calls.

§Type Parameter

T should be your #[derive(ParticleType)] enum. The COUNT const from the derive tells us how many types exist.

§Example
#[derive(ParticleType, Clone, Copy)]
enum Species { Red, Green, Blue }

Simulation::<Particle>::new()
    .with_interactions::<Species>(|m| {
        use Species::*;
        m.attract(Red, Green, 1.0, 0.3);
        m.repel(Green, Red, 0.5, 0.2);
        m.set_symmetric(Blue, Blue, -0.3, 0.25);
    })
    .run();
§Note

Using interactions automatically enables spatial hashing for neighbor queries. The spatial config cell_size will be set to the maximum interaction radius if not already configured larger.

Source

pub fn with_interactions_sized<F>(self, num_types: usize, configure: F) -> Self
where F: FnOnce(&mut InteractionMatrix),

Set up type-based interactions with a specific number of types.

Use this if you have more than 16 particle types.

Source

pub fn with_uniform<V: Into<UniformValue>>(self, name: &str, value: V) -> Self

Add a custom uniform that can be used in shader rules.

Custom uniforms are accessible in Rule::Custom as uniforms.name.

§Supported Types
  • f32, i32, u32 - scalar values
  • Vec2, Vec3, Vec4 - vector values
§Example
Simulation::<Particle>::new()
    .with_uniform("attractor", Vec3::ZERO)
    .with_uniform("strength", 1.0f32)
    .with_rule(Rule::Custom(r#"
        let dir = uniforms.attractor - p.position;
        p.velocity += normalize(dir) * uniforms.strength;
    "#.into()))
    .run();
Source

pub fn with_texture<C: Into<TextureConfig>>(self, name: &str, config: C) -> Self

Add a custom texture for use in shaders.

Custom textures are available in fragment, post-process, and compute shaders as tex_name (the texture) and tex_name_sampler (the sampler).

§Arguments
  • name - Name used to access the texture in shaders (becomes tex_name)
  • config - Texture configuration (can be a file path or TextureConfig)
§Supported Input Types
  • &str or String - Path to an image file (PNG, JPEG, GIF, etc.)
  • TextureConfig - Full control over texture data and sampling options
§Example
use rdpe::prelude::*;

// Load from file
Simulation::<Particle>::new()
    .with_texture("noise", "assets/noise.png")
    .with_fragment_shader(r#"
        let n = textureSample(tex_noise, tex_noise_sampler, in.uv * 0.5 + 0.5);
        return vec4<f32>(in.color * n.r, 1.0);
    "#)
    .run();

// Programmatic texture with options
Simulation::<Particle>::new()
    .with_texture("checker",
        TextureConfig::checkerboard(64, 8, [255,255,255,255], [0,0,0,255])
            .with_filter(FilterMode::Nearest)
            .with_address_mode(AddressMode::Repeat))
    .run();
Source

pub fn with_update<F>(self, callback: F) -> Self
where F: FnMut(&mut UpdateContext<'_>) + Send + 'static,

Set a callback that runs every frame to update custom uniforms.

The callback receives an UpdateContext with:

  • time() - current simulation time
  • delta_time() - time since last frame
  • mouse_ndc() - mouse position in normalized device coordinates
  • mouse_pressed() - is left mouse button down
  • set(name, value) - update a custom uniform
§Example
Simulation::<Particle>::new()
    .with_uniform("target", Vec3::ZERO)
    .with_update(|ctx| {
        // Make target orbit based on time
        let t = ctx.time();
        ctx.set("target", Vec3::new(t.cos(), 0.0, t.sin()));
    })
    .run();
Source

pub fn with_function(self, wgsl_code: &str) -> Self

Add a custom WGSL function that can be called from rules.

Custom functions are injected into the compute shader and can be called from Rule::Custom or other custom code.

§Example
Simulation::<Particle>::new()
    .with_function(r#"
        fn swirl(pos: vec3<f32>, strength: f32) -> vec3<f32> {
            let d = length(pos.xz);
            return vec3(-pos.z, 0.0, pos.x) * strength / (d + 0.1);
        }
    "#)
    .with_rule(Rule::Custom("p.velocity += swirl(p.position, 2.0);".into()))
    .run();
§Available in Functions

Your functions have access to:

  • All WGSL built-in functions
  • The Particle struct type
  • The Uniforms struct (via parameter passing)
  • Other custom functions defined before this one
Source

pub fn with_visuals<F>(self, configure: F) -> Self
where F: FnOnce(&mut VisualConfig),

Configure visual rendering options.

Visuals control how particles are rendered, separate from the behavioral rules that control how they move.

§Example
use rdpe::prelude::*;

Simulation::<Ball>::new()
    .with_visuals(|v| {
        v.blend_mode(BlendMode::Additive);  // Glowy particles
        v.shape(ParticleShape::Circle);
    })
    .with_rule(Rule::Gravity(9.8))
    .run();
§Available Options
  • blend_mode() - Alpha, Additive, or Multiply blending
  • shape() - Circle, Square, Ring, Star, Point
  • trails() - Render position history as trails
  • connections() - Draw lines between nearby particles
  • velocity_stretch() - Stretch particles in motion direction
Source

pub fn with_particle_inspector(self) -> Self

Stub for when egui feature is not enabled - provides IDE visibility.

Source

pub fn with_rule_inspector(self) -> Self

Stub for when egui feature is not enabled - provides IDE visibility.

Source

pub fn run(self) -> Result<(), SimulationError>

Run the simulation.

This is the final step that starts the simulation. It:

  1. Spawns all particles using the spawner function
  2. Generates WGSL compute shaders from the configured rules
  3. Initializes the GPU and creates buffers
  4. Opens a window and starts the render loop
§Blocking

This method blocks until the user closes the window. It runs the event loop on the main thread.

§Panics

Panics if:

  • No spawner was provided (forgot to call .with_spawner())
  • GPU initialization fails (no compatible GPU found)
§Window Controls
  • Left-click + drag: Rotate camera around the origin
  • Scroll wheel: Zoom in/out
  • Close window: Exits the application
§Example
Simulation::<Ball>::new()
    .with_particle_count(10_000)
    .with_bounds(1.0)
    .with_spawner(|_| Ball::default())
    .with_rule(Rule::Gravity(9.8))
    .with_rule(Rule::BounceWalls { restitution: 1.0 })
    .run()
    .expect("Simulation failed");  // Blocks here until window closed

// This code runs after window is closed
println!("Simulation ended");
§Errors

Returns SimulationError if:

  • No spawner function was provided (use .with_spawner())
  • Event loop creation fails
  • Window creation fails
  • GPU initialization fails

Trait Implementations§

Source§

impl<P: ParticleTrait + 'static> Default for Simulation<P>

Source§

fn default() -> Self

Creates a new simulation with default settings.

Equivalent to Simulation::new().

Auto Trait Implementations§

§

impl<P> Freeze for Simulation<P>

§

impl<P> !RefUnwindSafe for Simulation<P>

§

impl<P> Send for Simulation<P>

§

impl<P> !Sync for Simulation<P>

§

impl<P> Unpin for Simulation<P>
where P: Unpin,

§

impl<P> !UnwindSafe for Simulation<P>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T> WasmNotSend for T
where T: Send,