From 2358f8e093003085180779650feb4a613a0d5c7d Mon Sep 17 00:00:00 2001 From: djairoh Date: Wed, 29 Apr 2026 01:17:00 +0200 Subject: [PATCH] ft: 13: optional defocus blur --- scenes/noImage.json | 32 +++++++++++++++++++++++++++++ scenes/scene.json | 7 +++++-- src/camera.rs | 44 ++++++++++++++++++++++++++++++++++------ src/main.rs | 4 ++-- src/scenes/raw_camera.rs | 11 ++++++++-- src/vec3.rs | 27 ++++++++++++++++++++++-- 6 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 scenes/noImage.json diff --git a/scenes/noImage.json b/scenes/noImage.json new file mode 100644 index 0000000..1b5bd91 --- /dev/null +++ b/scenes/noImage.json @@ -0,0 +1,32 @@ +{ + "camera": { + "anti_alias_rate": 23, + "max_depth": 100, + "fov": 40.0, + "look_from": { "x": -10, "y": 5, "z": 10 }, + "look_at": { "x": 0.0, "y": 0.0, "z": -1.0 }, + "vup": { "x": 0.0, "y": 1.0, "z": 0.0 } + }, + "materials": [ + { "type": "metal", "albedo": { "x": 0.2, "y": 0.4, "z": 0.8 }, "prob": 1.0, "fuzz": 0.1 }, + { "type": "metal", "albedo": { "x": 0.7, "y": 0.4, "z": 0.2 }, "prob": 1.0, "fuzz": 0.1 }, + { "type": "lambertian", "albedo": { "x": 0.8, "y": 0.8, "z": 0.0 }, "prob": 1.0 }, + { "type": "lambertian", "albedo": { "x": 0.1, "y": 0.2, "z": 0.5 }, "prob": 1.0 }, + { "type": "dielectric", "refraction_index": 1.5}, + { "type": "dielectric", "refraction_index": 0.67}, + { "type": "metal", "albedo": { "x": 0.8, "y": 0.6, "z": 0.2 }, "prob": 1.0, "fuzz": 1.0 } + + ], + "objects": [ + { "type": "sphere", "center": { "x": 0, "y": 0.7, "z": -0.4 }, "radius": 0.2, "material": 0}, + { "type": "sphere", "center": { "x": 0.0, "y": 0.5, "z": -0.8 }, "radius": 0.1, "material": 1}, + { "type": "sphere", "center": { "x": 0.0, "y": -100.5, "z": -1.0 }, "radius": 100.0, "material": 2}, + { "type": "sphere", "center": { "x": 0.0, "y": 0.0, "z": -1.2 }, "radius": 0.5, "material": 3}, + { "type": "sphere", "center": { "x": -1, "y": 0, "z": -1 }, "radius": 0.4, "material": 5}, + { "type": "sphere", "center": { "x": 1, "y": 0, "z": -1 }, "radius": 0.5, "material": 6}, + { "type": "sphere", "center": { "x": 20, "y": 7, "z": -15 }, "radius": 10.5, "material": 0} + ] +} + + + diff --git a/scenes/scene.json b/scenes/scene.json index 8ea1af0..c8a95a6 100644 --- a/scenes/scene.json +++ b/scenes/scene.json @@ -4,10 +4,13 @@ "image_height": 1080, "anti_alias_rate": 23, "max_depth": 100, - "fov": 40.0, + "fov": 20.0, "look_from": { "x": -10, "y": 5, "z": 10 }, "look_at": { "x": 0.0, "y": 0.0, "z": -1.0 }, - "vup": { "x": 0.0, "y": 1.0, "z": 0.0 } + "vup": { "x": 0.0, "y": 1.0, "z": 0.0 }, + "defocus_blur": false, + "defocus_angle": 2, + "focus_dist": 15.68 }, "materials": [ { "type": "metal", "albedo": { "x": 0.2, "y": 0.4, "z": 0.8 }, "prob": 1.0, "fuzz": 0.1 }, diff --git a/src/camera.rs b/src/camera.rs index 289c990..ffbcf4a 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,7 +1,6 @@ use std::{f32::consts::PI, sync::Arc}; use log::info; -use serde::Deserialize; use crate::{ objects::{hit::Hit, traits::Hittable}, @@ -28,11 +27,15 @@ pub struct Camera { look_from: Vec3, look_at: Vec3, vup: Vec3, + defocus_angle: f32, + focus_dist: f32, // camera helpers u: Vec3, v: Vec3, w: Vec3, + defocus_disk_u: Vec3, + defocus_disk_v: Vec3, } // FIXME: kinda out of place in this file. @@ -55,9 +58,13 @@ impl Camera { look_from: Vec3::nil(), look_at: Vec3::new(0., 0., -1.), vup: Vec3::new(0., 1., 0.), + defocus_angle: 0., + focus_dist: 10., u: Vec3::default(), v: Vec3::default(), w: Vec3::default(), + defocus_disk_u: Vec3::default(), + defocus_disk_v: Vec3::default(), } } @@ -70,6 +77,8 @@ impl Camera { look_from: Vec3, look_at: Vec3, vup: Vec3, + defocus_angle: f32, + focus_dist: f32, ) -> Self { Self { image_width, @@ -84,15 +93,18 @@ impl Camera { look_from, look_at, vup, + defocus_angle, + focus_dist, u: Vec3::default(), v: Vec3::default(), w: Vec3::default(), + defocus_disk_u: Vec3::default(), + defocus_disk_v: Vec3::default(), } } fn init(&mut self) { // camera - let focal_length = (self.look_from - self.look_at).length(); let theta = deg_to_rad(self.fov); let h = (theta / 2.).tan(); self.w = (self.look_from - self.look_at).get_unit(); @@ -100,7 +112,7 @@ impl Camera { self.v = self.w.cross(&self.u); // viewport - let viewport_height = 2. * h * focal_length; + let viewport_height = 2. * h * self.focus_dist; let viewport_width = viewport_height * (self.image_width as f32 / self.image_height as f32); let viewport_u = viewport_width * self.u; let viewport_v = viewport_height * -self.v; @@ -109,8 +121,14 @@ impl Camera { self.pixel_delta_u = viewport_u / self.image_width as f32; self.pixel_delta_v = viewport_v / self.image_height as f32; self.pixel00_loc = - self.look_from - (focal_length * self.w) - viewport_u / 2. - viewport_v / 2.; + self.look_from - (self.focus_dist * self.w) - viewport_u / 2. - viewport_v / 2.; self.dirty = false; + + if self.defocus_angle != 0. { + let defocus_radius = self.focus_dist * deg_to_rad(self.defocus_angle/2.).tan(); + self.defocus_disk_u = self.u * defocus_radius; + self.defocus_disk_v = self.v * defocus_radius; + } } pub fn set_fov(&mut self, fov: f32) { @@ -155,6 +173,19 @@ impl Camera { } } + pub fn add_defocus_blur(&mut self, defocus_angle: f32, focus_dist: f32) { + if self.defocus_angle != defocus_angle || self.focus_dist != focus_dist { + self.defocus_angle = defocus_angle; + self.focus_dist = focus_dist; + self.dirty = true; + } + } + + fn defocus_disk_sample(&self) -> Vec3 { + let p = Vec3::random_in_unit_disk(); + return self.look_from + (p.x() * self.defocus_disk_u) + (p.y() * self.defocus_disk_v); + } + pub fn render(&mut self, hittables: &Vec>) { if self.dirty { self.init() @@ -175,8 +206,9 @@ impl Camera { let pixel_loc = pixel_tl + (x * self.pixel_delta_u / (self.anti_alias_rate + 1) as f32) + (y * self.pixel_delta_v / (self.anti_alias_rate + 1) as f32); - let ray_dir = pixel_loc - self.look_from; - let r = Ray::new(self.look_from, ray_dir); + let ray_orig = if self.defocus_angle > 0. { self.defocus_disk_sample() } else {self.look_from}; + let ray_dir = pixel_loc - ray_orig; + let r = Ray::new(ray_orig, ray_dir); pixel_colour += self.ray_colour(&hittables, &r, self.max_depth); } } diff --git a/src/main.rs b/src/main.rs index ca4423a..bea4234 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,8 +20,8 @@ use crate::vec3::Vec3; use dotenv::dotenv; use log::info; use pretty_env_logger; -use rand::seq::IndexedRandom; use rand::RngExt; +use rand::seq::IndexedRandom; fn random_material() -> Arc { let mut rng = rand::rng(); @@ -78,7 +78,7 @@ fn main() { pretty_env_logger::init(); // TODO: use cli arg for scenefile - let json_file = "./scenes/scenes.json"; + let json_file = "./scenes/scene.json"; // let json_file = "./scenes/failsMatBounds.json"; let json_str = fs::read_to_string(json_file).expect("Reading specified scene file failed!"); let mut scene: Scene = serde_json::from_str(&json_str).unwrap(); diff --git a/src/scenes/raw_camera.rs b/src/scenes/raw_camera.rs index 2833a4e..92fc6fb 100644 --- a/src/scenes/raw_camera.rs +++ b/src/scenes/raw_camera.rs @@ -5,8 +5,7 @@ use crate::{camera::Camera, vec3::Vec3}; #[derive(Deserialize)] pub struct RawCamera { // output - image_width: u32, // TODO: test these are now explicitly required (and that default impl does - // not make these optional) + image_width: u32, image_height: u32, // raytracing @@ -24,6 +23,10 @@ pub struct RawCamera { look_at: Vec3, #[serde(default)] vup: Vec3, + #[serde(default)] + defocus_angle: f32, + #[serde(default)] + focus_dist: f32, } impl Default for RawCamera { @@ -37,6 +40,8 @@ impl Default for RawCamera { look_from: Vec3::new(0., 0., 0.), look_at: Vec3::new(0., 0., -1.), vup: Vec3::new(0., 1., 0.), + defocus_angle: 0., + focus_dist: 10., } } } @@ -52,6 +57,8 @@ impl From for Camera { raw.look_from, raw.look_at, raw.vup, + raw.defocus_angle, + raw.focus_dist, ) } } diff --git a/src/vec3.rs b/src/vec3.rs index 0416d32..4046538 100644 --- a/src/vec3.rs +++ b/src/vec3.rs @@ -17,8 +17,8 @@ pub struct Vec3 { pub type Colour = Vec3; impl Vec3 { - pub fn new(r: f32, g: f32, b: f32) -> Self { - Self { x: r, y: g, z: b } + pub fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } } pub fn nil() -> Self { @@ -47,6 +47,16 @@ impl Vec3 { } } + pub fn random_in_unit_disk() -> Self { + loop { + let mut p = Vec3::random(); + p.z = 0.; + if p.length_squared() < 1. { + return p; + } + } + } + pub fn random_unit_hemisphere(n: &Self) -> Self { let v = Self::random_unit(); if n.dot(&v) > 0.0 { @@ -340,6 +350,19 @@ impl Mul<&Vec3> for f32 { } } +impl Mul for &f32 { + type Output = Vec3; + + fn mul(self, rhs: Vec3) -> Self::Output { + Vec3 { + x: rhs.x * self, + y: rhs.y * self, + z: rhs.z * self, + } + } + +} + impl MulAssign for Vec3 { fn mul_assign(&mut self, rhs: Self) { *self = Self {