stocking

git clone https://git.bradleygannon.com/stocking.git
Log | Files | Refs

camera.rs (7189B)


      1 use std::time::Instant;
      2 
      3 use crate::{
      4     color::LinearColor, hittable::Hittable, interval::Interval, random::RandomNumberGenerator,
      5     ray::Ray, vec3::Vec3,
      6 };
      7 
      8 #[derive(Clone, Debug, PartialEq, PartialOrd)]
      9 pub struct Image {
     10     data: Vec<Vec<LinearColor>>,
     11 }
     12 
     13 fn scale_f64_to_u8(value: f64) -> u8 {
     14     (value * 255.) as u8
     15 }
     16 
     17 impl Image {
     18     fn new(width: usize, height: usize) -> Self {
     19         Image {
     20             data: vec![vec![LinearColor::default(); width]; height],
     21         }
     22     }
     23     pub fn to_ppm_string(&self) -> String {
     24         let mut output = "P3\n".to_string();
     25         let height = self.data.len();
     26         let width = if height > 0 { self.data[0].len() } else { 0 };
     27         output += &format!("{width} {height}\n");
     28         output += "255\n";
     29         for row_data in self.data.iter() {
     30             for color in row_data.iter() {
     31                 let gamma_color = color.as_gamma();
     32                 output += &format!(
     33                     "{} {} {}\n",
     34                     scale_f64_to_u8(gamma_color.red()),
     35                     scale_f64_to_u8(gamma_color.green()),
     36                     scale_f64_to_u8(gamma_color.blue())
     37                 );
     38             }
     39         }
     40         output
     41     }
     42 }
     43 
     44 #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
     45 pub struct Camera {
     46     image_width: usize,
     47     image_height: usize,
     48     pixel_delta_u: Vec3,
     49     pixel_delta_v: Vec3,
     50     pixel_origin: Vec3,
     51     samples_per_pixel: usize,
     52     rng: RandomNumberGenerator,
     53     max_bounces: usize,
     54     center: Vec3,
     55     defocus_angle_degrees: f64,
     56     defocus_disk_u: Vec3,
     57     defocus_disk_v: Vec3,
     58 }
     59 
     60 #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
     61 pub struct CameraConfig {
     62     pub image_width: usize,
     63     pub aspect_ratio: f64,
     64     pub samples_per_pixel: usize,
     65     pub max_bounces: usize,
     66     pub field_of_view_degrees: f64,
     67     pub look_from: Vec3,
     68     pub look_at: Vec3,
     69     pub view_up: Vec3,
     70     pub defocus_angle_degrees: f64,
     71     pub focus_distance: f64,
     72     pub rng: RandomNumberGenerator,
     73 }
     74 
     75 impl Default for CameraConfig {
     76     fn default() -> Self {
     77         CameraConfig {
     78             image_width: 100,
     79             aspect_ratio: 1.,
     80             samples_per_pixel: 10,
     81             max_bounces: 10,
     82             field_of_view_degrees: 90.,
     83             look_from: Vec3::new(0., 0., 0.),
     84             look_at: Vec3::new(0., 0., -1.),
     85             view_up: Vec3::new(0., 1., 0.),
     86             defocus_angle_degrees: 0.,
     87             focus_distance: 10.,
     88             rng: RandomNumberGenerator::new_with_time_seed(),
     89         }
     90     }
     91 }
     92 
     93 impl Camera {
     94     pub fn new(config: &CameraConfig) -> Self {
     95         let CameraConfig {
     96             image_width,
     97             aspect_ratio,
     98             samples_per_pixel,
     99             max_bounces,
    100             field_of_view_degrees,
    101             look_from,
    102             look_at,
    103             view_up,
    104             defocus_angle_degrees,
    105             focus_distance,
    106             rng,
    107         } = *config;
    108 
    109         let image_height = ((image_width as f64 / aspect_ratio) as usize).max(1);
    110 
    111         let theta = field_of_view_degrees.to_radians();
    112         let h = (theta / 2.).tan();
    113         let viewport_height = 2. * h * focus_distance;
    114         let viewport_width = viewport_height * aspect_ratio;
    115 
    116         let w = (look_from - look_at).unit();
    117         let u = view_up.cross(&w).unit();
    118         let v = w.cross(&u);
    119 
    120         let viewport_u = viewport_width * u;
    121         let viewport_v = viewport_height * -v;
    122         let viewport_upper_left =
    123             look_from - (focus_distance * w) - viewport_u / 2. - viewport_v / 2.;
    124 
    125         let pixel_delta_u = viewport_u / image_width as f64;
    126         let pixel_delta_v = viewport_v / image_height as f64;
    127         let pixel_origin = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
    128 
    129         let defocus_radius = focus_distance * (defocus_angle_degrees / 2.).to_radians().tan();
    130         let defocus_disk_u = u * defocus_radius;
    131         let defocus_disk_v = v * defocus_radius;
    132 
    133         Camera {
    134             image_width,
    135             image_height,
    136             pixel_delta_u,
    137             pixel_delta_v,
    138             pixel_origin,
    139             samples_per_pixel,
    140             rng,
    141             max_bounces,
    142             center: look_from,
    143             defocus_angle_degrees,
    144             defocus_disk_u,
    145             defocus_disk_v,
    146         }
    147     }
    148 
    149     pub fn render(&mut self, world: &[Box<dyn Hittable>]) -> Image {
    150         let mut image = Image::new(self.image_width, self.image_height);
    151         let image_start = Instant::now();
    152         for j in 0..self.image_height {
    153             let line_start = Instant::now();
    154             for i in 0..self.image_width {
    155                 let mut pixel_color = LinearColor::new(0., 0., 0.);
    156                 for _ in 0..self.samples_per_pixel {
    157                     let r = self.get_ray(i, j);
    158                     pixel_color +=
    159                         self.ray_color(&r, world, self.max_bounces) / self.samples_per_pixel as f64;
    160                 }
    161                 image.data[j][i] = pixel_color;
    162             }
    163             let line_duration = line_start.elapsed();
    164             let pixels_per_second = self.image_width as f64 / line_duration.as_secs_f64();
    165             eprint!(
    166                 "\rLine {:>5}/{:>5} ({:.2} pixels / second)                    ",
    167                 j + 1,
    168                 self.image_height,
    169                 pixels_per_second
    170             );
    171         }
    172         let image_duration = image_start.elapsed();
    173         eprintln!(
    174             "\rDone in {:.1} seconds ({:.1} pixels / second)               ",
    175             image_duration.as_secs_f64(),
    176             self.image_width as f64 * self.image_height as f64 / image_duration.as_secs_f64()
    177         );
    178         image
    179     }
    180 
    181     fn ray_color(&mut self, r: &Ray, world: &[Box<dyn Hittable>], depth: usize) -> LinearColor {
    182         if depth == 0 {
    183             return LinearColor::new(0., 0., 0.);
    184         }
    185         if let Some(hit) = world.hit(r, Interval::new(0.001, f64::INFINITY)) {
    186             if let Some(scattered_ray) = hit.material().scatter(r, &hit, &mut self.rng) {
    187                 scattered_ray.attenuation() * self.ray_color(&scattered_ray.ray(), world, depth - 1)
    188             } else {
    189                 LinearColor::new(0., 0., 0.)
    190             }
    191         } else {
    192             let unit_direction = r.direction().unit();
    193             let a = 0.5 * (unit_direction.y() + 1.);
    194             (1. - a) * LinearColor::new(1., 1., 1.) + a * LinearColor::new(0.5, 0.7, 1.)
    195         }
    196     }
    197 
    198     fn get_ray(&mut self, i: usize, j: usize) -> Ray {
    199         let offset = Vec3::new(self.rng.next_f64() - 0.5, self.rng.next_f64() - 0.5, 0.);
    200         let pixel_sample = self.pixel_origin
    201             + (i as f64 + offset.x()) * self.pixel_delta_u
    202             + (j as f64 + offset.y()) * self.pixel_delta_v;
    203         let ray_origin = if self.defocus_angle_degrees <= 0. {
    204             self.center
    205         } else {
    206             self.defocus_disk_sample()
    207         };
    208         Ray::new(ray_origin, pixel_sample - ray_origin)
    209     }
    210 
    211     fn defocus_disk_sample(&mut self) -> Vec3 {
    212         let p = Vec3::random_in_unit_disk(&mut self.rng);
    213         self.center + (p.x() * self.defocus_disk_u) + (p.y() * self.defocus_disk_v)
    214     }
    215 }