diff --git a/Cargo.toml b/Cargo.toml index a29eb37..fa34ffa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,6 +135,8 @@ colorous = "1.0.12" web-time = "1.0.0" winit = "0.30.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +glam = { version = "0.30.10", features = ["rand"] } +rand = "0.9.2" [target.'cfg(target_os = "android")'.dev-dependencies] winit = { version = "0.30.0", features = ["android-native-activity"] } @@ -157,6 +159,8 @@ criterion = { version = "0.8.1", default-features = false, features = [ wasm-bindgen-test = "0.3" console_error_panic_hook = "0.1" tracing-web = "0.1" +# Allow `rand` crate to compile. +getrandom = { version = "0.3.4", features = ["wasm_js"] } [target.'cfg(not(any(target_os = "android", target_vendor = "apple", target_os = "redox", target_family = "wasm", target_os = "windows")))'.dev-dependencies] rustix = { version = "1.0.1", features = ["event"] } diff --git a/examples/raytracing/camera.rs b/examples/raytracing/camera.rs new file mode 100644 index 0000000..1f3492f --- /dev/null +++ b/examples/raytracing/camera.rs @@ -0,0 +1,66 @@ +use rand::Rng; + +use crate::ray::Ray; +use crate::vec3; +use crate::vec3::Point3; +use crate::vec3::Vec3; + +pub struct Camera { + origin: Point3, + lower_left_corner: Point3, + horizontal: Vec3, + vertical: Vec3, + u: Vec3, + v: Vec3, + _w: Vec3, + lens_radius: f32, +} + +impl Camera { + pub fn new( + position: Point3, + direction: Point3, + up: Vec3, + fov: f32, // Vertical field-of-view in degrees + aspect_ratio: f32, + aperture: f32, + focus_dist: f32, + ) -> Self { + let theta = fov.to_radians(); + let h = f32::tan(theta / 2.0); + let viewport_height = 2.0 * h; + let viewport_width = aspect_ratio * viewport_height; + + let w = (-direction).normalize(); + let u = (up.cross(w)).normalize(); + let v = w.cross(u); + + let origin = position; + let horizontal = focus_dist * viewport_width * u; + let vertical = focus_dist * viewport_height * v; + let lower_left_corner = origin - horizontal / 2.0 - vertical / 2.0 - focus_dist * w; + + let lens_radius = aperture / 2.0; + + Self { + origin, + lower_left_corner, + horizontal, + vertical, + u, + v, + _w: w, + lens_radius, + } + } + + pub fn get_ray(&self, s: f32, t: f32, rng: &mut impl Rng) -> Ray { + let rd = self.lens_radius * vec3::random_in_unit_disk(rng); + let offset = self.u * rd.x + self.v * rd.y; + + Ray::new( + self.origin + offset, + self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin - offset, + ) + } +} diff --git a/examples/raytracing/game.rs b/examples/raytracing/game.rs new file mode 100644 index 0000000..5b7abd9 --- /dev/null +++ b/examples/raytracing/game.rs @@ -0,0 +1,225 @@ +use std::time::{Duration, Instant}; + +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use softbuffer::Buffer; + +use crate::camera::Camera; +use crate::vec3::{Color, Point3, Vec3}; +use crate::world::World; + +pub struct Game { + world: World, + up: Vec3, + camera_position: Point3, + pub camera_yaw: f32, + pub camera_pitch: f32, + /// x = right, y = up, z = forwards + pub camera_velocity: Vec3, + elapsed_time: Instant, +} + +const SAMPLES_PER_PIXEL: i32 = 3; +const MAX_DEPTH: i32 = 5; +pub const MOVEMENT_SPEED: f32 = 10.0; +pub const MOUSE_SENSITIVITY: f32 = 0.005; +const DURATION_BETWEEN_TICKS: Duration = Duration::from_millis(10); + +impl Game { + pub fn new() -> Self { + let mut rng = SmallRng::from_os_rng(); + let position = Point3::new(13.0, 2.0, 3.0); + let looking_at = Point3::new(0.0, 0.0, 0.0); + let camera_direction = (looking_at - position).normalize(); + let up = Vec3::new(0.0, 1.0, 0.0); + Self { + world: World::random_scene(&mut rng), + up, + camera_position: position, + camera_yaw: camera_direction.x.atan2(camera_direction.z), + camera_pitch: camera_direction.y.clamp(-1.0, 1.0).asin(), + camera_velocity: Vec3::new(0.0, 0.0, 0.0), + elapsed_time: Instant::now(), + } + } + + pub fn draw( + &self, + buffer: &mut Buffer<'_, impl HasDisplayHandle, impl HasWindowHandle>, + scale_factor: f32, + ) { + self.draw_scene(buffer, scale_factor); + self.draw_ui(buffer, scale_factor); + } + + /// Draw the 3D scene. + fn draw_scene( + &self, + buffer: &mut Buffer<'_, impl HasDisplayHandle, impl HasWindowHandle>, + scale_factor: f32, + ) { + // Raytracing is expensive, so we only do it once every 4x4 pixel. + // + // FIXME(madsmtm): Avoid the need for this once we can do hardware scaling. + // https://github.com/rust-windowing/softbuffer/issues/177 + let scale_factor = scale_factor * 4.0; + + let width = buffer.width().get() as f32 / scale_factor; + let height = buffer.height().get() as f32 / scale_factor; + + let dist_to_focus = 10.0; + let aperture = 0.1; + let cam = Camera::new( + self.camera_position, + self.camera_direction(), + self.up, + 20.0, + width / height, + aperture, + dist_to_focus, + ); + + let mut pixels = vec![0; width as usize * height as usize]; + + let each_pixel = |rng: &mut SmallRng, i, pixel: &mut u32| { + let y = i % (width as usize); + let x = i / (width as usize); + let mut pixel_color = Color::default(); + for _ in 0..SAMPLES_PER_PIXEL { + let s = (y as f32 + rng.random::()) / (width - 1.0); + let t = 1.0 - (x as f32 + rng.random::()) / (height - 1.0); + let r = cam.get_ray(s, t, rng); + pixel_color += r.trace(&self.world, MAX_DEPTH, rng); + } + *pixel = color_to_pixel(pixel_color, SAMPLES_PER_PIXEL); + }; + + // Render in parallel with rayon. + #[cfg(not(target_family = "wasm"))] + { + use rayon::prelude::*; + + pixels + .par_iter_mut() + .enumerate() + .for_each_init(SmallRng::from_os_rng, move |rng, (i, pixel)| { + each_pixel(rng, i, pixel) + }); + }; + #[cfg(target_family = "wasm")] + { + let mut rng = SmallRng::from_os_rng(); + pixels + .iter_mut() + .enumerate() + .for_each(|(i, pixel)| each_pixel(&mut rng, i, pixel)); + } + + // Upscale by `scale_factor`. + let width = buffer.width().get() as usize; + buffer.iter_mut().enumerate().for_each(|(i, pixel)| { + let y = i % width; + let x = i / width; + let y = (y as f32 / scale_factor) as usize; + let x = (x as f32 / scale_factor) as usize; + if let Some(x) = pixels.get(x * (width as f32 / scale_factor) as usize + y) { + *pixel = *x; + } + }); + } + + /// Draw a simple example UI on top of the scene. + fn draw_ui( + &self, + buffer: &mut Buffer<'_, impl HasDisplayHandle, impl HasWindowHandle>, + scale_factor: f32, + ) { + struct Rect { + left: f32, + right: f32, + top: f32, + bottom: f32, + color: u32, + } + + let width = buffer.width().get() as f32 / scale_factor; + let height = buffer.height().get() as f32 / scale_factor; + let rects = &[ + Rect { + left: 10.0, + right: width - 10.0, + top: height - 90.0, + bottom: height - 10.0, + color: 0x00eeaaaa, + }, + Rect { + left: 30.0, + right: 70.0, + top: height - 70.0, + bottom: height - 30.0, + color: 0x00aaaaee, + }, + ]; + + let width = buffer.width().get(); + for (y, row) in buffer.chunks_exact_mut(width as usize).enumerate() { + for rect in rects { + let rect_vertical = + (rect.top * scale_factor) as usize..(rect.bottom * scale_factor) as usize; + let rect_horizontal = + (rect.left * scale_factor) as usize..(rect.right * scale_factor) as usize; + if rect_vertical.contains(&y) { + if let Some(row) = row.get_mut(rect_horizontal) { + row.fill(rect.color); + } + } + } + } + } + + fn tick(&mut self) { + let forward = self.camera_direction().with_y(0.0); + let right = forward.cross(self.up).normalize(); + let up = right.cross(forward); + let movement = forward * self.camera_velocity.z + + up * self.camera_velocity.y + + right * self.camera_velocity.x; + self.camera_position += movement * DURATION_BETWEEN_TICKS.as_secs_f32(); + } + + pub fn update(&mut self) { + // Update game state. + let now = Instant::now(); + while let Some(_remainder) = now + .duration_since(self.elapsed_time) + .checked_sub(DURATION_BETWEEN_TICKS) + { + self.elapsed_time += DURATION_BETWEEN_TICKS; + self.tick(); + } + } + + fn camera_direction(&self) -> Vec3 { + Vec3::new( + self.camera_pitch.cos() * self.camera_yaw.sin(), + self.camera_pitch.sin(), + self.camera_pitch.cos() * self.camera_yaw.cos(), + ) + } +} + +fn color_to_pixel(pixel_color: Color, samples_per_pixel: i32) -> u32 { + let mut r = pixel_color.x; + let mut g = pixel_color.y; + let mut b = pixel_color.z; + + let scale = 1.0 / samples_per_pixel as f32; + r = f32::sqrt(scale * r); + g = f32::sqrt(scale * g); + b = f32::sqrt(scale * b); + + (256.0 * b.clamp(0.0, 0.999)) as u32 + | (((256.0 * g.clamp(0.0, 0.999)) as u32) << 8) + | (((256.0 * r.clamp(0.0, 0.999)) as u32) << 16) +} diff --git a/examples/raytracing/main.rs b/examples/raytracing/main.rs new file mode 100644 index 0000000..2eed2d7 --- /dev/null +++ b/examples/raytracing/main.rs @@ -0,0 +1,146 @@ +//! A software raytracer based on [Ray Tracing in One Weekend]. +//! +//! Note that this is quite slow, you probably don't want to do realtime CPU raytracing in practice. +//! +//! [Ray Tracing in One Weekend]: https://raytracing.github.io/books/RayTracingInOneWeekend.html +use std::num::NonZeroU32; +use winit::event::{DeviceEvent, ElementState, KeyEvent, WindowEvent}; +use winit::event_loop::EventLoop; +use winit::keyboard::{Key, KeyCode, NamedKey, PhysicalKey}; +use winit::window::CursorGrabMode; + +use crate::game::{Game, MOUSE_SENSITIVITY, MOVEMENT_SPEED}; + +mod camera; +mod game; +mod material; +mod objects; +mod ray; +#[path = "../util/mod.rs"] +mod util; +mod vec3; +mod world; + +fn main() { + util::setup(); + + let event_loop = EventLoop::new().unwrap(); + let context = softbuffer::Context::new(event_loop.owned_display_handle()).unwrap(); + + let app = util::WinitAppBuilder::with_init( + |elwt| util::make_window(elwt, |w| w), + move |_elwt, window| { + let mut surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); + surface + .resize( + NonZeroU32::new(window.inner_size().width).unwrap(), + NonZeroU32::new(window.inner_size().height).unwrap(), + ) + .unwrap(); + let game = Game::new(); + (surface, game) + }, + ) + .with_event_handler(|window, surface, window_id, event, elwt| { + if window_id != window.id() { + return; + } + + match event { + WindowEvent::Resized(size) => { + let Some((surface, _)) = surface else { + tracing::error!("Resized fired before Resumed or after Suspended"); + return; + }; + + if let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + { + surface.resize(width, height).unwrap(); + } + } + WindowEvent::RedrawRequested => { + let Some((surface, game)) = surface else { + tracing::error!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; + + game.update(); + + let mut buffer = surface.buffer_mut().unwrap(); + game.draw(&mut buffer, window.scale_factor() as f32); + buffer.present().unwrap(); + window.request_redraw(); + } + WindowEvent::CloseRequested + | WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Named(NamedKey::Escape), + .. + }, + .. + } => { + elwt.exit(); + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key: PhysicalKey::Code(code), + state, + repeat: false, + .. + }, + .. + } => { + let Some((_, game)) = surface else { + tracing::error!("KeyboardInput fired before Resumed or after Suspended"); + return; + }; + + let value = match state { + ElementState::Pressed => MOVEMENT_SPEED, + ElementState::Released => -MOVEMENT_SPEED, + }; + + match code { + KeyCode::KeyW => game.camera_velocity.z += value, + KeyCode::KeyS => game.camera_velocity.z -= value, + KeyCode::KeyD => game.camera_velocity.x += value, + KeyCode::KeyA => game.camera_velocity.x -= value, + KeyCode::Space => game.camera_velocity.y += value, + KeyCode::ShiftLeft => game.camera_velocity.y -= value, + _ => {} + } + } + WindowEvent::Focused(focused) => { + window.set_cursor_visible(!focused); + window + .set_cursor_grab(if focused { + CursorGrabMode::Locked + } else { + CursorGrabMode::None + }) + .unwrap(); + } + _ => {} + } + }) + .with_device_event_handler(|_window, surface, event, _elwt| { + if let DeviceEvent::MouseMotion { delta } = event { + let Some((_, game)) = surface else { + tracing::error!("CursorMoved fired before Resumed or after Suspended"); + return; + }; + + game.camera_yaw -= delta.0 as f32 * MOUSE_SENSITIVITY; + game.camera_pitch -= delta.1 as f32 * MOUSE_SENSITIVITY; + game.camera_pitch = game.camera_pitch.clamp( + -std::f32::consts::FRAC_PI_2 + 0.01, + std::f32::consts::FRAC_PI_2 - 0.01, + ); + } + }); + + util::run_app(event_loop, app); +} diff --git a/examples/raytracing/material.rs b/examples/raytracing/material.rs new file mode 100644 index 0000000..aa7eb9d --- /dev/null +++ b/examples/raytracing/material.rs @@ -0,0 +1,136 @@ +use rand::Rng; + +use crate::objects::Hit; +use crate::ray::Ray; +use crate::vec3::{self, Color, Vec3}; + +#[derive(Debug)] +pub struct ScatterResult { + pub attenuation: Color, + pub scatter_direction: Vec3, +} + +#[derive(Debug, Clone)] +pub enum Material { + Lambertian(Lambertian), + Metal(Metal), + Dielectric(Dielectric), +} + +impl Material { + pub fn scatter(&self, r_in: &Ray, hit: &Hit, rng: &mut impl Rng) -> Option { + match self { + Self::Lambertian(material) => material.scatter(r_in, hit, rng), + Self::Metal(material) => material.scatter(r_in, hit, rng), + Self::Dielectric(material) => material.scatter(r_in, hit, rng), + } + } +} + +#[derive(Debug, Clone)] +pub struct Lambertian { + albedo: Color, +} + +impl Lambertian { + pub fn new(albedo: Color) -> Self { + Self { albedo } + } +} + +impl Lambertian { + pub fn scatter(&self, _r_in: &Ray, hit: &Hit, rng: &mut impl Rng) -> Option { + let mut scatter_direction = hit.normal + vec3::random_unit_vector(rng); + + fn near_zero(vec: Vec3) -> bool { + vec.x.abs() < f32::EPSILON && vec.y.abs() < f32::EPSILON && vec.z.abs() < f32::EPSILON + } + + // Catch degenerate scatter direction + if near_zero(scatter_direction) { + scatter_direction = hit.normal; + } + + Some(ScatterResult { + attenuation: self.albedo, + scatter_direction, + }) + } +} + +#[derive(Debug, Clone)] +pub struct Metal { + albedo: Color, + fuzz: f32, +} + +impl Metal { + pub fn new(albedo: Color, fuzz: f32) -> Self { + Self { + albedo, + fuzz: fuzz.min(1.0), + } + } +} + +impl Metal { + pub fn scatter(&self, r_in: &Ray, hit: &Hit, rng: &mut impl Rng) -> Option { + let reflected: Vec3 = r_in.direction.normalize().reflect(hit.normal); + let scatter_direction = reflected + self.fuzz * vec3::random_in_unit_sphere(rng); + if scatter_direction.dot(hit.normal) > 0.0 { + Some(ScatterResult { + attenuation: self.albedo, + scatter_direction, + }) + } else { + None + } + } +} + +#[derive(Debug, Clone)] +pub struct Dielectric { + ir: f32, +} + +impl Dielectric { + pub fn new(index_of_refraction: f32) -> Self { + Self { + ir: index_of_refraction, + } + } + + fn reflectance(cosine: f32, ref_idx: f32) -> f32 { + // Use Schlick's approximation for reflectance + let mut r0 = (1.0 - ref_idx) / (1.0 + ref_idx); + r0 = r0 * r0; + r0 + (1.0 - r0) * f32::powf(1.0 - cosine, 5.0) + } +} + +impl Dielectric { + pub fn scatter(&self, r_in: &Ray, hit: &Hit, rng: &mut impl Rng) -> Option { + let refraction_ratio = if hit.front_face { + 1.0 / self.ir + } else { + self.ir + }; + + let unit_direction = r_in.direction.normalize(); + let cos_theta = f32::min((-unit_direction).dot(hit.normal), 1.0); + let sin_theta = f32::sqrt(1.0 - cos_theta * cos_theta); + + let cannot_refract = refraction_ratio * sin_theta > 1.0; + let direction = + if cannot_refract || Self::reflectance(cos_theta, refraction_ratio) > rng.random() { + unit_direction.reflect(hit.normal) + } else { + unit_direction.refract(hit.normal, refraction_ratio) + }; + + Some(ScatterResult { + attenuation: Color::new(1.0, 1.0, 1.0), + scatter_direction: direction, + }) + } +} diff --git a/examples/raytracing/objects.rs b/examples/raytracing/objects.rs new file mode 100644 index 0000000..d2e6736 --- /dev/null +++ b/examples/raytracing/objects.rs @@ -0,0 +1,71 @@ +use std::ops::Range; + +use crate::material::Material; +use crate::ray::Ray; +use crate::vec3::{Point3, Vec3}; + +#[derive(Debug)] +pub struct Sphere { + center: Point3, + radius: f32, + mat: Material, +} + +impl Sphere { + pub fn new(center: Point3, radius: f32, mat: Material) -> Self { + Self { + center, + radius, + mat, + } + } + + pub fn hit(&self, ray: &Ray, ray_t: Range) -> Option { + let oc = ray.origin - self.center; + let a = ray.direction.length_squared(); + let half_b = oc.dot(ray.direction); + let c = oc.length_squared() - self.radius * self.radius; + + let discriminant = half_b * half_b - a * c; + if discriminant < 0.0 { + return None; + } + let sqrtd = discriminant.sqrt(); + + // Find the nearest root that lies in the acceptable range. + let mut root = (-half_b - sqrtd) / a; + if !ray_t.contains(&root) { + root = (-half_b + sqrtd) / a; + if !ray_t.contains(&root) { + return None; + } + } + + let distance = root; + let point = ray.at(distance); + let outward_normal = (point - self.center) / self.radius; + let front_face = ray.direction.dot(outward_normal) < 0.0; + let normal = if front_face { + outward_normal + } else { + -outward_normal + }; + + Some(Hit { + distance, + point, + normal, + mat: self.mat.clone(), + front_face, + }) + } +} + +#[derive(Debug)] +pub struct Hit { + pub point: Point3, + pub normal: Vec3, + pub mat: Material, + pub distance: f32, + pub front_face: bool, +} diff --git a/examples/raytracing/ray.rs b/examples/raytracing/ray.rs new file mode 100644 index 0000000..54e49a1 --- /dev/null +++ b/examples/raytracing/ray.rs @@ -0,0 +1,43 @@ +use rand::Rng; + +use crate::vec3::Color; +use crate::vec3::Point3; +use crate::vec3::Vec3; +use crate::world::World; + +#[derive(Default, Debug)] +pub struct Ray { + pub origin: Point3, + pub direction: Vec3, +} + +impl Ray { + pub fn new(origin: Point3, direction: Vec3) -> Self { + Self { origin, direction } + } + + pub fn at(&self, t: f32) -> Point3 { + self.origin + t * self.direction + } + + /// Find the color for a given ray. + pub fn trace(&self, world: &World, depth: i32, rng: &mut impl Rng) -> Color { + if depth <= 0 { + return Color::default(); + } + + if let Some(hit) = world.hit(self, 0.001..f32::INFINITY) { + if let Some(res) = hit.mat.scatter(self, &hit, rng) { + let scattered_ray = Ray::new(hit.point, res.scatter_direction); + // Hadamard product (element-wise product) + return res.attenuation * scattered_ray.trace(world, depth - 1, rng); + } + return Color::default(); + } + + // Sky color + let unit_direction = self.direction.normalize(); + let t = 0.5 * (unit_direction.y + 1.0); + (1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0) + } +} diff --git a/examples/raytracing/vec3.rs b/examples/raytracing/vec3.rs new file mode 100644 index 0000000..062931d --- /dev/null +++ b/examples/raytracing/vec3.rs @@ -0,0 +1,37 @@ +use rand::Rng; + +pub use glam::Vec3; +pub type Point3 = Vec3; +pub type Color = Vec3; + +pub fn random_in_unit_sphere(rng: &mut impl Rng) -> Vec3 { + loop { + let p = Vec3::new( + rng.random_range(-1.0..=1.0), + rng.random_range(-1.0..=1.0), + rng.random_range(-1.0..=1.0), + ); + if p.length_squared() >= 1.0 { + continue; + } + return p; + } +} + +pub fn random_unit_vector(rng: &mut impl Rng) -> Vec3 { + random_in_unit_sphere(rng).normalize() +} + +pub fn random_in_unit_disk(rng: &mut impl Rng) -> Vec3 { + loop { + let p = Vec3::new( + rng.random_range(-1.0..=1.0), + rng.random_range(-1.0..=1.0), + 0.0, + ); + if p.length_squared() >= 1.0 { + continue; + } + return p; + } +} diff --git a/examples/raytracing/world.rs b/examples/raytracing/world.rs new file mode 100644 index 0000000..44d9f91 --- /dev/null +++ b/examples/raytracing/world.rs @@ -0,0 +1,85 @@ +use std::ops::Range; + +use rand::Rng; + +use crate::material::{Dielectric, Lambertian, Material, Metal}; +use crate::objects::{Hit, Sphere}; +use crate::ray::Ray; +use crate::vec3::{Color, Point3, Vec3}; + +#[derive(Default, Debug)] +pub struct World { + spheres: Vec, +} + +impl World { + pub fn random_scene(rng: &mut impl Rng) -> Self { + let mut spheres = Vec::new(); + + let ground_material = Material::Lambertian(Lambertian::new(Color::new(0.5, 0.5, 0.5))); + spheres.push(Sphere::new( + Vec3::new(0.0, -1000.0, 0.0), + 1000.0, + ground_material, + )); + + for a in -11..11 { + for b in -11..11 { + let choose_mat = rng.random::(); + let center = Point3::new( + a as f32 + 0.9 * rng.random::(), + 0.2, + b as f32 + 0.9 * rng.random::(), + ); + + if (center - Point3::new(4.0, 0.2, 0.0)).length() > 0.9 { + if choose_mat < 0.8 { + // Diffuse + let albedo = rng.random::() * rng.random::(); + let sphere_material = Material::Lambertian(Lambertian::new(albedo)); + spheres.push(Sphere::new(center, 0.2, sphere_material)); + } else if choose_mat < 0.95 { + // Metal + let albedo = Vec3::new( + rng.random_range(0.5..=1.0), + rng.random_range(0.5..=1.0), + rng.random_range(0.5..=1.0), + ); + let fuzz = rng.random_range(0.0..=0.5); + let sphere_material = Material::Metal(Metal::new(albedo, fuzz)); + spheres.push(Sphere::new(center, 0.2, sphere_material)); + } else { + // Glass + let sphere_material = Material::Dielectric(Dielectric::new(1.5)); + spheres.push(Sphere::new(center, 0.2, sphere_material)); + } + } + } + } + + let material1 = Material::Dielectric(Dielectric::new(1.5)); + spheres.push(Sphere::new(Point3::new(0.0, 1.0, 0.0), 1.0, material1)); + + let material2 = Material::Lambertian(Lambertian::new(Color::new(0.4, 0.2, 0.1))); + spheres.push(Sphere::new(Point3::new(-4.0, 1.0, 0.0), 1.0, material2)); + + let material3 = Material::Metal(Metal::new(Color::new(0.7, 0.6, 0.5), 0.0)); + spheres.push(Sphere::new(Point3::new(4.0, 1.0, 0.0), 1.0, material3)); + + Self { spheres } + } + + pub fn hit(&self, ray: &Ray, ray_t: Range) -> Option { + let mut closest_so_far = ray_t.end; + let mut closest = None; + + for sphere in &self.spheres { + if let Some(hit) = sphere.hit(ray, ray_t.start..closest_so_far) { + closest_so_far = hit.distance; + closest = Some(hit); + } + } + + closest + } +} diff --git a/examples/util/winit_app.rs b/examples/util/winit_app.rs index 6896a7f..abbbba3 100644 --- a/examples/util/winit_app.rs +++ b/examples/util/winit_app.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use std::rc::Rc; use winit::application::ApplicationHandler; -use winit::event::WindowEvent; +use winit::event::{DeviceEvent, DeviceId, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::window::{Window, WindowAttributes, WindowId}; @@ -31,7 +31,8 @@ pub(crate) fn make_window( } /// Easily constructable winit application. -pub(crate) struct WinitApp { +pub(crate) struct WinitApp +{ /// Closure to initialize `state`. init: Init, @@ -41,6 +42,9 @@ pub(crate) struct WinitApp /// Closure to run on window events. event: Handler, + /// Closure to run on device events. + device_event: DeviceEventHandler, + /// Closure to run on about_to_wait events. about_to_wait: AboutToWaitHandler, @@ -82,20 +86,35 @@ where pub(crate) fn with_event_handler( self, handler: F, - ) -> WinitApp, &ActiveEventLoop)> + ) -> WinitApp< + T, + S, + Init, + InitSurface, + F, + impl FnMut(&mut T, Option<&mut S>, DeviceEvent, &ActiveEventLoop), + impl FnMut(&mut T, Option<&mut S>, &ActiveEventLoop), + > where F: FnMut(&mut T, Option<&mut S>, WindowId, WindowEvent, &ActiveEventLoop), { - WinitApp::new(self.init, self.init_surface, handler, |_, _, _| {}) + WinitApp::new( + self.init, + self.init_surface, + handler, + |_, _, _, _| {}, + |_, _, _| {}, + ) } } -impl - WinitApp +impl + WinitApp where Init: FnMut(&ActiveEventLoop) -> T, InitSurface: FnMut(&ActiveEventLoop, &mut T) -> S, Handler: FnMut(&mut T, Option<&mut S>, WindowId, WindowEvent, &ActiveEventLoop), + DeviceEventHandler: FnMut(&mut T, Option<&mut S>, DeviceEvent, &ActiveEventLoop), AboutToWaitHandler: FnMut(&mut T, Option<&mut S>, &ActiveEventLoop), { /// Create a new application. @@ -103,12 +122,14 @@ where init: Init, init_surface: InitSurface, event: Handler, + device_event: DeviceEventHandler, about_to_wait: AboutToWaitHandler, ) -> Self { Self { init, init_surface, event, + device_event, about_to_wait, state: None, surface_state: None, @@ -120,20 +141,45 @@ where pub(crate) fn with_about_to_wait_handler( self, about_to_wait: F, - ) -> WinitApp + ) -> WinitApp where F: FnMut(&mut T, Option<&mut S>, &ActiveEventLoop), { - WinitApp::new(self.init, self.init_surface, self.event, about_to_wait) + WinitApp::new( + self.init, + self.init_surface, + self.event, + self.device_event, + about_to_wait, + ) + } + + /// Build a new application. + #[allow(dead_code)] + pub(crate) fn with_device_event_handler( + self, + device_event: F, + ) -> WinitApp + where + F: FnMut(&mut T, Option<&mut S>, DeviceEvent, &ActiveEventLoop), + { + WinitApp::new( + self.init, + self.init_surface, + self.event, + device_event, + self.about_to_wait, + ) } } -impl ApplicationHandler - for WinitApp +impl ApplicationHandler + for WinitApp where Init: FnMut(&ActiveEventLoop) -> T, InitSurface: FnMut(&ActiveEventLoop, &mut T) -> S, Handler: FnMut(&mut T, Option<&mut S>, WindowId, WindowEvent, &ActiveEventLoop), + DeviceEventHandler: FnMut(&mut T, Option<&mut S>, DeviceEvent, &ActiveEventLoop), AboutToWaitHandler: FnMut(&mut T, Option<&mut S>, &ActiveEventLoop), { fn resumed(&mut self, el: &ActiveEventLoop) { @@ -166,4 +212,15 @@ where (self.about_to_wait)(state, surface_state, event_loop); } } + + fn device_event( + &mut self, + event_loop: &ActiveEventLoop, + _device_id: DeviceId, + event: DeviceEvent, + ) { + let state = self.state.as_mut().unwrap(); + let surface_state = self.surface_state.as_mut(); + (self.device_event)(state, surface_state, event, event_loop); + } }