Files
wsg/lib/examples/cameras/culling.rs
T
Jérôme Bousquié 24fbafc810 réorg doc
2026-09-25 19:08:20 +02:00

171 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! **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,
})
}