Files
wsg/lib/examples/cameras/culling.rs
T
Jérôme Bousquié e5f3636b42 examples: apply real texture assets to multi-mesh examples
- meshes/cube: procedural checkerboard -> uv_texture.jpg (8x8 UV grid)
- meshes/pbr: floor -> ground.jpeg, bump cube -> cave.jpg + caveNormal.jpg
  (normal map pre-encoded via sRGB OETF to cancel the GPU sRGB decode)
- lights/shadow: ground -> ground.jpeg, cube -> uv_texture.jpg
- effects/demo: ground -> ground.jpeg, cube -> uv_texture.jpg
- effects/fog: ground -> ground.jpeg (tiled 80x80), cubes -> stonewall.jpg
- effects/dof: ground -> ground.jpeg, cubes -> uv_texture.jpg
- cameras/culling: shared cube mesh -> uv_texture.jpg
- add lib/examples/assets/textures/ (19 assets, 6.5 MB)
- document assets + usage in examples READMEs, docs/user/examples.md,
  docs/user/meshes/materials.md (CARGO_MANIFEST_DIR pattern, sRGB caveat)
2026-09-26 10:49:13 +02:00

192 lines
6.5 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::resources::Texture;
use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError;
/// Texture asset directory, resolved against the crate root so the example works from any CWD.
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
/// 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), textured with
// the uv_texture.jpg atlas — the colourful labelled cells make it obvious exactly
// which cubes the GPU draws and which it culls.
let (device, queue) = {
let ctx = app.context();
(ctx.device.clone(), ctx.queue.clone())
};
let uv_tex = Texture::from_file(
&device,
&queue,
"uv_atlas",
&format!("{TEXTURES}/uv_texture.jpg"),
)
.unwrap();
app.scene.add_texture("uv_texture", uv_tex).unwrap();
app.scene
.add_material_texture("cube_mat", "standard", "uv_texture")
.unwrap();
app.scene
.create_mesh("cube_mesh", cube(0.5), Some("cube_mat"))
.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,
})
}