//! **GPU Frustum Culling** — demonstrates the GPU-driven culling pipeline. //! //! A grid of 15×15 cubes is placed in a large field. When GPU culling is enabled, //! cubes outside the camera frustum are skipped on the GPU (their indirect draw //! args are zeroed by the culling compute pass). Orbit the camera to see objects //! behind you simply not being drawn. //! //! To compare with/without culling, run twice: //! ```sh //! cargo run -p wsg-lib --example culling # culling ON (default) //! ``` //! Or modify `CULLING_ENABLED` in the source. //! //! ## Controls //! | Key | Action | //! |-----|--------| //! | Drag (LMB) | Orbit camera (look around to see culling) | //! | Wheel | Zoom in/out | //! | `R` | Reset camera | //! | `1` | Front view | //! | `2` | Side view | //! | `3` | Top view (see full grid) | //! //! ## What to look for //! - From the top view (`3`), you see the full 15×15 grid. //! - Orbit to the side: cubes behind you are culled (not rendered). //! - Zoom in close: only nearby cubes are drawn. //! - The culling happens 100% on the GPU (compute pass) — zero CPU cost. //! //! ## Build & Run //! ```sh //! cargo run -p wsg-lib --example culling //! ``` use glam::{Quat, Vec3}; use winit::event::MouseButton; use winit::keyboard::KeyCode; use wsg_lib::app::AppBuilder; use wsg_lib::camera::CameraController; use wsg_lib::core::Transform; use wsg_lib::mesh::{cube, plane}; use wsg_lib::AppHandler; use wsg_lib::utils::WsgError; /// Grid dimensions (15×15 = 225 cubes, fits within MAX_ENTITIES=256). const GRID: usize = 15; /// Spacing between cubes (world units). const SPACING: f32 = 1.2; /// Whether to enable GPU culling. const CULLING_ENABLED: bool = true; struct CullingDemo { camera: CameraController, angle: f32, } impl AppHandler for CullingDemo { fn setup(&mut self, app: &mut wsg_lib::App) { app.scene .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .unwrap(); // Large ground plane. let ground_size = (GRID as f32 * SPACING) * 1.5; app.scene .create_mesh("ground_mesh", plane(ground_size, ground_size, 1, 1), None) .unwrap(); app.scene.add_entity("ground", "ground_mesh").unwrap(); // One shared cube mesh (all entities reference the same GPU buffers). app.scene .create_mesh("cube_mesh", cube(0.5), None) .unwrap(); // Place the grid of cubes. let half = (GRID / 2) as f32; for i in 0..GRID { for j in 0..GRID { let x = i as f32 * SPACING - half; let z = j as f32 * SPACING - half; let label = format!("cube_{}_{}", i, j); let mut tf = Transform::identity(); tf.translation = Vec3::new(x, 0.25, z); app.scene .add_entity_with_transform(&label, "cube_mesh", tf) .unwrap(); } } // Directional light. let light_dir = Vec3::new(0.5, 1.0, 0.3).normalize(); app.scene .add_directional_light(light_dir, [1.0, 0.95, 0.88], 1.2) .unwrap(); app.scene.set_ambient([0.15, 0.15, 0.18]); // Camera: start at top view to see the full grid. self.camera.yaw = 0.0; self.camera.pitch = 1.2; self.camera.distance = 15.0; self.camera.target = Vec3::ZERO; self.camera.apply_to(app.scene.camera_mut()); } fn update(&mut self, app: &mut wsg_lib::App) { // Orbit camera. let (dx, dy) = app.input.mouse_delta(); if app.input.mouse_button_held(MouseButton::Left) { self.camera.orbit(dx, dy); } let (_, sy) = app.input.scroll_delta(); self.camera.zoom(sy); // Camera presets. if app.input.key_pressed(KeyCode::KeyR) { self.camera.yaw = 0.0; self.camera.pitch = 1.2; self.camera.distance = 15.0; } if app.input.key_pressed(KeyCode::Digit1) { self.camera.yaw = 0.0; self.camera.pitch = 0.1; self.camera.distance = 15.0; } if app.input.key_pressed(KeyCode::Digit2) { self.camera.yaw = std::f32::consts::FRAC_PI_2; self.camera.pitch = 0.1; self.camera.distance = 15.0; } if app.input.key_pressed(KeyCode::Digit3) { self.camera.yaw = 0.0; self.camera.pitch = 1.4; self.camera.distance = 18.0; } self.camera.apply_to(app.scene.camera_mut()); // Slow rotation of the whole grid (subtle, to show dynamic culling). self.angle += 0.002; for i in 0..GRID { for j in 0..GRID { let label = format!("cube_{}_{}", i, j); if let Some(base) = app.scene.entity_transform(&label) { let mut tf = *base; // Rotate each cube slightly (staggered by position for visual interest). let phase = (i as f32 + j as f32) * 0.1; tf.rotation = Quat::from_rotation_y(self.angle + phase); app.scene.set_entity_transform(&label, tf); } } } } fn render(&mut self, app: &mut wsg_lib::App, frame: &wsg_lib::core::Frame) { app.render_scene(frame.view()); } } #[pollster::main] async fn main() -> Result<(), WsgError> { let app = AppBuilder::new() .title("WSG Culling (20×20 grid)") .size(1024, 768) .with_culling(CULLING_ENABLED) .build() .await?; app.run(CullingDemo { camera: CameraController::default(), angle: 0.0, }) }