17 Commits

Author SHA1 Message Date
987bbe1c98 Starting builder pattern for Camera
I'm going to change the Camera construction to use the builder pattern.
This means I need to implement trait Default for Camera, and then a
bunch of functions for setting the properties.
2024-04-27 12:03:05 -05:00
f2d1a892b7 Enable more warnings, fix a couple of them
Yay for a high quality compiler
2024-04-27 11:16:31 -05:00
8e37fd8e97 Library & multiple binaries
Been playing with EGUI, and I'm gonna patch in a GUI app. The CLI
version will need to kick around, too, but be separate from the GUI one.
Enter: A multi-target crate!
2024-01-28 13:30:42 -06:00
a701b9407b Line renderer in terms of tile renderer
Done and done.
2023-09-25 11:25:42 -07:00
72f154510f Proper tile rendering
The Tile can now render a region of height > 1px!

I'm gonna rewrite the render_line() function to operate in terms of the
render_tile() function. A line is, after all, just a tile of height 1px.
2023-09-25 11:19:33 -07:00
3250f8e580 Hook up the new renderer
The threading code is gone, now. We're back to just having a single
loop to drive the whole thing.

Along with this, I realized that the Distrs container thing wasn't
actually being used. It's a real pain to cart it around, and very few
things actually use it.

TODO: Reinstate the small wiggle done by the uv mapping routine. This
version no longer nudges the coordinate, so I expect there to be some
small visual differences.
2023-09-25 10:50:47 -07:00
f03c6280a7 Renderer 2, now with 100% less threading tools
I've rewritten the renderer to see if I can make a better model the
second time around. I was having a rough time untangling parts and
refactoring it piece-by-piece.

Next is to hook up the new rendering parts into a single-threaded
build. Once the parts work again, I can look into thread pooling
machinery.
2023-09-25 08:20:27 -07:00
60b4407573 New Scene struct
The scene is more than just a list of hittables. It's any and all
hittables (so the list, yeah), and also the camera(s!) in the world.

This doens't compile, however. More work will need to be done to
untangle the other things that could previously see these scattered
components.
2023-09-23 14:40:34 -07:00
7c43c3fb82 Rewrite hittable list hit method using iter magic
The loop can go away completely and be replaced with an iterator. Yay
for Rust iterators!
2023-09-23 13:26:42 -07:00
4be7ba54bb Relocate world generation function 2023-09-23 13:07:40 -07:00
515f5b866a Fix: hit record selection mechanism
Because of the mutable record being used in the loop, the previous
version had a somewhat obscured way to track the nearest collision.

Switching to an optional (so I can have a non-optional Material in it)
means I'm not interrogating that value.... So it gets to be explicit
again.

I'll refactor the entire for-loop into an iterator with the min()
adapter at some point. For now: Material lifetimes!
2023-09-23 09:27:21 -07:00
bdc396accf Material references... but bad ordering
It looks like I messed up the preference for the HitRecords. The
geometry bounces correctly, but the record that sticks is not
necessarily the one closest to the camera.
2023-09-22 18:21:12 -07:00
4ce43e12af Gathered up the scene components
After nearly a month of not touching the project, I've finally finished
collecting the scene parts. :l

With that, the rearrange is complete. On to the next thing!
2023-09-17 12:16:33 -05:00
76233f82a4 Condensing the rendering components
All the rendering bits together... except for the ones I missed. Never
mind those. This one has section headers so I can try to stay organized.
I'm gonna need to actually *do* things in this file going forward.
2023-08-19 20:37:51 -05:00
9badea407d Rect in primitives, collect the tests
There's the rectangle!

Also the tests. Cargo doesn't complain, but my YcmCompleter (so
rust-analyze, I think) does. The tests are all one big slab again.
2023-08-19 20:01:33 -05:00
f5eae46f17 Condensing the primitives
Group the informational types together. `vec3.rs` was renamed, and
the Ray implementation was copied into it. The Rect (and possibly a
Point) struct will be moved in, next. It's bad to have a `misc` or
`util` section, but "primitives" doesn't really do it for me, either.

My thought is that the sections will be:
- Primitives
- Renderer
- Scene Components

The renderer section would cover the image description and generation.
Image size and pixel sample count, but also things like the tile size,
render command dispatching, and file write-out. Anything to do with
producing the render.

The scene components section covers anything that goes in the render.
Obvious parts are the spheres and their materials, but this will also
include the camera. After all, it exists "in the world", and there could
be multiple.
2023-08-19 19:28:43 -05:00
809d7b678b Clean up some leftovers
Quickly sweeping up a few bits and pieces that got left around. This
seems like an okay way to load the codebase into my brain before doing
the big rearranging.
2023-08-19 18:44:57 -05:00
14 changed files with 726 additions and 807 deletions

View File

@@ -5,6 +5,15 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "rustpt_cli"
path = "src/cli_tool.rs"
[[bin]]
name = "rustpt_gui"
path = "src/gui_tool.rs"
[dependencies]
rand = { version = "0.8.5", features = ["small_rng"] }
itertools = { version = "0.11.0" }
eframe = "0.25.0"

BIN
hitrecord_misordering.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

View File

@@ -1,76 +0,0 @@
/*
* let viewport = (aspect_ratio * 2.0, 2.0);
let focal_length = 1.0;
let origin = Vec3::new(0.0, 0.0, 0.0);
let horizontal = Vec3::new(viewport.0, 0.0, 0.0);
let vertical = Vec3::new(0.0, viewport.1, 0.0);
let lower_left_corner = origin - horizontal/2.0 - vertical/2.0 - Vec3::new(0.0, 0.0, focal_length);
*/
use crate::vec3::Vec3;
use crate::ray::Ray;
use crate::degrees_to_radians;
use rand::rngs::SmallRng;
#[derive (Clone, Copy)]
pub struct Camera {
origin: Vec3,
lower_left_corner: Vec3,
horizontal: Vec3,
vertical: Vec3,
u: Vec3, v: Vec3, /*w: Vec3,*/
lens_radius: f32,
}
impl Camera {
pub fn new(
lookfrom: Vec3,
lookat: Vec3,
vup: Vec3,
vfov: f32,
aspect_ratio: f32,
aperture: f32,
focus_dist: f32
) -> Camera {
let theta = degrees_to_radians(vfov);
let h = (theta / 2.0).tan();
let vp_height = 2.0 * h;
let vp_width = aspect_ratio * vp_height;
let w = Vec3::as_unit(lookfrom - lookat);
let u = Vec3::as_unit(Vec3::cross(vup, w));
let v = Vec3::cross(w, u);
let orig = lookfrom;
let horiz = u * vp_width * focus_dist;
let verti = v * vp_height * focus_dist;
let lower_left_corner = orig - horiz / 2.0 - verti / 2.0 - w * focus_dist;
Camera{
origin: orig,
lower_left_corner,
horizontal: horiz,
vertical: verti,
u, v, /* w,*/
lens_radius: aperture / 2.0,
}
}
pub fn get_ray(&self, s: f32, t: f32, srng: &mut SmallRng) -> Ray {
let rd = Vec3::rand_in_unit_disk(srng) * self.lens_radius;
let offset = self.u * rd.x + self.v * rd.y;
let dir = self.lower_left_corner
+ self.horizontal * s
+ self.vertical * t
- self.origin - offset;
Ray{
orig: self.origin + offset,
dir,
}
}
}

75
src/cli_tool.rs Normal file
View File

@@ -0,0 +1,75 @@
#![warn(clippy::all, rust_2018_idioms, rust_2018_compatibility)]
use rustpt::primitives::{
Vec2i,
Vec3,
};
use rustpt::scene::{
Camera,
Scene
};
use rustpt::renderer::{
Tile,
RenderProperties,
};
use rand::SeedableRng;
use rand::rngs::SmallRng;
fn main() {
// image
let aspect_ratio = 3.0 / 2.0;
let image = Vec2i {
x: 400,
y: (400.0 / aspect_ratio) as i32
};
let render_config = RenderProperties {
samples: 10,
bounces: 50
};
// random generator
let mut small_rng = SmallRng::seed_from_u64(0);
// Scene (now includes camera)
let scene = Scene {
camera: Camera::new(
Vec3::new(13.0, 2.0, 3.0), // lookfrom
Vec3::zero(), // lookat
Vec3::new(0.0, 1.0, 0.0), // vup
20.0,
aspect_ratio,
0.1, // aperture
10.0, // dist_to_focus
),
world: Scene::random_world(&mut small_rng)
};
// render
// The render loop should now be a job submission mechanism
// Iterate lines, submitting them as tasks to the thread.
println!("P3\n{} {}\n255", image.x, image.y);
// TILE BASED RENDERER
// let tile = Tile::render_tile(
// Rect { x: 0, y: 0, w: image.x, h: image.y },
// image,
// &scene,
// &render_config,
// &mut small_rng
// );
// for pixel in tile.pixels.iter().rev() {
// println!("{}", pixel.print_ppm(render_config.samples));
// }
// LINE BASED RENDERER
for row in (0..image.y).rev() {
let tile = Tile::render_line(row, image, &scene, &render_config, &mut small_rng);
eprintln!("Printing scanline #{}", row);
for pixel in tile.pixels {
println!("{}", pixel.print_ppm(render_config.samples))
}
}
eprintln!("Done!");
}

36
src/gui_tool.rs Normal file
View File

@@ -0,0 +1,36 @@
#![warn(clippy::all, rust_2018_idioms)]
use eframe::{egui, Frame};
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([800.0, 600.0])
.with_min_inner_size([50.0, 50.0])
.with_title("RustPT GUI Tool"),
..Default::default()
};
eframe::run_native(
"app name?",
options,
Box::new(
| cc | Box::new(RtApp::new(cc))
))
}
#[derive(Default)]
struct RtApp;
impl RtApp {
fn new(cc: &eframe::CreationContext<'_>) -> Self {
Self::default()
}
}
impl eframe::App for RtApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
egui::SidePanel::left("Render Properties").show(ctx, |ui| {
ui.heading("Render Properties");
ui.label("")
});
}
}

View File

@@ -1,90 +0,0 @@
use crate::vec3::Vec3;
use crate::ray::Ray;
use crate::material::Material;
pub struct HitRecord{
pub p: Vec3,
pub normal: Vec3,
pub material: Option<Material>,
pub t: f32,
pub front_face: bool,
}
impl HitRecord{
pub fn set_face_normal(&mut self, r: Ray, outward_normal: Vec3) -> (){
self.front_face = Vec3::dot(r.dir, outward_normal) < 0.0;
self.normal = if self.front_face { outward_normal } else { -outward_normal };
}
}
#[derive (Clone)]
pub enum Hittable {
Sphere { center: Vec3, radius: f32, material: Option<Material> },
HittableList { hittables: Vec<Hittable> }
}
impl Hittable {
pub fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
match self {
Hittable::HittableList { hittables } => {
let mut might_return = HitRecord {
p: Vec3::zero(),
normal: Vec3::zero(),
material: None,
t: t_max,
front_face: false,
};
let mut hit_anything = false;
for item in hittables {
if let Some(record) = item.hit(r, t_min, might_return.t){
hit_anything = true;
might_return = record;
}
}
if hit_anything{
return Some(might_return);
} else { return None; }
}
Hittable::Sphere { center, radius, material } => {
let oc = r.orig - *center;
let a = r.dir.length_squared();
let half_b = Vec3::dot(oc, r.dir);
let c = oc.length_squared() - radius * radius;
let discriminant = half_b*half_b - a*c;
if discriminant < 0.0 {
return None;
}
let sqrtd = discriminant.sqrt();
// nearest root that lies within tolerance
let mut root = (-half_b - sqrtd) / a;
if root < t_min || root > t_max {
root = (-half_b + sqrtd) / a;
if root < t_min || root > t_max {
return None;
}
}
let mut record = HitRecord{
p: r.at(root),
normal: (r.at(root) - *center) / *radius,
material: *material,
t: root,
front_face: false,
};
let outward_normal = (record.p - *center) / *radius;
record.set_face_normal(r, outward_normal);
Some(record)
}
}
}
pub fn push(&mut self, item: Hittable) {
if let Hittable::HittableList { hittables } = self {
hittables.push(item);
}
}
}

4
src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
#![warn(clippy::all, rust_2018_idioms, rust_2018_compatibility)]
pub mod primitives;
pub mod scene;
pub mod renderer;

View File

@@ -1,367 +0,0 @@
mod vec3;
mod ray;
mod camera;
mod material;
mod hittable;
mod thread_utils;
use crate::vec3::Vec3;
use crate::ray::Ray;
use crate::hittable::Hittable;
use crate::material::Material;
use crate::camera::Camera;
use crate::thread_utils::RenderCommand;
use rand::{Rng, SeedableRng};
use rand::rngs::SmallRng;
use rand::distributions::Uniform;
use itertools;
use itertools::Itertools;
use std::ops;
use std::thread;
fn main() {
// image
let aspect_ratio = 3.0 / 2.0;
let image = (
1920,
(1920.0 / aspect_ratio) as i32
);
let samples_per_pixel: u32 = 10;
let max_depth = 50;
// random generator
let mut small_rng = SmallRng::seed_from_u64(0);
// world
let world = random_scene(&mut small_rng);
// camera
let lookfrom = Vec3::new(13.0, 2.0, 3.0);
let lookat = Vec3::zero();
let vup = Vec3::new(0.0, 1.0, 0.0);
let dist_to_focus = 10.0;
let aperture = 0.1;
let cam = Camera::new(
lookfrom,
lookat,
vup,
20.0,
aspect_ratio,
aperture,
dist_to_focus
);
// render
// The render loop should now be a job submission mechanism
// Iterate lines, submitting them as tasks to the thread.
println!("P3\n{} {}\n255", image.0, image.1);
let context = RenderContext {
camera: cam,
image,
max_depth,
samples_per_pixel,
world,
};
thread::scope(|s| {
let (mut dispatcher, scanline_receiver) = thread_utils::Dispatcher::new(&small_rng, 12);
s.spawn(move || {
for y in (0..image.1).rev() {
eprintln!("Submitting scanline: {}", y);
let job = RenderCommand::Line { line_num: y, context: context.clone() };
dispatcher.submit_job(job).unwrap();
}
dispatcher.submit_job(RenderCommand::Stop).unwrap();
// ... also I happen to know there are 4 threads.
});
/*
* Store received results in the segments buffer.
* Some will land before their previous segments and will need to be held
* until the next-to-write arrives.
*
* Elements are sorted in reverse order so that they can be popped from the
* Vec quickly.
*
* The queue is scanned every single time a new item is received. In the
* happy path where the received item is next-up, it'll be buffered, checked
* and then printed. In the case where it isn't, it'll get buffered and
* stick around for more loops. When the next-to-write finally lands, it
* means the n+1 element is up, now. If that element is already in the buffer
* we want to write it out. Hence the loop that scans the whole buffer each
* receive.
*
* TODO: There could be an up-front conditional that checks to see if the
* received item *is* the next-to-write and skip the buffering step.
* But I need to make the concept work at all, first.
*/
let mut raster_segments = Vec::<thread_utils::RenderResult>::new();
let mut sl_output_index = image.1-1; // scanlines count down, start at image height.
while let Ok(scanline) = scanline_receiver.recv() {
eprintln!("Received scanline: {}", scanline.line_num);
raster_segments.push(scanline);
raster_segments.sort_by( |a, b| b.cmp(a) );
loop {
if raster_segments.len() == 0 { break; } // can this ever happen? Not while every
// single element gets pushed to the
// buffer first. With the happy path
// short-circuit noted above, it could.
let last_ind = raster_segments.len() - 1;
if raster_segments[last_ind].line_num == sl_output_index{
let scanline = raster_segments.pop().unwrap();
print_scanline(scanline, samples_per_pixel);
sl_output_index -= 1;
} else {
break;
}
}
}
eprintln!("Size of raster_segments at finish: {}", raster_segments.len());
});
// TODO: Dispatcher shutdown mechanism. Right now, we might technically be leaking threads.
eprintln!("Done!");
}
fn print_scanline(scanline: thread_utils::RenderResult, samples_per_pixel: u32){
eprintln!("Printing scanline num: {}", scanline.line_num);
for color in &scanline.line {
println!("{}", color.print_ppm(samples_per_pixel));
}
}
#[derive (Clone)]
pub struct RenderContext{
image: (i32, i32),
samples_per_pixel: u32,
max_depth: u32,
world: Hittable,
camera: Camera,
}
pub struct DistributionContianer {
distrib_zero_one: Uniform<f32>,
distrib_plusminus_one: Uniform<f32>,
}
impl DistributionContianer {
fn new() -> Self {
DistributionContianer {
distrib_zero_one: Uniform::new(0.0, 1.0),
distrib_plusminus_one: Uniform::new(-1.0, 1.0),
}
}
}
fn render_line(y: i32, small_rng: &mut SmallRng, context: RenderContext, distr: &DistributionContianer) -> Vec<Vec3> {
//TODO: Ensure that the compiler hoists the distribution's out as constants
// else, do so manually
(0..context.image.0).map(|x| {
sample_pixel(x, y, small_rng, &context, distr)
}).collect()
}
fn sample_pixel(x: i32, y: i32, small_rng: &mut SmallRng, context: &RenderContext, distr: &DistributionContianer) -> Vec3{
(0..context.samples_per_pixel).into_iter().fold(
Vec3::zero(),
|color, _sample| {
let u = ((x as f32) + small_rng.sample(distr.distrib_zero_one)) / ((context.image.0 - 1) as f32);
let v = ((y as f32) + small_rng.sample(distr.distrib_zero_one)) / ((context.image.1 - 1) as f32);
let ray = context.camera.get_ray(u, v, small_rng);
color + ray_color(ray, &context.world, context.max_depth, small_rng, distr.distrib_plusminus_one)
}
)
}
fn range2d(bounds: (i32, i32, i32, i32)) -> impl Iterator<Item = (i32, i32)> {
let rheight = bounds.1..(bounds.1+bounds.3);
rheight.flat_map(move |y| {
let rwidth = bounds.0..(bounds.0+bounds.2);
rwidth.map( move |x| {
(x, y)
})
})
}
#[derive (Copy, Clone)]
struct Rect {
x: i32,
y: i32,
w: i32,
h: i32,
}
/* Iterable that produces pixels left-to-right, top-to-bottom.
* `Tile`s represent the render space, not the finished image.
* There is no internal pixel buffer
*/
type TileCursorIter = itertools::Product<ops::Range<i32>, ops::Range<i32>>;
struct Tile {
bounds: Rect,
context: RenderContext,
small_rng: SmallRng,
rand_distr: DistributionContianer,
cursor: TileCursorIter,
}
impl Tile{
fn new(
bounds: Rect,
context: RenderContext,
small_rng: SmallRng,
rand_distr: DistributionContianer
) -> Self
{
Tile { bounds, context, small_rng, rand_distr,
cursor: (bounds.x..(bounds.x + bounds.w))
.cartesian_product(bounds.y..(bounds.y + bounds.h)
)
}
}
}
impl Iterator for Tile {
type Item = Vec3;
fn next(&mut self) -> Option<Self::Item> {
if let Some((x, y)) = self.cursor.next(){
Some(sample_pixel(
x, y,
&mut self.small_rng,
&self.context,
&self.rand_distr,
))
} else {
None
}
}
}
fn ray_color(r: Ray, world: &Hittable, depth: u32, srng: &mut SmallRng, distrib: Uniform<f32> ) -> Vec3 {
// recursion depth guard
if depth == 0 {
return Vec3::zero();
}
if let Some(rec) = world.hit(r, 0.001, f32::INFINITY){
let mut scattered = Ray {
orig: Vec3::zero(),
dir: Vec3::zero()
};
let mut attenuation = Vec3::zero();
match rec.material {
Some(mat) => {
if mat.scatter(r, rec, &mut attenuation, &mut scattered, srng) {
return attenuation * ray_color(scattered, world, depth-1, srng, distrib);
};
},
None => return Vec3::zero(),
}
}
let unitdir = Vec3::as_unit(r.dir);
let t = 0.5 * (unitdir.y + 1.0);
return Vec3::ones() * (1.0 - t) + Vec3::new(0.5, 0.7, 1.0) * t
}
fn random_scene(srng: &mut SmallRng) -> Hittable {
let mat_ground = Material::Lambertian { albedo: Vec3::new(0.5, 0.5, 0.5) };
let mut world = Hittable::HittableList { hittables : Vec::<Hittable>::new() };
world.push( Hittable::Sphere { center: Vec3::new(0.0, -1000.0, 0.0), radius: 1000.0, material: Some(mat_ground) });
let distrib_zero_one = Uniform::new(0.0, 1.0);
for a in -11..11 {
for b in -11..11 {
let choose_mat = srng.sample(distrib_zero_one);
let center = Vec3 {
x: a as f32 + 0.9 * srng.sample(distrib_zero_one),
y: 0.2,
z: b as f32 + 0.9 * srng.sample(distrib_zero_one),
};
if (center - Vec3::new(4.0, 0.2, 0.0)).length() > 0.9 {
if choose_mat < 0.8 {
// diffuse
let albedo = Vec3::rand(srng, distrib_zero_one) * Vec3::rand(srng, distrib_zero_one);
let sphere_material = Material::Lambertian { albedo };
world.push(
Hittable::Sphere {
center,
radius: 0.2,
material: Some(sphere_material),
}
);
} else if choose_mat < 0.95 {
// metal
let distr_albedo = Uniform::new(0.5, 1.0);
let distr_fuzz = Uniform::new(0.0, 0.5);
let albedo = Vec3::rand(srng, distr_albedo);
let fuzz = srng.sample(distr_fuzz);
let material = Material::Metal { albedo, fuzz };
world.push(
Hittable::Sphere {
center,
radius: 0.2,
material: Some(material),
}
);
} else {
// glass
let material = Material::Dielectric { index_refraction: 1.5 };
world.push(
Hittable::Sphere{
center,
radius: 0.2,
material: Some(material),
}
);
};
}
}
}
let material1 = Material::Dielectric { index_refraction: 1.5 };
world.push( Hittable::Sphere{
center: Vec3::new(0.0, 1.0, 0.0),
radius: 1.0,
material: Some(material1)
});
let material2 = Material::Lambertian { albedo: Vec3::new(0.4, 0.2, 0.1) };
world.push( Hittable::Sphere {
center: Vec3::new(-4.0, 1.0, 0.0),
radius: 1.0,
material: Some(material2)
});
let material3 = Material::Metal { albedo: Vec3::new(0.7, 0.6, 0.5), fuzz: 0.0 };
world.push( Hittable::Sphere {
center: Vec3::new(4.0, 1.0, 0.0),
radius: 1.0,
material: Some(material3)
});
return world;
}
pub fn degrees_to_radians(degrees: f32) -> f32 {
degrees * std::f32::consts::PI / 180.0
}

View File

@@ -1,92 +0,0 @@
use crate::ray::Ray;
use crate::hittable::HitRecord;
use crate::vec3;
use crate::vec3::Vec3;
use rand::Rng;
use rand::rngs::SmallRng;
use rand::distributions::Uniform;
#[derive(Copy, Clone, Debug)]
pub enum Material{
Lambertian { albedo: Vec3 },
Metal { albedo:Vec3, fuzz: f32 },
Dielectric { index_refraction: f32 },
}
impl Material {
pub fn scatter(
&self,
ray_in: Ray,
rec: HitRecord,
attenuation: &mut Vec3,
scattered: &mut Ray,
srng: &mut SmallRng,
) -> bool {
match self {
Material::Lambertian { albedo } => {
let scatter_dir = rec.normal + Vec3::rand_unit_vector(srng);
// The compiler might be smart enough to compute this ^^^ just once. In which case,
// I don't need to do this weird dance. Oh well. It'll work.
let scatter_dir = if scatter_dir.near_zero() { // if near zero,
rec.normal // replace with normal
} else {
scatter_dir // else preserve current
};
//TODO: Revisit this out-parameter pattern
// It's a side effect of C++'s obtuse move semantics (and the RTIOW author not
// using them at all)
*scattered = Ray{
orig: rec.p,
dir: scatter_dir
};
*attenuation = *albedo; // deref on both sides? Wacky
return true;
},
Material::Metal { albedo, fuzz } => {
let reflected = Vec3::reflect(
Vec3::as_unit(ray_in.dir),
rec.normal
);
*scattered = Ray{
orig: rec.p,
dir: reflected + Vec3::rand_in_unit_sphere(srng) * *fuzz,
};
*attenuation = *albedo;
return Vec3::dot(scattered.dir, rec.normal) > 0.0;
},
Material::Dielectric { index_refraction } => {
*attenuation = Vec3::ones();
let refraction_ratio = if rec.front_face { 1.0 / index_refraction } else { *index_refraction };
let unit_direction = Vec3::as_unit(ray_in.dir);
let cos_theta = vec3::min(Vec3::dot(-unit_direction, rec.normal), 1.0);
let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
let cannot_refract = refraction_ratio * sin_theta > 1.0;
let distrib_zero_one = Uniform::new(0.0, 1.0);
let direction = if cannot_refract || Material::reflectance(cos_theta, refraction_ratio) > srng.sample(distrib_zero_one) {
Vec3::reflect(unit_direction, rec.normal)
} else {
Vec3::refract(unit_direction, rec.normal, refraction_ratio)
};
*scattered = Ray {
orig: rec.p,
dir: direction
};
return true;
},
}
}
fn reflectance(cosine: f32, ref_idx: f32) -> f32 {
// Schlick's approximation for reflectance.
let r0 = (1.0 - ref_idx) / (1.0 + ref_idx);
let r0 = r0 * r0;
return r0 + (1.0 - r0) * (1.0 - cosine).powf(5.0);
}
}

View File

@@ -17,6 +17,96 @@ use rand::Rng;
use rand::rngs::SmallRng;
use rand::distributions::Uniform;
pub type Vec2i = Vec2<i32>;
pub type Vec2f = Vec2<f32>;
#[derive (Clone, Copy, PartialEq, PartialOrd, Debug)]
pub struct Vec2<T>{
pub x: T,
pub y: T,
}
impl Vec2<f32> {
pub fn zero() -> Vec2<f32> {
Vec2{ x: 0.0, y: 0.0 }
}
pub fn ones() -> Vec2<f32> {
Vec2{ x: 1.0, y: 1.0 }
}
pub fn rand(srng: &mut SmallRng, distrib: Uniform<f32>) -> Vec2<f32> {
Vec2 { x: srng.sample(distrib), y: srng.sample(distrib) }
}
}
impl <T> Vec2<T>
where T: std::ops::Mul{
pub fn new(x: T, y: T) -> Vec2<T> {
Vec2{x, y}
}
}
impl <T> Add for Vec2 <T>
where T: std::ops::Add<Output = T>{
type Output = Vec2<T>;
fn add(self, other: Vec2<T>) -> Vec2<T> {
Vec2 { x: self.x + other.x, y: self.y + other.y }
}
}
impl <T> Mul for Vec2<T>
where T: std::ops::Mul<Output = T>{
type Output = Vec2<T>;
fn mul(self, other: Vec2<T>) -> Vec2<T> {
Vec2 {
x: self.x * other.x,
y: self.y * other.y
}
}
}
impl Div<f32> for Vec2<f32>{
type Output = Vec2<f32>;
fn div(self, other: f32) -> Vec2<f32> {
Vec2 {
x: 1.0/other * self.x,
y: 1.0/other * self.y
}
}
}
impl Div<i32> for Vec2<i32>{
type Output = Vec2<i32>;
fn div(self, other: i32) -> Vec2<i32> {
Vec2 {
x: self.x / other,
y: self.y / other
}
}
}
impl <T> Div<Vec2<T>> for Vec2<T>
where T: std::ops::Div<Output = T>{
type Output = Vec2<T>;
fn div(self, other: Vec2<T>) -> Vec2<T> {
Vec2 {
x: self.x / other.x,
y: self.y / other.y
}
}
}
impl <T> Display for Vec2<T>
where T: Display { // nested type still needs to have Display
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
let str = format!("{} {}", self.x, self.y);
fmt.write_str(&str)
}
}
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub struct Vec3{
pub x: f32,
@@ -97,9 +187,9 @@ impl Vec3{
let g = (self.y * scale).sqrt();
let b = (self.z * scale).sqrt();
let ir = (clamp(r, 0.0, 0.999) * 256.0) as i32;
let ig = (clamp(g, 0.0, 0.999) * 256.0) as i32;
let ib = (clamp(b, 0.0, 0.999) * 256.0) as i32;
let ir = (r.clamp( 0.0, 0.999) * 256.0) as i32;
let ig = (g.clamp( 0.0, 0.999) * 256.0) as i32;
let ib = (b.clamp( 0.0, 0.999) * 256.0) as i32;
format!("{} {} {}", ir, ig, ib)
}
@@ -116,7 +206,7 @@ impl Vec3{
}
pub fn refract(uv: Vec3, n: Vec3, etai_over_etat: f32) -> Vec3 {
let cos_theta = min(Vec3::dot(-uv, n), 1.0);
let cos_theta = Vec3::dot(-uv, n).min(1.0);
let r_out_perp = (uv + n * cos_theta) * etai_over_etat;
let r_out_parallel = n * -(1.0 - r_out_perp.length_squared()).abs().sqrt();
r_out_perp + r_out_parallel
@@ -280,7 +370,7 @@ impl Neg for Vec3{
}
impl Display for Vec3 {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
let str = format!("{} {} {}", self.x, self.y, self.z);
fmt.write_str(&str)?;
Ok(())
@@ -288,16 +378,40 @@ impl Display for Vec3 {
}
}
pub fn clamp(input: f32, lower: f32, upper: f32) -> f32 {
min(max(input, lower), upper)
#[derive(Copy, Clone)]
pub struct Ray{
pub orig: Vec3,
pub dir: Vec3,
}
pub fn min(a: f32, b: f32) -> f32 {
if a < b { a } else { b }
impl Ray{
pub fn at(&self, t: f32) -> Vec3 {
self.orig + self.dir*t
}
}
pub fn max(a: f32, b: f32) -> f32 {
if a > b { a } else { b }
#[derive (Copy, Clone)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
}
impl Rect{
pub fn pos(&self) -> Vec2i {
Vec2i {
x: self.x,
y: self.y,
}
}
pub fn size(&self) -> Vec2i {
Vec2i {
x: self.w - self.x,
y: self.h - self.y,
}
}
}
#[cfg(test)]
@@ -559,5 +673,17 @@ mod test{
eprintln!("Diff: {}", diff);
assert!(Vec3::near_zero(&diff));
}
}
#[test]
fn check_lerp(){
let ray = Ray{
orig: Vec3::new(0.0, 0.0, 0.0),
dir: Vec3::new(1.0, 1.0, 0.0)
};
let half = ray.at(0.5);
assert_eq!(
half,
Vec3::new(0.5, 0.5, 0.0)
);
}
}

View File

@@ -1,32 +0,0 @@
use crate::vec3::Vec3;
#[derive(Copy, Clone)]
pub struct Ray{
pub orig: Vec3,
pub dir: Vec3,
}
impl Ray{
pub fn at(&self, t: f32) -> Vec3 {
self.orig + self.dir*t
}
}
#[cfg(test)]
mod test{
use super::*;
#[test]
fn check_lerp(){
let ray = Ray{
orig: Vec3::new(0.0, 0.0, 0.0),
dir: Vec3::new(1.0, 1.0, 0.0)
};
let half = ray.at(0.5);
assert_eq!(
half,
Vec3::new(0.5, 0.5, 0.0)
);
}
}

138
src/renderer.rs Normal file
View File

@@ -0,0 +1,138 @@
use crate::primitives::{
Vec2i,
Vec2f,
Vec3,
Ray,
Rect,
};
use crate::scene::{
Hittable,
Scene,
};
use rand::rngs::SmallRng;
use itertools::{self, Itertools};
const SKY_COLOR: Vec3 = Vec3 { x: 0.5, y: 0.7, z: 1.0};
pub struct RenderProperties {
pub samples: u32, // samples are averaged results over a pixel
pub bounces: u32, // bounces are how far the ray will travel (in hits not total distance)
}
fn to_uv(coord: Vec2i, img_size: Vec2i) -> Vec2f {
let u = (coord.x as f32) / ((img_size.x - 1) as f32);
let v = (coord.y as f32) / ((img_size.y - 1) as f32);
Vec2f::new(u, v)
}
fn ray_color(
r: Ray, surface: &Hittable, depth: u32,
rng: &mut SmallRng,
) -> Vec3 {
// recursion guard
if depth == 0 {
return Vec3::zero();
}
// cast a ray, interrogate hit record
if let Some(record) = surface.hit(r, 0.001, f32::INFINITY){
let mut scattered = Ray {
orig: Vec3::zero(),
dir: Vec3::zero(),
};
let mut attenuation = Vec3::zero();
if record.material.scatter(
r,
&record,
&mut attenuation,
&mut scattered,
rng
) {
return attenuation * ray_color(
scattered, surface, depth-1, rng
);
}
} // TODO: explicit else block
// Rust gets angry about the inner if{} block because it evaluates to ()
// when the else path is taken. This is a problem for a function
// that returns Vec3 and not ().
{ // when nothing is struck, return sky color
let unitdir = Vec3::as_unit(r.dir);
let t = 0.5 * (unitdir.y + 1.0);
return Vec3::ones() * (1.0 - t) + SKY_COLOR * t
}
}
fn sample_pixel(
coord: Vec2i, // location in image/screen space
scene: &Scene, // scene we're drawing
render_props: &RenderProperties,
img_size: Vec2i,
// Supplied by the execution environment (the thread)
rng: &mut SmallRng,
) -> Vec3{
(0..render_props.samples)
.fold(
Vec3::zero(),
|color, _sample| -> Vec3 {
let uv = to_uv(coord, img_size);
let ray = scene.camera.get_ray(uv.x, uv.y, rng);
if ray.dir.x.is_nan() {
panic!("Ray dir.x is NAN");
}
color + ray_color(ray, &scene.world, render_props.bounces, rng)
}
)
}
pub struct Tile {
_bounds: Rect,
pub pixels: Vec<Vec3>,
}
impl Tile {
pub fn render_tile(
bounds: Rect, // bounds of the region to render
img_size: Vec2i, // final image resolution (needed for proper UV mapping)
scene: &Scene,
properties: &RenderProperties, // TODO: Place image size in render properties?
rng: &mut SmallRng,
) -> Self {
let pixel_iter = (bounds.y..(bounds.y + bounds.h))
.cartesian_product( bounds.x..(bounds.x + bounds.w));
let pixels = pixel_iter.map(
|coord| -> Vec3 {
sample_pixel(
Vec2i{x: coord.1, y: coord.0},
scene,
properties,
img_size,
rng,
)
}
).collect();
Self {
_bounds: bounds,
pixels
}
}
pub fn render_line(
y: i32, // bounding rect and line
img_size: Vec2i,
scene: &Scene,
properties: &RenderProperties,
rng: &mut SmallRng, // rng utils
) -> Self {
Tile::render_tile(
Rect{ x: 0, y, w: img_size.x, h: 1 },
img_size,
scene,
properties,
rng
)
}
}

326
src/scene.rs Normal file
View File

@@ -0,0 +1,326 @@
use crate::primitives::{Vec3, Ray};
use rand::Rng;
use rand::rngs::SmallRng;
use rand::distributions::Uniform;
pub struct HitRecord{
pub p: Vec3,
pub normal: Vec3,
pub material: Material,
pub t: f32,
pub front_face: bool,
}
impl HitRecord{
pub fn set_face_normal(&mut self, r: Ray, outward_normal: Vec3) -> (){
self.front_face = Vec3::dot(r.dir, outward_normal) < 0.0;
self.normal = if self.front_face { outward_normal } else { -outward_normal };
}
}
#[derive (Clone)]
pub enum Hittable {
Sphere { center: Vec3, radius: f32, material: Material },
HittableList { hittables: Vec<Hittable> }
}
impl Hittable {
pub fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
match self {
Hittable::HittableList { hittables } => {
hittables.iter()
.map( |obj| -> Option<HitRecord> {
obj.hit(r, t_min, t_max)
}).filter(|obj| obj.is_some())
.min_by(|lhs, rhs| {
let lhs = lhs.as_ref().unwrap();
let rhs = rhs.as_ref().unwrap();
lhs.t.partial_cmp(&rhs.t).expect("Couldn't compare??")
}).unwrap_or(None)
}
Hittable::Sphere { center, radius, material } => {
let oc = r.orig - *center;
let a = r.dir.length_squared();
let half_b = Vec3::dot(oc, r.dir);
let c = oc.length_squared() - radius * radius;
let discriminant = half_b*half_b - a*c;
if discriminant < 0.0 {
return None;
}
let sqrtd = discriminant.sqrt();
// nearest root that lies within tolerance
let mut root = (-half_b - sqrtd) / a;
if root < t_min || root > t_max {
root = (-half_b + sqrtd) / a;
if root < t_min || root > t_max {
return None;
}
}
let mut record = HitRecord{
p: r.at(root),
normal: (r.at(root) - *center) / *radius,
material: *material,
t: root,
front_face: false,
};
let outward_normal = (record.p - *center) / *radius;
record.set_face_normal(r, outward_normal);
Some(record)
}
}
}
pub fn push(&mut self, item: Hittable) {
if let Hittable::HittableList { hittables } = self {
hittables.push(item);
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum Material{
Lambertian { albedo: Vec3 },
Metal { albedo:Vec3, fuzz: f32 },
Dielectric { index_refraction: f32 },
}
impl Material {
pub fn scatter(
&self,
ray_in: Ray,
rec: &HitRecord,
attenuation: &mut Vec3,
scattered: &mut Ray,
srng: &mut SmallRng,
) -> bool {
match self {
Material::Lambertian { albedo } => {
let scatter_dir = rec.normal + Vec3::rand_unit_vector(srng);
// The compiler might be smart enough to compute this ^^^ just once. In which case,
// I don't need to do this weird dance. Oh well. It'll work.
let scatter_dir = if scatter_dir.near_zero() { // if near zero,
rec.normal // replace with normal
} else {
scatter_dir // else preserve current
};
//TODO: Revisit this out-parameter pattern
// It's a side effect of C++'s obtuse move semantics (and the RTIOW author not
// using them at all)
*scattered = Ray{
orig: rec.p,
dir: scatter_dir
};
*attenuation = *albedo; // deref on both sides? Wacky
return true;
},
Material::Metal { albedo, fuzz } => {
let reflected = Vec3::reflect(
Vec3::as_unit(ray_in.dir),
rec.normal
);
*scattered = Ray{
orig: rec.p,
dir: reflected + Vec3::rand_in_unit_sphere(srng) * *fuzz,
};
*attenuation = *albedo;
return Vec3::dot(scattered.dir, rec.normal) > 0.0;
},
Material::Dielectric { index_refraction } => {
*attenuation = Vec3::ones();
let refraction_ratio = if rec.front_face { 1.0 / index_refraction } else { *index_refraction };
let unit_direction = Vec3::as_unit(ray_in.dir);
let cos_theta = Vec3::dot(-unit_direction, rec.normal).min(1.0);
let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
let cannot_refract = refraction_ratio * sin_theta > 1.0;
let distrib_zero_one = Uniform::new(0.0, 1.0);
let direction = if cannot_refract || Material::reflectance(cos_theta, refraction_ratio) > srng.sample(distrib_zero_one) {
Vec3::reflect(unit_direction, rec.normal)
} else {
Vec3::refract(unit_direction, rec.normal, refraction_ratio)
};
*scattered = Ray {
orig: rec.p,
dir: direction
};
return true;
},
}
}
fn reflectance(cosine: f32, ref_idx: f32) -> f32 {
// Schlick's approximation for reflectance.
let r0 = (1.0 - ref_idx) / (1.0 + ref_idx);
let r0 = r0 * r0;
return r0 + (1.0 - r0) * (1.0 - cosine).powf(5.0);
}
}
// Camera
pub fn degrees_to_radians(degrees: f32) -> f32 {
degrees * std::f32::consts::PI / 180.0
}
pub struct Camera {
origin: Vec3,
lower_left_corner: Vec3,
horizontal: Vec3,
vertical: Vec3,
u: Vec3, v: Vec3, /*w: Vec3,*/
lens_radius: f32,
}
impl Camera {
pub fn new() -> Camera {
Self::default()
}
pub fn get_ray(&self, s: f32, t: f32, srng: &mut SmallRng) -> Ray {
let rd = Vec3::rand_in_unit_disk(srng) * self.lens_radius;
let offset = self.u * rd.x + self.v * rd.y;
let dir = self.lower_left_corner
+ self.horizontal * s
+ self.vertical * t
- self.origin - offset;
Ray{
orig: self.origin + offset,
dir,
}
}
}
impl Default for Camera {
fn default() -> Self {
// defaults are the same as the hard-coded properties passed from main
// ... except for the `lookfrom` position: (13.0, 2.0, 3.0) became (10.0, 0..)
let lookfrom = Vec3::new(10.0, 0.0, 0.0);
let lookat = Vec3 { x: 1.0, y: 0.0, z: 0.0 };
let vup = Vec3::new(0.0, 1.0, 0.0);
let vfov = 20.0;
let aspect_ratio = 3.0 / 2.0;
let aperture = 0.1;
let focus_dist = 10.0;
let theta = degrees_to_radians(vfov);
let h = (theta / 2.0).tan();
let vp_height = 2.0 * h;
let vp_width = aspect_ratio * vp_height;
let w = Vec3::as_unit(lookfrom - lookat);
let u = Vec3::as_unit(Vec3::cross(vup, w));
let v = Vec3::cross(w, u);
let orig = lookfrom;
let horiz = u * vp_width * focus_dist;
let verti = v * vp_height * focus_dist;
let lower_left_corner = orig - horiz / 2.0 - verti / 2.0 - w * focus_dist;
Camera{
origin: orig,
lower_left_corner,
horizontal: horiz,
vertical: verti,
u, v, /* w,*/
lens_radius: aperture / 2.0,
}
}
}
pub struct Scene {
pub camera: Camera,
pub world: Hittable,
}
impl Scene {
pub fn random_world(srng: &mut SmallRng) -> Hittable {
let mat_ground = Material::Lambertian { albedo: Vec3::new(0.5, 0.5, 0.5) };
let mut world = Hittable::HittableList { hittables : Vec::<Hittable>::new() };
world.push( Hittable::Sphere { center: Vec3::new(0.0, -1000.0, 0.0), radius: 1000.0, material: mat_ground });
let distrib_zero_one = Uniform::new(0.0, 1.0);
for a in -11..11 {
for b in -11..11 {
let choose_mat = srng.sample(distrib_zero_one);
let center = Vec3 {
x: a as f32 + 0.9 * srng.sample(distrib_zero_one),
y: 0.2,
z: b as f32 + 0.9 * srng.sample(distrib_zero_one),
};
if (center - Vec3::new(4.0, 0.2, 0.0)).length() > 0.9 {
if choose_mat < 0.8 {
// diffuse
let albedo = Vec3::rand(srng, distrib_zero_one) * Vec3::rand(srng, distrib_zero_one);
let sphere_material = Material::Lambertian { albedo };
world.push(
Hittable::Sphere {
center,
radius: 0.2,
material: sphere_material,
}
);
} else if choose_mat < 0.95 {
// metal
let distr_albedo = Uniform::new(0.5, 1.0);
let distr_fuzz = Uniform::new(0.0, 0.5);
let albedo = Vec3::rand(srng, distr_albedo);
let fuzz = srng.sample(distr_fuzz);
let material = Material::Metal { albedo, fuzz };
world.push(
Hittable::Sphere {
center,
radius: 0.2,
material: material,
}
);
} else {
// glass
let material = Material::Dielectric { index_refraction: 1.5 };
world.push(
Hittable::Sphere{
center,
radius: 0.2,
material: material,
}
);
};
}
}
}
let material1 = Material::Dielectric { index_refraction: 1.5 };
world.push( Hittable::Sphere{
center: Vec3::new(0.0, 1.0, 0.0),
radius: 1.0,
material: material1
});
let material2 = Material::Lambertian { albedo: Vec3::new(0.4, 0.2, 0.1) };
world.push( Hittable::Sphere {
center: Vec3::new(-4.0, 1.0, 0.0),
radius: 1.0,
material: material2
});
let material3 = Material::Metal { albedo: Vec3::new(0.7, 0.6, 0.5), fuzz: 0.0 };
world.push( Hittable::Sphere {
center: Vec3::new(4.0, 1.0, 0.0),
radius: 1.0,
material: material3
});
world
}
}

View File

@@ -1,138 +0,0 @@
use crate::RenderContext;
use crate::Vec3;
use crate::{render_line, DistributionContianer};
use core::cmp::Ordering;
use std::thread;
use std::sync::mpsc;
use rand::rngs::SmallRng;
#[derive (Clone)]
pub enum RenderCommand{
Stop,
Line { line_num: i32, context: RenderContext },
}
pub struct RenderResult {
pub line_num: i32,
pub line: Vec<Vec3>,
}
impl Ord for RenderResult {
fn cmp(&self, other: &Self) -> Ordering {
if self.line_num > other.line_num {
Ordering::Less
} else if self.line_num < other.line_num {
Ordering::Greater
} else {
Ordering::Equal
}
}
}
impl PartialOrd for RenderResult {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for RenderResult {
fn eq(&self, other: &Self) -> bool {
self.line_num == other.line_num
}
}
impl Eq for RenderResult {}
/*
* The dispatcher will hold a list of threads, and a list of command input channels to match.
* Helper functions exist to input jobs serially, and then dispatch them to an open thread.
*
* Since receivers can be matched to several senders, the input end of the result channel will
* be cloned and given to each of the threads.
* TODO: Consider holding a copy of the render_tx end in case threads exit early and need to
* be restored.
*/
pub struct Dispatcher{
handles: Vec<thread::JoinHandle<()>>,
command_transmitters: Vec<mpsc::SyncSender<RenderCommand>>,
next_to_feed: usize, // gonna do a round-robin style dispatch, ig.
}
impl Dispatcher {
pub fn new(srng: &SmallRng, num_threads: usize) -> (Dispatcher, mpsc::Receiver<RenderResult> ) {
let mut handles = Vec::new();
let mut command_transmitters = Vec::<mpsc::SyncSender<RenderCommand>>::new();
let (render_tx, render_rx) = mpsc::sync_channel::<RenderResult>(1);
for _ in 0..num_threads {
// create new command tx/rx pairs. Store tx in the list, give rx to the thread.
let (command_tx, command_rx) = mpsc::sync_channel::<RenderCommand>(1);
// TODO: Pick appropriate command queue depth (or make it controllable, even)
let mut srng = srng.clone();
let threads_result_tx = render_tx.clone();
let distribs = DistributionContianer::new();
let thread_handle = thread::spawn(move || {
while let Ok(job) = command_rx.recv() {
match job {
RenderCommand::Stop => {
break;
}
RenderCommand::Line { line_num, context } => {
let line = render_line(line_num, &mut srng, context, &distribs);
let result = RenderResult { line_num, line };
threads_result_tx.send(result).unwrap();
}
}
}
});
handles.push(thread_handle);
command_transmitters.push(command_tx);
}
// finally, stash everything in the Dispatcher struct and return.
(
Dispatcher{
handles,
command_transmitters,
next_to_feed: 0,
},
render_rx
)
}
//TODO: Reconsider round-robin dispatch
// When passing the message to threads which are still busy, this function
// will block (it's a sync_channel). While blocked, other threads could
// become available and left idle.
pub fn submit_job(&mut self, command: RenderCommand) -> Result<(), mpsc::SendError<RenderCommand>> {
// Stop command is special. We'll broadcast it to all threads.
if let RenderCommand::Stop = command {
for channel in &self.command_transmitters {
return channel.send(command.clone());
}
}
// Check that `next_to_feed` is in-bounds, and then insert.
// index is post-incremented with this function call.
// wrap when at length (0-indexed so last valid index is len-1)
if self.next_to_feed == self.handles.len() {
self.next_to_feed = 0;
} else if self.next_to_feed > self.handles.len() {
panic!("How the hell did a +=1 skip past the maximum allowed size?");
}
match self.command_transmitters.get(self.next_to_feed){
Some(target) => target.send(command).unwrap(),
None => panic!("oh god oh fuck"),
}
self.next_to_feed += 1;
Ok(())
}
}