e5f3636b42
- 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)
192 lines
6.5 KiB
Rust
192 lines
6.5 KiB
Rust
//! **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,
|
||
})
|
||
}
|