A software rasterizer made in Rust for the masterclass hosted by @quartenia and Traverse Research.
The engine has a nice ui created with egui and uses the Catppuccin color palette.
The project is split up in the rusterizer crate which is the main engine library and rusterizer_demo which shows
some basic usage and serves as a quick hello-world template.
Make sure you have Rust installed before compiling the project.
To use the library, simply add the crates/rusterizer folder to your project and make sure you have
[workspace]
members = [
"crates/*"
]
[dependencies]
rusterizer = { path = "crates/rusterizer" }in your Cargo.toml file.
To compile the demo, simply either
- Run
cargo r -r(remove-rto run in debug, but that is SLOW) - Open the project in any IDE of your choosing and run the demo project. I have included run configurations for JetBrains' RustRover.
The engine's Texture struct allows the user to make textures in a lot of formats. The textures are then stored in the smallest
unsigned integer that fits all channels. Sampler functions then allow the user to get any data type they want. Depending on the
format, this can be through a remap (Snorm, Unorm and UnormSrgb) or a bitcast (any other type). Because of this versatility,
there is a bit of a performance overhead over using a texture with a fixed format.
Users have the option to sample textures using bilinear sampling. This method of sampling gives back a color on a sub-pixel level
by making use of linear interpolation. The filter mode can be set in the texture::SamplerConfig struct.

Mip levels are selected based on the depth of a pixel. The image below shows the inverted effect and amplified for demonstrational purposes.

The engine renders the triangles in tiles. Each of the tiles is rendered on a separate thread, making use of the amazing
rayon crate that handles the job threads for me.
I optimised this further by binning the triangles or simpler said that each tile only processes the triangles that overlap it. This binning is done at the beginning and each time the camera moves. This means that when you're stationary you'd get the best performance.
As with ray tracing, the performance is determined by the worst tile.
Since the rendering is done on the CPU, adding this was a breeze. The UI shows an option for adding a model, which shows all
glTF files present in the assets folder or any of its subdirectories. Adding a model that is already loaded just spawns a new
instance at (0, 0, 0).
Models can be imported into the engine from a glTF file using the add_model function in the Scene struct.
The models are loaded in via the resource manager and loaded in as a model instance in the scene. This means that the
models do not need to loaded in multiple times. The albedo, occlusion and emission maps of the model are used in the
rendering. To make the emissive maps work, PBR Neutral tone mapping is applied.
// In the user init function:
let mut scene = Scene::default();
scene
.add_model(ModelInstance::from_path(
resources,
PathBuf::from("assets/luca_cube.gltf"),
Transform::from_translation(point3(4.0, 0.0, -4.0)),
))
.with_name("Test scene 2".into());
scenes.add_select_scene(scene);The engine renders models with a BPR-ish shader. This is because I did not have enough time to implement the skybox mipmaps, and thus I needed to be creative with the resources that I have. Environment maps can be added to a scene like this:
scene
.with_environment(Texture::from(PathBuf::from(
"assets/environment.hdr", // EXR is also supported
)));In the shader, normal mapping is applied and the skybox is sampled based on the normal and view direction. Then, based on the roughness and metallic properties, color contributions are weighed to make the PBR-ish look that can be seen in the spotlight image at the top of this document.
To run an application, make your own struct that implements the rusterizer::application::Application trait. You are not
required to implement all the user functions, but I give you the option. Then, add this as a main function:
fn main() -> Result<(), Box<dyn Error>> {
run_application(
Box::new(UserApp::default()),
"Rusterizer",
(1280, 720).into(),
)?;
Ok(())
}You can add scenes to the project by adding them from the init function provided by the Application trait.
Any custom UI gets appended to the end of the side panel content.
The only controls in the engine are for the camera and the ones already handled by puffin.
- Move the camera: Hold right click and
- WASD: Move
- Move the mouse: Pan
- Play/pause profiling view: Space
The engine implements the puffin_egui trait, which allows for scoped profiling. To enable the profiling plugin,
run the project with the environment variable PUFFIN_ENABLED=true.
Profiling in RustRover can also be done through the Flamegraph run configuration, for which you need to install the
flamegraph crate globally by running cargo install flamegraph
The model 'A Beautiful Game' (574,528 tris) rendering at ~6fps on a 2345x1341 viewport with 64x64 tiles.
CPU: AMD Ryzen 9 7945HX3D (16 core, 32 thread, 1MB L1, 16MB L2, 128MB L3)
The 'Lucaverse' bug (which is still in the engine sadly)



