Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Interactive Demo

The demo below is a Nightshade application built for WebGPU and embedded in this page. It is the same engine binary that runs natively, compiled to WebAssembly.

Controls

  • Mouse drag: orbit the camera around the scene.
  • Scroll wheel: zoom in and out.

What is in the scene

Three primitives, a cube, a sphere, and a torus, sit on a procedural nebula background. The cube and the torus carry emissive material parameters, which the bloom post-process spreads into a glow around the bright pixels. The sphere is a polished chrome metal that reflects the image-based lighting captured from the procedural sky. An infinite grid sits at y = 0 for reference. A pan-orbit camera tracks the scene origin. Each object rotates and bobs every frame with smooth sinusoidal interpolation.

Browser requirements

The demo needs WebGPU.

  • Chrome and Edge: 113 or newer (enabled by default).
  • Firefox: 141 or newer (enabled by default).
  • Safari: 18 or newer (Technology Preview).

A blank frame means the browser does not yet expose WebGPU.

Source code

The whole demo is two files. The Cargo.toml declares the dependency on Nightshade and adds the wasm-bindgen plumbing for the web build. The src/main.rs is the entire game.

Cargo.toml

[package]
name = "hello-nightshade"
version = "0.1.0"
edition = "2024"

[dependencies]
nightshade = { git = "https://github.com/matthewjberger/nightshade" }

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"

[profile.release]
opt-level = "z"
lto = true

src/main.rs

use nightshade::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    launch(HelloNightshade::default())
}

#[derive(Default)]
struct HelloNightshade {
    cube: Option<Entity>,
    sphere: Option<Entity>,
    torus: Option<Entity>,
    time: f32,
}

impl State for HelloNightshade {
    fn initialize(&mut self, world: &mut World) {
        world.resources.window.title = "Hello Nightshade".to_string();
        world.resources.user_interface.enabled = false;
        world.resources.graphics.atmosphere = Atmosphere::Nebula;
        capture_procedural_atmosphere_ibl(world, Atmosphere::Nebula, 0.0);
        world.resources.graphics.bloom_enabled = true;
        world.resources.graphics.bloom_intensity = 0.15;
        world.resources.graphics.show_grid = true;

        let camera = spawn_pan_orbit_camera(
            world,
            Vec3::new(0.0, 0.0, 0.0),
            8.0,
            0.5,
            0.3,
            "Camera".to_string(),
        );
        world.resources.active_camera = Some(camera);

        spawn_sun(world);

        let cube = spawn_mesh_at(
            world,
            "Cube",
            Vec3::new(-3.0, 0.0, 0.0),
            Vec3::new(1.0, 1.0, 1.0),
        );
        spawn_material(
            world,
            cube,
            "CubeMaterial".to_string(),
            Material {
                base_color: [0.2, 0.6, 1.0, 1.0],
                metallic: 0.8,
                roughness: 0.2,
                emissive_factor: [0.1, 0.3, 0.5],
                emissive_strength: 2.0,
                ..Default::default()
            },
        );
        self.cube = Some(cube);

        let sphere = spawn_mesh_at(
            world,
            "Sphere",
            Vec3::new(0.0, 0.0, 0.0),
            Vec3::new(1.2, 1.2, 1.2),
        );
        spawn_material(
            world,
            sphere,
            "SphereMaterial".to_string(),
            Material {
                base_color: [1.0, 1.0, 1.0, 1.0],
                metallic: 1.0,
                roughness: 0.0,
                ..Default::default()
            },
        );
        self.sphere = Some(sphere);

        let torus = spawn_mesh_at(
            world,
            "Torus",
            Vec3::new(3.0, 0.0, 0.0),
            Vec3::new(0.8, 0.8, 0.8),
        );
        spawn_material(
            world,
            torus,
            "TorusMaterial".to_string(),
            Material {
                base_color: [0.3, 1.0, 0.4, 1.0],
                metallic: 0.7,
                roughness: 0.3,
                emissive_factor: [0.15, 0.5, 0.2],
                emissive_strength: 2.0,
                ..Default::default()
            },
        );
        self.torus = Some(torus);
    }

    fn run_systems(&mut self, world: &mut World) {
        pan_orbit_camera_system(world);

        let dt = world.resources.window.timing.delta_time;
        self.time += dt;

        if let Some(entity) = self.cube {
            if let Some(transform) = world.core.get_local_transform_mut(entity) {
                transform.rotation = nalgebra_glm::quat_angle_axis(self.time * 0.8, &Vec3::y())
                    * nalgebra_glm::quat_angle_axis(self.time * 0.5, &Vec3::x());
                transform.translation.y = (self.time * 1.5).sin() * 0.5;
            }
            mark_local_transform_dirty(world, entity);
        }

        if let Some(entity) = self.sphere {
            if let Some(transform) = world.core.get_local_transform_mut(entity) {
                transform.rotation = nalgebra_glm::quat_angle_axis(self.time * 0.3, &Vec3::y());
                let pulse = 1.0 + (self.time * 2.0).sin() * 0.1;
                transform.scale = Vec3::new(1.2 * pulse, 1.2 * pulse, 1.2 * pulse);
            }
            mark_local_transform_dirty(world, entity);
        }

        if let Some(entity) = self.torus {
            if let Some(transform) = world.core.get_local_transform_mut(entity) {
                transform.rotation = nalgebra_glm::quat_angle_axis(self.time * 1.2, &Vec3::z())
                    * nalgebra_glm::quat_angle_axis(self.time * 0.7, &Vec3::x());
                transform.translation.y = (self.time * 1.2 + 2.0).sin() * 0.5;
            }
            mark_local_transform_dirty(world, entity);
        }
    }
}

The initialize method sets up the window, the atmosphere, IBL capture, bloom, the grid, the camera, the sun, and three meshes with their materials. The run_systems method drives the camera controller and advances per-entity rotation and bobbing. Every transform mutation is followed by a mark_local_transform_dirty call so the frame schedule's transform propagation pass picks up the change.