Skip to content

quasariumm/rusterizer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rusterizer

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.

Engine screenshot

Installation / Set-up

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 -r to 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.

Features

Wide selection of texture formats

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.

Bilinear Sampling

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. Bilinear sampling example

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

Tiled rendering

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.

Runtime model loading and instantiation

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).

Model rendering with tone mapping

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);

PBR-ish rendering

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.

Usage

Basic usage

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.

Controls

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

Profiling

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.

puffin

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

Honorable mentions

img.png

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)

img.png

The 'Lucaverse' bug (which is still in the engine sadly)

About

A software rasterizer made in Rust for the Traverse Research masterclass

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages