Drone Simulation

For the last month, we've been hard at work developing a drone simulation, which is now working to help us develop flight controller software.

But aren't there tons of ready-made flight controllers out there? Why roll out our own? and why our own simulation??

Why we did this

Well, for a start, we wanted to learn, and isn't doing it yourself the best way to do that? Isn't learning enough motivation to waste a handful of weeks on development?

Rolling our own controller and simulation is the way to make sure we learn the process behind it and how to tweak it. In the end, our project is about learning, not about the final product, so it just makes sense.

First, we started with a simulation, then made made an OpenGL visualization for it, graphed out data with Python and finally started building the controller software.

I personally chose the Rust language because not only is it fast and memory safe, but its also usable in embedded systems. This means our controller code from the simulation could be reused as-is on the actual hardware.

Being a compiled language without garbage collection was a hard requirement due to optimization, and, wanting to avoid the low level memory management of C/C++, Rust was chosen.

How it works

In reality, our simulator is essentially a drone simulation on top of rapier, our physics engine of choice. It only handles rigid bodies, but that's about all we need. Aerodynamics can be calculated elsewhere and imported.

Every drone controller tick (so @600Hz), the simulation gets four throttles from the drone controller, supplying the drone's angular velocity, rotation and time (translation will also be used in the future for fully autonomous navigation).

This is made easy by using an interface, or as they're called in the rust world, a trait, which is implemented by every single drone controller we write.

pub trait DroneController {
    // Allow downcast of trait -> class.
    //
    // This gives us the ability to have a Box<dyn DroneController>
    // And transform it into a pointer to its underlying class.
    fn as_any(&self) -> &dyn Any;
    fn as_mut_any(&mut self) -> &mut dyn Any;

    /*
     * Methods called by the simulation (Drone struct), to transmit information to the controller.
     */
    fn set_rotation(&mut self, _rotation: nalgebra::Unit<nalgebra::Quaternion<f32>>) {}
    fn set_angular_velocity(&mut self, _angvel: nalgebra::Vector3<f32>) {}
    fn set_time(&mut self, _time: f32) {}
    fn set_motor_characteristics(&self, _motor_characteristics: &MotorCharacteristics) {}

    /*
     * Throttle should be between 0 and 1. Values will be clamped the Drone class.
     * Method called by the simulation (Drone struct), to get the motors output.
     */
    fn get_motor_throttles(&mut self) -> [f32; 4];
}

The motor throttles, gathered from the controller, are then simply applied onto the drone rigid body, by the Drone struct, as a force and a torque.

Air resistance is also calculated by this struct, through the following snippet:

drag.x = -calculate_drag(velocity.x, side_area, DRAG_CONSTANT);
drag.y = -calculate_drag(velocity.y, up_area, DRAG_CONSTANT);
drag.z = -calculate_drag(velocity.z, side_area, DRAG_CONSTANT);

body.add_force(drag, true);

Keeping in mind our drone is about cylinder shaped, this works out fine.

There are tons of other little implementation details, and the graphical part, which wasn't really touched here, was super fun! Having proper shadows come out of an old 4th gen Intel laptop without a performance hit is always fun :)

But there's no need to bore anyone with that.

The Flight Controller

For now, we only have manually controlled flying, through joystick or keyboard input, either recorded or in real time. For that, we implemented a PID controller, it's called that because the output is the addition of:

  • a value Proportional to the error
  • the Integral of the error
  • the Derivative of the error

The expression can be written down as:

u(t)=Kpe(t)+Ki0te(τ)dτ+Kdde(t)dt u(t) = K_{p} e(t) + K_{i} \int_{0}^{t} e(\tau ) d \tau + K_{d} \frac{de(t)}{dt}

This is done for each of the axis of rotation.

In reality, this is honestly over complicated for our needs within the simulation as is, as inaccuracies haven't really been added to the inputs of the controller and motor step times aren't really taken into account, but when those are added, and in the real world, this type of controller is perfect.

Comparing to a simple proportional controller, the PID has a faster response time and is better against overshoot.

  • The Integral term makes sure the longer an error is present, the stronger the reaction against it, so if there's an external force and the proportional factor alone doesn't combat it properly, it'll get near zero over time thanks to adding, essentially, the sum of all errors over time to our applied force. It also makes sure our values reaches the target, instead of approaching it.
  • The Derivative term greatly reduces overshoot, because as the error gets smaller, by the nature of the other factors the derivative gets smaller too, so less force is applied.

Turning our desired effect into throttles

This is done by adding the pitch, yaw and roll as an offset of the throttle on each motor.

For example, if we want to pitch forward, we will remove x throttle from the front 2 motors and add it to the back.

Basic implementation used shown in the following snippet:

let forces_to_apply = vector![
    error.x * self.proportional_multiplier.x
        + self.state.error_sum.x * self.integral_multiplier.x
        + error_dif.x * self.diferential_multiplier.x,
    error.y * self.proportional_multiplier.y
        + self.state.error_sum.y * self.integral_multiplier.y
        + error_dif.y * self.diferential_multiplier.y,
    error.z * self.proportional_multiplier.z
        + self.state.error_sum.z * self.integral_multiplier.z
        + error_dif.z * self.diferential_multiplier.z,
];

let pitch = forces_to_apply.x;
let yaw = forces_to_apply.y;
let roll = forces_to_apply.z;

let mut motors: [f32; 4] = [
    throttle - pitch + yaw + roll,
    throttle - pitch - yaw - roll,
    throttle + pitch + yaw - roll,
    throttle + pitch - yaw + roll,
];

Data Analysis

Every simulation tick, we store the time, the drones angular velocity, its target angular velocity, how the forces applied are affecting the drones angular velocity and how the controller wanted them to be affected.

Note that the latter two sometimes do not match, as the motors have limits, when requiring a lot of lift or near 0 lift, we are limited on how much we can rotate. These are the values we are currently monitoring when using our manual control scheme. For autonomous flying these will, of course, be different

At the end of a simulation, all of these are written out to a .csv file, which is later parsed and graphed out by python with mathplotlib + pandas.

Graphs will be shared at a latter date! Maybe in another blog post?

Batch running

To test out different controller configurations and how they respond to different inputs, a simulation is automatically ran for every drone controller configuration represented in configurations/ for every input recording in inputs/. The end result is something like this:

configurations/
├── PID_med+.toml
├── PI_med+.toml
...
inputs/
├── all_agressive.csv
├── x_axis.csv
├── y_axis.csv
...
results/
├── all_agressive_PID_med+.csv
├── all_agressive_PI_med+.csv
├── x_axis_PID_med+.csv
├── x_axis_PI_med+.csv
├── y_axis_PID_med+.csv
├── y_axis_PI_med+.csv
...

Contribution to the community

Source code will be available soon, there's still some cleaning up to do, and that is going to get pushed forward for a while until we have our entry video ready.

For example, as of writing, everything is treated by the renderer as a cube, so yeah, that works for us but isn't quite going to cut it. Metal (the modern rendering API for Macs) isn't implemented yet, as I don't have a mac, so it isn't quite cross-platform.

I hope the code will be of use as a learning resource, but realistically, there are wayyy better alternatives, this was just plain fun!

Francisco Lima and the team.

Stars Background
Stars Background
© 2026 AtlaSat — Built with ❤️ by the team