In this three part blog series, we will simulate a group of virtual agents (boids) that will swim around an enclosed space behaving like a school of fish. This is a beautiful application of procedural graphics generation, where simple rules create complex patterns. It is almost entirely inspired by Sebastian Lague's Coding Adventure with boids.
We'll be running this is experiment in Rust, in order to
break our headhave fun with some graphics code
- get dirty with Rust, which is an awesome programming language
- feel like a God
By the end of this series, we will have beautiful swirling patterns with a mind of their own. However we must start small. In this part, we will set up our scene and animate a single boid. In the end, you will have something like this.
Let's begin by setting up our environment. Skip these steps if you know you've met the requirements.
The code for this project is hosted on a GitHub repository. After each section, any significant additions to the code is tagged and linked in a commit. You can skip over a section and start directly from the source code at that point.
It is expected that you approach this blog with a certain level of programming experience and a basic understanding of Rust. If you're completely new to programming, this might not be the best place to start. If you're new to Rust, briefly going through the Rust Book will certainly help.
While experience with git is not required, it might help with navigating through the different tagged commits of the project.
In this section, we will set up our first scene.
cargo is the package manager for the Rust ecosystem. Running
cargo new rboids creates a few files for starting of our Rust project.
Since this is a visual project, we'll first add the graphics rendering library. We'll use a high level rendering library so as to not get bogged down by low level details.
three is perfectly suited for this purpose. Add
three = "0.4.0" under dependencies in
Let's create a window and a camera along with some objects to our scene. We'll edit the
main function in
Here's a breakdown of the code snippet:
three::Windowis a top level struct which contains all the objects added to the scene. It is taken as a
mutvariable because adding a new object changes its state.
- The camera is our eye in the scene. A perspective camera creates the effect of depth i.e. objects located farther, appear smaller. The
look_atfunction sets the position of the camera at coordinates
(5, 5, 5)and points its viewport/lens/eye towards the origin
(0, 0, 0).
- Adding the origin represents the general style of adding any mesh to the scene. A mesh represents the body of an object. Its geometry is the shape of the skeleton and its material is skin put over the skeleton. In this particular case, the wire-frame material just highlights the lines of the geometry, leaving the object itself transparent.
win.factory.meshcreates the mesh and applies it to
- The start scene snippet is the game loop. The game-loop terminates if we hit the
Escbutton or close the window. Any changes made to the scene must be performed inside this loop. The
renderfunction will then draw these changes on the screen.
The module system for Rust is fairly complex. Right now, all we're doing is importing the
three module. It contains the
Window struct, which we access by specifying the full path, namely
three::Window. Additionally, we import the
Object trait because it contains the
look_at method among others. We can use
look_at with any struct that implements the
Open a terminal inside
rboids folder and execute
cargo run. It might take some time because it is building the project for the first time. The following screen will pop up when compiling is done.
Bells and whistles
Let's add a few more objects to our scene. First we need reference points to observe the x, y and z axes. To do this, we need to create 3 more sphere-like origins, the only change being, we set their positions differently.
Let's also add a cone to see what a single boid looks like. A cylinder with 0 radius for one of its ends looks like a cone. The snippet also includes a cone to indicate the blind spot for a boid.
We'll change a bit of the existing code and also add an orbital controller to the camera. This way we can interact with the scene.
Here are the changes we made:
- We changed the position of the camera and added a controller to it. The
.upvector tells the controller that the y-axis is to be taken as the upright orientation of the camera
- Inside the game loop, we received input from the window and passed it to the controller. This appropriately adjusts the position and orientation of the camera.
- We added some reference points, enlarged the origin sphere, and added two cones.
I want to point out that the methods for
Orbit builder have different types, other than the arguments we used to pass it.
position requires a value of type
<Into<Point3<f32>> but we passed it into an array of
f32 value. It works because of the
Into trait. Any type that can be converted to
Into is valid. After all this,
cargo run should create the following scene.
A single boid is represented by a black cone. The green sphere is the vision radius and the red cone is the boid's blind spot. A boid can only see other boids inside its vision radius and ignores them if they're in its blind spot. While this is a handy visualization, we won't be using it when simulating a flock.
You'll also notice that the cone is pointing in the positive y-direction but the sphere has its axis along the z-axis. This is just how the library implements these objects. It will become important when we try to make a boid move in the direction of its velocity.
If you are curious, try changing the velocity of the cone before you move on. Play around with different values. You can start with this snippet.
Best foot forward
Here, we will give the cone a velocity and change its position. The
nalgebra crates will help us with the mathematics. A feature of the Rust module system is that, it allows one module to export an external module. So we'll only add
ncollide3d to our dependencies as it already exports the
All calculations related to velocity, position, and collision of boids will be calculated separately from the graphics and rendering code. Create a new file called
boid.rs inside the
src directory. Since this file is in the
src folder, Rust automatically considers this to be a module named
boid. Add the following lines to file.
Boid has two fields—its position and velocity with types
Vector3<f32>, respectively. By explicitly stating the absolute path, we can now use
Vector3 freely inside this module.
Both these types are aliases for more general types. Rust's type system is enforced strictly by the compiler, which can make programming with generic types especially difficult. To avoid writing large generic types,
nalgebra creates type aliases for commonly used specific types.
The diagram below shows how
Vector3 are derived by aliasing the very general
Matrix struct. The ovals are type aliases and lead to more general type aliases or structs. Structs are represented by the square boxes.
Alright. Let's add helpful methods to
Boid. We'll do this by implementing functions inside the
- The most important method here is
frame_update, which takes the time difference between two frames, multiplies with a constant scaling factor, and then adds that fraction of velocity to the position of the boid.
Vec<T>is a contiguous growable array type. It is similar to dynamically resizable lists/vectors from other languages. We'll use it to pass position and velocity as arguments when creating a new boid.
Vector3<T>has a method to create an object from
- The other functions act as getter methods that return the
velfields as an array of
We'll connect all of this with the
mod boid allows us to access the public structs and methods from
The game loop calls
frame_update to update the position of the boid. It then uses the boid's position to set the position of the cone we're viewing in the scene.
Voila! You now have a rudimentarily animated moving cone.
Notice how the cone is moving along the x-axis but pointing towards the y-axis? That's because we're only updating its position and not its orientation. We'll fix that in the next section.
Look where you're going
There is a glaring problem with our animation. The cone did not point in the direction of its velocity. It always points towards the y-axis, which is its default orientation. Let's fix the cone's orientation by rotating it. The
three::Object trait will help us move and rotate the cone in one method.
A Quaternion is a 4x4 matrix that is used to represent a 3D rotation. We'll need to find the rotation from the y-axis to the velocity of the cone.
rotation_between method takes two vectors and returns a rotation from the first vector to the second. It's type is
Option<UnitQuaternion> because it fails when both the vectors are exactly opposite to each other. This happens because there are multiple possible rotations.
It will happen when
self.vel is equivalent to
-Vector3::y_axis(). We handle failure using
unwrap_or. If the method returns a valid rotation, we use it. If it fails and returns a
unwrap_or returns a 180 degree rotation around the x-axis. This rotation will transform a vector along positive y-axis to one along negative y-axis.
Let's use this method in the game loop.
Now we're setting the setting the cone's position and orientation using the methods we defined for a boid.
Voila! A better-moving cone animation.
In this part, we:
- Set up our environment
- Created a graphical scene setup
- Created a simple animation of a moving cone
Things will get more interesting and complex. Before moving on, play around with the values, tinker around with stuff, and look through the documentation for interesting methods. And if you wanna get your hands dirty, try to change the velocity of the cone.
All criticism and questions are welcome, file an issue at the repo, or comment below with your GitHub account.