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)
187 lines
6.7 KiB
Rust
187 lines
6.7 KiB
Rust
//! # Fog Example (Étape 25)
|
||
//!
|
||
//! Demonstrates distance fog: objects fade into the fog color as they recede,
|
||
//! creating the illusion of an infinite world (Skyrim/GTA pattern).
|
||
//!
|
||
//! The scene has a row of cubes receding into the distance and scattered spheres,
|
||
//! all sitting on a large ground plane. Switch fog modes with number keys to
|
||
//! compare the three attenuation curves.
|
||
//!
|
||
//! ## Pipeline
|
||
//! Fog is applied in the main pass fragment shader (after lighting, before tone
|
||
//! mapping). It uses the fragment's world-space distance to the camera and
|
||
//! blends the final color toward `fog_color`.
|
||
//!
|
||
//! ## Controls
|
||
//! | Key | Action |
|
||
//! |-----|--------|
|
||
//! | Drag (LMB) | Orbit camera |
|
||
//! | Wheel | Zoom |
|
||
//! | `1` | Linear fog (near=5, far=30) |
|
||
//! | `2` | Exponential fog (density=0.04) |
|
||
//! | `3` | Exponential² fog (density=0.06) — best for masking |
|
||
//! | `4` | Fog OFF |
|
||
//! | `R` | Reset camera |
|
||
//!
|
||
//! ## Build & Run
|
||
//! ```sh
|
||
//! cargo run -p wsg-lib --example fog --features "all-prims"
|
||
//! ```
|
||
|
||
use glam::Vec3;
|
||
use winit::event::MouseButton;
|
||
use winit::keyboard::KeyCode;
|
||
use wsg_lib::app::AppBuilder;
|
||
use wsg_lib::camera::CameraController;
|
||
use wsg_lib::core::{FogConfig, ToneMapper, Transform};
|
||
use wsg_lib::mesh::{cube, icosphere, 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");
|
||
|
||
struct FogDemo {
|
||
camera: CameraController,
|
||
}
|
||
|
||
impl AppHandler for FogDemo {
|
||
fn setup(&mut self, app: &mut wsg_lib::App) {
|
||
app.scene
|
||
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
||
.unwrap();
|
||
|
||
// Textured ground + cubes: the fog falloff reads much better on textured surfaces.
|
||
// ground.jpeg tiles across the 80×80 floor (Repeat sampler); stonewall.jpg on the cubes.
|
||
let (device, queue) = {
|
||
let ctx = app.context();
|
||
(ctx.device.clone(), ctx.queue.clone())
|
||
};
|
||
let ground_tex =
|
||
Texture::from_file(&device, &queue, "ground", &format!("{TEXTURES}/ground.jpeg")).unwrap();
|
||
app.scene.add_texture("ground_texture", ground_tex).unwrap();
|
||
app.scene
|
||
.add_material_texture("ground_mat", "standard", "ground_texture")
|
||
.unwrap();
|
||
let wall_tex =
|
||
Texture::from_file(&device, &queue, "wall", &format!("{TEXTURES}/stonewall.jpg")).unwrap();
|
||
app.scene.add_texture("wall_texture", wall_tex).unwrap();
|
||
app.scene
|
||
.add_material_texture("wall_mat", "standard", "wall_texture")
|
||
.unwrap();
|
||
|
||
// Large ground plane — will fade into fog at distance.
|
||
app.scene
|
||
.create_mesh("ground_mesh", plane(80.0, 80.0, 1, 1), Some("ground_mat"))
|
||
.unwrap();
|
||
app.scene.add_entity("ground", "ground_mesh").unwrap();
|
||
|
||
// Row of cubes receding into the distance.
|
||
app.scene
|
||
.create_mesh("cube_mesh", cube(1.0), Some("wall_mat"))
|
||
.unwrap();
|
||
for i in 0..15 {
|
||
let z = -2.0 - i as f32 * 2.5;
|
||
let mut tf = Transform::identity();
|
||
tf.translation = Vec3::new(0.0, 0.5, z);
|
||
app.scene
|
||
.add_entity_with_transform(&format!("cube_{i}"), "cube_mesh", tf)
|
||
.unwrap();
|
||
}
|
||
|
||
// Scattered spheres at various distances.
|
||
app.scene
|
||
.create_mesh("sphere_mesh", icosphere(0.8, 3), None)
|
||
.unwrap();
|
||
let positions = [
|
||
Vec3::new(3.0, 0.8, -5.0),
|
||
Vec3::new(-4.0, 0.8, -10.0),
|
||
Vec3::new(5.0, 0.8, -15.0),
|
||
Vec3::new(-3.0, 0.8, -20.0),
|
||
Vec3::new(0.0, 0.8, -30.0),
|
||
];
|
||
for (i, pos) in positions.iter().enumerate() {
|
||
let mut tf = Transform::identity();
|
||
tf.translation = *pos;
|
||
app.scene
|
||
.add_entity_with_transform(&format!("sphere_{i}"), "sphere_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.85], 1.2)
|
||
.unwrap();
|
||
app.scene.set_ambient([0.08, 0.08, 0.1]);
|
||
|
||
// Camera — positioned to look down the row of cubes.
|
||
self.camera.yaw = 0.0;
|
||
self.camera.pitch = 0.15;
|
||
self.camera.distance = 8.0;
|
||
self.camera.target = Vec3::new(0.0, 0.5, -8.0);
|
||
self.camera.apply_to(app.scene.camera_mut());
|
||
|
||
// Print initial fog status.
|
||
eprintln!("[Fog] Initial: Exponential² (density=0.06)");
|
||
eprintln!("[Fog] Keys: 1=linear 2=exp 3=exp² 4=off R=reset");
|
||
}
|
||
|
||
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);
|
||
|
||
// Fog mode switching.
|
||
if app.input.key_pressed(KeyCode::Digit1) {
|
||
app.renderer_mut()
|
||
.set_fog(Some(FogConfig::linear([0.7, 0.75, 0.85], 5.0, 30.0)));
|
||
eprintln!("[Fog] → Linear (near=5, far=30)");
|
||
}
|
||
if app.input.key_pressed(KeyCode::Digit2) {
|
||
app.renderer_mut()
|
||
.set_fog(Some(FogConfig::exponential([0.7, 0.75, 0.85], 0.04)));
|
||
eprintln!("[Fog] → Exponential (density=0.04)");
|
||
}
|
||
if app.input.key_pressed(KeyCode::Digit3) {
|
||
app.renderer_mut()
|
||
.set_fog(Some(FogConfig::exponential2([0.7, 0.75, 0.85], 0.06)));
|
||
eprintln!("[Fog] → Exponential² (density=0.06)");
|
||
}
|
||
if app.input.key_pressed(KeyCode::Digit4) {
|
||
app.renderer_mut().set_fog(None);
|
||
eprintln!("[Fog] → OFF");
|
||
}
|
||
if app.input.key_pressed(KeyCode::KeyR) {
|
||
self.camera.yaw = 0.0;
|
||
self.camera.pitch = 0.15;
|
||
self.camera.distance = 8.0;
|
||
}
|
||
|
||
self.camera.apply_to(app.scene.camera_mut());
|
||
}
|
||
|
||
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 — Fog (3 modes)")
|
||
.size(1024, 640)
|
||
.with_fog(FogConfig::exponential2([0.7, 0.75, 0.85], 0.06))
|
||
.with_hdr(ToneMapper::Aces)
|
||
.build()
|
||
.await?;
|
||
app.run(FogDemo {
|
||
camera: CameraController::default(),
|
||
})
|
||
}
|