Hello again. In part 1, we began our ambitious experiment of simulating a school of fish. We set up a scene and added some basic animation for a moving cone. Those were baby steps. You can download the project at this stage from this tagged version.
In this part, we will,
- Generate boids and give them a random velocity
- Add the first rule to give boids life
At the end of it our boids will become a little intelligent and behave like this.
A pinch of randomness
In this section, we'll generate many boids with and give them a random velocity. The
rand crate will help us, so add
rand = "0.7.3" to project dependencies. We'll also use the
UnitSphere distribution from
rand_distr crate, so add
rand_distr = "0.2.2" to the dependencies as well.
Let's create the
spawn_boids function in
boid.rs. At the top of the file, add the following imports and constant values.
This function spawns boids at random points inside a sphere. It takes the following parameters:
c- The coordinates for the centre of the sphere
r- The radius of the sphere or the bounds inside which boids can be spawned
n- The number of boids to spawn
thread_rng() initializes a system seeded random number generator. We also intialise a
Vec<Boid> to collect all the instances we generate.
Here's the next section, where we generate random points for a boid's starting position.
Here's what how it works:
off_valueis any value between positive and negative radius limits
UnitSphereis a random distribution which picks a random point on the surface of a unit sphere. It generates a unit vector which represents the offset direction.
- We create the
Point3position of the boid by adding the offset vector to the centre vector. The offset vector is derived by multiplying offset value with offset direction.
Then we generate random velocities.
It is similar to generating random positions. A random magnitude and a random direction vector to creates a random velocity. Using the position and velocity, we create a
Boid and push it into
boids. To use
spawn_boids the following changes to
main.rs are necessary.
- Create an equivalent for
- Modify the render function to handle a lists of boids and cones
Change the imports to freely use
Mesh struct without the absolute path.
The roles of
spawn_cones is to:
SPAWN_NUMBERof cone meshes
- Add them to the Window
- Push them into a
- Return the list of meshes
We have not manipulated the
transformation of the cone in anyway. We'll do that inside the game loop.
We create a list of boids and cones each. Inside the game loop we update the objects on each frame. This section is perfectly suited for iterator syntax, so bear with me if it's not immediately obvious.
- Iterate over each
frame_updateon it. Since
frame_updatemutates the position, the reference must be mutable.
iter_mutallows this by creating an iterator of mutable references in
- Next the updated boid is used to set the cone's position and orientation a.k.a transform.
set_transformdoes not require a mutable reference so just
iterwill do. However, each cone needs information from its respective boid. To achieve this, zip the boid and cone iterators together. The iterator yields a tuple containing the related boid and cone, which can then be used in
cargo run, shows this cool animation of rockets firing in all directions.
We've created most of the graphical parts to simulate a flock of birds. However, we are lacking the most important components that gives life to these boids - the rules to interact with boundaries and each other.
Finding the right direction
Right now, the boids simply ignore the boundaries of the box. In this section, we will add the logic to keep boids confined inside an enclosed space. It is a bit involved so pay close attention.
To do this, we need to create obstacles and a mechanism to detect them. The boids will only move in the unobstructed direction. The
ncollide3d library will help us here. For obstacles, we'll use shapes that implement the
RayCast trait. The trait contains the
intersects_ray method which the boid will use to find an unobstructed direction.
The first obstacle
We encounter our first problem when creating a list of obstacle shapes. Let's try to define its type. Suppose we specify a
Vec that can contain any
struct that implements
There are many distinct structs that implement the
RayCast trait. For example, we have the Cylinder, the Plane, and the Cuboid among others. It won't work because a
Vec can only contain objects of the same type and hence, size.
T: RayCast does not tell the compiler anything about memory required by
Cuboid. They might require different amounts of memory. The solution is to use dynamic dispatch, which is analogous to virtual functions from languages like Java and C++.
This way, we tell the compiler that each element is a fixed-size pointer i.e.
Box which points to a struct implementing the
dyn RayCast tells the compiler that it will have to (dynamically) find out which type of shape is calling
intersect_ray at run time.
We are not done yet. Shapes are always created at origin. If we want to compute collision with a cylinder at
(10, 0, 0), we'll have to translate it before checking for ray collision. So we have a
Vec containing the pointer to a shape and a related
Isometry i.e. offset/translation/rotation to be performed.
let mut obstacles: Vec<(Box<dyn RayCast<f32>>, Isometry<f32>)> = Vec::new();
We'll use this definition to write out a function that creates some obstacles.
We create 6 planes that together create an enclosed cubical space. The cylinder is added just for fun. Note that obstacles are just logically entities that we will use for calculations. To show them in the scene, we will create
Mesh objects that are similar in shape to obstacles. We'll refactor the code that adds meshes, by moving it to a dedicated function.
Here we simply use a cuboid to visually represent 6 planes. We did not use a cuboid for creating the obstacles because of performance reasons. Ray casting inside solid object like a cuboid is slower.
cargo run should show something like this.
I have a ray gun
The boids don't care about the obstacles yet. They need to detect the obstacles and then find the closest unobstructed direction. To do this, a boid will fire off rays in all directions, like the image shown below.
The directions are not random, they are equally spaced on the surface of a sphere. The technique to generate the directions is also taken from Sebastian Lague's code.
For the purpose of this experiment, you don't need to understand this completely. Just that the expression samples equally spaced points from the surface of a sphere and stores it in an array. You can check out the mathematics behind it here, and explain it to me sometime.
What's more important is that, we've used a macro called
lazy_static = "1.4.0" to project dependencies. We are trying to create a static array of ray directions. These are some things to consider.
- The array of ray directions does not change once initialized.
- This array does not belong to any one
Boidinstance but can be considered a "class property" from other languages.
- Rust forbids
constvariables from containing heap allocated data and
Vector3is a heap allocated value.
- Rust does not allow for loops when declaring a
staticvariable. This is to ensure that the amount of memory allocated is known at compile time.
The conclusion is that, we're in a pickle. Rust's strict rules do not allow the common pattern of declaring a static array with dynamically allocated data.
lazy_static solves this by creating a one-off type that can be intialised only once. The variable still behaves like an array but it is only allocated when accessed for the first time. This solves our problem.
There is one flaw in the directions we created. The first value
Vector3::new(0.0, 0.0, 1.0) i.e. positive z-axis. Rays get equally spaced starting from the positive z-axis. The above image shows a boid with a velocity not aligned with the axis of ray cast sphere. We will need to orient these rays along the boid's velocity before firing them.
Next, we implement the function that does the collision checking. It will perform the following steps:
- Check for obstruction. If not obstructed, skip the following steps.
- Iterate over all ray directions
- Correct the ray direction as per orientation
- Construct a
Rayfor the corrected orientation
- Iterate over all obstacles and check for intersection with ray
- Store and return unobstructed ray direction
- Change the boid's velocity
A ray consists of a point and a direction (somewhat like a
Firing a ray means extending a line from the
origin for a specific length along the given
dir. If the line intersects any shape, it registers a collision. With this information, we can implement the
This function iterates over obstacles and returns true if any one of them intersects the given ray . Here
5.0 is the length of the ray and the limit of our collision detection. The next function is
unobstructed_dir. It finds the closes unobstructed ray direction.
Let's see how it works.
- It received the list of obstacles and their corresponding translations we created earlier.
- There is a very slim possibility that the boid is completely surrounded by obstacles. In this case, the function returns None. This is indicated by its return type
This snippet is similar to the one we used to rotate the cone in the direction of its velocity. Here we create a rotation to orient the rays with respect to the boid's velocity. The next section performs the actual intersection checking.
We iterate over ray directions, orient them, and check for collision. The first unobstructed direction is returned. We'll use the returned value to update
self.vel but only if its current direction is blocked.
This is the
frame_update function we created earlier. These are the changes:
- We added an input parameter to the function. It will receive the obstacle list.
- We fire in the current direction and check if it is blocked
- If it is blocked, we find an unblocked direction and update
self.vel. Only its velocity direction changes, with its magnitude staying the same.
- In case there is no unblocked direction, we do nothing.
The if-let idiom is used to perform steps 3 and 4 concisely. You can read about it here.
With these changes, we are 75% of the way there. The boids no longer escape the box. You can enjoy your serene animation over some soft music.
In this part of the series, we:
- Generated boids and cones using rng (Random Number Generation)
- Added obstacle avoidance to make a pretty animation
In the next and last part (phew!) of this series, we'll get the boids to behave like a flock. We'll do this by adding rules to change a boid's velocity based on its neigbours. Till then, ciao!
All criticism and questions are welcome. File an issue at the repo or comment below with your GitHub account.