Compare commits
23 Commits
2358f8e093
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8332ce6204 | |||
| 64cad7eda6 | |||
| 71985a6c34 | |||
| b4b214ebf7 | |||
| d8ad450553 | |||
| d4f4b6a715 | |||
| 194328d92f | |||
| 6a1e50fb7a | |||
| 383f739808 | |||
| 549707fbb3 | |||
| 10f9c0984d | |||
| eb90c36ae8 | |||
| a44e61c1f7 | |||
| 17ad6e30db | |||
| bf980a28ec | |||
| ae73e626b9 | |||
| c3d37f4758 | |||
| e7018a84ed | |||
| f1ac226dbb | |||
| 27bdce5882 | |||
| 388fbcbb8a | |||
| e88422cb2f | |||
| 8f244fc6d8 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/target
|
||||
output.png
|
||||
*.png
|
||||
.env
|
||||
!textures/*.png
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1240,6 +1240,7 @@ dependencies = [
|
||||
"ops",
|
||||
"pretty_env_logger",
|
||||
"rand 0.10.1",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -5,11 +5,12 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
dotenv = "0.15.0"
|
||||
image = "0.25.10"
|
||||
image = {version = "0.25.10", features = ["png"]}
|
||||
is_close = "0.1.3"
|
||||
log = "0.4.29"
|
||||
ops = "0.6.0"
|
||||
pretty_env_logger = "0.5.0"
|
||||
rand = "0.10.1"
|
||||
rayon = "1.12.0"
|
||||
serde = {version = "1.0.228", features = ["derive"]}
|
||||
serde_json = "1.0.149"
|
||||
|
||||
38
scenes/bench.json
Normal file
38
scenes/bench.json
Normal 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}}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
37
scenes/highDef.json
Normal file
37
scenes/highDef.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"filename": "highDef.png",
|
||||
"image_width": 1920,
|
||||
"image_height": 1080,
|
||||
"max_depth": 50,
|
||||
"camera": {
|
||||
"anti_alias_rate": 23,
|
||||
"fov": 20.0,
|
||||
"look_from": [-10, 5, 10],
|
||||
"look_at": [0.0, 0.0, -1.0],
|
||||
"vup": [0.0, 1.0, 0.0],
|
||||
"defocus_angle": 0,
|
||||
"focus_dist": 15.68
|
||||
},
|
||||
"materials": [
|
||||
{ "type": "metal", "albedo": [0.2, 0.4, 0.8 ], "prob": 1.0, "fuzz": 0.1},
|
||||
{ "type": "metal", "albedo": [0.7, 0.4, 0.2 ], "prob": 1.0, "fuzz": 0.1},
|
||||
{ "type": "lambertian", "albedo": [0.8, 0.8, 0.0 ], "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": 0.67},
|
||||
{ "type": "metal", "albedo": [0.8, 0.6, 0.2 ], "prob": 1.0, "fuzz": 1.0}
|
||||
|
||||
],
|
||||
"objects": [
|
||||
{ "type": "sphere", "center": [0, 0.7, -0.4], "radius": 0.2, "material": 0},
|
||||
{ "type": "sphere", "center": [0.0, 0.5, -0.8], "radius": 0.1, "material": 1},
|
||||
{ "type": "sphere", "center": [0.0, -100.5, -1.0], "radius": 100.0, "material": 2},
|
||||
{ "type": "sphere", "center": [0.0, 0.0, -1.2], "radius": 0.5, "material": 3},
|
||||
{ "type": "sphere", "center": [-1, 0, -1], "radius": 0.4, "material": 5},
|
||||
{ "type": "sphere", "center": [1, 0, -1], "radius": 0.5, "material": 6},
|
||||
{ "type": "sphere", "center": [20, 7, -15], "radius": 10.5, "material": 0}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
{
|
||||
"camera": {
|
||||
"filename": "output.png",
|
||||
"image_width": 1920,
|
||||
"image_height": 1080,
|
||||
"max_depth": 50,
|
||||
"camera": {
|
||||
"anti_alias_rate": 23,
|
||||
"max_depth": 100,
|
||||
"fov": 20.0,
|
||||
"look_from": { "x": -10, "y": 5, "z": 10 },
|
||||
"look_at": { "x": 0.0, "y": 0.0, "z": -1.0 },
|
||||
"vup": { "x": 0.0, "y": 1.0, "z": 0.0 },
|
||||
"defocus_blur": false,
|
||||
"defocus_angle": 2,
|
||||
"fov": 70.0,
|
||||
"look_from": [-10, 4, 15],
|
||||
"look_at": [-11.0, 0.0, 0.0],
|
||||
"vup": [0.0, 1.0, 0.0],
|
||||
"defocus_angle": 0,
|
||||
"focus_dist": 15.68
|
||||
},
|
||||
"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": "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": "dielectric", "refraction_index": 0.67},
|
||||
{ "type": "metal", "albedo": { "x": 0.8, "y": 0.6, "z": 0.2 }, "prob": 1.0, "fuzz": 1.0 }
|
||||
|
||||
{ "type": "normal"}
|
||||
],
|
||||
"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}
|
||||
{ "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}}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
35
scenes/texture.json
Normal file
35
scenes/texture.json
Normal 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}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
127
src/camera.rs
127
src/camera.rs
@@ -1,22 +1,11 @@
|
||||
use std::{f32::consts::PI, sync::Arc};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
objects::{hit::Hit, traits::Hittable},
|
||||
ray::Ray,
|
||||
vec3::{Colour, Vec3},
|
||||
};
|
||||
use crate::{ray::Ray, vec3::Vec3};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Camera {
|
||||
// output
|
||||
image_width: u32,
|
||||
image_height: u32,
|
||||
|
||||
// raytracing
|
||||
anti_alias_rate: u32,
|
||||
max_depth: u32,
|
||||
pixel00_loc: Vec3,
|
||||
pixel_delta_u: Vec3,
|
||||
pixel_delta_v: Vec3,
|
||||
@@ -44,12 +33,9 @@ fn deg_to_rad(deg: f32) -> f32 {
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
pub fn new(image_width: u32, image_height: u32) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
image_width,
|
||||
image_height,
|
||||
anti_alias_rate: 1,
|
||||
max_depth: 10,
|
||||
dirty: true,
|
||||
fov: 60.,
|
||||
pixel00_loc: Vec3::default(),
|
||||
@@ -69,10 +55,7 @@ impl Camera {
|
||||
}
|
||||
|
||||
pub fn new_full(
|
||||
image_width: u32,
|
||||
image_height: u32,
|
||||
anti_alias_rate: u32,
|
||||
max_depth: u32,
|
||||
fov: f32,
|
||||
look_from: Vec3,
|
||||
look_at: Vec3,
|
||||
@@ -81,10 +64,7 @@ impl Camera {
|
||||
focus_dist: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
image_width,
|
||||
image_height,
|
||||
anti_alias_rate,
|
||||
max_depth,
|
||||
pixel00_loc: Vec3::default(),
|
||||
pixel_delta_u: 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
|
||||
let theta = deg_to_rad(self.fov);
|
||||
let h = (theta / 2.).tan();
|
||||
@@ -113,13 +97,13 @@ impl Camera {
|
||||
|
||||
// viewport
|
||||
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_v = viewport_height * -self.v;
|
||||
|
||||
// variables
|
||||
self.pixel_delta_u = viewport_u / self.image_width as f32;
|
||||
self.pixel_delta_v = viewport_v / self.image_height as f32;
|
||||
self.pixel_delta_u = viewport_u / width;
|
||||
self.pixel_delta_v = viewport_v / height;
|
||||
self.pixel00_loc =
|
||||
self.look_from - (self.focus_dist * self.w) - viewport_u / 2. - viewport_v / 2.;
|
||||
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) {
|
||||
if 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) {
|
||||
if self.anti_alias_rate != rate {
|
||||
self.anti_alias_rate = rate;
|
||||
@@ -181,65 +179,8 @@ impl Camera {
|
||||
}
|
||||
}
|
||||
|
||||
fn defocus_disk_sample(&self) -> Vec3 {
|
||||
pub fn defocus_disk_sample(&self) -> Vec3 {
|
||||
let p = Vec3::random_in_unit_disk();
|
||||
return self.look_from + (p.x() * self.defocus_disk_u) + (p.y() * self.defocus_disk_v);
|
||||
}
|
||||
|
||||
pub fn render(&mut self, hittables: &Vec<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) {
|
||||
return att * self.ray_colour(hittables, &scattered, depth - 1);
|
||||
}
|
||||
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)
|
||||
self.look_from + (p.x() * self.defocus_disk_u) + (p.y() * self.defocus_disk_v)
|
||||
}
|
||||
}
|
||||
|
||||
102
src/main.rs
102
src/main.rs
@@ -3,110 +3,32 @@
|
||||
mod camera;
|
||||
mod objects;
|
||||
mod ray;
|
||||
mod raytracer;
|
||||
mod scenes;
|
||||
mod vec3;
|
||||
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use std::{env, fs};
|
||||
|
||||
use crate::objects::materials::dielectric::Dielectric;
|
||||
use crate::objects::materials::lambertian::{Lambertian, Metal};
|
||||
use crate::objects::materials::traits::Material;
|
||||
use crate::objects::sphere::Sphere;
|
||||
use crate::objects::traits::Hittable;
|
||||
use crate::ray::Ray;
|
||||
use crate::raytracer::render;
|
||||
use crate::scenes::scene::Scene;
|
||||
use crate::vec3::Vec3;
|
||||
use dotenv::dotenv;
|
||||
use log::info;
|
||||
use pretty_env_logger;
|
||||
use rand::RngExt;
|
||||
use rand::seq::IndexedRandom;
|
||||
|
||||
fn random_material() -> Arc<dyn Material> {
|
||||
let mut rng = rand::rng();
|
||||
let col = Vec3::new(rng.random(), rng.random(), rng.random());
|
||||
match rng.random_range(0..3) {
|
||||
0 => Arc::new(Lambertian::new(col, 1.)),
|
||||
1 => Arc::new(Metal::new(col, 1., rng.random())),
|
||||
2 => Arc::new(Dielectric::new(rng.random_range(0.3..1.9))),
|
||||
_ => Arc::new(Metal::new(col, 0., rng.random())),
|
||||
}
|
||||
}
|
||||
|
||||
fn random_sphere_on_floor<T: Hittable>(
|
||||
materials: &Vec<Arc<dyn Material>>,
|
||||
existing: &Vec<T>,
|
||||
max_size: f32,
|
||||
) -> Sphere {
|
||||
let mut rng = rand::rng();
|
||||
let r = rng.random_range(0.1..max_size);
|
||||
let mut sphere = Sphere::xyz(
|
||||
rng.random_range((-50.)..50.),
|
||||
rng.random_range(0.1..max_size),
|
||||
rng.random_range((-50.)..50.),
|
||||
r,
|
||||
materials.choose(&mut rng).unwrap().clone(),
|
||||
);
|
||||
|
||||
let mut isct = true;
|
||||
let mut attempts_left = 49;
|
||||
while isct && attempts_left > 0 {
|
||||
isct = false;
|
||||
for h in existing {
|
||||
if h.inside(&h.closest_on_surface(sphere.center())) {
|
||||
info!("Generating sphere failed, {} attempts left", attempts_left);
|
||||
isct = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if isct {
|
||||
sphere.set_center(Vec3::new(
|
||||
rng.random_range(0.5..100.),
|
||||
r,
|
||||
rng.random_range(0.5..100.),
|
||||
));
|
||||
}
|
||||
attempts_left -= 1;
|
||||
}
|
||||
sphere
|
||||
}
|
||||
|
||||
// TODO: implement scene serialization
|
||||
fn main() {
|
||||
dotenv().ok();
|
||||
pretty_env_logger::init();
|
||||
|
||||
// TODO: use cli arg for scenefile
|
||||
let json_file = "./scenes/scene.json";
|
||||
// let json_file = "./scenes/failsMatBounds.json";
|
||||
// TODO: better cli parsing || add random flag to generate random scene
|
||||
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 mut scene: Scene = serde_json::from_str(&json_str).unwrap();
|
||||
scene.render();
|
||||
|
||||
// random spheres code; thought: make this available as cli flag?
|
||||
// let mut materials: Vec<Arc<dyn Material>> = vec![Arc::new(Lambertian::rgb(0.1, 0.1, 0.2, 0.8))];
|
||||
// for i in 0..15 {
|
||||
// info!("Generating {}th material.", i + 1);
|
||||
// materials.push(random_material());
|
||||
// }
|
||||
//
|
||||
// let mut world = vec![Sphere::xyz(0., -1000.5, -1., 1000., materials[0].clone())];
|
||||
// for i in 0..40 {
|
||||
// info!("Generating {}th sphere.", i + 1);
|
||||
// world.push(random_sphere_on_floor(
|
||||
// &materials,
|
||||
// &world,
|
||||
// ((i + 1) as f32).ln() + (15. as f32).log(10.),
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// // let mut c = Camera::new(400, 300);
|
||||
// let mut c = Camera::new(1920, 1080);
|
||||
// c.set_fov(60.);
|
||||
// c.set_anti_alias_rate(1);
|
||||
// c.set_max_depth(10);
|
||||
// c.set_look_from(Vec3::new(-60., 10., 1.));
|
||||
// c.set_look_at(Vec3::new(0., 0., 0.));
|
||||
// c.render(&world);
|
||||
let mut scene: Scene = Scene::random();
|
||||
render(&mut scene);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
pub mod circle;
|
||||
pub mod cube;
|
||||
pub mod cylinder;
|
||||
pub mod hit;
|
||||
pub mod materials;
|
||||
pub mod mesh;
|
||||
pub mod quad;
|
||||
pub mod sphere;
|
||||
pub mod traits;
|
||||
pub mod triangle;
|
||||
|
||||
71
src/objects/circle.rs
Normal file
71
src/objects/circle.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
49
src/objects/cube.rs
Normal file
49
src/objects/cube.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
objects::{materials::traits::Material, quad::Quad, traits::Hittable},
|
||||
ray::Ray,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
use super::hit::Hit;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Cube {
|
||||
faces: Vec<Arc<dyn Hittable>>,
|
||||
}
|
||||
|
||||
impl Cube {
|
||||
pub fn new(
|
||||
p1: Vec3,
|
||||
p2: Vec3,
|
||||
p3: Vec3,
|
||||
p4: Vec3,
|
||||
p5: Vec3,
|
||||
p6: Vec3,
|
||||
p7: Vec3,
|
||||
p8: Vec3,
|
||||
material: Arc<dyn Material>,
|
||||
) -> Self {
|
||||
let faces: Vec<Arc<dyn Hittable>> = vec![
|
||||
Arc::new(Quad::new(p1, p2, p3, p4, material.clone())),
|
||||
Arc::new(Quad::new(p1, p5, p6, p2, material.clone())),
|
||||
Arc::new(Quad::new(p2, p6, p7, p3, material.clone())),
|
||||
Arc::new(Quad::new(p5, p8, p4, p1, material.clone())),
|
||||
Arc::new(Quad::new(p5, p6, p7, p8, material.clone())),
|
||||
Arc::new(Quad::new(p4, p3, p7, p8, material.clone())),
|
||||
];
|
||||
Self { faces }
|
||||
}
|
||||
}
|
||||
|
||||
impl Hittable for Cube {
|
||||
fn hit(&self, r: &Ray) -> Option<Hit> {
|
||||
Hit::hit_list(&self.faces, r)
|
||||
}
|
||||
|
||||
fn to_uv(&self, _point: &Vec3) -> Vec3 {
|
||||
// TODO: map to [0.1] relative to specific face
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,47 @@ pub struct Hit {
|
||||
n: Vec3,
|
||||
mat: Arc<dyn Material>,
|
||||
front_face: bool,
|
||||
u: f32,
|
||||
v: f32,
|
||||
}
|
||||
|
||||
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 {
|
||||
t: t,
|
||||
p: p,
|
||||
t,
|
||||
p,
|
||||
n: if front_face { n } else { -n },
|
||||
mat: mat,
|
||||
front_face: front_face,
|
||||
mat,
|
||||
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,11 @@ impl Hit {
|
||||
self.front_face
|
||||
}
|
||||
|
||||
pub fn uv(&self) -> (f32, f32) {
|
||||
(self.u, self.v)
|
||||
}
|
||||
|
||||
// TODO: use front_face to discard back-hits for culling
|
||||
pub fn hit_list(hittables: &Vec<Arc<dyn Hittable>>, r: &Ray) -> Option<Hit> {
|
||||
let mut closest: Option<Hit> = None;
|
||||
for hittable in hittables {
|
||||
@@ -57,6 +93,25 @@ impl Hit {
|
||||
}
|
||||
}
|
||||
}
|
||||
return 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod dielectric;
|
||||
pub mod lambertian;
|
||||
pub mod normal;
|
||||
pub mod traits;
|
||||
pub mod texture;
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde::Deserialize;
|
||||
use crate::{
|
||||
objects::{hit::Hit, materials::traits::Material},
|
||||
ray::Ray,
|
||||
vec3::{Colour, Vec3},
|
||||
vec3::Colour,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -16,21 +16,19 @@ pub struct Dielectric {
|
||||
|
||||
impl Dielectric {
|
||||
pub fn new(refraction_index: f32) -> Self {
|
||||
Self {
|
||||
refraction_index: refraction_index,
|
||||
}
|
||||
Self { refraction_index }
|
||||
}
|
||||
|
||||
fn _reflectance(cos: f32, refraction_index: f32) -> f32 {
|
||||
fn reflectance(cos: f32, refraction_index: f32) -> f32 {
|
||||
// Shlick's approximation
|
||||
let mut r0 = (1. - refraction_index) / (1. + refraction_index);
|
||||
r0 *= r0;
|
||||
return r0 + (1. - r0) * (1. - cos).powf(5.);
|
||||
r0 + (1. - r0) * (1. - cos).powf(5.)
|
||||
}
|
||||
}
|
||||
|
||||
impl Material for Dielectric {
|
||||
fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Ray, Colour)> {
|
||||
fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Option<Ray>, Colour)> {
|
||||
let ri = if hit.front_face() {
|
||||
1. / self.refraction_index
|
||||
} else {
|
||||
@@ -45,13 +43,13 @@ impl Material for Dielectric {
|
||||
let sin_theta = sqrt(1. - cos_theta * cos_theta);
|
||||
let cannot_refract = ri * sin_theta > 1.;
|
||||
|
||||
let dir: Vec3;
|
||||
let mut rng = rand::rng();
|
||||
if cannot_refract || Dielectric::_reflectance(cos_theta, ri) > rng.random::<f32>() {
|
||||
dir = unit.reflect(hit.n());
|
||||
let dir = if cannot_refract || Dielectric::reflectance(cos_theta, ri) > rng.random::<f32>()
|
||||
{
|
||||
unit.reflect(hit.n())
|
||||
} else {
|
||||
dir = unit.refract(hit.n(), ri);
|
||||
}
|
||||
Some((Ray::new(*hit.p(), dir), Colour::new(1., 1., 1.)))
|
||||
unit.refract(hit.n(), ri)
|
||||
};
|
||||
Some((Some(Ray::new(*hit.p(), dir)), Colour::new(1., 1., 1.)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,22 +15,19 @@ pub struct Lambertian {
|
||||
|
||||
impl Lambertian {
|
||||
pub fn new(albedo: Colour, prob: f32) -> Self {
|
||||
Self {
|
||||
albedo: albedo,
|
||||
prob: prob,
|
||||
}
|
||||
Self { albedo, prob }
|
||||
}
|
||||
|
||||
pub fn rgb(r: f32, g: f32, b: f32, prob: f32) -> Self {
|
||||
Self {
|
||||
albedo: Colour::new(r, g, b),
|
||||
prob: prob,
|
||||
prob,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Material for Lambertian {
|
||||
fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Ray, Colour)> {
|
||||
fn scatter(&self, hit: &Hit, _ray: &Ray) -> Option<(Option<Ray>, Colour)> {
|
||||
let mut rng = rand::rng();
|
||||
if self.prob >= rng.random::<f32>() {
|
||||
let mut dir = *hit.n() + Vec3::random_unit();
|
||||
@@ -38,9 +35,9 @@ impl Material for Lambertian {
|
||||
dir = *hit.n();
|
||||
}
|
||||
let scattered = Ray::new(*hit.p(), dir);
|
||||
return Some((scattered, self.albedo));
|
||||
return Some((Some(scattered), self.albedo));
|
||||
}
|
||||
return None;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,30 +50,26 @@ pub struct Metal {
|
||||
|
||||
impl Metal {
|
||||
pub fn new(albedo: Colour, prob: f32, fuzz: f32) -> Self {
|
||||
Self {
|
||||
albedo: albedo,
|
||||
prob: prob,
|
||||
fuzz: fuzz,
|
||||
}
|
||||
Self { albedo, prob, fuzz }
|
||||
}
|
||||
|
||||
pub fn rgb(r: f32, g: f32, b: f32, prob: f32, fuzz: f32) -> Self {
|
||||
Self {
|
||||
albedo: Colour::new(r, g, b),
|
||||
prob: prob,
|
||||
fuzz: fuzz,
|
||||
prob,
|
||||
fuzz,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Material for Metal {
|
||||
fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Ray, Colour)> {
|
||||
fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Option<Ray>, Colour)> {
|
||||
let mut rng = rand::rng();
|
||||
if self.prob >= rng.random::<f32>() {
|
||||
let mut refl = ray.dir().reflect(hit.n());
|
||||
refl = refl.get_unit() + self.fuzz * Vec3::random_unit();
|
||||
return Some((Ray::new(*hit.p(), refl), self.albedo));
|
||||
return Some((Some(Ray::new(*hit.p(), refl)), self.albedo));
|
||||
}
|
||||
return None;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
16
src/objects/materials/normal.rs
Normal file
16
src/objects/materials/normal.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
objects::{hit::Hit, materials::traits::Material},
|
||||
ray::Ray,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Normal {}
|
||||
|
||||
impl Material for Normal {
|
||||
fn scatter(&self, hit: &Hit, _ray: &Ray) -> Option<(Option<Ray>, Vec3)> {
|
||||
Some((None, (hit.n() + 1.) / 2.))
|
||||
}
|
||||
}
|
||||
80
src/objects/materials/texture.rs
Normal file
80
src/objects/materials/texture.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{objects::hit::Hit, ray::Ray, vec3::Colour};
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub trait Material: Debug {
|
||||
fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Ray, Colour)>;
|
||||
pub trait Material: Debug + Send + Sync{
|
||||
fn scatter(&self, hit: &Hit, ray: &Ray) -> Option<(Option<Ray>, Colour)>;
|
||||
}
|
||||
|
||||
0
src/objects/mesh.rs
Normal file
0
src/objects/mesh.rs
Normal file
69
src/objects/quad.rs
Normal file
69
src/objects/quad.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crate::objects::hit::Hit;
|
||||
use crate::objects::traits::Hittable;
|
||||
use crate::objects::triangle::Triangle;
|
||||
use crate::ray::Ray;
|
||||
use crate::{objects::materials::traits::Material, vec3::Vec3};
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Quad {
|
||||
p1: Vec3,
|
||||
p2: Vec3,
|
||||
p3: Vec3,
|
||||
p4: Vec3,
|
||||
material: Arc<dyn Material>,
|
||||
normal: Vec3,
|
||||
|
||||
// helpers
|
||||
t1: Triangle,
|
||||
t2: Triangle,
|
||||
}
|
||||
|
||||
impl Quad {
|
||||
pub fn new(p1: Vec3, p2: Vec3, p3: Vec3, p4: Vec3, material: Arc<dyn Material>) -> Self {
|
||||
Self {
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
p4,
|
||||
material: material.clone(),
|
||||
normal: (p2 - p1).cross(&(p4 - p1)).get_unit(),
|
||||
t1: Triangle::new(p1, p2, p4, material.clone()),
|
||||
t2: Triangle::new(p2, p3, p4, material),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Quad {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Triangle")
|
||||
.field("p1", &self.p1)
|
||||
.field("p2", &self.p2)
|
||||
.field("p3", &self.p3)
|
||||
.field("p4", &self.p4)
|
||||
.field("material", &self.material)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hittable for Quad {
|
||||
fn hit(&self, r: &Ray) -> Option<Hit> {
|
||||
let mh = self.t1.calculate_hit(r).or(self.t2.calculate_hit(r))?;
|
||||
let uvw = self.to_uv(mh.p());
|
||||
|
||||
Some(Hit::from_minimal(
|
||||
mh,
|
||||
self.normal,
|
||||
self.material.clone(),
|
||||
false,
|
||||
*uvw.x(),
|
||||
*uvw.y(),
|
||||
))
|
||||
}
|
||||
|
||||
fn to_uv(&self, point: &Vec3) -> Vec3 {
|
||||
let u = (*point - self.p1).dot(&(self.p2 - self.p1)) / (self.p2 - self.p1).length_squared();
|
||||
let v = (*point - self.p1).dot(&(self.p4 - self.p1)) / (self.p4 - self.p1).length_squared();
|
||||
Vec3::new(u, v, 0.)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
use core::f32::math::sqrt;
|
||||
use std::f32::consts::PI;
|
||||
use std::fmt::{self, Debug};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::Vec3;
|
||||
use crate::objects::hit::Hit;
|
||||
use crate::objects::materials::traits::Material;
|
||||
use crate::objects::traits::Hittable;
|
||||
use crate::ray::Ray;
|
||||
use crate::Vec3;
|
||||
|
||||
pub struct Sphere {
|
||||
center: Vec3,
|
||||
@@ -29,7 +28,7 @@ impl Debug for Sphere {
|
||||
impl Sphere {
|
||||
pub fn new(center: Vec3, r: f32, mat: Arc<dyn Material>) -> Self {
|
||||
Self {
|
||||
center: center,
|
||||
center,
|
||||
radius: r,
|
||||
material: mat,
|
||||
}
|
||||
@@ -42,16 +41,6 @@ impl Sphere {
|
||||
material: mat,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center(&self) -> &Vec3 {
|
||||
&self.center
|
||||
}
|
||||
|
||||
pub fn set_center(&mut self, center: Vec3) {
|
||||
if self.center != center {
|
||||
self.center = center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hittable for Sphere {
|
||||
@@ -70,25 +59,26 @@ impl Hittable for Sphere {
|
||||
let t = if tl > 0.001 { tl } else { tr };
|
||||
let p = r.at(t);
|
||||
let out_n = (p - self.center) / self.radius;
|
||||
let uv = self.to_uv(&p);
|
||||
Some(Hit::new(
|
||||
t,
|
||||
p,
|
||||
self.normal_at(&p),
|
||||
(p - self.center).get_unit(),
|
||||
self.material.clone(),
|
||||
out_n.dot(r.dir()) < 0.,
|
||||
*uv.x(),
|
||||
*uv.y(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn normal_at(&self, p: &Vec3) -> Vec3 {
|
||||
(*p - self.center).get_unit()
|
||||
}
|
||||
|
||||
fn inside(&self, p: &Vec3) -> bool {
|
||||
(*p - self.center).length() < self.radius
|
||||
}
|
||||
|
||||
fn closest_on_surface(&self, p: &Vec3) -> Vec3 {
|
||||
self.normal_at(p) * self.radius
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::objects::hit::Hit;
|
||||
use crate::Ray;
|
||||
use crate::Vec3;
|
||||
use crate::objects::hit::Hit;
|
||||
use crate::vec3::Vec3;
|
||||
|
||||
pub trait Hittable: Debug {
|
||||
pub trait Hittable: Debug + Send + Sync {
|
||||
fn to_uv(&self, point: &Vec3) -> Vec3;
|
||||
fn hit(&self, r: &Ray) -> Option<Hit>;
|
||||
fn normal_at(&self, p: &Vec3) -> Vec3;
|
||||
// fn intersect<H: Hittable>(&self, o: &H) -> bool;
|
||||
fn inside(&self, p: &Vec3) -> bool;
|
||||
fn closest_on_surface(&self, p: &Vec3) -> Vec3;
|
||||
}
|
||||
|
||||
115
src/objects/triangle.rs
Normal file
115
src/objects/triangle.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use crate::objects::hit::{Hit, MinimalHit};
|
||||
use crate::objects::traits::Hittable;
|
||||
use crate::ray::Ray;
|
||||
use crate::{objects::materials::traits::Material, vec3::Vec3};
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Triangle {
|
||||
p1: Vec3,
|
||||
p2: Vec3,
|
||||
p3: Vec3,
|
||||
material: Arc<dyn Material>,
|
||||
normal: Vec3,
|
||||
|
||||
// barycentric helpers
|
||||
v0: Vec3,
|
||||
v1: Vec3,
|
||||
d00: f32,
|
||||
d01: f32,
|
||||
d11: f32,
|
||||
denom: f32,
|
||||
a4: f32,
|
||||
}
|
||||
|
||||
impl Triangle {
|
||||
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 {
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
material,
|
||||
normal: (p2 - p1).cross(&(p3 - p1)).get_unit(),
|
||||
v0,
|
||||
v1,
|
||||
d00,
|
||||
d01,
|
||||
d11,
|
||||
denom: d00 * d11 - d01 * d01,
|
||||
a4,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_hit(&self, r: &Ray) -> Option<MinimalHit> {
|
||||
// check if ray parallel to plane
|
||||
let dot = self.normal.dot(r.dir());
|
||||
if dot == 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let d = (-self.normal).dot(&self.p1);
|
||||
// hitpoint on plane
|
||||
let t = -(self.normal.dot(&(r.origin() + d))) / dot;
|
||||
// hits behind camera
|
||||
if t < 0. {
|
||||
return None;
|
||||
};
|
||||
|
||||
let p = r.at(t);
|
||||
let a3 = (self.p3 - p).cross(&(self.p2 - p)).length();
|
||||
let a2 = (self.p3 - p).cross(&(self.p1 - p)).length();
|
||||
let a1 = (self.p2 - p).cross(&(self.p1 - p)).length();
|
||||
|
||||
let diff = (self.a4 - a1 - a2 - a3).abs();
|
||||
if diff < 0.001 {
|
||||
Some(MinimalHit::new(t, p))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Triangle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Triangle")
|
||||
.field("p1", &self.p1)
|
||||
.field("p2", &self.p2)
|
||||
.field("p3", &self.p3)
|
||||
.field("material", &self.material)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hittable for Triangle {
|
||||
fn hit(&self, r: &Ray) -> Option<Hit> {
|
||||
let mh = self.calculate_hit(r)?;
|
||||
let uvw = self.to_uv(mh.p());
|
||||
|
||||
Some(Hit::from_minimal(
|
||||
mh,
|
||||
self.normal,
|
||||
self.material.clone(),
|
||||
self.normal.dot(r.dir()) < 0.,
|
||||
*uvw.x(),
|
||||
*uvw.y(),
|
||||
))
|
||||
}
|
||||
|
||||
fn to_uv(&self, point: &Vec3) -> Vec3 {
|
||||
let v2 = *point - self.p1;
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,7 @@ pub struct Ray {
|
||||
|
||||
impl Ray {
|
||||
pub fn new(origin: Vec3, dir: Vec3) -> Self {
|
||||
Self {
|
||||
origin: origin,
|
||||
dir: dir,
|
||||
}
|
||||
Self { origin, dir }
|
||||
}
|
||||
|
||||
pub fn at(&self, t: f32) -> Vec3 {
|
||||
|
||||
99
src/raytracer.rs
Normal file
99
src/raytracer.rs
Normal 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)
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
pub mod raw_camera;
|
||||
pub mod material_def;
|
||||
pub mod hittable_def;
|
||||
pub mod scene;
|
||||
|
||||
137
src/scenes/hittable_def.rs
Normal file
137
src/scenes/hittable_def.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/scenes/material_def.rs
Normal file
34
src/scenes/material_def.rs
Normal 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,
|
||||
}
|
||||
|
||||
@@ -4,15 +4,9 @@ use crate::{camera::Camera, vec3::Vec3};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RawCamera {
|
||||
// output
|
||||
image_width: u32,
|
||||
image_height: u32,
|
||||
|
||||
// raytracing
|
||||
#[serde(default)]
|
||||
anti_alias_rate: u32,
|
||||
#[serde(default)]
|
||||
max_depth: u32,
|
||||
|
||||
// camera
|
||||
#[serde(default)]
|
||||
@@ -32,10 +26,7 @@ pub struct RawCamera {
|
||||
impl Default for RawCamera {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
image_width: 400,
|
||||
image_height: 300,
|
||||
anti_alias_rate: 1,
|
||||
max_depth: 10,
|
||||
fov: 70.,
|
||||
look_from: Vec3::new(0., 0., 0.),
|
||||
look_at: Vec3::new(0., 0., -1.),
|
||||
@@ -49,10 +40,7 @@ impl Default for RawCamera {
|
||||
impl From<RawCamera> for Camera {
|
||||
fn from(raw: RawCamera) -> Self {
|
||||
Camera::new_full(
|
||||
raw.image_width,
|
||||
raw.image_height,
|
||||
raw.anti_alias_rate,
|
||||
raw.max_depth,
|
||||
raw.fov,
|
||||
raw.look_from,
|
||||
raw.look_at,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::warn;
|
||||
use rand::RngExt;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
@@ -14,19 +14,164 @@ use crate::{
|
||||
sphere::Sphere,
|
||||
traits::Hittable,
|
||||
},
|
||||
scenes::raw_camera::RawCamera,
|
||||
scenes::{hittable_def::HittableDef, material_def::MaterialDef, raw_camera::RawCamera},
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Scene {
|
||||
pub camera: Camera,
|
||||
pub materials: Vec<Arc<dyn Material>>,
|
||||
pub objects: Vec<Arc<dyn Hittable>>,
|
||||
camera: Camera,
|
||||
objects: Vec<Arc<dyn Hittable>>,
|
||||
|
||||
// image
|
||||
filename: String,
|
||||
image_width: u32,
|
||||
image_height: u32,
|
||||
|
||||
// raytracing // TODO: think about organisation of these vars, also in Camera
|
||||
max_depth: u32,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub fn render(&mut self) {
|
||||
self.camera.render(&self.objects);
|
||||
pub fn new(
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +180,10 @@ struct SceneDef {
|
||||
pub camera: RawCamera,
|
||||
pub materials: Vec<MaterialDef>,
|
||||
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 {
|
||||
@@ -55,61 +204,11 @@ impl<'de> Deserialize<'de> for Scene {
|
||||
.collect();
|
||||
Ok(Self {
|
||||
camera: Camera::from(conc.camera),
|
||||
materials: mats,
|
||||
objects: objs,
|
||||
filename: conc.filename,
|
||||
image_width: conc.image_width,
|
||||
image_height: conc.image_height,
|
||||
max_depth: conc.max_depth,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawSphere {
|
||||
pub center: Vec3,
|
||||
pub radius: f32,
|
||||
pub material: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
enum HittableDef {
|
||||
Sphere(RawSphere),
|
||||
}
|
||||
|
||||
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) => {
|
||||
if s.material as usize >= materials.len() {
|
||||
warn!(
|
||||
"Sphere specified nonexistent material {}; skipping...",
|
||||
s.material
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(Arc::new(Sphere::new(
|
||||
s.center,
|
||||
s.radius,
|
||||
materials.get(s.material as usize).unwrap().clone(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
enum MaterialDef {
|
||||
Lambertian(Lambertian),
|
||||
Metal(Metal),
|
||||
Dielectric(Dielectric),
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
src/vec3.rs
90
src/vec3.rs
@@ -1,13 +1,13 @@
|
||||
use core::f32::math::sqrt;
|
||||
use is_close::default;
|
||||
use rand::RngExt;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign},
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Vec3 {
|
||||
x: f32,
|
||||
y: f32,
|
||||
@@ -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 {
|
||||
let mut rng = rand::rng();
|
||||
Self {
|
||||
@@ -59,11 +67,7 @@ impl Vec3 {
|
||||
|
||||
pub fn random_unit_hemisphere(n: &Self) -> Self {
|
||||
let v = Self::random_unit();
|
||||
if n.dot(&v) > 0.0 {
|
||||
v
|
||||
} else {
|
||||
-v
|
||||
}
|
||||
if n.dot(&v) > 0.0 { v } else { -v }
|
||||
}
|
||||
|
||||
pub fn x(&self) -> &f32 {
|
||||
@@ -83,7 +87,7 @@ impl Vec3 {
|
||||
}
|
||||
|
||||
pub fn length_squared(&self) -> f32 {
|
||||
(self.x * self.x + self.y * self.y + self.z * self.z) as f32
|
||||
self.x * self.x + self.y * self.y + self.z * self.z
|
||||
}
|
||||
|
||||
pub fn dot(&self, other: &Self) -> f32 {
|
||||
@@ -123,10 +127,10 @@ impl Vec3 {
|
||||
let r_out_perp = etai_over_etat * (*self + cos_theta * n);
|
||||
let r_out_parr = -sqrt((1. - r_out_perp.length_squared()).abs()) * n;
|
||||
|
||||
return 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
|
||||
let r = if self.x > 0. {
|
||||
sqrt(self.x).clamp(0., 1.)
|
||||
@@ -148,7 +152,7 @@ impl Vec3 {
|
||||
let ig = (255.599 * g) as u8;
|
||||
let ib = (255.599 * b) as u8;
|
||||
|
||||
image::Rgb([ir, ig, ib])
|
||||
(ir, ig, ib)
|
||||
}
|
||||
|
||||
pub fn clone(&self) -> Self {
|
||||
@@ -160,6 +164,29 @@ impl Vec3 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Vec3 {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
([self.x, self.y, self.z] as [f32; 3]).serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Vec3 {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let arr = <[f32; 3]>::deserialize(deserializer)?;
|
||||
Ok(Self {
|
||||
x: arr[0],
|
||||
y: arr[1],
|
||||
z: arr[2],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Vec3 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -190,6 +217,18 @@ impl Neg for Vec3 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<&Vec3> for &Vec3 {
|
||||
type Output = Vec3;
|
||||
|
||||
fn add(self, rhs: &Vec3) -> Vec3 {
|
||||
Vec3 {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
z: self.z + rhs.z,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Vec3> for Vec3 {
|
||||
type Output = Self;
|
||||
|
||||
@@ -202,6 +241,18 @@ impl Add<Vec3> for Vec3 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<f32> for &Vec3 {
|
||||
type Output = Vec3;
|
||||
|
||||
fn add(self, f: f32) -> Vec3 {
|
||||
Vec3 {
|
||||
x: self.x + f,
|
||||
y: self.y + f,
|
||||
z: self.z + f,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<f32> for Vec3 {
|
||||
type Output = Self;
|
||||
|
||||
@@ -360,7 +411,6 @@ impl Mul<Vec3> for &f32 {
|
||||
z: rhs.z * self,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl MulAssign<Vec3> for Vec3 {
|
||||
@@ -394,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 {
|
||||
type Output = Self;
|
||||
|
||||
fn div(self, i: i32) -> Self::Output {
|
||||
let f: f32 = i as f32;
|
||||
Self {
|
||||
x: self.x / f,
|
||||
y: self.y / f,
|
||||
z: self.z / f,
|
||||
}
|
||||
self / i as f32
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
textures/bluegrid.png
Normal file
BIN
textures/bluegrid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 424 B |
BIN
textures/earthmap1k.png
Normal file
BIN
textures/earthmap1k.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 572 KiB |
BIN
textures/red.png
Normal file
BIN
textures/red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 192 B |
BIN
textures/yellow.png
Normal file
BIN
textures/yellow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 192 B |
Reference in New Issue
Block a user