Compare commits

...

12 Commits

Author SHA1 Message Date
8332ce6204 rm: unimplemented cylinder code 2026-05-31 13:52:50 +02:00
64cad7eda6 ft: moved random scene code to function; cleaned up tri/quad/cube hit logic; made camera attributes private 2026-05-31 13:51:34 +02:00
71985a6c34 fx: cmt 2026-05-13 14:17:53 +02:00
b4b214ebf7 fx: debug statement 2026-05-13 13:52:57 +02:00
d8ad450553 add: texture files 2026-05-13 13:29:07 +02:00
d4f4b6a715 fmt: ran clippy 2026-05-13 13:27:37 +02:00
194328d92f fx: texture colouring 2026-05-13 13:25:57 +02:00
6a1e50fb7a ft (wip): textures 2026-05-12 18:20:06 +02:00
383f739808 refactor: scene/raytracer/camera logic 2026-05-08 17:16:21 +02:00
549707fbb3 fx: json files 2026-05-03 16:06:54 +02:00
10f9c0984d wip: parallelization 2026-05-03 14:58:56 +02:00
eb90c36ae8 refactor: scene deserialization organisation 2026-05-02 18:02:53 +02:00
36 changed files with 915 additions and 552 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/target /target
output.png *.png
.env .env
!textures/*.png

1
Cargo.lock generated
View File

@@ -1240,6 +1240,7 @@ dependencies = [
"ops", "ops",
"pretty_env_logger", "pretty_env_logger",
"rand 0.10.1", "rand 0.10.1",
"rayon",
"serde", "serde",
"serde_json", "serde_json",
] ]

View File

@@ -5,11 +5,12 @@ edition = "2024"
[dependencies] [dependencies]
dotenv = "0.15.0" dotenv = "0.15.0"
image = "0.25.10" image = {version = "0.25.10", features = ["png"]}
is_close = "0.1.3" is_close = "0.1.3"
log = "0.4.29" log = "0.4.29"
ops = "0.6.0" ops = "0.6.0"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
rand = "0.10.1" rand = "0.10.1"
rayon = "1.12.0"
serde = {version = "1.0.228", features = ["derive"]} serde = {version = "1.0.228", features = ["derive"]}
serde_json = "1.0.149" serde_json = "1.0.149"

38
scenes/bench.json Normal file
View File

@@ -0,0 +1,38 @@
{
"filename": "testing.png",
"image_width": 800,
"image_height": 600,
"max_depth": 50,
"camera": {
"anti_alias_rate": 16,
"fov": 70.0,
"look_from": [-10, 1, 15],
"look_at": [-11.0, 7.0, 0.0],
"vup": [0.0, 1.0, 0.0],
"defocus_angle": 0,
"focus_dist": 15.68
},
"materials": [
{ "type": "lambertian", "albedo": [0.2, 0.2, 0.2], "prob": 0.8 },
{ "type": "lambertian", "albedo": [0.9, 0.9, 0.0], "prob": 1.0, "fuzz": 0.1 },
{ "type": "dielectric", "refraction_index": 1.5},
{ "type": "normal"}
],
"objects": [
{ "type": "sphere", "center": [0.0, 0.0, -1.2], "radius": 0.5, "material": { "type": "metal", "albedo": [0.7, 0.4, 0.2], "prob": 1.0, "fuzz": 0.1 }},
{ "type": "sphere", "center": [-1, 0, -1], "radius": 0.4, "material": 3},
{ "type": "sphere", "center": [1, 0, -1], "radius": 0.5, "material": { "type": "metal", "albedo": [0.8, 0.6, 0.2], "prob": 1.0, "fuzz": 1.0 }},
{ "type": "triangle", "p1": [-4, 0, -4], "p2": [0, 0, -4], "p3": [-2, 2, -4], "material": 1},
{ "type": "triangle", "p1": [0, 0, -4], "p2": [4, 0, -4], "p3": [2, 2, -4], "material": 1},
{ "type": "triangle", "p1": [-2, 2, -4], "p2": [2, 2, -4], "p3": [0, 4, -4], "material": 3},
{ "type": "quad", "p1": [-20, -1, -20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [-20, 20, -20], "material": 0},
{ "type": "quad", "p1": [-20, -1, 20], "p2": [-20, -1, -20], "p3": [-20, 20, -20], "p4": [-20, 20, 20], "material": 0},
{ "type": "quad", "p1": [-20, -1, 20], "p2": [20, -1, 20], "p3": [20, -1, -20], "p4": [-20, -1, -20], "material": 0},
{ "type": "quad", "p1": [20, -1, 20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [20, 20, 20], "material": 0},
{ "type": "cube", "p1": [8, 0, 2], "p2": [12, 0, 2], "p3": [12, 4, 2], "p4": [8, 4, 2], "p5": [8, 0, -2], "p6": [12, 0, -2], "p7": [12, 4, -2], "p8": [8, 4, -2], "material": 3},
{ "type": "circle", "center": [-9, 3, 0], "radius": 3, "normal": [0, 1, 0.5], "material": {"type": "metal", "albedo": [0.9, 0.9, 0.9], "prob": 1.0, "fuzz": 0.3}}
]
}

View File

@@ -1,22 +0,0 @@
{
"camera": {
"image_width": 1920,
"image_height": 1080,
"anti_alias_rate": 1,
"max_depth": 10,
"fov": 90.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 }
],
"objects": [
{ "type": "sphere", "center": { "x": 0, "y": 0.7, "z": -0.4 }, "radius": 0.2, "material": 1}
]
}

View File

@@ -1,35 +1,35 @@
{ {
"camera": { "filename": "highDef.png",
"image_width": 1920, "image_width": 1920,
"image_height": 1080, "image_height": 1080,
"anti_alias_rate": 23,
"max_depth": 50, "max_depth": 50,
"camera": {
"anti_alias_rate": 23,
"fov": 20.0, "fov": 20.0,
"look_from": { "x": -10, "y": 5, "z": 10 }, "look_from": [-10, 5, 10],
"look_at": { "x": 0.0, "y": 0.0, "z": -1.0 }, "look_at": [0.0, 0.0, -1.0],
"vup": { "x": 0.0, "y": 1.0, "z": 0.0 }, "vup": [0.0, 1.0, 0.0],
"defocus_blur": false, "defocus_angle": 0,
"defocus_angle": 2,
"focus_dist": 15.68 "focus_dist": 15.68
}, },
"materials": [ "materials": [
{ "type": "metal", "albedo": { "x": 0.2, "y": 0.4, "z": 0.8 }, "prob": 1.0, "fuzz": 0.1 }, { "type": "metal", "albedo": [0.2, 0.4, 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": "metal", "albedo": [0.7, 0.4, 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": [0.8, 0.8, 0.0 ], "prob": 1.0},
{ "type": "lambertian", "albedo": { "x": 0.1, "y": 0.2, "z": 0.5 }, "prob": 1.0 }, { "type": "lambertian", "albedo": [0.1, 0.2, 0.5 ], "prob": 1.0},
{ "type": "dielectric", "refraction_index": 1.5}, { "type": "dielectric", "refraction_index": 1.5},
{ "type": "dielectric", "refraction_index": 0.67}, { "type": "dielectric", "refraction_index": 0.67},
{ "type": "metal", "albedo": { "x": 0.8, "y": 0.6, "z": 0.2 }, "prob": 1.0, "fuzz": 1.0 } { "type": "metal", "albedo": [0.8, 0.6, 0.2 ], "prob": 1.0, "fuzz": 1.0}
], ],
"objects": [ "objects": [
{ "type": "sphere", "center": { "x": 0, "y": 0.7, "z": -0.4 }, "radius": 0.2, "material": 0}, { "type": "sphere", "center": [0, 0.7, -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": [0.0, 0.5, -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": [0.0, -100.5, -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": [0.0, 0.0, -1.2], "radius": 0.5, "material": 3},
{ "type": "sphere", "center": { "x": -1, "y": 0, "z": -1 }, "radius": 0.4, "material": 5}, { "type": "sphere", "center": [-1, 0, -1], "radius": 0.4, "material": 5},
{ "type": "sphere", "center": { "x": 1, "y": 0, "z": -1 }, "radius": 0.5, "material": 6}, { "type": "sphere", "center": [1, 0, -1], "radius": 0.5, "material": 6},
{ "type": "sphere", "center": { "x": 20, "y": 7, "z": -15 }, "radius": 10.5, "material": 0} { "type": "sphere", "center": [20, 7, -15], "radius": 10.5, "material": 0}
] ]
} }

View File

@@ -1,32 +0,0 @@
{
"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}
]
}

View File

@@ -1,12 +1,13 @@
{ {
"camera": { "filename": "output.png",
"image_width": 1920, "image_width": 1920,
"image_height": 1080, "image_height": 1080,
"anti_alias_rate": 23,
"max_depth": 50, "max_depth": 50,
"fov": 90.0, "camera": {
"look_from": [15, 4, 15], "anti_alias_rate": 23,
"look_at": [0.0, 0.0, 0.0], "fov": 70.0,
"look_from": [-10, 4, 15],
"look_at": [-11.0, 0.0, 0.0],
"vup": [0.0, 1.0, 0.0], "vup": [0.0, 1.0, 0.0],
"defocus_angle": 0, "defocus_angle": 0,
"focus_dist": 15.68 "focus_dist": 15.68
@@ -28,7 +29,8 @@
{ "type": "quad", "p1": [-20, -1, 20], "p2": [-20, -1, -20], "p3": [-20, 20, -20], "p4": [-20, 20, 20], "material": 0}, { "type": "quad", "p1": [-20, -1, 20], "p2": [-20, -1, -20], "p3": [-20, 20, -20], "p4": [-20, 20, 20], "material": 0},
{ "type": "quad", "p1": [-20, -1, 20], "p2": [20, -1, 20], "p3": [20, -1, -20], "p4": [-20, -1, -20], "material": 0}, { "type": "quad", "p1": [-20, -1, 20], "p2": [20, -1, 20], "p3": [20, -1, -20], "p4": [-20, -1, -20], "material": 0},
{ "type": "quad", "p1": [20, -1, 20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [20, 20, 20], "material": 0}, { "type": "quad", "p1": [20, -1, 20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [20, 20, 20], "material": 0},
{ "type": "cube", "p1": [8, 0, 2], "p2": [12, 0, 2], "p3": [12, 4, 2], "p4": [8, 4, 2], "p5": [8, 0, -2], "p6": [12, 0, -2], "p7": [12, 4, -2], "p8": [8, 4, -2], "material": 3} { "type": "cube", "p1": [8, 0, 2], "p2": [12, 0, 2], "p3": [12, 4, 2], "p4": [8, 4, 2], "p5": [8, 0, -2], "p6": [12, 0, -2], "p7": [12, 4, -2], "p8": [8, 4, -2], "material": 3},
{ "type": "circle", "center": [-9, 3, 0], "radius": 3, "normal": [0, 1, 0.5], "material": {"type": "metal", "albedo": [0.9, 0.9, 0.9], "prob": 1.0, "fuzz": 0.3}}
] ]
} }

35
scenes/texture.json Normal file
View File

@@ -0,0 +1,35 @@
{
"filename": "textured.png",
"image_width": 1920,
"image_height": 1080,
"max_depth": 50,
"camera": {
"anti_alias_rate": 2,
"fov": 50.0,
"look_from": [-3, 4, 10],
"look_at": [-3, 0.0, -10.0],
"vup": [0.0, 1.0, 0.0],
"defocus_angle": 0,
"focus_dist": 15.68
},
"materials": [
{ "type": "lambertian", "albedo": [0.2, 0.2, 0.2], "prob": 0.8 },
{"type": "texture", "source": "./textures/earthmap1k.png"},
{"type": "texture", "source": "./textures/bluegrid.png"}
],
"objects": [
{ "type": "sphere", "center": [0.0, 0.0, -1.2], "radius": 0.9, "material": 2},
{ "type": "sphere", "center": [-2, 0, -1], "radius": 0.8, "material": 1},
{ "type": "triangle", "p1": [0, 0, -4], "p2": [4, 0, -4], "p3": [2, 2, -4], "material": 2},
{ "type": "triangle", "p1": [-2, 2, -4], "p2": [2, 2, -4], "p3": [0, 4, -4], "material": 1},
{ "type": "quad", "p1": [-20, -1, -20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [-20, 20, -20], "material": 0},
{ "type": "quad", "p1": [-20, -1, 20], "p2": [-20, -1, -20], "p3": [-20, 20, -20], "p4": [-20, 20, 20], "material": 0},
{ "type": "quad", "p1": [-20, -1, 20], "p2": [20, -1, 20], "p3": [20, -1, -20], "p4": [-20, -1, -20], "material": 0},
{ "type": "quad", "p1": [20, -1, 20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [20, 20, 20], "material": 0},
{ "type": "quad", "p1": [-4, 3, -4], "p2": [-1, 3, -4], "p3": [-1, 6, -4], "p4": [-4, 6, -4], "material": 1},
{ "type": "quad", "p1": [-8, 3, -4], "p2": [-5, 3, -4], "p3": [-5, 6, -4], "p4": [-8, 6, -4], "material": 2}
]
}

View File

@@ -1,22 +1,11 @@
use std::{f32::consts::PI, sync::Arc}; use std::f32::consts::PI;
use log::info; use crate::{ray::Ray, vec3::Vec3};
use crate::{
objects::{hit::Hit, traits::Hittable},
ray::Ray,
vec3::{Colour, Vec3},
};
#[derive(Debug)] #[derive(Debug)]
pub struct Camera { pub struct Camera {
// output
image_width: u32,
image_height: u32,
// raytracing // raytracing
anti_alias_rate: u32, anti_alias_rate: u32,
max_depth: u32,
pixel00_loc: Vec3, pixel00_loc: Vec3,
pixel_delta_u: Vec3, pixel_delta_u: Vec3,
pixel_delta_v: Vec3, pixel_delta_v: Vec3,
@@ -44,12 +33,9 @@ fn deg_to_rad(deg: f32) -> f32 {
} }
impl Camera { impl Camera {
pub fn new(image_width: u32, image_height: u32) -> Self { pub fn new() -> Self {
Self { Self {
image_width,
image_height,
anti_alias_rate: 1, anti_alias_rate: 1,
max_depth: 10,
dirty: true, dirty: true,
fov: 60., fov: 60.,
pixel00_loc: Vec3::default(), pixel00_loc: Vec3::default(),
@@ -69,10 +55,7 @@ impl Camera {
} }
pub fn new_full( pub fn new_full(
image_width: u32,
image_height: u32,
anti_alias_rate: u32, anti_alias_rate: u32,
max_depth: u32,
fov: f32, fov: f32,
look_from: Vec3, look_from: Vec3,
look_at: Vec3, look_at: Vec3,
@@ -81,10 +64,7 @@ impl Camera {
focus_dist: f32, focus_dist: f32,
) -> Self { ) -> Self {
Self { Self {
image_width,
image_height,
anti_alias_rate, anti_alias_rate,
max_depth,
pixel00_loc: Vec3::default(), pixel00_loc: Vec3::default(),
pixel_delta_u: Vec3::default(), pixel_delta_u: Vec3::default(),
pixel_delta_v: Vec3::default(), pixel_delta_v: Vec3::default(),
@@ -103,7 +83,11 @@ impl Camera {
} }
} }
fn init(&mut self) { pub fn init(&mut self, width: u32, height: u32) {
if !self.dirty {
return;
}
// camera // camera
let theta = deg_to_rad(self.fov); let theta = deg_to_rad(self.fov);
let h = (theta / 2.).tan(); let h = (theta / 2.).tan();
@@ -113,13 +97,13 @@ impl Camera {
// viewport // viewport
let viewport_height = 2. * h * self.focus_dist; 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_width = viewport_height * (width as f32 / height as f32);
let viewport_u = viewport_width * self.u; let viewport_u = viewport_width * self.u;
let viewport_v = viewport_height * -self.v; let viewport_v = viewport_height * -self.v;
// variables // variables
self.pixel_delta_u = viewport_u / self.image_width as f32; self.pixel_delta_u = viewport_u / width;
self.pixel_delta_v = viewport_v / self.image_height as f32; self.pixel_delta_v = viewport_v / height;
self.pixel00_loc = self.pixel00_loc =
self.look_from - (self.focus_dist * 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; self.dirty = false;
@@ -131,6 +115,27 @@ impl Camera {
} }
} }
pub fn get_anti_alias_rate(&self) -> u32 {
self.anti_alias_rate
}
pub fn get_pixel_tl(&self, x: u32, y: u32) -> Vec3 {
self.pixel00_loc + (x * self.pixel_delta_u) + (y * self.pixel_delta_v)
}
pub fn get_ray(&self, pixel_tl: Vec3, x: u32, y: u32) -> Ray {
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_orig = if self.defocus_angle > 0. {
self.defocus_disk_sample()
} else {
self.look_from
};
let ray_dir = pixel_loc - ray_orig;
Ray::new(ray_orig, ray_dir)
}
pub fn set_fov(&mut self, fov: f32) { pub fn set_fov(&mut self, fov: f32) {
if self.fov != fov { if self.fov != fov {
self.fov = fov; self.fov = fov;
@@ -138,13 +143,6 @@ impl Camera {
} }
} }
pub fn set_max_depth(&mut self, depth: u32) {
if self.max_depth != depth {
self.max_depth = depth;
self.dirty = true;
}
}
pub fn set_anti_alias_rate(&mut self, rate: u32) { pub fn set_anti_alias_rate(&mut self, rate: u32) {
if self.anti_alias_rate != rate { if self.anti_alias_rate != rate {
self.anti_alias_rate = rate; self.anti_alias_rate = rate;
@@ -181,72 +179,8 @@ impl Camera {
} }
} }
fn defocus_disk_sample(&self) -> Vec3 { pub fn defocus_disk_sample(&self) -> Vec3 {
let p = Vec3::random_in_unit_disk(); let p = Vec3::random_in_unit_disk();
self.look_from + (p.x() * self.defocus_disk_u) + (p.y() * self.defocus_disk_v) self.look_from + (p.x() * self.defocus_disk_u) + (p.y() * self.defocus_disk_v)
} }
pub fn render(&mut self, hittables: &Vec<Arc<dyn Hittable>>) {
if self.dirty {
self.init()
}
let mut imgbuf = image::ImageBuffer::new(self.image_width, self.image_height);
// render
for j in 0..self.image_height {
info!("{}\tScanlines remaining.", (self.image_height - j));
for i in 0..self.image_width {
let pixel_tl =
self.pixel00_loc + (i * self.pixel_delta_u) + (j * self.pixel_delta_v);
let mut pixel_colour = Colour::default();
for y in 1..(self.anti_alias_rate + 1) {
for x in 1..(self.anti_alias_rate + 1) {
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_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);
}
}
let pixel = imgbuf.get_pixel_mut(i, j);
*pixel =
(pixel_colour / (self.anti_alias_rate * self.anti_alias_rate) as f32).output();
}
}
info!("Writing image file...");
imgbuf.save("output.png").unwrap();
}
fn ray_colour(&self, hittables: &Vec<Arc<dyn Hittable>>, r: &Ray, depth: u32) -> Colour {
if depth == 0 {
return Colour::default();
}
let closest = Hit::hit_list(hittables, r);
if let Some(hit) = closest {
if let Some((scattered, att)) = hit.mat().scatter(&hit, r) {
if let Some(ray) = scattered {
return att * self.ray_colour(hittables, &ray, depth - 1);
}
return att;
}
return Colour::default();
}
// background
let unit_dir = r.dir().get_unit();
let a = 0.5 * (unit_dir.y() + 1.);
(1.0 - a) * Colour::new(1., 1., 1.) + a * Colour::new(0.5, 0.7, 1.0)
}
} }

View File

@@ -3,118 +3,32 @@
mod camera; mod camera;
mod objects; mod objects;
mod ray; mod ray;
mod raytracer;
mod scenes; mod scenes;
mod vec3; mod vec3;
use std::fs; use std::{env, fs};
use std::sync::Arc;
use crate::camera::Camera;
use crate::objects::materials::dielectric::Dielectric;
use crate::objects::materials::lambertian::{Lambertian, Metal};
use crate::objects::sphere::Sphere;
use crate::objects::traits::Hittable;
use crate::ray::Ray; use crate::ray::Ray;
use crate::raytracer::render;
use crate::scenes::scene::Scene; use crate::scenes::scene::Scene;
use crate::vec3::Vec3; use crate::vec3::Vec3;
use dotenv::dotenv; use dotenv::dotenv;
use rand::RngExt;
// TODO: implement scene serialization // TODO: implement scene serialization
fn main() { fn main() {
dotenv().ok(); dotenv().ok();
pretty_env_logger::init(); pretty_env_logger::init();
// TODO: use cli arg for scenefile // TODO: better cli parsing || add random flag to generate random scene
let json_file = "./scenes/scene.json"; let mut json_file = "./scenes/scene.json";
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
json_file = &args[1];
}
let json_str = fs::read_to_string(json_file).expect("Reading specified scene file failed!"); 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(); let mut scene: Scene = serde_json::from_str(&json_str).unwrap();
scene.render(); let mut scene: Scene = Scene::random();
return; render(&mut scene);
// random spheres code; thought: make this available as cli flag?
let ground = Lambertian::rgb(0.5, 0.5, 0.5, 1.);
let mut world: Vec<Arc<dyn Hittable>> = vec![Arc::new(Sphere::xyz(
0.,
-1000.,
0.,
1000.,
Arc::new(ground),
))];
let mut rng = rand::rng();
let point = Vec3::new(4., 0.2, 0.);
for a in -11..11 {
for b in -11..11 {
let mat = rng.random_range((0.)..1.);
let center = Vec3::new(
a as f32 + 0.9 * rng.random_range((0.)..1.),
0.2,
b as f32 + 0.9 * rng.random_range((0.)..1.),
);
if (center - point).length() > 0.9 {
if mat < 0.8 {
// diffuse
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Lambertian::new(Vec3::random() * Vec3::random(), 1.)),
)));
} else if mat < 0.95 {
// metal
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Metal::rgb(
rng.random_range(0.5..1.),
rng.random_range(0.5..1.),
rng.random_range(0.5..1.),
1.,
rng.random_range((0.)..0.5),
)),
)));
} else {
// glass
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Dielectric::new(1.5)),
)));
}
}
}
}
world.push(Arc::new(Sphere::xyz(
0.,
1.,
0.,
1.,
Arc::new(Dielectric::new(1.5)),
)));
world.push(Arc::new(Sphere::xyz(
-4.,
1.,
0.,
1.,
Arc::new(Lambertian::rgb(0.4, 0.2, 0.1, 1.)),
)));
world.push(Arc::new(Sphere::xyz(
4.,
1.,
0.,
1.,
Arc::new(Metal::rgb(0.7, 0.6, 0.5, 1., 0.)),
)));
let mut c = Camera::new(1920, 1080);
c.set_fov(20.);
c.set_anti_alias_rate(23);
c.set_max_depth(50);
c.set_look_from(Vec3::new(13., 2., 3.));
c.set_look_at(Vec3::new(0., 0., 0.));
c.add_defocus_blur(0.6, 10.);
c.render(&world);
} }

View File

@@ -1,7 +1,10 @@
pub mod circle;
pub mod cube;
pub mod cylinder;
pub mod hit; pub mod hit;
pub mod materials; pub mod materials;
pub mod sphere; pub mod mesh;
pub mod triangle;
pub mod quad; pub mod quad;
pub mod cube; pub mod sphere;
pub mod traits; pub mod traits;
pub mod triangle;

71
src/objects/circle.rs Normal file
View File

@@ -0,0 +1,71 @@
use std::{f32::consts::PI, sync::Arc};
use crate::{
objects::{materials::traits::Material, traits::Hittable},
ray::Ray,
vec3::Vec3,
};
use super::hit::Hit;
#[derive(Debug)]
pub struct Circle {
radius: f32,
center: Vec3,
normal: Vec3,
material: Arc<dyn Material>,
}
impl Circle {
pub fn new(radius: f32, center: Vec3, normal: Vec3, material: Arc<dyn Material>) -> Self {
Self {
radius,
center,
normal: normal.get_unit(),
material,
}
}
}
impl Hittable for Circle {
fn hit(&self, r: &Ray) -> Option<Hit> {
// check if ray parallel to plane
let dot = self.normal.dot(r.dir());
if dot == 0.0 {
return None;
}
// hitpoint on plane
let t = self.normal.dot(&(self.center - r.origin())) / dot;
// hits behind camera
if t < 0. {
return None;
};
let p = r.at(t);
if (p - self.center).length() < self.radius {
let uv = self.to_uv(&p);
return Some(Hit::new(
t,
p,
self.normal,
self.material.clone(),
self.normal.dot(r.dir()) < 0.,
*uv.x(),
*uv.y(),
));
}
None
}
fn to_uv(&self, point: &Vec3) -> Vec3 {
let p = *point - self.center;
// TODO: add rotated texture support
Vec3::new(
0.5 + p.y().atan2(*p.x()) / (2. * PI),
1. - (p.z() / self.radius).acos() / PI,
0.0,
)
}
}

View File

@@ -11,7 +11,6 @@ use super::hit::Hit;
#[derive(Debug)] #[derive(Debug)]
pub struct Cube { pub struct Cube {
faces: Vec<Arc<dyn Hittable>>, faces: Vec<Arc<dyn Hittable>>,
material: Arc<dyn Material>,
} }
impl Cube { impl Cube {
@@ -34,7 +33,7 @@ impl Cube {
Arc::new(Quad::new(p5, p6, p7, p8, material.clone())), Arc::new(Quad::new(p5, p6, p7, p8, material.clone())),
Arc::new(Quad::new(p4, p3, p7, p8, material.clone())), Arc::new(Quad::new(p4, p3, p7, p8, material.clone())),
]; ];
Self { faces, material } Self { faces }
} }
} }
@@ -43,8 +42,8 @@ impl Hittable for Cube {
Hit::hit_list(&self.faces, r) Hit::hit_list(&self.faces, r)
} }
fn normal_at(&self, _p: &Vec3) -> Vec3 { fn to_uv(&self, _point: &Vec3) -> Vec3 {
// TODO: normal calc for cube // TODO: map to [0.1] relative to specific face
todo!() todo!()
} }
} }

View File

@@ -12,16 +12,47 @@ pub struct Hit {
n: Vec3, n: Vec3,
mat: Arc<dyn Material>, mat: Arc<dyn Material>,
front_face: bool, front_face: bool,
u: f32,
v: f32,
} }
impl Hit { impl Hit {
pub fn new(t: f32, p: Vec3, n: Vec3, mat: Arc<dyn Material>, front_face: bool) -> Self { pub fn new(
t: f32,
p: Vec3,
n: Vec3,
mat: Arc<dyn Material>,
front_face: bool,
u: f32,
v: f32,
) -> Self {
Self { Self {
t, t,
p, p,
n: if front_face { n } else { -n }, n: if front_face { n } else { -n },
mat, mat,
front_face, front_face,
u,
v,
}
}
pub fn from_minimal(
mh: MinimalHit,
n: Vec3,
mat: Arc<dyn Material>,
front_face: bool,
u: f32,
v: f32,
) -> Self {
Self {
t: mh.t,
p: mh.p,
n,
mat,
front_face,
u,
v,
} }
} }
@@ -45,6 +76,10 @@ impl Hit {
self.front_face self.front_face
} }
pub fn uv(&self) -> (f32, f32) {
(self.u, self.v)
}
// TODO: use front_face to discard back-hits for culling // TODO: use front_face to discard back-hits for culling
pub fn hit_list(hittables: &Vec<Arc<dyn Hittable>>, r: &Ray) -> Option<Hit> { pub fn hit_list(hittables: &Vec<Arc<dyn Hittable>>, r: &Ray) -> Option<Hit> {
let mut closest: Option<Hit> = None; let mut closest: Option<Hit> = None;
@@ -61,3 +96,22 @@ impl Hit {
closest closest
} }
} }
pub struct MinimalHit {
t: f32,
p: Vec3,
}
impl MinimalHit {
pub fn new(t: f32, p: Vec3) -> Self {
Self { t, p }
}
pub fn t(&self) -> f32 {
self.t
}
pub fn p(&self) -> &Vec3 {
&self.p
}
}

View File

@@ -2,3 +2,4 @@ pub mod dielectric;
pub mod lambertian; pub mod lambertian;
pub mod normal; pub mod normal;
pub mod traits; pub mod traits;
pub mod texture;

View File

@@ -16,9 +16,7 @@ pub struct Dielectric {
impl Dielectric { impl Dielectric {
pub fn new(refraction_index: f32) -> Self { pub fn new(refraction_index: f32) -> Self {
Self { Self { refraction_index }
refraction_index,
}
} }
fn reflectance(cos: f32, refraction_index: f32) -> f32 { fn reflectance(cos: f32, refraction_index: f32) -> f32 {
@@ -46,7 +44,8 @@ impl Material for Dielectric {
let cannot_refract = ri * sin_theta > 1.; let cannot_refract = ri * sin_theta > 1.;
let mut rng = rand::rng(); let mut rng = rand::rng();
let dir = if cannot_refract || Dielectric::reflectance(cos_theta, ri) > rng.random::<f32>() { let dir = if cannot_refract || Dielectric::reflectance(cos_theta, ri) > rng.random::<f32>()
{
unit.reflect(hit.n()) unit.reflect(hit.n())
} else { } else {
unit.refract(hit.n(), ri) unit.refract(hit.n(), ri)

View File

@@ -15,10 +15,7 @@ pub struct Lambertian {
impl Lambertian { impl Lambertian {
pub fn new(albedo: Colour, prob: f32) -> Self { pub fn new(albedo: Colour, prob: f32) -> Self {
Self { Self { albedo, prob }
albedo,
prob,
}
} }
pub fn rgb(r: f32, g: f32, b: f32, prob: f32) -> Self { pub fn rgb(r: f32, g: f32, b: f32, prob: f32) -> Self {
@@ -53,11 +50,7 @@ pub struct Metal {
impl Metal { impl Metal {
pub fn new(albedo: Colour, prob: f32, fuzz: f32) -> Self { pub fn new(albedo: Colour, prob: f32, fuzz: f32) -> Self {
Self { Self { albedo, prob, fuzz }
albedo,
prob,
fuzz,
}
} }
pub fn rgb(r: f32, g: f32, b: f32, prob: f32, fuzz: f32) -> Self { pub fn rgb(r: f32, g: f32, b: f32, prob: f32, fuzz: f32) -> Self {

View File

@@ -0,0 +1,80 @@
use image::{DynamicImage, ImageReader};
use log::trace;
use crate::{
objects::{hit::Hit, materials::traits::Material},
ray::Ray,
vec3::Vec3,
};
// TODO: overhaul texture implementation so we can support textures for arbitrary materials.
// Basically remove this struct, and instead make lambertian/metal/dielectric/take either a texture
// or a flat colour
// I think we can use an enum ColourSource with a function to fetch a specific colour depending on
// if we have a texture/flat colour. and yeah.
#[derive(Debug)]
pub struct Texture {
source: DynamicImage,
width: u32,
height: u32,
stride: usize,
}
impl Texture {
pub fn new(texture: &str) -> Self {
let img = ImageReader::open(texture).unwrap().decode().unwrap(); // FIXME: unwraps
trace!(
"texture '{}' is {} by {} pixels, with {} bytes total",
texture,
img.width(),
img.height(),
img.as_bytes().len()
);
let stride = match img.color() {
// TODO: support other types of image
image::ColorType::L8 => todo!(),
image::ColorType::La8 => todo!(),
image::ColorType::Rgb8 => 3,
image::ColorType::Rgba8 => 4,
image::ColorType::L16 => todo!(),
image::ColorType::La16 => todo!(),
image::ColorType::Rgb16 => todo!(),
image::ColorType::Rgba16 => todo!(),
image::ColorType::Rgb32F => todo!(),
image::ColorType::Rgba32F => todo!(),
_ => 3,
};
Self {
width: img.width(),
height: img.height(),
source: img,
stride,
}
}
fn _idx(&self, u: f32, v: f32) -> usize {
let x: usize = (u * ((self.width - 1) as f32)) as usize;
let y: usize = (v * ((self.height - 1) as f32)) as usize;
y * self.width as usize + x
}
fn _at(&self, u: f32, v: f32) -> Vec3 {
let b = self.source.as_bytes();
let idx = self._idx(u, v);
Vec3::from_u8(
b[self.stride * idx],
b[self.stride * idx + 2],
b[self.stride * idx + 1],
)
}
}
impl Material for Texture {
fn scatter(&self, hit: &Hit, _ray: &Ray) -> Option<(Option<Ray>, Vec3)> {
let (u, v) = hit.uv();
let col = self._at(u, 1. - v);
Some((None, col))
}
}

0
src/objects/mesh.rs Normal file
View File

View File

@@ -1,5 +1,4 @@
use crate::objects::hit::Hit; use crate::objects::hit::Hit;
use crate::objects::sphere::Sphere;
use crate::objects::traits::Hittable; use crate::objects::traits::Hittable;
use crate::objects::triangle::Triangle; use crate::objects::triangle::Triangle;
use crate::ray::Ray; use crate::ray::Ray;
@@ -14,6 +13,10 @@ pub struct Quad {
p4: Vec3, p4: Vec3,
material: Arc<dyn Material>, material: Arc<dyn Material>,
normal: Vec3, normal: Vec3,
// helpers
t1: Triangle,
t2: Triangle,
} }
impl Quad { impl Quad {
@@ -23,35 +26,12 @@ impl Quad {
p2, p2,
p3, p3,
p4, p4,
material, material: material.clone(),
normal: (p2 - p1).cross(&(p4 - p1)).get_unit(), normal: (p2 - p1).cross(&(p4 - p1)).get_unit(),
t1: Triangle::new(p1, p2, p4, material.clone()),
t2: Triangle::new(p2, p3, p4, material),
} }
} }
pub fn corner_spheres(&self) -> Vec<Sphere> {
vec![
Sphere::new(self.p1, 1., self.material.clone()),
Sphere::new(self.p2, 1., self.material.clone()),
Sphere::new(self.p3, 1., self.material.clone()),
Sphere::new(self.p4, 1., self.material.clone()),
]
}
pub fn hit(
p1: Vec3,
p2: Vec3,
p3: Vec3,
p4: Vec3,
material: Arc<dyn Material>,
normal: Vec3,
r: &Ray,
) -> Option<Hit> {
let isct1 = Triangle::hit(p1, p2, p4, material.clone(), normal, r);
let isct2 = Triangle::hit(p2, p3, p4, material.clone(), normal, r);
if isct1.is_some() {
return isct1;
}
isct2
}
} }
impl Debug for Quad { impl Debug for Quad {
@@ -68,19 +48,22 @@ impl Debug for Quad {
impl Hittable for Quad { impl Hittable for Quad {
fn hit(&self, r: &Ray) -> Option<Hit> { fn hit(&self, r: &Ray) -> Option<Hit> {
Quad::hit( let mh = self.t1.calculate_hit(r).or(self.t2.calculate_hit(r))?;
self.p1, let uvw = self.to_uv(mh.p());
self.p2,
self.p3, Some(Hit::from_minimal(
self.p4, mh,
self.material.clone(),
self.normal, self.normal,
r, self.material.clone(),
) false,
*uvw.x(),
*uvw.y(),
))
} }
fn normal_at(&self, _p: &Vec3) -> Vec3 { fn to_uv(&self, point: &Vec3) -> Vec3 {
// FIXME: might cause ownership issues let u = (*point - self.p1).dot(&(self.p2 - self.p1)) / (self.p2 - self.p1).length_squared();
self.normal let v = (*point - self.p1).dot(&(self.p4 - self.p1)) / (self.p4 - self.p1).length_squared();
Vec3::new(u, v, 0.)
} }
} }

View File

@@ -1,13 +1,13 @@
use core::f32::math::sqrt; use core::f32::math::sqrt;
use std::f32::consts::PI;
use std::fmt::{self, Debug}; use std::fmt::{self, Debug};
use std::sync::Arc; use std::sync::Arc;
use crate::Vec3;
use crate::objects::hit::Hit; use crate::objects::hit::Hit;
use crate::objects::materials::traits::Material; use crate::objects::materials::traits::Material;
use crate::objects::traits::Hittable; use crate::objects::traits::Hittable;
use crate::ray::Ray; use crate::ray::Ray;
use crate::Vec3;
pub struct Sphere { pub struct Sphere {
center: Vec3, center: Vec3,
@@ -15,7 +15,6 @@ pub struct Sphere {
material: Arc<dyn Material>, material: Arc<dyn Material>,
} }
impl Debug for Sphere { impl Debug for Sphere {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Sphere") f.debug_struct("Sphere")
@@ -60,17 +59,26 @@ impl Hittable for Sphere {
let t = if tl > 0.001 { tl } else { tr }; let t = if tl > 0.001 { tl } else { tr };
let p = r.at(t); let p = r.at(t);
let out_n = (p - self.center) / self.radius; let out_n = (p - self.center) / self.radius;
let uv = self.to_uv(&p);
Some(Hit::new( Some(Hit::new(
t, t,
p, p,
self.normal_at(&p), (p - self.center).get_unit(),
self.material.clone(), self.material.clone(),
out_n.dot(r.dir()) < 0., out_n.dot(r.dir()) < 0.,
*uv.x(),
*uv.y(),
)) ))
} }
} }
fn normal_at(&self, p: &Vec3) -> Vec3 { fn to_uv(&self, point: &Vec3) -> Vec3 {
(*p - self.center).get_unit() let p = *point - self.center;
// TODO: add rotated texture support
Vec3::new(
0.5 + p.y().atan2(*p.x()) / (2. * PI),
1. - (p.z() / self.radius).acos() / PI,
0.0,
)
} }
} }

View File

@@ -1,10 +1,10 @@
use std::fmt::Debug; use std::fmt::Debug;
use crate::objects::hit::Hit;
use crate::Ray; use crate::Ray;
use crate::Vec3; use crate::objects::hit::Hit;
use crate::vec3::Vec3;
pub trait Hittable: Debug + Send + Sync { pub trait Hittable: Debug + Send + Sync {
fn to_uv(&self, point: &Vec3) -> Vec3;
fn hit(&self, r: &Ray) -> Option<Hit>; fn hit(&self, r: &Ray) -> Option<Hit>;
fn normal_at(&self, p: &Vec3) -> Vec3;
} }

View File

@@ -1,4 +1,4 @@
use crate::objects::hit::Hit; use crate::objects::hit::{Hit, MinimalHit};
use crate::objects::traits::Hittable; use crate::objects::traits::Hittable;
use crate::ray::Ray; use crate::ray::Ray;
use crate::{objects::materials::traits::Material, vec3::Vec3}; use crate::{objects::materials::traits::Material, vec3::Vec3};
@@ -11,50 +11,66 @@ pub struct Triangle {
p3: Vec3, p3: Vec3,
material: Arc<dyn Material>, material: Arc<dyn Material>,
normal: Vec3, normal: Vec3,
// barycentric helpers
v0: Vec3,
v1: Vec3,
d00: f32,
d01: f32,
d11: f32,
denom: f32,
a4: f32,
} }
impl Triangle { impl Triangle {
pub fn new(p1: Vec3, p2: Vec3, p3: Vec3, material: Arc<dyn Material>) -> Self { pub fn new(p1: Vec3, p2: Vec3, p3: Vec3, material: Arc<dyn Material>) -> Self {
// barycentric helpers cached to save computations
let v0 = p2 - p1;
let v1 = p3 - p1;
let d00 = v0.dot(&v0);
let d01 = v0.dot(&v1);
let d11 = v1.dot(&v1);
let a4 = (p3 - p1).cross(&(p2 - p1)).length();
Self { Self {
p1, p1,
p2, p2,
p3, p3,
material, material,
normal: (p2 - p1).cross(&(p3 - p1)).get_unit(), normal: (p2 - p1).cross(&(p3 - p1)).get_unit(),
v0,
v1,
d00,
d01,
d11,
denom: d00 * d11 - d01 * d01,
a4,
} }
} }
pub fn hit( pub fn calculate_hit(&self, r: &Ray) -> Option<MinimalHit> {
p1: Vec3,
p2: Vec3,
p3: Vec3,
material: Arc<dyn Material>,
normal: Vec3,
r: &Ray,
) -> Option<Hit> {
// check if ray parallel to plane // check if ray parallel to plane
let dot = normal.dot(r.dir()); let dot = self.normal.dot(r.dir());
if dot == 0.0 { if dot == 0.0 {
return None; return None;
} }
let d = (-normal).dot(&p1); let d = (-self.normal).dot(&self.p1);
// hitpoint on plane // hitpoint on plane
let t = -(normal.dot(&(r.origin() + d))) / dot; let t = -(self.normal.dot(&(r.origin() + d))) / dot;
// hits behind camera // hits behind camera
if t < 0. { if t < 0. {
return None; return None;
}; };
let p = r.at(t); let p = r.at(t);
let a4 = (p3 - p1).cross(&(p2 - p1)).length(); let a3 = (self.p3 - p).cross(&(self.p2 - p)).length();
let a3 = (p3 - p).cross(&(p2 - p)).length(); let a2 = (self.p3 - p).cross(&(self.p1 - p)).length();
let a2 = (p3 - p).cross(&(p1 - p)).length(); let a1 = (self.p2 - p).cross(&(self.p1 - p)).length();
let a1 = (p2 - p).cross(&(p1 - p)).length();
let diff = (a4 - a1 - a2 - a3).abs(); let diff = (self.a4 - a1 - a2 - a3).abs();
if diff < 0.001 { if diff < 0.001 {
Some(Hit::new(t, p, normal, material, normal.dot(&-r.dir()) > 0.)) Some(MinimalHit::new(t, p))
} else { } else {
None None
} }
@@ -74,18 +90,26 @@ impl Debug for Triangle {
impl Hittable for Triangle { impl Hittable for Triangle {
fn hit(&self, r: &Ray) -> Option<Hit> { fn hit(&self, r: &Ray) -> Option<Hit> {
Triangle::hit( let mh = self.calculate_hit(r)?;
self.p1, let uvw = self.to_uv(mh.p());
self.p2,
self.p3, Some(Hit::from_minimal(
self.material.clone(), mh,
self.normal, self.normal,
r, self.material.clone(),
) self.normal.dot(r.dir()) < 0.,
*uvw.x(),
*uvw.y(),
))
} }
fn normal_at(&self, _p: &Vec3) -> Vec3 { fn to_uv(&self, point: &Vec3) -> Vec3 {
// FIXME: might cause ownership issues let v2 = *point - self.p1;
self.normal let d20 = v2.dot(&self.v0);
let d21 = v2.dot(&self.v1);
let v = (self.d11 * d20 - self.d01 * d21) / self.denom;
let w = (self.d00 * d21 - self.d01 * d20) / self.denom;
Vec3::new(1. - v - w, v, w)
} }
} }

View File

@@ -7,10 +7,7 @@ pub struct Ray {
impl Ray { impl Ray {
pub fn new(origin: Vec3, dir: Vec3) -> Self { pub fn new(origin: Vec3, dir: Vec3) -> Self {
Self { Self { origin, dir }
origin,
dir,
}
} }
pub fn at(&self, t: f32) -> Vec3 { pub fn at(&self, t: f32) -> Vec3 {

99
src/raytracer.rs Normal file
View File

@@ -0,0 +1,99 @@
use image::{ExtendedColorType, ImageEncoder, codecs::png::PngEncoder};
use log::{error, info, trace};
use rayon::iter::IntoParallelIterator;
use rayon::prelude::*;
use std::{fs::File, sync::Arc, time::Instant};
use crate::{
objects::{hit::Hit, traits::Hittable},
ray::Ray,
scenes::scene::Scene,
vec3::Colour,
};
pub fn write_image(filename: &str, pixels: &[u8], width: u32, height: u32) {
match File::create(filename) {
Ok(out) => {
let enc = PngEncoder::new(out);
if let Err(e) = enc.write_image(pixels, width, height, ExtendedColorType::Rgb8) {
error!("Failed writing image: {}", e);
}
}
Err(e) => error!("Failed to create image file: {} ", e),
}
}
pub fn render(scene: &mut Scene) {
scene.init();
// TODO: currently splits per vertical line, but could be more granular (per chunk)
let mut pixels = vec![0_u8; (scene.get_image_width() * scene.get_image_height() * 3) as usize];
let scanlines: Vec<(usize, &mut [u8])> = pixels
.chunks_mut(scene.get_image_width() as usize * 3)
.enumerate()
.collect();
let now = Instant::now();
scanlines.into_par_iter().for_each(|(i, chunk)| {
render_chunk(chunk, scene, i as u32);
});
info!(
"rendering took {}.{} seconds.",
now.elapsed().as_secs(),
now.elapsed().subsec_nanos()
);
info!("Writing image file...");
write_image(
&scene.get_filename(),
&pixels,
scene.get_image_width() as u32,
scene.get_image_height() as u32,
);
}
pub fn render_chunk(chunk: &mut [u8], scene: &Scene, y: u32) {
let camera = &scene.get_camera();
let aa_rate = &scene.get_camera().get_anti_alias_rate();
for i in 0..scene.get_image_width() {
let pixel_tl = camera.get_pixel_tl(i, y);
let mut pixel_colour = Colour::default();
for y in 1..(aa_rate + 1) {
for x in 1..(aa_rate + 1) {
let r = camera.get_ray(pixel_tl, x, y);
pixel_colour += ray_colour(&scene.get_objects(), &r, scene.get_max_depth());
}
}
let (r, g, b) = (pixel_colour / (aa_rate * aa_rate) as f32).output();
chunk[(i * 3) as usize] = r;
chunk[(i * 3) as usize + 1] = g;
chunk[(i * 3) as usize + 2] = b;
}
trace!("Line {}\tfinished.", y);
}
pub fn ray_colour(hittables: &Vec<Arc<dyn Hittable>>, r: &Ray, depth: u32) -> Colour {
if depth == 0 {
return Colour::default();
}
let closest = Hit::hit_list(hittables, r);
if let Some(hit) = closest {
if let Some((scattered, att)) = hit.mat().scatter(&hit, r) {
if let Some(ray) = scattered {
return att * ray_colour(hittables, &ray, depth - 1);
}
return att;
}
return Colour::default();
}
// background
let unit_dir = r.dir().get_unit();
let a = 0.5 * (unit_dir.y() + 1.);
(1.0 - a) * Colour::new(1., 1., 1.) + a * Colour::new(0.5, 0.7, 1.0)
}

View File

@@ -1,2 +1,4 @@
pub mod raw_camera; pub mod raw_camera;
pub mod material_def;
pub mod hittable_def;
pub mod scene; pub mod scene;

137
src/scenes/hittable_def.rs Normal file
View File

@@ -0,0 +1,137 @@
use std::sync::Arc;
use log::warn;
use serde::Deserialize;
use crate::{
objects::{
circle::Circle, cube::Cube, materials::traits::Material, quad::Quad, sphere::Sphere,
traits::Hittable, triangle::Triangle,
},
scenes::material_def::MaterialDef,
vec3::Vec3,
};
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub(crate) enum HittableDef {
Sphere(RawSphere),
Triangle(RawTriangle),
Quad(RawQuad),
Cube(RawCube),
Circle(RawCircle),
}
impl HittableDef {
pub(crate) fn into_arc(self, materials: &Vec<Arc<dyn Material>>) -> Option<Arc<dyn Hittable>> {
match self {
HittableDef::Sphere(s) => {
let material = s.material.into_arc(materials);
match material {
Some(m) => Some(Arc::new(Sphere::new(s.center, s.radius, m))),
None => None,
}
}
HittableDef::Triangle(t) => {
let material = t.material.into_arc(materials);
match material {
Some(m) => Some(Arc::new(Triangle::new(t.p1, t.p2, t.p3, m))),
None => None,
}
}
HittableDef::Quad(q) => {
let material = q.material.into_arc(materials);
match material {
Some(m) => Some(Arc::new(Quad::new(q.p1, q.p2, q.p3, q.p4, m))),
None => None,
}
}
HittableDef::Cube(c) => {
let material = c.material.into_arc(materials);
match material {
Some(m) => Some(Arc::new(Cube::new(
c.p1, c.p2, c.p3, c.p4, c.p5, c.p6, c.p7, c.p8, m,
))),
None => None,
}
}
HittableDef::Circle(c) => {
let material = c.material.into_arc(materials);
match material {
Some(m) => Some(Arc::new(Circle::new(c.radius, c.center, c.normal, m))),
None => None,
}
}
}
}
}
#[derive(Deserialize)]
pub(crate) struct RawSphere {
pub center: Vec3,
pub radius: f32,
pub material: RawMaterial,
}
#[derive(Deserialize)]
pub(crate) struct RawTriangle {
pub p1: Vec3,
pub p2: Vec3,
pub p3: Vec3,
pub material: RawMaterial,
}
#[derive(Deserialize)]
pub(crate) struct RawQuad {
pub p1: Vec3,
pub p2: Vec3,
pub p3: Vec3,
pub p4: Vec3,
pub material: RawMaterial,
}
#[derive(Deserialize)]
pub(crate) struct RawCube {
pub p1: Vec3,
pub p2: Vec3,
pub p3: Vec3,
pub p4: Vec3,
pub p5: Vec3,
pub p6: Vec3,
pub p7: Vec3,
pub p8: Vec3,
pub material: RawMaterial,
}
#[derive(Deserialize)]
pub(crate) struct RawCircle {
radius: f32,
center: Vec3,
normal: Vec3,
material: RawMaterial,
}
#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum RawMaterial {
Ref(u32),
Direct(MaterialDef),
}
impl RawMaterial {
pub fn into_arc(self, materials: &Vec<Arc<dyn Material>>) -> Option<Arc<dyn Material>> {
match self {
RawMaterial::Ref(r) => {
if r as usize >= materials.len() {
warn!(
"Hittable specified nonexistent material index {}; skipping...",
r
);
return None;
}
Some(materials.get(r as usize).unwrap().clone())
}
RawMaterial::Direct(m) => Some(m.into_arc()),
}
}
}

View File

@@ -0,0 +1,34 @@
use std::sync::Arc;
use serde::Deserialize;
use crate::objects::materials::{dielectric::Dielectric, lambertian::{Lambertian, Metal}, normal::Normal, texture::Texture, traits::Material};
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub(crate) enum MaterialDef {
Lambertian(Lambertian),
Metal(Metal),
Dielectric(Dielectric),
Normal(Normal),
Texture(RawTexture),
}
impl MaterialDef {
pub(crate) fn into_arc(self) -> Arc<dyn Material> {
match self {
MaterialDef::Lambertian(l) => Arc::new(l),
MaterialDef::Metal(m) => Arc::new(m),
MaterialDef::Dielectric(d) => Arc::new(d),
MaterialDef::Normal(n) => Arc::new(n),
MaterialDef::Texture(t) => Arc::new(Texture::new(&t.source)),
}
}
}
#[derive(Deserialize)]
pub(crate) struct RawTexture {
pub source: String,
}

View File

@@ -4,15 +4,9 @@ use crate::{camera::Camera, vec3::Vec3};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct RawCamera { pub struct RawCamera {
// output
image_width: u32,
image_height: u32,
// raytracing // raytracing
#[serde(default)] #[serde(default)]
anti_alias_rate: u32, anti_alias_rate: u32,
#[serde(default)]
max_depth: u32,
// camera // camera
#[serde(default)] #[serde(default)]
@@ -32,10 +26,7 @@ pub struct RawCamera {
impl Default for RawCamera { impl Default for RawCamera {
fn default() -> Self { fn default() -> Self {
Self { Self {
image_width: 400,
image_height: 300,
anti_alias_rate: 1, anti_alias_rate: 1,
max_depth: 10,
fov: 70., fov: 70.,
look_from: Vec3::new(0., 0., 0.), look_from: Vec3::new(0., 0., 0.),
look_at: Vec3::new(0., 0., -1.), look_at: Vec3::new(0., 0., -1.),
@@ -49,10 +40,7 @@ impl Default for RawCamera {
impl From<RawCamera> for Camera { impl From<RawCamera> for Camera {
fn from(raw: RawCamera) -> Self { fn from(raw: RawCamera) -> Self {
Camera::new_full( Camera::new_full(
raw.image_width,
raw.image_height,
raw.anti_alias_rate, raw.anti_alias_rate,
raw.max_depth,
raw.fov, raw.fov,
raw.look_from, raw.look_from,
raw.look_at, raw.look_at,

View File

@@ -1,47 +1,177 @@
use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
use log::warn; use rand::RngExt;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
camera::Camera, camera::Camera,
objects::{ objects::{
cube::Cube,
materials::{ materials::{
dielectric::Dielectric, dielectric::Dielectric,
lambertian::{Lambertian, Metal}, lambertian::{Lambertian, Metal},
normal::Normal,
traits::Material, traits::Material,
}, },
quad::Quad,
sphere::Sphere, sphere::Sphere,
traits::Hittable, traits::Hittable,
triangle::Triangle,
}, },
scenes::raw_camera::RawCamera, scenes::{hittable_def::HittableDef, material_def::MaterialDef, raw_camera::RawCamera},
vec3::Vec3, vec3::Vec3,
}; };
#[derive(Debug)]
pub struct Scene { pub struct Scene {
pub camera: Camera, camera: Camera,
pub materials: Vec<Arc<dyn Material>>, objects: Vec<Arc<dyn Hittable>>,
pub objects: Vec<Arc<dyn Hittable>>,
}
impl Debug for Scene { // image
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { filename: String,
f.debug_struct("Scene") image_width: u32,
.field("camera", &self.camera) image_height: u32,
.field("materials", &self.materials)
.field("objects", &self.objects) // raytracing // TODO: think about organisation of these vars, also in Camera
.finish() max_depth: u32,
}
} }
impl Scene { impl Scene {
pub fn render(&mut self) { pub fn new(
self.camera.render(&self.objects); camera: Camera,
objects: Vec<Arc<dyn Hittable>>,
filename: String,
image_width: u32,
image_height: u32,
max_depth: u32,
) -> Self {
Self {
camera,
objects,
filename,
image_width,
image_height,
max_depth,
}
}
pub fn random() -> Self {
let ground = Lambertian::rgb(-2.5, 0.5, 0.5, 1.);
let mut world: Vec<Arc<dyn Hittable>> = vec![Arc::new(Sphere::xyz(
0.,
-1000.,
0.,
1000.,
Arc::new(ground),
))];
let mut rng = rand::rng();
let point = Vec3::new(4., 0.2, 0.);
for a in -11..11 {
for b in -11..11 {
let mat = rng.random_range((0.)..1.);
let center = Vec3::new(
a as f32 + 0.9 * rng.random_range((0.)..1.),
0.2,
b as f32 + 0.9 * rng.random_range((0.)..1.),
);
if (center - point).length() > 0.9 {
if mat < 0.8 {
// diffuse
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Lambertian::new(Vec3::random() * Vec3::random(), 1.)),
)));
} else if mat < 0.95 {
// metal
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Metal::rgb(
rng.random_range(0.5..1.),
rng.random_range(0.5..1.),
rng.random_range(0.5..1.),
1.,
rng.random_range((0.)..0.5),
)),
)));
} else {
// glass
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Dielectric::new(1.5)),
)));
}
}
}
}
world.push(Arc::new(Sphere::xyz(
0.,
1.,
0.,
1.,
Arc::new(Dielectric::new(1.5)),
)));
world.push(Arc::new(Sphere::xyz(
-4.,
1.,
0.,
1.,
Arc::new(Lambertian::rgb(0.4, 0.2, 0.1, 1.)),
)));
world.push(Arc::new(Sphere::xyz(
4.,
1.,
0.,
1.,
Arc::new(Metal::rgb(0.7, 0.6, 0.5, 1., 0.)),
)));
let mut c = Camera::new();
c.set_fov(20.);
c.set_anti_alias_rate(2);
c.set_vup(Vec3::new(0., 1., 0.));
c.set_look_from(Vec3::new(13., 2., 3.));
c.set_look_at(Vec3::new(0., 0., 0.));
c.add_defocus_blur(0.6, 10.);
Self {
camera: c,
objects: world,
filename: "random.png".to_string(),
image_width: 1920,
image_height: 1080,
max_depth: 50,
}
}
pub fn get_camera(&self) -> &Camera {
&self.camera
}
pub fn get_image_width(&self) -> u32 {
self.image_width
}
pub fn get_image_height(&self) -> u32 {
self.image_height
}
pub fn get_objects(&self) -> &Vec<Arc<dyn Hittable>> {
&self.objects
}
pub fn get_max_depth(&self) -> u32 {
self.max_depth
}
pub fn get_filename(&self) -> &String {
&self.filename
}
pub fn init(&mut self) {
self.camera
.init(self.get_image_width(), self.get_image_height());
} }
} }
@@ -50,6 +180,10 @@ struct SceneDef {
pub camera: RawCamera, pub camera: RawCamera,
pub materials: Vec<MaterialDef>, pub materials: Vec<MaterialDef>,
pub objects: Vec<HittableDef>, pub objects: Vec<HittableDef>,
pub filename: String,
pub image_width: u32,
pub image_height: u32,
pub max_depth: u32,
} }
impl<'de> Deserialize<'de> for Scene { impl<'de> Deserialize<'de> for Scene {
@@ -70,138 +204,11 @@ impl<'de> Deserialize<'de> for Scene {
.collect(); .collect();
Ok(Self { Ok(Self {
camera: Camera::from(conc.camera), camera: Camera::from(conc.camera),
materials: mats,
objects: objs, objects: objs,
filename: conc.filename,
image_width: conc.image_width,
image_height: conc.image_height,
max_depth: conc.max_depth,
}) })
} }
} }
#[derive(Deserialize)]
#[serde(untagged)]
enum RawMaterial {
Ref(u32),
Direct(MaterialDef),
}
impl RawMaterial {
pub fn into_arc(self, materials: &Vec<Arc<dyn Material>>) -> Option<Arc<dyn Material>> {
match self {
RawMaterial::Ref(r) => {
if r as usize >= materials.len() {
warn!("Sphere specified nonexistent material {}; skipping...", r);
return None;
}
Some(materials.get(r as usize).unwrap().clone())
}
RawMaterial::Direct(m) => Some(m.into_arc()),
}
}
}
#[derive(Deserialize)]
struct RawSphere {
pub center: Vec3,
pub radius: f32,
pub material: RawMaterial,
}
#[derive(Deserialize)]
struct RawTriangle {
pub p1: Vec3,
pub p2: Vec3,
pub p3: Vec3,
pub material: RawMaterial,
}
#[derive(Deserialize)]
struct RawQuad {
pub p1: Vec3,
pub p2: Vec3,
pub p3: Vec3,
pub p4: Vec3,
pub material: RawMaterial,
}
#[derive(Deserialize)]
struct RawCube {
pub p1: Vec3,
pub p2: Vec3,
pub p3: Vec3,
pub p4: Vec3,
pub p5: Vec3,
pub p6: Vec3,
pub p7: Vec3,
pub p8: Vec3,
pub material: RawMaterial,
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
enum HittableDef {
Sphere(RawSphere),
Triangle(RawTriangle),
Quad(RawQuad),
Cube(RawCube),
}
impl HittableDef {
fn into_arc(self, materials: &Vec<Arc<dyn Material>>) -> Option<Arc<dyn Hittable>> {
// THOUGHT: i think this can be done better; in the map/filter call up there?
match self {
HittableDef::Sphere(s) => {
let material = s.material.into_arc(materials);
if let Some(m) = material {
Some(Arc::new(Sphere::new(s.center, s.radius, m)))
} else {
None
}
}
HittableDef::Triangle(t) => {
let material = t.material.into_arc(materials);
if let Some(m) = material {
Some(Arc::new(Triangle::new(t.p1, t.p2, t.p3, m)))
} else {
None
}
}
HittableDef::Quad(q) => {
let material = q.material.into_arc(materials);
if let Some(m) = material {
Some(Arc::new(Quad::new(q.p1, q.p2, q.p3, q.p4, m)))
} else {
None
}
}
HittableDef::Cube(c) => {
let material = c.material.into_arc(materials);
if let Some(m) = material {
Some(Arc::new(Cube::new(
c.p1, c.p2, c.p3, c.p4, c.p5, c.p6, c.p7, c.p8, m,
)))
} else {
None
}
}
}
}
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
enum MaterialDef {
Lambertian(Lambertian),
Metal(Metal),
Dielectric(Dielectric),
Normal(Normal),
}
impl MaterialDef {
fn into_arc(self) -> Arc<dyn Material> {
match self {
MaterialDef::Lambertian(l) => Arc::new(l),
MaterialDef::Metal(m) => Arc::new(m),
MaterialDef::Dielectric(d) => Arc::new(d),
MaterialDef::Normal(n) => Arc::new(n),
}
}
}

View File

@@ -29,6 +29,14 @@ impl Vec3 {
} }
} }
pub fn from_u8(r: u8, b: u8, g: u8) -> Self {
Self {
x: r as f32 / 255.,
y: g as f32 / 255.,
z: b as f32 / 255.,
}
}
pub fn random() -> Self { pub fn random() -> Self {
let mut rng = rand::rng(); let mut rng = rand::rng();
Self { Self {
@@ -122,7 +130,7 @@ impl Vec3 {
r_out_perp + r_out_parr r_out_perp + r_out_parr
} }
pub fn output(self) -> image::Rgb<u8> { pub fn output(self) -> (u8, u8, u8) {
// gamma correction // gamma correction
let r = if self.x > 0. { let r = if self.x > 0. {
sqrt(self.x).clamp(0., 1.) sqrt(self.x).clamp(0., 1.)
@@ -144,7 +152,7 @@ impl Vec3 {
let ig = (255.599 * g) as u8; let ig = (255.599 * g) as u8;
let ib = (255.599 * b) as u8; let ib = (255.599 * b) as u8;
image::Rgb([ir, ig, ib]) (ir, ig, ib)
} }
pub fn clone(&self) -> Self { pub fn clone(&self) -> Self {
@@ -436,16 +444,20 @@ impl Div<Vec3> for Vec3 {
} }
} }
} }
impl Div<u32> for Vec3 {
type Output = Self;
fn div(self, u: u32) -> Self::Output {
self / u as f32
}
}
impl Div<i32> for Vec3 { impl Div<i32> for Vec3 {
type Output = Self; type Output = Self;
fn div(self, i: i32) -> Self::Output { fn div(self, i: i32) -> Self::Output {
let f: f32 = i as f32; self / i as f32
Self {
x: self.x / f,
y: self.y / f,
z: self.z / f,
}
} }
} }

BIN
textures/bluegrid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

BIN
textures/earthmap1k.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

BIN
textures/red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

BIN
textures/yellow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B