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 implementParticleTrait(via#[derive(Particle)])
§Builder Methods
| Method | Required | Description |
|---|---|---|
with_particle_count | No | Number of particles (default: 10,000) |
with_bounds | No | Simulation cube half-size (default: 1.0) |
with_particle_size | No | Base particle render size (default: 0.015) |
with_spawner | Yes | Function to create each particle |
with_rule | No | Add behavior rules (can call multiple times) |
with_spatial_config | Conditional | Required 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>
impl<P: ParticleTrait + 'static> Simulation<P>
Sourcepub fn with_particle_count(self, count: u32) -> Self
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)
// ...Sourcepub fn with_bounds(self, bounds: f32) -> Self
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
// ...Sourcepub fn with_particle_size(self, size: f32) -> Self
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.
Sourcepub fn with_spawner<F>(self, spawner: F) -> Self
pub fn with_spawner<F>(self, spawner: F) -> Self
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 infoctx.random_in_sphere(r),ctx.random_on_sphere(r)- positionsctx.random_in_cube(size),ctx.random_in_bounds()- box positionsctx.random_direction()- unit vectorsctx.random_hue(s, v),ctx.rainbow(s, v)- colorsctx.grid_position(cols, rows, layers)- structured layoutsctx.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 },
}
})Sourcepub fn with_rule(self, rule: impl Into<Rule>) -> Self
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:
- Forces first: Gravity, attraction, neighbor interactions
- Constraints: Speed limits, drag
- Boundaries last: BounceWalls, WrapWalls
§Arguments
rule- TheRuleto 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.
Sourcepub fn with_spatial_config(self, cell_size: f32, grid_resolution: u32) -> Self
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
| Scenario | cell_size | grid_resolution |
|---|---|---|
| Small interactions (radius < 0.1) | 0.1 | 32 |
| Medium interactions (radius 0.1-0.3) | 0.2-0.3 | 32 |
| Large bounds (> 1.0) | bounds / 16 | 32 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.
Sourcepub fn with_max_neighbors(self, max: u32) -> Self
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 })Sourcepub fn with_adjacency(self, max_neighbors: u32, radius: f32) -> Self
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 particleadjacency_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.
Sourcepub fn with_inbox(self) -> Self
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 inboxinbox_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.
Sourcepub fn with_field(self, name: impl Into<String>, config: FieldConfig) -> Self
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 positionfield_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)
Sourcepub fn with_volume_render(self, config: VolumeConfig) -> Self
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();Sourcepub fn with_fragment_shader(self, wgsl_code: &str) -> Self
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
smoothstepon distance to create ring shapes - Animation: Use
uniforms.timefor pulsing, rotation, etc. - Custom shapes: Discard fragments with
discard;to cut out shapes
Sourcepub fn with_vertex_shader(self, wgsl_code: &str) -> Self
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 positionparticle_color: vec3<f32>- Particle color (if color field exists)scale: f32- Per-particle scale multiplierquad_pos: vec2<f32>- Quad vertex offset (-1 to 1)base_size: f32- Base particle size from configparticle_size: f32- Computed size (base_size * scale)uniforms.view_proj- View-projection matrixuniforms.time- Current simulation timeuniforms.delta_time- Time since last frame
Must set:
out.clip_position: vec4<f32>- Final clip-space positionout.color: vec3<f32>- Color to pass to fragment shaderout.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_posbefore applying to clip position - Size pulsing: Multiply
particle_sizeby time-based factor - Billboarding variants: Custom billboard orientation
- Screen-space effects: Modify clip position directly
Sourcepub fn with_vertex_effect(self, effect: VertexEffect) -> Self
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
VertexEffect::Rotate- Spin particles around their facing axisVertexEffect::Wobble- Sinusoidal position offsetVertexEffect::Pulse- Size oscillationVertexEffect::Wave- Coordinated wave across particlesVertexEffect::Jitter- Random shakeVertexEffect::ScaleByDistance- Size based on distance from pointVertexEffect::FadeByDistance- Opacity based on distance
§Note
If both with_vertex_effect() and with_vertex_shader() are used,
vertex effects are ignored and the custom shader takes precedence.
Sourcepub fn with_emitter(self, emitter: Emitter) -> Self
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- TheEmitterconfiguration
§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.
Sourcepub fn with_lifecycle<F>(self, configure: F) -> Self
pub fn with_lifecycle<F>(self, configure: F) -> Self
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 = alivescale: 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 frameRule::Lifetime(duration)- kill particles after durationRule::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
Sourcepub fn with_lifecycle_preset(self, lifecycle: Lifecycle) -> Self
pub fn with_lifecycle_preset(self, lifecycle: Lifecycle) -> Self
Sourcepub fn with_sub_emitter(self, sub_emitter: SubEmitter) -> Self
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
- When a particle of
parent_typedies (viaRule::Lifetimeorkill_particle()) - The death event is recorded with position, velocity, and color
- A secondary compute pass spawns
countchildren at the death location - 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))Sourcepub fn with_interactions<F>(self, configure: F) -> Selfwhere
F: FnOnce(&mut InteractionMatrix),
pub fn with_interactions<F>(self, configure: F) -> Selfwhere
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.
Sourcepub fn with_interactions_sized<F>(self, num_types: usize, configure: F) -> Selfwhere
F: FnOnce(&mut InteractionMatrix),
pub fn with_interactions_sized<F>(self, num_types: usize, configure: F) -> Selfwhere
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.
Sourcepub fn with_uniform<V: Into<UniformValue>>(self, name: &str, value: V) -> Self
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 valuesVec2,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();Sourcepub fn with_texture<C: Into<TextureConfig>>(self, name: &str, config: C) -> Self
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 (becomestex_name)config- Texture configuration (can be a file path orTextureConfig)
§Supported Input Types
&strorString- 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();Sourcepub fn with_update<F>(self, callback: F) -> Self
pub fn with_update<F>(self, callback: F) -> Self
Set a callback that runs every frame to update custom uniforms.
The callback receives an UpdateContext with:
time()- current simulation timedelta_time()- time since last framemouse_ndc()- mouse position in normalized device coordinatesmouse_pressed()- is left mouse button downset(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();Sourcepub fn with_function(self, wgsl_code: &str) -> Self
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
Particlestruct type - The
Uniformsstruct (via parameter passing) - Other custom functions defined before this one
Sourcepub fn with_visuals<F>(self, configure: F) -> Selfwhere
F: FnOnce(&mut VisualConfig),
pub fn with_visuals<F>(self, configure: F) -> Selfwhere
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 blendingshape()- Circle, Square, Ring, Star, Pointtrails()- Render position history as trailsconnections()- Draw lines between nearby particlesvelocity_stretch()- Stretch particles in motion direction
Sourcepub fn with_particle_inspector(self) -> Self
pub fn with_particle_inspector(self) -> Self
Stub for when egui feature is not enabled - provides IDE visibility.
Sourcepub fn with_rule_inspector(self) -> Self
pub fn with_rule_inspector(self) -> Self
Stub for when egui feature is not enabled - provides IDE visibility.
Sourcepub fn run(self) -> Result<(), SimulationError>
pub fn run(self) -> Result<(), SimulationError>
Run the simulation.
This is the final step that starts the simulation. It:
- Spawns all particles using the spawner function
- Generates WGSL compute shaders from the configured rules
- Initializes the GPU and creates buffers
- 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>
impl<P: ParticleTrait + 'static> Default for Simulation<P>
Source§fn default() -> Self
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> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.