rdpe 0.1.0

Reaction Diffusion Particle Engine - GPU particle simulations made easy
Documentation
//! # Inbox Example: Energy Transfer
//!
//! Demonstrates particle-to-particle communication using the inbox system.
//! Particles transfer energy to their neighbors, creating a diffusion effect.
//!
//! ## What This Demonstrates
//!
//! - `.with_inbox()` - enable particle messaging system
//! - `inbox_send(particle_idx, channel, value)` - send data to a particle
//! - `inbox_receive_at(idx, channel)` - receive accumulated data
//! - Energy diffusion using neighbor communication
//! - Heat map coloring (blue → cyan → yellow → red)
//!
//! ## How the Inbox System Works
//!
//! The inbox provides per-particle communication channels:
//!
//! 1. **Sending**: In neighbor rules, send values to other particles
//!    ```wgsl
//!    inbox_send(other_idx, 0u, amount);  // Send to channel 0
//!    ```
//!
//! 2. **Receiving**: In custom rules, read accumulated values
//!    ```wgsl
//!    let received = inbox_receive_at(index, 0u);  // Sum of all sends
//!    ```
//!
//! Values sent in one frame are accumulated and available the next frame.
//! Each particle has 4 channels (0-3) for different data types.
//!
//! ## Inbox vs Fields
//!
//! - **Inbox**: Direct particle-to-particle, discrete messages
//! - **Fields**: Continuous 3D grid, automatic diffusion
//!
//! Use inbox when you need precise particle targeting.
//! Use fields for continuous pheromone-like effects.
//!
//! ## Try This
//!
//! - Increase transfer rate (0.02) for faster diffusion
//! - Add more hot spots by changing the modulo pattern
//! - Use channel 1 for a second energy type
//! - Add `Rule::Separate` to spread particles apart
//!
//! Run with: `cargo run --example inbox`

use rand::Rng;
use rdpe::prelude::*;

#[derive(Particle, Clone)]
struct EnergyParticle {
    position: Vec3,
    velocity: Vec3,
    #[color]
    color: Vec3,
    /// Energy level (0.0 to 1.0)
    energy: f32,
}

fn main() {
    let mut rng = rand::thread_rng();

    // Create particles in a grid pattern
    // Some particles start with high energy, others with none
    let particles: Vec<_> = (0..5000)
        .map(|i| {
            let x = (i % 50) as f32 / 50.0 * 2.0 - 1.0;
            let z = (i / 50) as f32 / 100.0 * 2.0 - 1.0;
            let pos = Vec3::new(
                x + rng.gen_range(-0.01..0.01),
                0.0,
                z + rng.gen_range(-0.01..0.01),
            );

            // Seed a few "hot spots" with high energy
            let energy = if (i % 500) < 10 {
                1.0 // Hot particle
            } else {
                0.0 // Cold particle
            };

            (pos, energy)
        })
        .collect();

    Simulation::<EnergyParticle>::new()
        .with_particle_count(5000)
        .with_bounds(1.5)
        .with_spawner(move |ctx| {
            let (pos, energy) = particles[ctx.index as usize];
            let color = energy_to_color(energy);
            EnergyParticle {
                position: pos,
                velocity: Vec3::ZERO,
                color,
                energy,
            }
        })
        // Enable particle communication!
        .with_inbox()
        .with_spatial_config(0.15, 32)

        // Neighbor rule: transfer energy to neighbors
        .with_rule(Rule::NeighborCustom(r#"
            let transfer_radius = 0.1;

            if neighbor_dist < transfer_radius && neighbor_dist > 0.001 {
                // Transfer some energy to neighbor (through their inbox)
                // Transfer more to closer neighbors
                let weight = 1.0 - neighbor_dist / transfer_radius;
                let transfer_amount = p.energy * 0.02 * weight;

                // Send energy to neighbor's inbox channel 0
                inbox_send(other_idx, 0u, transfer_amount);
            }
        "#.into()))

        // Custom rule: receive energy and update visuals
        .with_rule(Rule::Custom(r#"
            // Receive accumulated energy from inbox channel 0
            let received = inbox_receive_at(index, 0u);

            // Also lose some energy (conservation by what we sent)
            // Count approximate neighbors for energy loss
            p.energy = p.energy * 0.96 + received;

            // Clamp energy
            p.energy = clamp(p.energy, 0.0, 1.0);

            // Update color based on energy (cold = blue, hot = red)
            if p.energy < 0.33 {
                // Blue to cyan
                let t = p.energy / 0.33;
                p.color = mix(vec3<f32>(0.1, 0.2, 0.8), vec3<f32>(0.2, 0.8, 0.9), t);
            } else if p.energy < 0.66 {
                // Cyan to yellow
                let t = (p.energy - 0.33) / 0.33;
                p.color = mix(vec3<f32>(0.2, 0.8, 0.9), vec3<f32>(1.0, 0.9, 0.2), t);
            } else {
                // Yellow to red
                let t = (p.energy - 0.66) / 0.34;
                p.color = mix(vec3<f32>(1.0, 0.9, 0.2), vec3<f32>(1.0, 0.3, 0.1), t);
            }

            // Add subtle floating motion
            p.velocity.y = sin(uniforms.time * 2.0 + p.position.x * 5.0) * 0.01;
            p.position.y = sin(uniforms.time + p.position.x * 3.0 + p.position.z * 2.0) * 0.05;
        "#.into()))

        .run().expect("Simulation failed");
}

fn energy_to_color(energy: f32) -> Vec3 {
    if energy < 0.33 {
        let t = energy / 0.33;
        Vec3::new(0.1, 0.2, 0.8).lerp(Vec3::new(0.2, 0.8, 0.9), t)
    } else if energy < 0.66 {
        let t = (energy - 0.33) / 0.33;
        Vec3::new(0.2, 0.8, 0.9).lerp(Vec3::new(1.0, 0.9, 0.2), t)
    } else {
        let t = (energy - 0.66) / 0.34;
        Vec3::new(1.0, 0.9, 0.2).lerp(Vec3::new(1.0, 0.3, 0.1), t)
    }
}