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.