Nature of Rust — Particles

Jack Sim
CodeX
Published in
6 min readAug 17, 2021

So about five years ago now, I was completing my PhD and wanted to teach myself how to code for a bit of fun. I had no idea where to start and found it very difficult to what to do. That was until I found the Coding Train YouTube channel. The challenges shown on this channel were easy to follow along with and a fun way to pick up new concepts.

Recently, and also several years ago, the channel posted a series called the Nature of Code in Processing (Java) and p5 JavaScript. Seeing these videos gave me the idea to give these examples a go, but in Rust.

So here goes….

What is a Particle?

For this example, a particle will be defined as an object with the following attributes:

  • Initial Position (x, y)
  • Initial Velocity (Vx, Vy)
  • Initial Size: e.g. radius of circle
  • Initial Colour: needed if the particle changes throughout it’s lifetime
  • Initial Shape
  • Lifetime: the particle will exist in the window for a fixed lifetime

In addition to these attributes, the particles can be influenced by forces, such as gravity, and the particle object will need to be able to handle this. In Newton’s Second Law can be summarised by the following equation:

Force = Mass x Acceleration

To keep it simple in the example I’ll assume that the mass of all particles is 1, and therefore the equation becomes:

Force = Acceleration

Using this I can alter the velocity of the particle and update the position of the particle within the screen.

How do you create a canvas in Rust?

This is the first challenge to overcome for this problem, we want to draw a particle, but where and how do you draw anything in Rust. This is relatively simple to solve. There are two libraries in Rust called piston and piston_window that enable you to create a canvas and draw shapes to the canvas. To use them, in the Cargo.toml file, add the following dependencies:

.....
[dependencies]
piston = "0.53.0"
piston_window = "0.120.0"

And into the main.rs file add the following:

extern crate piston_window;
use piston_window::*;
fn main() {
let mut window: PistonWindow = WindowSettings::new("Name",
[400, 400])
.exit_on_esc(true)
.build()
.unwrap();
while let Some(e) = window.next() {
window.draw_2d(&e, |c, g, _device| {
clear([0.0, 0.0, 0.0, 1.0], g);
});
}
}

When run, this creates a window called “Name”, with a size of 400 pixels by 400 pixels. The line clear([0.0, 0.0, 0.0, 1.0], g) is setting the colour of the window to black. The array of four numbers works in a red, green, blue, alpha format and by altering the values between 0.0 and 1.0 the colour of the window can be altered. This format of array is used to set the colour of all objects that are placed into the window when using the piston libraries.

Creating the particle struct

Now we have a window, it’s time to think about the particle struct and what attributes it needs to have. The particle definition above has already defined some of these attributes, but for completeness and ease later on, there are some additional ones.

pub struct Particle {
pub pos: Vec<f64>,
pub vel: Vec<f64>,
pub acc: Vec<f64>,
lifetime: f64,
size: f64,
colour: [f64; 4],
max_vel: f64,
max_acc: f64,
height: f64,
width: f64,
}

Implementing the particle

Now that the particle has been defined it needs to be implemented and associated function created. The first of these is the new function, which will be used to create a particle and initialise it’s attributes. This function will set the initial position of the particle, it’s velocity, acceleration and the height and width of the canvas the particle is being drawn in.

impl Particle {
pub fn new(pos: Vec<f64>, vel: Vec<f64>, acc: Vec<f64>,
height: u32, width: u32) -> Particle {
return Particle {
pos: pos, vel: vel, acc: acc,
lifetime: 1.0, size: 10.,
colour: [1.0, 0.0, 0.0, 1.0],
max_speed: 10.0, max_acc: 0.5,
height: height as f64,
width: width as f64,
};
}
}

Displaying the particle

Now that the particle has been initialised, it needs to be drawn to the screen. For this, the particle’s position and it’s size needs to be given in an array to the piston window function ellipse. The ellipse function needs a colour array, position and size array and details of the window that the ellipse is being drawn in. To generate the position and size array, the particle implementation can have a function called show.

impl Particle {    ....    pub fn show(&self) -> [f64; 4] {
return [self.pos[0], self.pos[1], self.size, self.size];
}
}

In the main.rs file, lets create a particle in the middle of the canvas (x = 200, y = 200). It will be initialised with a random velocity and a there will be a small, downwards acceleration acting on it. To set the colour of the particle, the lifetime attribute of the structure will be passed in the piston window color::alpha function to set the transparency of the particle on the screen.

extern crate piston_window;
use piston_window::*;
use rand::Rng;
pub const WIDTH: u32 = 400;
pub const HEIGHT: u32 = 400;
fn main() {
let mut rng = rand::thread_rng();
let mut particle = Particle::new(Particle::new(
vec![200., 200.],
vec![rng.gen_range(-1.0..1.0), rng.gen_range(-1.0..1.0)],
vec![0.0, 0.1],
HEIGHT,
WIDTH,
);
let mut window: PistonWindow = WindowSettings::new("Name",
[400, 400])
.exit_on_esc(true)
.build()
.unwrap();
while let Some(e) = window.next() {
window.draw_2d(&e, |c, g, _device| {
clear([0.0, 0.0, 0.0, 1.0], g);
ellipse(color::alpha(particle.lifetime),
particle.show(), c.transform, g);
});
}
}

Updating the particle

Running the code above generates a static particle on the screen. To get it to move the particle must be updated between frames, to do this another function is added into the implementation of the particle called update.

impl Particle {....    pub fn update(&mut self) {
self.pos[0] = self.pos[0] + self.vel[0];
self.pos[1] = self.pos[1] + self.vel[1];
self.vel[0] = self.vel[0] + self.acc[0];
self.vel[1] = self.vel[1] + self.acc[1];

self.check_limits();
self.edges();
self.lifetime = self.lifetime - 0.002;
}
}

In this function there are two other functions that are linked to the particle. These are edges, which checks if the particle is at an edge, and if it is will reverse the velocity in that plane of movement. This makes sure the particle will not disappear off the screen. The second function check_limits looks to see if the max velocity or acceleration has been exceeded. If it has the velocity or acceleration of the particle are reset to the maximum allowed.

impl Particle {....fn check_limits(&mut self) {
if self.vel[0] > self.max_vel {
self.vel[0] = self.max_vel;
}
if self.vel[1] > self.max_vel {
self.vel[1] = self.max_vel;
}
if self.acc[0] > self.max_acc {
self.acc[0] = self.max_acc;
}
if self.acc[1] > self.max_acc {
self.acc[1] = self.max_acc;
}
}
fn edges(&mut self) {
if self.pos[1] >= self.height || self.pos[1] <= 0.0 {
self.vel[1] = self.vel[1] * -1.0;
}
if self.pos[0] >= self.width || self.pos[0] <= 0.0 {
self.vel[0] = self.vel[0] * -1.0;
}
}
}

The final thing to mention about the update function, is the way the lifetime attribute is being decreased each time the function is called. As stated earlier in the article, particles need to have a lifetime and by decreasing it each frame, the particle will become more transparent, until at last it cannot be seen. When it can no longer be seen on the screen, the particle has reached the end of it’s lifetime.

Now in the main.rs file within the while loop that is drawing the canvas, the line particle.update() can be added. This will ensure that each time the loop is completed the particles attributes will be updated and it moves across the screen.

Particle emitters

Now that we have a single particle it’s possible to have some fun. One type of particle system is a particle emitter. Here particles are emitted from a fixed point with a random velocity and with a slight gravitational force produce a cascading effect.

To create the emitter, particles are stored in a vector and with each loop of the canvas all are drawn and updated. Due to the frame rate with piston_window a new particle is created every 10 frames, so the performance of the program is not affected.

The final thing to do is remove particles that have come to the end of their lifetime from the vector of all particles. By looping backwards through the vector of particles and removing particles where the lifetime attribute is equal to or less than zero.

To see the code for the particle emitter, please refer to my github repo for the project

Summary

I hope this has been a useful introduction into particles and how to create a particle simulation in Rust using the Piston and Piston_Window crates. This is a simple overview and there are many ways particles can be used beyond what I’ve created here. Feel free to have an experiment with the code and try to come up with other examples, if you do please let me know in the comments to this article.

Thank you very much for reading :)

--

--