This commit is contained in:
2019-11-09 12:28:52 -08:00
62 changed files with 941 additions and 957 deletions

30
rtiow/renderer/Cargo.toml Normal file
View File

@@ -0,0 +1,30 @@
[package]
name = "renderer"
version = "0.1.0"
authors = ["Bill Thiede <git@xinu.tv>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bench]]
harness = false
name = "spheres"
[dependencies]
chrono = "*"
core_affinity = "0.5"
cpuprofiler = { version = "0.0.3", optional = true }
image = "0.19.0"
lazy_static = "1.1.0"
log = "0.4.5"
num_cpus = "1.8.0"
rand = "0.5.5"
serde = "1.0.79"
serde_derive = "1.0.79"
serde_json = "1.0.41"
structopt = "0.2.10"
[dev-dependencies]
criterion = "0.2"
[features]
profile = ["cpuprofiler"]

View File

@@ -0,0 +1,37 @@
#[macro_use]
extern crate criterion;
use criterion::Criterion;
use criterion::ParameterizedBenchmark;
use rtiow::hitable::Hit;
use rtiow::material::Lambertian;
use rtiow::ray::Ray;
use rtiow::sphere::Sphere;
use rtiow::texture::ConstantTexture;
use rtiow::vec3::Vec3;
fn criterion_benchmark(c: &mut Criterion) {
let sphere = Sphere::new(
Vec3::new(0., 0., 0.),
1.,
Lambertian::new(ConstantTexture::new([1., 0., 0.])),
);
let rays = vec![
// Hit
Ray::new([0., 0., -2.], [0., 0., 1.], 0.),
// Miss
Ray::new([0., 0., -2.], [0., 0., -1.], 0.),
];
c.bench(
"sphere",
ParameterizedBenchmark::new(
"Sphere",
move |b, r| b.iter(|| sphere.hit(*r, 0., 1.)),
rays,
),
);
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

172
rtiow/renderer/src/aabb.rs Normal file
View File

@@ -0,0 +1,172 @@
use std::fmt;
use crate::ray::Ray;
use crate::vec3::Vec3;
#[derive(Debug, Clone, PartialEq)]
pub struct AABB {
bounds: [Vec3; 2],
}
impl fmt::Display for AABB {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({})-({})", self.bounds[0], self.bounds[1])
}
}
fn min(x: f32, y: f32) -> f32 {
if x < y {
x
} else {
y
}
}
fn max(x: f32, y: f32) -> f32 {
if x > y {
x
} else {
y
}
}
impl AABB {
pub fn new(min: Vec3, max: Vec3) -> AABB {
AABB { bounds: [min, max] }
}
pub fn volume(&self) -> f32 {
(self.min().x - self.max().x).abs()
* (self.min().y - self.max().y).abs()
* (self.min().z - self.max().z).abs()
}
pub fn longest_axis(&self) -> usize {
let xd = (self.min().x - self.max().x).abs();
let yd = (self.min().y - self.max().y).abs();
let zd = (self.min().z - self.max().z).abs();
if xd >= yd && xd >= zd {
return 0;
}
if yd >= xd && yd >= zd {
return 1;
}
2
}
pub fn mid_point(&self) -> Vec3 {
self.bounds[0] + (self.bounds[1] - self.bounds[0]) / 2.
}
pub fn min(&self) -> Vec3 {
self.bounds[0]
}
pub fn max(&self) -> Vec3 {
self.bounds[1]
}
pub fn hit2(&self, r: Ray, t_min: f32, t_max: f32) -> bool {
let mut t_min = t_min;
let mut t_max = t_max;
for axis in 0..3 {
let d_inv = 1.0 / r.direction[axis];
let t0 = min(
(self.min()[axis] - r.origin[axis]) * d_inv,
(self.max()[axis] - r.origin[axis]) * d_inv,
);
let t1 = max(
(self.min()[axis] - r.origin[axis]) * d_inv,
(self.max()[axis] - r.origin[axis]) * d_inv,
);
t_min = max(t0, t_min);
t_max = min(t1, t_max);
if t_max <= t_min {
return false;
}
}
true
}
pub fn hit_precompute(&self, r: Ray, t0: f32, t1: f32) -> bool {
// TODO(wathiede): this has bugs.
let mut t_min = (self.bounds[r.sign[0]].x - r.origin.x) * r.inv_direction.x;
let mut t_max = (self.bounds[1 - r.sign[0]].x - r.origin.x) * r.inv_direction.x;
let t_y_min = (self.bounds[r.sign[0]].y - r.origin.y) * r.inv_direction.y;
let t_y_max = (self.bounds[1 - r.sign[0]].y - r.origin.y) * r.inv_direction.y;
if t_min > t_y_max || t_y_min > t_max {
return false;
}
if t_y_min > t_min {
t_min = t_y_min;
}
if t_y_max < t_max {
t_max = t_y_max;
}
let t_z_min = (self.bounds[r.sign[2]].z - r.origin.z) * r.inv_direction.z;
let t_z_max = (self.bounds[1 - r.sign[2]].z - r.origin.z) * r.inv_direction.z;
if t_min > t_z_max || t_z_min > t_max {
return false;
}
if t_z_min > t_min {
t_min = t_z_min;
}
if t_z_max < t_max {
t_max = t_z_max;
}
t_min < t1 && t_max > t0
}
pub fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> bool {
let mut t_min = t_min;
let mut t_max = t_max;
for axis in 0..3 {
let t0 = ((self.min()[axis] - r.origin[axis]) * r.inv_direction[axis])
.min((self.max()[axis] - r.origin[axis]) * r.inv_direction[axis]);
let t1 = ((self.min()[axis] - r.origin[axis]) * r.inv_direction[axis])
.max((self.max()[axis] - r.origin[axis]) * r.inv_direction[axis]);
t_min = t0.max(t_min);
t_max = t1.min(t_max);
if t_max <= t_min {
return false;
}
}
true
}
pub fn hit_fast(&self, r: Ray, _t_min: f32, _t_max: f32) -> bool {
let b = self;
let mut t1 = (b.min()[0] - r.origin[0]) * 1. / r.direction[0];
let mut t2 = (b.max()[0] - r.origin[0]) * 1. / r.direction[0];
let mut tmin = t1.min(t2);
let mut tmax = t1.max(t2);
for i in 1..3 {
t1 = (b.min()[i] - r.origin[i]) * 1. / r.direction[i];
t2 = (b.max()[i] - r.origin[i]) * 1. / r.direction[i];
tmin = tmin.max(t1.min(t2));
tmax = tmax.min(t1.max(t2));
}
tmax > tmin.max(0.0)
}
}
pub fn surrounding_box(box0: &AABB, box1: &AABB) -> AABB {
let min = Vec3::new(
box0.min().x.min(box1.min().x),
box0.min().y.min(box1.min().y),
box0.min().z.min(box1.min().z),
);
let max = Vec3::new(
box0.max().x.max(box1.max().x),
box0.max().y.max(box1.max().y),
box0.max().z.max(box1.max().z),
);
AABB::new(min, max)
}

View File

@@ -0,0 +1,5 @@
extern crate askama;
fn main() {
askama::rerun_if_templates_changed();
}

300
rtiow/renderer/src/bvh.rs Normal file
View File

@@ -0,0 +1,300 @@
use std;
use std::fmt;
use std::time::Instant;
use log::info;
use rand;
use rand::Rng;
use crate::aabb::surrounding_box;
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::ray::Ray;
enum BVHNode {
Leaf(Box<dyn Hit>),
Branch {
left: Box<BVHNode>,
right: Box<BVHNode>,
bbox: Option<AABB>,
},
}
impl fmt::Display for BVHNode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BVHNode::Leaf(ref hit) => write!(
f,
"Leaf: {}",
hit.bounding_box(0., 0.)
.map_or("NO BBOX".to_owned(), |bb| bb.to_string())
),
BVHNode::Branch { bbox, .. } => write!(
f,
"Branch: {}",
// TODO(wathiede): removing this .clone() results in a complaint about moving out
// of a borrow.
bbox.clone()
.map_or("NO BBOX".to_owned(), |bb| bb.to_string())
),
}
}
}
// Lint is wrong when dealing with Box<Trait>
#[allow(clippy::borrowed_box)]
fn box_x_compare(ah: &Box<dyn Hit>, bh: &Box<dyn Hit>) -> std::cmp::Ordering {
match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) {
(Some(box_left), Some(box_right)) => {
box_left.min().x.partial_cmp(&box_right.min().x).unwrap()
}
_ => panic!("hit missing bounding box"),
}
}
#[allow(clippy::borrowed_box)]
fn box_y_compare(ah: &Box<dyn Hit>, bh: &Box<dyn Hit>) -> std::cmp::Ordering {
match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) {
(Some(box_left), Some(box_right)) => {
box_left.min().y.partial_cmp(&box_right.min().y).unwrap()
}
_ => panic!("hit missing bounding box"),
}
}
#[allow(clippy::borrowed_box)]
fn box_z_compare(ah: &Box<dyn Hit>, bh: &Box<dyn Hit>) -> std::cmp::Ordering {
match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) {
(Some(box_left), Some(box_right)) => {
box_left.min().z.partial_cmp(&box_right.min().z).unwrap()
}
_ => panic!("hit missing bounding box"),
}
}
// Return the first element from the vector, which should be the only element.
// TODO(wathiede): we really want a .first_into() (.first() doesn't work because it
// returns a reference.)
fn vec_first_into<T>(v: Vec<T>) -> T {
if v.len() != 1 {
panic!(format!(
"vec_first_into called for vector length != 1, length {}",
v.len()
));
}
v.into_iter().next().unwrap()
}
fn vec_split_into<T>(v: Vec<T>, offset: usize) -> (Vec<T>, Vec<T>) {
let mut left_half = Vec::new();
let mut right_half = Vec::new();
v.into_iter().enumerate().for_each(|(i, h)| {
if i < offset {
left_half.push(h);
} else {
right_half.push(h);
}
});
(left_half, right_half)
}
impl BVHNode {
fn new(l: Vec<Box<dyn Hit>>, t_min: f32, t_max: f32) -> BVHNode {
if l.len() == 1 {
BVHNode::Leaf(vec_first_into(l))
} else {
let mut l: Vec<Box<dyn Hit>> = l.into_iter().collect();
let mut rng = rand::thread_rng();
match rng.gen_range(0, 3) {
0 => l.sort_by(box_x_compare),
1 => l.sort_by(box_y_compare),
2 => l.sort_by(box_z_compare),
val => panic!("unknown axis {}", val),
}
let half = l.len() / 2;
let (left_half, right_half) = vec_split_into(l, half);
let left = Box::new(BVHNode::new(left_half, t_min, t_max));
let right = Box::new(BVHNode::new(right_half, t_min, t_max));
let bbox = BVHNode::surrounding_box(left.as_ref(), right.as_ref(), t_min, t_max);
BVHNode::Branch { left, right, bbox }
}
}
fn surrounding_box(left: &dyn Hit, right: &dyn Hit, t_min: f32, t_max: f32) -> Option<AABB> {
match (
left.bounding_box(t_min, t_max),
right.bounding_box(t_min, t_max),
) {
(Some(left_bbox), Some(right_bbox)) => Some(surrounding_box(&left_bbox, &right_bbox)),
(None, Some(right_bbox)) => Some(right_bbox),
(Some(left_bbox), None) => Some(left_bbox),
(None, None) => None,
}
}
fn volume(&self) -> f32 {
let bbox = self.bounding_box(0., 0.).unwrap();
(bbox.min().x - bbox.max().x).abs()
* (bbox.min().y - bbox.max().y).abs()
* (bbox.min().z - bbox.max().z).abs()
}
}
impl Hit for BVHNode {
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
match self {
BVHNode::Leaf(ref hit) => hit.hit(r, t_min, t_max),
BVHNode::Branch {
ref left,
ref right,
ref bbox,
} => match bbox {
Some(ref bbox) => {
if bbox.hit(r, t_min, t_max) {
match (left.hit(r, t_min, t_max), right.hit(r, t_min, t_max)) {
(Some(hit_left), Some(hit_right)) => {
if hit_left.t < hit_right.t {
Some(hit_left)
} else {
Some(hit_right)
}
}
(Some(hit_left), None) => Some(hit_left),
(None, Some(hit_right)) => Some(hit_right),
(None, None) => None,
};
}
None
}
None => None,
},
}
}
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
match self {
BVHNode::Leaf(ref hit) => hit.bounding_box(t_min, t_max),
BVHNode::Branch { ref bbox, .. } => bbox.clone(),
}
}
}
pub struct BVH {
root: BVHNode,
}
impl BVH {
pub fn new(l: Vec<Box<dyn Hit>>, t_min: f32, t_max: f32) -> BVH {
let count = l.len();
let start = Instant::now();
let bvh = BVH {
root: BVHNode::new(l, t_min, t_max),
};
let runtime = start.elapsed();
info!(
"BVH build time {}.{} seconds for {} hitables",
runtime.as_secs(),
runtime.subsec_millis(),
count
);
bvh
}
}
fn print_tree(f: &mut fmt::Formatter, depth: usize, bvhn: &BVHNode) -> fmt::Result {
let vol = bvhn.volume();
writeln!(f, "{:.*}{}{}", 2, vol, " ".repeat(depth * 2), bvhn)?;
if let BVHNode::Branch { left, right, .. } = bvhn {
print_tree(f, depth + 1, left)?;
print_tree(f, depth + 1, right)?;
}
Ok(())
}
impl fmt::Display for BVH {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Root")?;
print_tree(f, 1, &self.root)
}
}
impl Hit for BVH {
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
self.root.hit(r, t_min, t_max)
}
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
self.root.bounding_box(t_min, t_max)
}
}
#[cfg(test)]
mod tests {
use crate::aabb::AABB;
use crate::material::Lambertian;
use crate::material::Metal;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::vec3::Vec3;
use super::*;
#[test]
fn bbox_two_spheres() {
let two_spheres_bvh = BVH::new(
vec![
Box::new(Sphere::new(
Vec3::new(0., 0., 0.),
0.5,
Lambertian::new(ConstantTexture::new(Vec3::new(0.1, 0.2, 0.5))),
)),
Box::new(Sphere::new(
Vec3::new(1., 0., 0.),
0.5,
Metal::new(Vec3::new(0.6, 0.6, 0.6), 0.2),
)),
],
0.,
1.,
);
assert_eq!(
AABB::new(Vec3::new(-0.5, -0.5, -0.5), Vec3::new(1.5, 0.5, 0.5)),
two_spheres_bvh.bounding_box(0., 1.).unwrap(),
"BVH:\n{}",
two_spheres_bvh
);
}
#[test]
fn bbox_three_spheres() {
let three_spheres_bvh = BVH::new(
vec![
Box::new(Sphere::new(
Vec3::new(0., 0., 0.),
0.5,
Lambertian::new(ConstantTexture::new(Vec3::new(0.1, 0.2, 0.5))),
)),
Box::new(Sphere::new(
Vec3::new(1., 0., 0.),
0.5,
Metal::new(Vec3::new(0.6, 0.6, 0.6), 0.2),
)),
Box::new(Sphere::new(
Vec3::new(0., 1., 0.),
0.5,
Metal::new(Vec3::new(0.6, 0.6, 0.6), 0.2),
)),
],
0.,
1.,
);
assert_eq!(
AABB::new(Vec3::new(-0.5, -0.5, -0.5), Vec3::new(1.5, 1.5, 0.5)),
three_spheres_bvh.bounding_box(0., 1.).unwrap(),
"BVH:\n{}",
three_spheres_bvh
);
}
}

View File

@@ -0,0 +1,82 @@
use std::f32::consts::PI;
use rand;
use rand::Rng;
use crate::ray::Ray;
use crate::vec3::cross;
use crate::vec3::Vec3;
fn random_in_unit_disk() -> Vec3 {
let mut rng = rand::thread_rng();
let v = Vec3::new(1., 1., 0.);
loop {
let p = 2. * Vec3::new(rng.gen_range(0., 1.), rng.gen_range(0., 1.), 0.) - v;
if p.squared_length() < 1. {
return p;
}
}
}
pub struct Camera {
origin: Vec3,
lower_left_corner: Vec3,
horizontal: Vec3,
vertical: Vec3,
u: Vec3,
v: Vec3,
lens_radius: f32,
time_min: f32,
time_max: f32,
}
impl Camera {
// Parameters match the example C++ code.
#[allow(clippy::too_many_arguments)]
// vfov is top to bottom in degrees
pub fn new(
lookfrom: Vec3,
lookat: Vec3,
vup: Vec3,
vfov: f32,
aspect: f32,
aperture: f32,
focus_dist: f32,
time_min: f32,
time_max: f32,
) -> Camera {
let theta = vfov * PI / 180.;
let half_height = (theta / 2.).tan();
let half_width = aspect * half_height;
let origin = lookfrom;
let w = (lookfrom - lookat).unit_vector();
let u = cross(vup, w).unit_vector();
let v = cross(w, u);
Camera {
lower_left_corner: origin
- half_width * focus_dist * u
- half_height * focus_dist * v
- focus_dist * w,
horizontal: 2. * half_width * focus_dist * u,
vertical: 2. * half_height * focus_dist * v,
origin,
u: cross(vup, w).unit_vector(),
v: cross(w, u),
lens_radius: aperture / 2.,
time_min,
time_max,
}
}
pub fn get_ray(&self, u: f32, v: f32) -> Ray {
let mut rng = rand::thread_rng();
let rd = self.lens_radius * random_in_unit_disk();
let offset = self.u * rd.x + self.v * rd.y;
let time = self.time_min + rng.gen_range(0., 1.) * (self.time_max - self.time_min);
Ray::new(
self.origin + offset,
self.lower_left_corner + self.horizontal * u + self.vertical * v - self.origin - offset,
time,
)
}
}

View File

@@ -0,0 +1,88 @@
use std;
use rand;
use rand::Rng;
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::material::Isotropic;
use crate::ray::Ray;
use crate::texture::Texture;
use crate::vec3::Vec3;
pub struct ConstantMedium<H, T>
where
H: Hit,
T: Texture,
{
density: f32,
material: Isotropic<T>,
hitable: H,
}
impl<H, T> ConstantMedium<H, T>
where
H: Hit,
T: Texture,
{
pub fn new(hitable: H, density: f32, texture: T) -> ConstantMedium<H, T>
where
T: Texture,
{
ConstantMedium {
density,
material: Isotropic::new(texture),
hitable,
}
}
}
impl<H, T> Hit for ConstantMedium<H, T>
where
H: Hit,
T: Texture,
{
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
let mut rng = rand::thread_rng();
if let Some(mut rec1) = self.hitable.hit(r, std::f32::MIN, std::f32::MAX) {
if let Some(mut rec2) = self.hitable.hit(r, rec1.t + 0.001, std::f32::MAX) {
if rec1.t < t_min {
rec1.t = t_min;
}
if rec2.t > t_max {
rec2.t = t_max;
}
if rec1.t >= rec2.t {
return None;
}
if rec1.t < 0. {
rec1.t = 0.;
}
let distance_inside_boundary = (rec2.t - rec1.t) * r.direction.length();
let hit_distance = -(1. / self.density) * rng.gen_range::<f32>(0., 1.).ln();
if hit_distance < distance_inside_boundary {
let t = rec1.t + hit_distance / r.direction.length();
let normal = Vec3::new(
rng.gen_range(0., 1.),
rng.gen_range(0., 1.),
rng.gen_range(0., 1.),
)
.unit_vector();
return Some(HitRecord {
t,
p: r.point_at_parameter(t),
normal,
material: &self.material,
uv: (0., 0.),
});
}
}
}
None
}
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
self.hitable.bounding_box(t_min, t_max)
}
}

View File

@@ -0,0 +1,90 @@
use std::sync::Arc;
use crate::aabb::AABB;
use crate::flip_normals::FlipNormals;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::hitable_list::HitableList;
use crate::material::Material;
use crate::ray::Ray;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::rect::YZRect;
use crate::vec3::Vec3;
pub struct Cuboid {
p_min: Vec3,
p_max: Vec3,
walls: HitableList,
}
impl Cuboid {
// This clippy doesn't work right with Arc.
#[allow(clippy::needless_pass_by_value)]
pub fn new(p_min: Vec3, p_max: Vec3, material: Arc<dyn Material>) -> Cuboid {
Cuboid {
p_min,
p_max,
walls: HitableList::new(vec![
Box::new(XYRect::new(
p_min.x,
p_max.x,
p_min.y,
p_max.y,
p_max.z,
Arc::clone(&material),
)),
Box::new(FlipNormals::new(XYRect::new(
p_min.x,
p_max.x,
p_min.y,
p_max.y,
p_min.z,
Arc::clone(&material),
))),
Box::new(XZRect::new(
p_min.x,
p_max.x,
p_min.z,
p_max.z,
p_max.y,
Arc::clone(&material),
)),
Box::new(FlipNormals::new(XZRect::new(
p_min.x,
p_max.x,
p_min.z,
p_max.z,
p_min.y,
Arc::clone(&material),
))),
Box::new(YZRect::new(
p_min.y,
p_max.y,
p_min.z,
p_max.z,
p_max.x,
Arc::clone(&material),
)),
Box::new(FlipNormals::new(YZRect::new(
p_min.y,
p_max.y,
p_min.z,
p_max.z,
p_min.x,
Arc::clone(&material),
))),
]),
}
}
}
impl Hit for Cuboid {
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
self.walls.hit(r, t_min, t_max)
}
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
Some(AABB::new(self.p_min, self.p_max))
}
}

View File

@@ -0,0 +1,39 @@
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::ray::Ray;
pub struct FlipNormals<H>
where
H: Hit,
{
hitable: H,
}
impl<H> FlipNormals<H>
where
H: Hit,
{
pub fn new(hitable: H) -> FlipNormals<H> {
FlipNormals { hitable }
}
}
impl<H> Hit for FlipNormals<H>
where
H: Hit,
{
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
if let Some(rec) = self.hitable.hit(r, t_min, t_max) {
return Some(HitRecord {
normal: -rec.normal,
..rec
});
};
None
}
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
self.hitable.bounding_box(t_min, t_max)
}
}

View File

@@ -0,0 +1,28 @@
use std::sync::Arc;
use crate::aabb::AABB;
use crate::material::Material;
use crate::ray::Ray;
use crate::vec3::Vec3;
pub struct HitRecord<'m> {
pub t: f32,
pub uv: (f32, f32),
pub p: Vec3,
pub normal: Vec3,
pub material: &'m dyn Material,
}
pub trait Hit: Send + Sync {
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord>;
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB>;
}
impl Hit for Arc<dyn Hit> {
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
(**self).hit(r, t_min, t_max)
}
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
(**self).bounding_box(t_min, t_max)
}
}

View File

@@ -0,0 +1,48 @@
use std;
use crate::aabb::surrounding_box;
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::ray::Ray;
use crate::vec3::Vec3;
#[derive(Default)]
pub struct HitableList {
list: Vec<Box<dyn Hit>>,
}
impl HitableList {
pub fn new(list: Vec<Box<dyn Hit>>) -> HitableList {
HitableList { list }
}
}
impl Hit for HitableList {
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
let mut min_hit = None;
let mut closest_so_far = t_max;
for hitable in &self.list {
if let Some(h) = hitable.hit(r, t_min, closest_so_far) {
closest_so_far = h.t;
min_hit = Some(h);
}
}
min_hit
}
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
// TODO(wathiede): I don't understand the version from the book, this implementation
// returns a superset of all bounding boxes.
let super_box = AABB::new(
Vec3::new(std::f32::MAX, std::f32::MAX, std::f32::MAX),
Vec3::new(std::f32::MIN, std::f32::MIN, std::f32::MIN),
);
Some(self.list.iter().fold(super_box, |acc, hitable| {
match hitable.bounding_box(t_min, t_max) {
Some(bbox) => surrounding_box(&acc, &bbox),
None => acc,
}
}))
}
}

227
rtiow/renderer/src/human.rs Normal file
View File

@@ -0,0 +1,227 @@
#![allow(dead_code)]
//! From https://raw.githubusercontent.com/BobGneu/human-format-rs/master/src/lib.rs
//! `human` provides facilitates creating a formatted string, converting between numbers that are beyond typical
//! needs for humans into a simpler string that conveys the gist of the meaning of the number.
//!
//! Print some human readable strings
//!
//! ```rust
//! use rtiow::human;
//!
//! // "1.00 k"
//! let tmpStr = human::Formatter::new()
//! .format(1000.0);
//! # assert_eq!(tmpStr, "1.00 k");
//!
//! // "1.00 M"
//! let tmpStr2 = human::Formatter::new()
//! .format(1000000.0);
//! # assert_eq!(tmpStr2, "1.00 M");
//!
//! // "1.00 B"
//! let tmpStr3 = human::Formatter::new()
//! .format(1000000000.0);
//! # assert_eq!(tmpStr3, "1.00 B");
//! ```
//!
//! If you are so inspired you can even try playing with units and customizing your `Scales`
//!
//! For more examples you should review the examples on github: [tests/demo.rs](https://github.com/BobGneu/human-format-rs/blob/master/tests/demo.rs)
//!
#[derive(Debug)]
struct ScaledValue {
value: f32,
suffix: String,
}
/// Entry point to the lib. Use this to handle your formatting needs.
#[derive(Debug)]
pub struct Formatter {
decimals: usize,
separator: String,
scales: Scales,
forced_units: String,
forced_suffix: String,
}
/// Provide a customized scaling scheme for your own modeling.
#[derive(Debug)]
pub struct Scales {
base: u32,
suffixes: Vec<String>,
}
impl Formatter {
/// Initializes a new `Formatter` with default values.
pub fn new() -> Self {
Formatter {
decimals: 2,
separator: " ".to_owned(),
scales: Scales::si(),
forced_units: "".to_owned(),
forced_suffix: "".to_owned(),
}
}
/// Sets the decimals value for formatting the string.
pub fn with_decimals(&mut self, decimals: usize) -> &mut Self {
self.decimals = decimals;
self
}
/// Sets the separator value for formatting the string.
pub fn with_separator(&mut self, separator: &str) -> &mut Self {
self.separator = separator.to_owned();
self
}
/// Sets the scales value.
pub fn with_scales(&mut self, scales: Scales) -> &mut Self {
self.scales = scales;
self
}
/// Sets the units value.
pub fn with_units(&mut self, units: &str) -> &mut Self {
self.forced_units = units.to_owned();
self
}
/// Sets the expected suffix value.
pub fn with_suffix(&mut self, suffix: &str) -> &mut Self {
self.forced_suffix = suffix.to_owned();
self
}
/// Formats the number into a string
pub fn format(&self, value: f64) -> String {
if value < 0.0 {
return format!("-{}", self.format(value * -1.0));
}
let scaled_value = self.scales.to_scaled_value(value);
format!(
"{:.width$}{}{}{}",
scaled_value.value,
self.separator,
scaled_value.suffix,
self.forced_units,
width = self.decimals
)
}
/// Parse a string back into a float value.
pub fn parse(&self, value: &str) -> f64 {
let v: Vec<&str> = value.split(&self.separator).collect();
let result = v.get(0).unwrap().parse::<f64>().unwrap();
let mut suffix = v.get(1).unwrap().to_string();
let new_len = suffix.len() - self.forced_units.len();
suffix.truncate(new_len);
let magnitude_multiplier = self.scales.get_magnitude_multipler(&suffix);
(result * magnitude_multiplier)
}
}
impl Scales {
/// Instantiates a new `Scales` with SI keys
pub fn new() -> Self {
Scales::si()
}
/// Instantiates a new `Scales` with SI keys
pub fn si() -> Self {
Scales {
base: 1000,
suffixes: vec![
"".to_owned(),
"k".to_owned(),
"M".to_owned(),
"B".to_owned(),
"T".to_owned(),
"P".to_owned(),
"E".to_owned(),
"Z".to_owned(),
"Y".to_owned(),
],
}
}
/// Instantiates a new `Scales` with Binary keys
pub fn binary() -> Self {
Scales {
base: 1000,
suffixes: vec![
"".to_owned(),
"ki".to_owned(),
"Mi".to_owned(),
"Gi".to_owned(),
"Ti".to_owned(),
"Pi".to_owned(),
"Ei".to_owned(),
"Zi".to_owned(),
"Yi".to_owned(),
],
}
}
/// Sets the base for the `Scales`
pub fn with_base(&mut self, base: u32) -> &mut Self {
self.base = base;
self
}
/// Sets the suffixes listing appropriately
pub fn with_suffixes(&mut self, suffixes: Vec<&str>) -> &mut Self {
self.suffixes = Vec::new();
for suffix in suffixes {
// This should be to_owned to be clear about intent.
// https://users.rust-lang.org/t/to-string-vs-to-owned-for-string-literals/1441/6
self.suffixes.push(suffix.to_owned());
}
self
}
fn get_magnitude_multipler(&self, value: &str) -> f64 {
for ndx in 0..self.suffixes.len() {
if value == self.suffixes[ndx] {
return self.base.pow(ndx as u32) as f64;
}
}
return 0.0;
}
fn to_scaled_value(&self, value: f64) -> ScaledValue {
let mut index: usize = 0;
let mut _value: f64 = value;
loop {
if _value < (self.base as f64) {
break;
}
_value /= self.base as f64;
index += 1;
}
ScaledValue {
value: (value / self.base.pow((index) as u32) as f64) as f32,
suffix: self.suffixes[index].to_owned(),
}
}
}

View File

@@ -0,0 +1,191 @@
use std::fmt;
use log::info;
use crate::aabb::surrounding_box;
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::ray::Ray;
pub enum KDTree {
Leaf(Box<dyn Hit>),
Branch {
left: Box<KDTree>,
right: Box<KDTree>,
bbox: Option<AABB>,
},
}
// Return the first element from the vector, which should be the only element.
// TODO(wathiede): we really want a .first_into() (.first() doesn't work because it
// returns a reference.)
fn vec_first_into<T>(v: Vec<T>) -> T {
if v.len() != 1 {
panic!(format!(
"vec_first_into called for vector length != 1, length {}",
v.len()
));
}
v.into_iter().next().unwrap()
}
fn vec_split_into<T>(v: Vec<T>, offset: usize) -> (Vec<T>, Vec<T>) {
let mut left_half = Vec::new();
let mut right_half = Vec::new();
v.into_iter().enumerate().for_each(|(i, h)| {
if i < offset {
left_half.push(h);
} else {
right_half.push(h);
}
});
(left_half, right_half)
}
impl KDTree {
pub fn new(l: Vec<Box<dyn Hit>>, t_min: f32, t_max: f32) -> KDTree {
if l.is_empty() {
panic!("Attempt to build k-d tree with no Hit objects");
}
if l.len() == 1 {
return KDTree::Leaf(vec_first_into(l));
}
if l.len() == 2 {
let (left_vec, right_vec) = vec_split_into(l, 1);
let left = vec_first_into(left_vec);
let right = vec_first_into(right_vec);
let bbox = surrounding_box(
&left.bounding_box(t_min, t_max).unwrap(),
&right.bounding_box(t_min, t_max).unwrap(),
);
return KDTree::Branch {
left: Box::new(KDTree::Leaf(left)),
right: Box::new(KDTree::Leaf(right)),
bbox: Some(bbox),
};
}
let bbox = l[0].bounding_box(t_min, t_max).unwrap();
let mut mid_point = bbox.mid_point();
let bbox = l[1..].iter().fold(bbox, |acc, h| {
let new_bbox = &h.bounding_box(t_min, t_max).unwrap();
mid_point = mid_point + new_bbox.mid_point();
surrounding_box(&acc, new_bbox)
});
mid_point = mid_point / l.len() as f32;
let axis = bbox.longest_axis();
let mut left_half = Vec::new();
let mut right_half = Vec::new();
match axis {
0 => l.into_iter().for_each(|h| {
if h.bounding_box(t_min, t_max).unwrap().mid_point().x < mid_point.x {
left_half.push(h);
} else {
right_half.push(h);
}
}),
1 => l.into_iter().for_each(|h| {
if h.bounding_box(t_min, t_max).unwrap().mid_point().y < mid_point.y {
left_half.push(h);
} else {
right_half.push(h);
}
}),
2 => l.into_iter().for_each(|h| {
if h.bounding_box(t_min, t_max).unwrap().mid_point().z < mid_point.z {
left_half.push(h);
} else {
right_half.push(h);
}
}),
_ => panic!("Unreachable"),
};
KDTree::Branch {
left: Box::new(KDTree::new(left_half, t_min, t_max)),
right: Box::new(KDTree::new(right_half, t_min, t_max)),
bbox: Some(bbox),
}
}
fn volume(&self) -> f32 {
let bbox = self.bounding_box(0., 0.).unwrap();
(bbox.min().x - bbox.max().x).abs()
* (bbox.min().y - bbox.max().y).abs()
* (bbox.min().z - bbox.max().z).abs()
}
}
fn print_tree(f: &mut fmt::Formatter, depth: usize, kdt: &KDTree) -> fmt::Result {
let vol = kdt.volume();
write!(f, "{:8.2}{}", vol, " ".repeat(depth * 2))?;
match kdt {
KDTree::Leaf(ref hit) => writeln!(
f,
"Leaf: {}",
hit.bounding_box(0., 0.)
.map_or("NO BBOX".to_owned(), |bb| bb.to_string())
)?,
KDTree::Branch { bbox, .. } => writeln!(
f,
"Branch: {}",
// TODO(wathiede): removing this .clone() results in a complaint about moving out
// of a borrow.
bbox.clone()
.map_or("NO BBOX".to_owned(), |bb| bb.to_string())
)?,
}
if let KDTree::Branch { left, right, .. } = kdt {
print_tree(f, depth + 1, left)?;
print_tree(f, depth + 1, right)?;
}
Ok(())
}
impl fmt::Display for KDTree {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "kd-tree")?;
print_tree(f, 1, &self)
}
}
impl Hit for KDTree {
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
match self.bounding_box(t_min, t_max) {
Some(bbox) => {
if !bbox.hit(r, t_min, t_max) {
return None;
}
}
None => {
info!("KDTree::hit no bbox: {:?}", r);
return None;
}
}
match self {
KDTree::Leaf(ref hit) => hit.hit(r, t_min, t_max),
KDTree::Branch { left, right, bbox } => match bbox {
None => None,
Some(_bbox) => match (left.hit(r, t_min, t_max), right.hit(r, t_min, t_max)) {
(Some(hit_left), Some(hit_right)) => {
if hit_left.t < hit_right.t {
Some(hit_left)
} else {
Some(hit_right)
}
}
(Some(hit_left), None) => Some(hit_left),
(None, Some(hit_right)) => Some(hit_right),
(None, None) => None,
},
},
}
}
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
match self {
KDTree::Leaf(ref hit) => hit.bounding_box(t_min, t_max),
KDTree::Branch { bbox, .. } => bbox.clone(),
}
}
}

23
rtiow/renderer/src/lib.rs Normal file
View File

@@ -0,0 +1,23 @@
pub mod aabb;
pub mod bvh;
pub mod camera;
pub mod constant_medium;
pub mod cuboid;
pub mod flip_normals;
pub mod hitable;
pub mod hitable_list;
pub mod human;
pub mod kdtree;
pub mod material;
pub mod moving_sphere;
pub mod noise;
pub mod output;
pub mod ray;
pub mod rect;
pub mod renderer;
pub mod rotate;
pub mod scenes;
pub mod sphere;
pub mod texture;
pub mod translate;
pub mod vec3;

View File

@@ -0,0 +1,274 @@
use std::sync::Arc;
use rand;
use rand::Rng;
use crate::hitable::HitRecord;
use crate::ray::Ray;
use crate::texture::Texture;
use crate::vec3::dot;
use crate::vec3::Vec3;
fn random_in_unit_sphere() -> Vec3 {
let mut rng = rand::thread_rng();
let v = Vec3::new(1., 1., 1.);
loop {
let p =
2. * Vec3::new(
rng.gen_range(0., 1.),
rng.gen_range(0., 1.),
rng.gen_range(0., 1.),
) - v;
if p.squared_length() < 1. {
return p;
}
}
}
#[derive(Default)]
pub struct ScatterResponse {
pub scattered: Ray,
pub attenutation: Vec3,
pub reflected: bool,
}
pub trait Material: Send + Sync {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse;
fn emitted(&self, _u: f32, _v: f32, _p: Vec3) -> Vec3 {
Vec3::new(0., 0., 0.)
}
}
impl Material for Arc<dyn Material> {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
(**self).scatter(r_in, rec)
}
fn emitted(&self, u: f32, v: f32, p: Vec3) -> Vec3 {
(**self).emitted(u, v, p)
}
}
impl Material for Box<dyn Material> {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
(**self).scatter(r_in, rec)
}
fn emitted(&self, u: f32, v: f32, p: Vec3) -> Vec3 {
(**self).emitted(u, v, p)
}
}
pub struct Isotropic<T>
where
T: Texture,
{
albedo: T,
}
impl<T> Isotropic<T>
where
T: Texture,
{
pub fn new(texture: T) -> Isotropic<T> {
Isotropic { albedo: texture }
}
}
impl<T> Material for Isotropic<T>
where
T: Texture,
{
fn scatter(&self, _r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
let (u, v) = rec.uv;
ScatterResponse {
attenutation: self.albedo.value(u, v, rec.p),
scattered: Ray::new(rec.p, random_in_unit_sphere(), 0.),
reflected: true,
}
}
}
pub struct Lambertian<T>
where
T: Texture,
{
albedo: T,
}
impl<T> Lambertian<T>
where
T: Texture,
{
pub fn new(texture: T) -> Lambertian<T> {
Lambertian { albedo: texture }
}
}
impl<T> Material for Lambertian<T>
where
T: Texture,
{
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
let target = rec.p + rec.normal + random_in_unit_sphere();
let (u, v) = rec.uv;
ScatterResponse {
attenutation: self.albedo.value(u, v, rec.p),
scattered: Ray::new(rec.p, target - rec.p, r_in.time),
reflected: true,
}
}
}
pub struct Metal {
albedo: Vec3,
fuzzy: f32,
}
fn reflect(v: Vec3, n: Vec3) -> Vec3 {
v - 2. * dot(v, n) * n
}
impl Metal {
pub fn new<V>(albedo: V, fuzzy: f32) -> Metal
where
V: Into<Vec3>,
{
let fuzzy = fuzzy.min(1.);
Metal {
albedo: albedo.into(),
fuzzy,
}
}
}
impl Material for Metal {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
let reflected = reflect(r_in.direction.unit_vector(), rec.normal);
let scattered = Ray::new(
rec.p,
reflected + self.fuzzy * random_in_unit_sphere(),
r_in.time,
);
ScatterResponse {
scattered,
attenutation: self.albedo,
reflected: dot(scattered.direction, rec.normal) > 0.,
}
}
}
fn refract(v: Vec3, n: Vec3, ni_over_nt: f32) -> Option<Vec3> {
let uv = v.unit_vector();
let dt = dot(uv, n);
let discriminant = 1. - ni_over_nt * ni_over_nt * (1. - dt * dt);
if discriminant > 0. {
return Some(ni_over_nt * (uv - n * dt) - n * discriminant.sqrt());
}
None
}
fn schlick(cosine: f32, ref_idx: f32) -> f32 {
let mut r0 = (1. - ref_idx) / (1. + ref_idx);
r0 = r0 * r0;
r0 + (1. - r0) * (1. - cosine).powf(5.)
}
pub struct Dielectric {
ref_idx: f32,
}
impl Dielectric {
pub fn new(ref_idx: f32) -> Dielectric {
Dielectric { ref_idx }
}
}
impl Material for Dielectric {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
let reflected = reflect(r_in.direction, rec.normal);
let (outward_normal, ni_over_nt, cosine) = if dot(r_in.direction, rec.normal) > 0. {
(
-rec.normal,
self.ref_idx,
self.ref_idx * dot(r_in.direction, rec.normal) / r_in.direction.length(),
)
} else {
(
rec.normal,
1. / self.ref_idx,
-dot(r_in.direction, rec.normal) / r_in.direction.length(),
)
};
let scattered = if let Some(refracted) = refract(r_in.direction, outward_normal, ni_over_nt)
{
let mut rng = rand::thread_rng();
if rng.gen_range(0., 1.) < schlick(cosine, self.ref_idx) {
Ray::new(rec.p, reflected, r_in.time)
} else {
Ray::new(rec.p, refracted, r_in.time)
}
} else {
Ray::new(rec.p, reflected, r_in.time)
};
ScatterResponse {
attenutation: Vec3::new(1., 1., 1.),
scattered,
reflected: true,
}
}
}
pub struct DiffuseLight<T>
where
T: Texture,
{
emit: T,
}
impl<T> DiffuseLight<T>
where
T: Texture,
{
pub fn new(emit: T) -> DiffuseLight<T> {
DiffuseLight { emit }
}
}
impl<T> Material for DiffuseLight<T>
where
T: Texture,
{
fn scatter(&self, _r_in: &Ray, _rec: &HitRecord) -> ScatterResponse {
ScatterResponse {
scattered: Default::default(),
attenutation: Default::default(),
reflected: false,
}
}
fn emitted(&self, u: f32, v: f32, p: Vec3) -> Vec3 {
self.emit.value(u, v, p)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::texture::ConstantTexture;
#[test]
fn arc_material() {
let white: Arc<dyn Material> =
Arc::new(Lambertian::new(ConstantTexture::new([0.73, 0.73, 0.73])));
fn material_fn<M>(m: M)
where
M: Material,
{
let _ = m;
}
material_fn(Arc::clone(&white));
material_fn(Arc::clone(&white));
}
}

View File

@@ -0,0 +1,101 @@
use crate::aabb::surrounding_box;
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::material::Material;
use crate::ray::Ray;
use crate::sphere::get_sphere_uv;
use crate::vec3::dot;
use crate::vec3::Vec3;
pub struct MovingSphere<M>
where
M: Material,
{
center0: Vec3,
center1: Vec3,
radius: f32,
material: M,
time0: f32,
time1: f32,
}
impl<M> MovingSphere<M>
where
M: Material,
{
pub fn new(
center0: Vec3,
center1: Vec3,
radius: f32,
time0: f32,
time1: f32,
material: M,
) -> MovingSphere<M> {
MovingSphere {
center0,
center1,
radius,
material,
time0,
time1,
}
}
fn center(&self, time: f32) -> Vec3 {
self.center0
+ ((time - self.time0) / (self.time1 - self.time0)) * (self.center1 - self.center0)
}
}
impl<M> Hit for MovingSphere<M>
where
M: Material,
{
fn hit(&self, r: Ray, t0: f32, t1: f32) -> Option<HitRecord> {
let oc = r.origin - self.center(r.time);
let a = dot(r.direction, r.direction);
let b = dot(oc, r.direction);
let c = dot(oc, oc) - self.radius * self.radius;
let discriminant = b * b - a * c;
if discriminant > 0. {
let temp = (-b - (b * b - a * c).sqrt()) / a;
if temp < t1 && temp > t0 {
let point = r.point_at_parameter(temp);
let uv = get_sphere_uv((point - self.center(r.time)) / self.radius);
return Some(HitRecord {
t: temp,
uv,
p: point,
normal: (point - self.center(r.time)) / self.radius,
material: &self.material,
});
}
let temp = (-b + (b * b - a * c).sqrt()) / a;
if temp < t1 && temp > t0 {
let point = r.point_at_parameter(temp);
let uv = get_sphere_uv((point - self.center(r.time)) / self.radius);
return Some(HitRecord {
t: temp,
uv,
p: point,
normal: (point - self.center(r.time)) / self.radius,
material: &self.material,
});
}
}
None
}
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
let t_min_bb = AABB::new(
self.center0 - Vec3::new(self.radius, self.radius, self.radius),
self.center0 + Vec3::new(self.radius, self.radius, self.radius),
);
let t_max_bb = AABB::new(
self.center1 - Vec3::new(self.radius, self.radius, self.radius),
self.center1 + Vec3::new(self.radius, self.radius, self.radius),
);
Some(surrounding_box(&t_min_bb, &t_max_bb))
}
}

View File

@@ -0,0 +1,68 @@
use log::trace;
/// Implements the concepts from https://lodev.org/cgtutor/randomnoise.html
use rand;
use crate::noise::NoiseSource;
use crate::vec3::Vec3;
const NOISE_SIZE: usize = 128;
pub struct Lode {
// Using fixed array causes stack overflow.
noise: Vec<Vec<Vec<f32>>>, //[[[f32; NOISE_SIZE]; NOISE_SIZE]; NOISE_SIZE],
}
impl Lode {
pub fn new<R>(rng: &mut R) -> Lode
where
R: rand::Rng,
{
let mut noise = vec![vec![vec![0.; NOISE_SIZE]; NOISE_SIZE]; NOISE_SIZE];
// Squelch warning about only being used to index, as this way reads more consistently.
#[allow(clippy::needless_range_loop)]
for x in 0..NOISE_SIZE {
for y in 0..NOISE_SIZE {
for z in 0..NOISE_SIZE {
noise[x][y][z] = rng.gen_range(0., 1.);
}
}
}
Lode { noise }
}
}
impl NoiseSource for Lode {
fn value(&self, p: Vec3) -> f32 {
// Fractional part of vector components
let x = p.x - p.x.floor();
let y = p.y - p.y.floor();
let z = p.z - p.z.floor();
// Wrap around the arrays
let x1 = (p.x.floor() as usize + NOISE_SIZE) % NOISE_SIZE;
let y1 = (p.y.floor() as usize + NOISE_SIZE) % NOISE_SIZE;
let z1 = (p.z.floor() as usize + NOISE_SIZE) % NOISE_SIZE;
// Neighbor values
let x2 = (x1 as usize + NOISE_SIZE - 1) % NOISE_SIZE;
let y2 = (y1 as usize + NOISE_SIZE - 1) % NOISE_SIZE;
let z2 = (z1 as usize + NOISE_SIZE - 1) % NOISE_SIZE;
trace!(target: "noise", "p {}", p);
trace!(target: "noise", "x {:.2} y {:.2} z {:.2}", x, y, z);
trace!(target: "noise", "x1 {:.2} y1 {:.2} z1 {:.2}", x1, y1, z1);
trace!(target: "noise", "x2 {:.2} y2 {:.2} z2 {:.2}", x2, y2, z2);
let mut value = 0.;
// Smooth the noise with bilinear interpolation. Completely untested
value += x * y * z * self.noise[x1][y1][z1];
value += (1. - x) * y * z * self.noise[x2][y1][z1];
value += x * (1. - y) * z * self.noise[x1][y2][z1];
value += x * y * (1. - z) * self.noise[x1][y1][z2];
value += (1. - x) * (1. - y) * z * self.noise[x2][y2][z1];
value += x * (1. - y) * (1. - z) * self.noise[x1][y2][z2];
value += (1. - x) * y * (1. - z) * self.noise[x2][y1][z2];
value += (1. - x) * (1. - y) * (1. - z) * self.noise[x2][y2][z2];
trace!(target: "noise", "luma {}", value);
value
}
}

View File

@@ -0,0 +1,96 @@
pub mod lode;
pub mod perlin;
use std::f32::consts::PI;
use serde_derive::Deserialize;
use crate::vec3::Vec3;
pub trait NoiseSource: Send + Sync {
/// value returns noise on the interval [0., 1.).
fn value(&self, p: Vec3) -> f32;
fn marble(&self, p: Vec3, period: Vec3, power: f32, size: usize, scale: f32) -> f32 {
let p = p / scale;
// TODO(wathiede): can't understand why 255 works for perlin and lode, maybe it's near 360
// degrees and interacts with the sine function?
let xyz_value = p.x * period.x / 255.
+ p.y * period.y / 255.
+ p.z * period.z / 255.
+ power * self.turbulence(p, size);
(xyz_value * PI).sin().abs()
}
fn turbulence(&self, p: Vec3, factor: usize) -> f32 {
let mut value = 0.;
let initial_factor = factor;
let mut factor = factor;
while factor >= 1 {
value += self.value(p / factor as f32) * factor as f32;
factor /= 2;
}
0.5 * value / initial_factor as f32
}
fn scaled(&self, p: Vec3, scale: f32) -> f32 {
let p = p / scale;
self.value(p)
}
}
impl NoiseSource for Box<dyn NoiseSource> {
fn value(&self, p: Vec3) -> f32 {
(**self).value(p)
}
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(tag = "type")]
pub enum NoiseType {
Scale(f32),
Turbulence(usize),
Marble {
period: Vec3,
power: f32,
size: usize,
scale: f32,
},
}
impl NoiseType {
pub fn to_url(&self) -> String {
match &self {
NoiseType::Scale(scale) => format!("scale/{}", scale),
NoiseType::Turbulence(turbulence) => format!("turbulence/{}", turbulence),
NoiseType::Marble {
period,
power,
size,
scale,
} => format!(
"marble/period/{},{},{}/power/{}/size/{}/scale/{}",
period.x, period.y, period.z, power, size, scale
),
}
}
pub fn parameters(&self) -> Vec<(&str, String)> {
match &self {
NoiseType::Scale(scale) => vec![("Scale", scale.to_string())],
NoiseType::Turbulence(turbulence) => vec![("Turbulence", turbulence.to_string())],
NoiseType::Marble {
period,
power,
size,
scale,
} => vec![
("Period", period.to_string()),
("Power", power.to_string()),
("Size", size.to_string()),
("Scale", scale.to_string()),
],
}
}
}

View File

@@ -0,0 +1,152 @@
// There are many math functions in this file, so we allow single letter variable names.
#![allow(clippy::many_single_char_names)]
use log::trace;
use rand::Rng;
use crate::noise::NoiseSource;
use crate::vec3::dot;
use crate::vec3::Vec3;
pub struct Perlin {
ran_vec: Vec<Vec3>,
perm_x: Vec<usize>,
perm_y: Vec<usize>,
perm_z: Vec<usize>,
}
fn perlin_generate<R>(rng: &mut R) -> Vec<Vec3>
where
R: Rng,
{
(0..256)
.map(|_| {
Vec3::new(
rng.gen_range(-1., 1.),
rng.gen_range(-1., 1.),
rng.gen_range(-1., 1.),
)
.unit_vector()
})
.collect()
}
fn perlin_generate_perm<R>(rng: &mut R) -> Vec<usize>
where
R: Rng,
{
let mut p: Vec<usize> = (0..256).map(|i| i).collect();
rng.shuffle(&mut p);
p
}
fn perlin_interp(c: [[[Vec3; 2]; 2]; 2], u: f32, v: f32, w: f32) -> f32 {
// Hermite cubic to round off interpolation and attempt to address 'mach bands'.
let uu = u * u * (3. - 2. * u);
let vv = v * v * (3. - 2. * v);
let ww = w * w * (3. - 2. * w);
let mut accum = 0.;
for i in 0..2 {
for j in 0..2 {
for k in 0..2 {
let weight_v = Vec3::new(u - i as f32, v - j as f32, w - k as f32);
let i = i as f32;
let j = j as f32;
let k = k as f32;
accum += (i * uu + (1. - i) * (1. - uu))
* (j * vv + (1. - j) * (1. - vv))
* (k * ww + (1. - k) * (1. - ww))
* dot(c[i as usize][j as usize][k as usize], weight_v);
}
}
}
//info!("u {} v {} accum {}", u, v, accum);
accum
}
impl Perlin {
pub fn new<R>(rng: &mut R) -> Perlin
where
R: Rng,
{
let p = Perlin {
ran_vec: perlin_generate(rng),
perm_x: perlin_generate_perm(rng),
perm_y: perlin_generate_perm(rng),
perm_z: perlin_generate_perm(rng),
};
trace!(target: "perlin",
"ran_vec: {}",
p.ran_vec
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(", ")
);
trace!(target: "perlin",
"perm_x: {}",
p.perm_x
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(", ")
);
trace!(target: "perlin",
"perm_y: {}",
p.perm_y
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(", ")
);
trace!(target: "perlin",
"perm_z: {}",
p.perm_z
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(", ")
);
p
}
pub fn turb(&self, p: Vec3, depth: usize) -> f32 {
let mut accum = 0.;
let mut temp_p = p;
let mut weight = 1.;
for _ in 0..depth {
accum += weight * self.value(temp_p);
weight *= 0.5;
temp_p = temp_p * 0.2;
}
accum.abs()
}
}
impl NoiseSource for Perlin {
fn value(&self, p: Vec3) -> f32 {
let u = p.x - p.x.floor();
let v = p.y - p.y.floor();
let w = p.z - p.z.floor();
let i = p.x.floor() as usize;
let j = p.y.floor() as usize;
let k = p.z.floor() as usize;
let mut c: [[[Vec3; 2]; 2]; 2] = Default::default();
// Squelch warning about di only being used to index, as this way reads more consistently.
#[allow(clippy::needless_range_loop)]
for di in 0..2 {
for dj in 0..2 {
for dk in 0..2 {
c[di][dj][dk] = self.ran_vec[self.perm_x[(i + di) & 255]
^ self.perm_y[(j + dj) & 255]
^ self.perm_z[(k + dk) & 255]]
}
}
}
(1. + perlin_interp(c, u, v, w)) * 0.5
}
}

View File

@@ -0,0 +1,222 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::BufWriter;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
use std::time;
use chrono::Local;
use image;
use lazy_static::lazy_static;
use log::info;
use serde_derive::Serialize;
use crate::renderer::Scene;
use crate::vec3::Vec3;
// Main RGB image output from rendering the scene.
pub const MAIN_IMAGE: &str = "@final";
// Debug image for adaptive pixels subsampling.
// Red indicates recursion hit maximum depth splitting the pixel.
// Green indicates no subdivision necessary
pub const ADAPTIVE_DEPTH: &str = "adaptive_depth";
// Grey scale showing rays cast per pixel.
pub const RAYS_PER_PIXEL: &str = "rays_per_pixel";
lazy_static! {
static ref DEBUGGER: Arc<Mutex<Debugger>> = Arc::new(Mutex::new(Debugger::new()));
}
#[derive(Serialize)]
struct ImageMetadata {
name: String,
image: String,
binary: String,
ratio: f32,
size: (usize, usize),
format: ImageType,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Data<'s> {
timestamp: i64,
render_time_seconds: f32,
scene: &'s Scene,
image_metadata: Vec<ImageMetadata>,
}
#[derive(Clone, Copy, Serialize)]
pub enum ImageType {
RGB01,
Grey01,
GreyNormalized,
}
struct Image {
w: usize,
h: usize,
pix: Vec<Vec3>,
}
impl Image {
fn new(w: usize, h: usize) -> Image {
Image {
w,
h,
pix: (0..w * h).map(|_| [0., 0., 0.].into()).collect(),
}
}
fn put_pixel(&mut self, x: usize, y: usize, p: Vec3) {
let offset = x + y * self.w;
self.pix[offset] = p;
}
}
struct Debugger {
images: HashMap<String, (ImageType, Image)>,
}
impl Debugger {
fn new() -> Debugger {
Debugger {
images: HashMap::new(),
}
}
}
pub fn register_image(name: String, dimensions: (usize, usize), it: ImageType) {
let mut debugger = DEBUGGER.lock().unwrap();
debugger
.images
.insert(name, (it, Image::new(dimensions.0, dimensions.1)));
}
pub fn set_pixel(name: &str, x: usize, y: usize, pixel: Vec3) {
let mut debugger = DEBUGGER.lock().unwrap();
let (_it, img) = debugger
.images
.get_mut(name)
.expect(&format!("couldn't find image named '{}'", name));
let y_inv = img.h - y - 1;
img.put_pixel(x, y_inv, pixel);
}
pub fn set_pixel_grey(name: &str, x: usize, y: usize, grey: f32) {
let mut debugger = DEBUGGER.lock().unwrap();
let (_it, img) = debugger
.images
.get_mut(name)
.expect(&format!("couldn't find image named '{}'", name));
let y_inv = img.h - y - 1;
img.put_pixel(x, y_inv, [grey, grey, grey].into());
}
trait ImageSaver {
fn save<Q>(&self, path: Q) -> std::io::Result<()>
where
Q: AsRef<Path> + Sized;
}
pub fn write_images<P: AsRef<Path>>(
scene: &Scene,
render_time: time::Duration,
output_dir: P,
) -> std::io::Result<()> {
let output_dir: &Path = output_dir.as_ref();
let debugger = DEBUGGER.lock().unwrap();
let now = Local::now();
// Write out images in consistent order.
let mut names = debugger.images.keys().collect::<Vec<_>>();
names.sort();
let mut image_metadata = Vec::new();
for name in &names {
let (it, img) = debugger.images.get(*name).unwrap();
let image = format!("{}.png", name);
let binary = format!("{}.json", name);
let ratio = img.w as f32 / img.h as f32;
let size = (img.w, img.h);
let image_path = output_dir.join(&image);
let binary_path = output_dir.join(&binary);
image_metadata.push(ImageMetadata {
name: name.to_string(),
image,
binary,
ratio,
size,
format: *it,
});
info!("Saving {}", image_path.to_string_lossy());
match it {
ImageType::RGB01 => {
let mut out_img = image::RgbImage::new(img.w as u32, img.h as u32);
out_img
.enumerate_pixels_mut()
.enumerate()
.for_each(|(i, (_x, _y, p))| {
let pixel = img.pix[i];
*p = image::Rgb([
(pixel[0] * 255.).min(255.) as u8,
(pixel[1] * 255.).min(255.) as u8,
(pixel[2] * 255.).min(255.) as u8,
])
});
out_img.save(image_path)?;
}
ImageType::Grey01 => {
let mut out_img = image::GrayImage::new(img.w as u32, img.h as u32);
out_img
.enumerate_pixels_mut()
.enumerate()
.for_each(|(i, (_x, _y, p))| {
let pixel = img.pix[i];
*p = image::Luma([(pixel[0] * 255.).min(255.) as u8])
});
out_img.save(image_path)?;
}
ImageType::GreyNormalized => {
let mut out_img = image::GrayImage::new(img.w as u32, img.h as u32);
let max_val = img.pix.iter().map(|v| v.x).fold(0., f32::max);
out_img
.enumerate_pixels_mut()
.enumerate()
.for_each(|(i, (_x, _y, p))| {
let pixel = img.pix[i];
*p = image::Luma([(pixel[0] / max_val * 255.).min(255.) as u8])
});
out_img.save(image_path)?;
}
};
info!("Saving {}", binary_path.to_string_lossy());
let f = File::create(output_dir.join(binary_path))?;
let f = BufWriter::new(f);
match it {
ImageType::RGB01 => {
serde_json::ser::to_writer(
f,
&img.pix
.iter()
.map(|v| [v.x, v.y, v.z])
.collect::<Vec<[f32; 3]>>(),
)?;
}
ImageType::Grey01 | ImageType::GreyNormalized => {
serde_json::ser::to_writer(f, &img.pix.iter().map(|v| v.x).collect::<Vec<f32>>())?;
}
};
}
let f = File::create(output_dir.join("data.json"))?;
let f = BufWriter::new(f);
serde_json::ser::to_writer(
f,
&Data {
timestamp: now.timestamp(),
render_time_seconds: render_time.as_secs_f32(),
scene,
image_metadata,
},
)?;
Ok(())
}

35
rtiow/renderer/src/ray.rs Normal file
View File

@@ -0,0 +1,35 @@
use crate::vec3::Vec3;
#[derive(Copy, Clone, Debug, Default)]
pub struct Ray {
pub origin: Vec3,
pub direction: Vec3,
pub time: f32,
pub inv_direction: Vec3,
pub sign: [usize; 3],
}
impl Ray {
pub fn new<V>(origin: V, direction: V, time: f32) -> Ray
where
V: Into<Vec3>,
{
let direction = direction.into();
let origin = origin.into();
let inv = 1. / direction;
Ray {
origin,
direction,
time,
inv_direction: inv,
sign: [
(inv.x < 0.) as usize,
(inv.y < 0.) as usize,
(inv.z < 0.) as usize,
],
}
}
pub fn point_at_parameter(self, t: f32) -> Vec3 {
self.origin + self.direction * t
}
}

191
rtiow/renderer/src/rect.rs Normal file
View File

@@ -0,0 +1,191 @@
// There are many math functions in this file, so we allow single letter variable names.
#![allow(clippy::many_single_char_names)]
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::material::Material;
use crate::ray::Ray;
use crate::vec3::Vec3;
pub struct XYRect<M>
where
M: Material,
{
x0: f32,
x1: f32,
y0: f32,
y1: f32,
k: f32,
material: M,
}
impl<M> XYRect<M>
where
M: Material,
{
pub fn new(x0: f32, x1: f32, y0: f32, y1: f32, k: f32, material: M) -> XYRect<M> {
XYRect {
x0,
x1,
y0,
y1,
k,
material,
}
}
}
impl<M> Hit for XYRect<M>
where
M: Material,
{
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
let t = (self.k - r.origin.z) / r.direction.z;
if t < t_min || t > t_max {
return None;
}
let x = r.origin.x + t * r.direction.x;
let y = r.origin.y + t * r.direction.y;
if x < self.x0 || x > self.x1 || y < self.y0 || y > self.y1 {
return None;
}
let u = (x - self.x0) / (self.x1 - self.x0);
let v = (y - self.y0) / (self.y1 - self.y0);
Some(HitRecord {
t,
uv: (u, v),
p: r.point_at_parameter(t),
normal: Vec3::new(0., 0., 1.),
material: &self.material,
})
}
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
Some(AABB::new(
Vec3::new(self.x0, self.y0, self.k - 0.0001),
Vec3::new(self.x1, self.y1, self.k + 0.0001),
))
}
}
pub struct XZRect<M>
where
M: Material,
{
x0: f32,
x1: f32,
z0: f32,
z1: f32,
k: f32,
material: M,
}
impl<M> XZRect<M>
where
M: Material,
{
pub fn new(x0: f32, x1: f32, z0: f32, z1: f32, k: f32, material: M) -> XZRect<M> {
XZRect {
x0,
x1,
z0,
z1,
k,
material,
}
}
}
impl<M> Hit for XZRect<M>
where
M: Material,
{
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
let t = (self.k - r.origin.y) / r.direction.y;
if t < t_min || t > t_max {
return None;
}
let x = r.origin.x + t * r.direction.x;
let z = r.origin.z + t * r.direction.z;
if x < self.x0 || x > self.x1 || z < self.z0 || z > self.z1 {
return None;
}
let u = (x - self.x0) / (self.x1 - self.x0);
let v = (z - self.z0) / (self.z1 - self.z0);
Some(HitRecord {
t,
uv: (u, v),
p: r.point_at_parameter(t),
normal: Vec3::new(0., 1., 0.),
material: &self.material,
})
}
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
Some(AABB::new(
Vec3::new(self.x0, self.k - 0.0001, self.z0),
Vec3::new(self.x1, self.k + 0.0001, self.z1),
))
}
}
pub struct YZRect<M>
where
M: Material,
{
y0: f32,
y1: f32,
k: f32,
z0: f32,
z1: f32,
material: M,
}
impl<M> YZRect<M>
where
M: Material,
{
pub fn new(y0: f32, y1: f32, z0: f32, z1: f32, k: f32, material: M) -> YZRect<M> {
YZRect {
y0,
y1,
z0,
z1,
k,
material,
}
}
}
impl<M> Hit for YZRect<M>
where
M: Material,
{
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
let t = (self.k - r.origin.x) / r.direction.x;
if t < t_min || t > t_max {
return None;
}
let y = r.origin.y + t * r.direction.y;
let z = r.origin.z + t * r.direction.z;
if y < self.y0 || y > self.y1 || z < self.z0 || z > self.z1 {
return None;
}
let u = (y - self.y0) / (self.y1 - self.y0);
let v = (z - self.z0) / (self.z1 - self.z0);
Some(HitRecord {
t,
uv: (u, v),
p: r.point_at_parameter(t),
normal: Vec3::new(1., 0., 0.),
material: &self.material,
})
}
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
Some(AABB::new(
Vec3::new(self.k - 0.0001, self.y0, self.z0),
Vec3::new(self.k + 0.0001, self.y1, self.z1),
))
}
}

View File

@@ -0,0 +1,649 @@
use std::fmt;
use std::ops::AddAssign;
use std::ops::Range;
use std::path::Path;
use std::path::PathBuf;
use std::str;
use std::sync;
use std::sync::mpsc::sync_channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::SyncSender;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
use std::time;
use core_affinity;
use log::info;
use log::trace;
use num_cpus;
use rand;
use rand::Rng;
use serde_derive::Serialize;
use structopt::StructOpt;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::human;
use crate::material::Lambertian;
use crate::output;
use crate::ray::Ray;
use crate::scenes;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::texture::EnvMap;
use crate::vec3::Vec3;
#[derive(Debug)]
pub enum Model {
BVH,
Bench,
Book,
CornellBox,
CornellSmoke,
Final,
Mandelbrot,
PerlinDebug,
Spheramid,
Test,
Tutorial,
}
impl Model {
pub fn scene(&self, opt: &Opt) -> Scene {
match self {
Model::BVH => scenes::bvh::new(&opt),
Model::Bench => scenes::bench::new(&opt),
Model::Book => scenes::book::new(&opt),
Model::CornellBox => scenes::cornell_box::new(&opt),
Model::CornellSmoke => scenes::cornell_smoke::new(&opt),
Model::Final => scenes::final_scene::new(&opt),
Model::Mandelbrot => scenes::mandelbrot::new(&opt),
Model::PerlinDebug => scenes::perlin_debug::new(&opt),
Model::Spheramid => scenes::spheramid::new(&opt),
Model::Test => scenes::test::new(&opt),
Model::Tutorial => scenes::tutorial::new(&opt),
}
}
}
#[derive(Debug)]
pub struct ModelParseError(String);
impl fmt::Display for ModelParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "unknown model enum type '{}'", self.0)
}
}
impl str::FromStr for Model {
type Err = ModelParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"bench" => Ok(Model::Bench),
"book" => Ok(Model::Book),
"bvh" => Ok(Model::BVH),
"cornell_box" => Ok(Model::CornellBox),
"cornell_smoke" => Ok(Model::CornellSmoke),
"final" => Ok(Model::Final),
"mandelbrot" => Ok(Model::Mandelbrot),
"perlin_debug" => Ok(Model::PerlinDebug),
"spheramid" => Ok(Model::Spheramid),
"test" => Ok(Model::Test),
"tutorial" => Ok(Model::Tutorial),
_ => Err(ModelParseError(s.to_owned())),
}
}
}
impl std::string::ToString for Model {
fn to_string(&self) -> String {
match self {
Model::BVH => "bvh".to_string(),
Model::Bench => "bench".to_string(),
Model::Book => "book".to_string(),
Model::CornellBox => "cornell_box".to_string(),
Model::CornellSmoke => "cornell_smoke".to_string(),
Model::Final => "final".to_string(),
Model::Mandelbrot => "mandelbrot".to_string(),
Model::PerlinDebug => "perlin_debug".to_string(),
Model::Spheramid => "spheramid".to_string(),
Model::Test => "test".to_string(),
Model::Tutorial => "tutorial".to_string(),
}
}
}
#[derive(Debug, StructOpt)]
#[structopt(name = "tracer", about = "An experimental ray tracer.")]
pub struct Opt {
/// Image width
#[structopt(short = "w", long = "width", default_value = "512")]
pub width: usize,
/// Image height
#[structopt(short = "h", long = "height", default_value = "512")]
pub height: usize,
/// Number of threads
#[structopt(short = "t", long = "num_threads")]
pub num_threads: Option<usize>,
/// Sub-samples per pixel
#[structopt(short = "s", long = "subsample", default_value = "8")]
pub subsamples: usize,
/// Select scene to render, one of: "bench", "book", "tutorial", "bvh", "test", "cornell_box",
/// "cornell_smoke", "perlin_debug", "final"
#[structopt(long = "model", default_value = "book")]
pub model: Model,
/// Path to store pprof profile data, i.e. /tmp/cpuprofile.pprof
#[structopt(long = "pprof", parse(from_os_str))]
pub pprof: Option<PathBuf>,
/// Use acceleration data structure, may be BVH or kd-tree depending on scene.
#[structopt(long = "use_accel")]
pub use_accel: bool,
/// Output directory
#[structopt(parse(from_os_str), default_value = "/tmp/tracer")]
pub output: PathBuf,
}
pub fn opt_hash(opt: &Opt) -> String {
// TODO(wathiede): add threads.
format!(
"w:{}-h:{}-s:{}-pprof:{}-model:{}-use_accel:{}-{}",
opt.width,
opt.height,
opt.subsamples,
opt.pprof.is_some(),
opt.model.to_string(),
opt.use_accel,
opt.output.display().to_string().replace("/", "_")
)
}
// TODO(wathiede): implement the skips and then the renderer could use json as an input file type.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Scene {
#[serde(skip)]
pub world: Box<dyn Hit>,
#[serde(skip)]
pub camera: Camera,
pub subsamples: usize,
/// overrides subsamples setting.
pub adaptive_subsampling: Option<f32>,
pub num_threads: Option<usize>,
pub width: usize,
pub height: usize,
pub global_illumination: bool,
#[serde(skip)]
pub env_map: Option<EnvMap>,
}
impl Default for Scene {
fn default() -> Scene {
let lookfrom = Vec3::new(20., 20., 20.);
let lookat = Vec3::new(0., 0., 0.);
let dist_to_focus = (lookfrom - lookat).length();
let aperture = 0.1;
let time_min = 0.;
let time_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
70.,
1.,
aperture,
dist_to_focus,
time_min,
time_max,
);
Scene {
world: Box::new(Sphere::new(
Vec3::new(0., 0., 0.),
1.0,
Lambertian::new(ConstantTexture::new([0., 1., 0.])),
)),
camera,
subsamples: 0,
adaptive_subsampling: None,
num_threads: None,
width: 0,
height: 0,
global_illumination: false,
env_map: None,
}
}
}
// color will trace ray up to 50 bounces deep accumulating color as it goes. If
// global_illumination is true, a default light background color is assumed and will light the
// world. If false, it is expected the scene has emissive light sources.
fn color(
r: Ray,
world: &dyn Hit,
depth: usize,
global_illumination: bool,
env_map: &Option<EnvMap>,
) -> (Vec3, usize) {
if let Some(rec) = world.hit(r, 0.001, std::f32::MAX) {
let (u, v) = rec.uv;
let emitted = rec.material.emitted(u, v, rec.p);
let scatter_response = rec.material.scatter(&r, &rec);
if depth < 50 && scatter_response.reflected {
let (c, rays) = color(
scatter_response.scattered,
world,
depth + 1,
global_illumination,
env_map,
);
return (emitted + scatter_response.attenutation * c, rays + 1);
} else {
return (emitted, 1);
}
}
if global_illumination {
return match env_map {
Some(env_map) => (env_map.color(r.direction.unit_vector()), 1),
None => {
let unit_direction = r.direction.unit_vector();
// No hit, choose color from background.
let t = 0.5 * (unit_direction.y + 1.);
(
Vec3::new(1., 1., 1.) * (1. - t) + Vec3::new(0.5, 0.7, 1.) * t,
1,
)
}
};
}
// No global illumination, so background is black.
(Vec3::new(0., 0., 0.), 1)
}
const MAX_ADAPTIVE_DEPTH: usize = 10;
fn trace_pixel_adaptive(
depth: usize,
threshold: f32,
x: usize,
y: usize,
x_range: Range<f32>,
y_range: Range<f32>,
scene: &Scene,
) -> (Vec3, usize) {
let w = scene.width as f32;
let h = scene.height as f32;
let x_mid = x_range.start + ((x_range.end - x_range.start) / 2.);
let y_mid = y_range.start + ((y_range.end - y_range.start) / 2.);
let mc = ((x_mid + x as f32) / w, (y_mid + y as f32) / h);
let (center, rays) = color(
scene.camera.get_ray(mc.0, mc.1),
scene.world.as_ref(),
0,
scene.global_illumination,
&scene.env_map,
);
if depth == 0 {
output::set_pixel(output::ADAPTIVE_DEPTH, x, y, [1., 0., 0.].into());
return (center, rays);
}
// t = top
// m = middle
// b = bottom
// l = left
// c = center
// r = right
let tl = (
(x_range.start + x as f32) / w,
(y_range.start + y as f32) / h,
);
let tr = ((x_range.end + x as f32) / w, (y_range.start + y as f32) / h);
let bl = ((x_range.start + x as f32) / w, (y_range.end + y as f32) / h);
let br = ((x_range.end + x as f32) / w, (y_range.end + y as f32) / h);
let (corners, rays) = [tl, tr, mc, bl, br]
.iter()
.map(|(u, v)| {
color(
scene.camera.get_ray(*u, *v),
scene.world.as_ref(),
0,
scene.global_illumination,
&scene.env_map,
)
})
.fold(
([0., 0., 0.].into(), 0),
|(p1, r1): (Vec3, usize), (p2, r2): (Vec3, usize)| ((p1 + p2), (r1 + r2)),
);
let corners = corners / 5.;
if (corners - center).length() > threshold {
let tl = trace_pixel_adaptive(
depth - 1,
threshold,
x,
y,
x_range.start..x_mid,
y_range.start..y_mid,
scene,
);
let tr = trace_pixel_adaptive(
depth - 1,
threshold,
x,
y,
x_mid..x_range.end,
y_range.start..y_mid,
scene,
);
let bl = trace_pixel_adaptive(
depth - 1,
threshold,
x,
y,
x_range.start..x_mid,
y_mid..y_range.end,
scene,
);
let br = trace_pixel_adaptive(
depth - 1,
threshold,
x,
y,
x_mid..x_range.end,
y_mid..y_range.end,
scene,
);
let pixel = (tl.0 + tr.0 + bl.0 + br.0) / 4.;
let rays = tl.1 + tr.1 + bl.1 + br.1;
(pixel, rays)
} else {
if depth == MAX_ADAPTIVE_DEPTH {
output::set_pixel(output::ADAPTIVE_DEPTH, x, y, [0., 1., 0.].into());
}
(corners, rays)
}
}
fn trace_pixel_random(x: usize, y: usize, scene: &Scene) -> (Vec3, usize) {
let mut rng = rand::thread_rng();
let u = (rng.gen_range(0., 1.) + x as f32) / scene.width as f32;
let v = (rng.gen_range(0., 1.) + y as f32) / scene.height as f32;
let ray = scene.camera.get_ray(u, v);
color(
ray,
scene.world.as_ref(),
0,
scene.global_illumination,
&scene.env_map,
)
}
#[derive(Clone, Copy)]
struct RenderStats {
rays: usize,
pixels: usize,
}
impl AddAssign for RenderStats {
fn add_assign(&mut self, other: Self) {
*self = Self {
rays: self.rays + other.rays,
pixels: self.pixels + other.pixels,
}
}
}
fn progress(
last_stat: &RenderStats,
current_stat: &RenderStats,
time_diff: time::Duration,
pixel_total: usize,
) -> String {
let human = human::Formatter::new();
let pixel_diff = current_stat.pixels - last_stat.pixels;
let ray_diff = current_stat.rays - last_stat.rays;
format!(
"{:7} / {:7}pixels ({:2}%) {:7}pixels/s {:7}rays/s",
human.format(current_stat.pixels as f64),
human.format(pixel_total as f64),
100 * current_stat.pixels / pixel_total,
human.format(pixel_diff as f64 / time_diff.as_secs_f64()),
human.format(ray_diff as f64 / time_diff.as_secs_f64())
)
}
impl Default for RenderStats {
fn default() -> Self {
RenderStats { rays: 0, pixels: 0 }
}
}
enum Request {
Pixel { x: usize, y: usize },
Line { width: usize, y: usize },
}
enum Response {
Pixel {
x: usize,
y: usize,
pixel: Vec3,
rs: RenderStats,
},
Line {
y: usize,
pixels: Vec<Vec3>,
rs: RenderStats,
},
}
fn render_pixel(scene: &Scene, x: usize, y: usize) -> (Vec3, usize) {
let (pixel, rays) = if let Some(threshold) = scene.adaptive_subsampling {
trace_pixel_adaptive(
MAX_ADAPTIVE_DEPTH,
threshold,
x,
y,
0.0..1.0,
0.0..1.0,
scene,
)
} else {
let (pixel, rays) = (0..scene.subsamples)
.map(|_| trace_pixel_random(x, y, scene))
.fold(
([0., 0., 0.].into(), 0),
|(p1, r1): (Vec3, usize), (p2, r2): (Vec3, usize)| ((p1 + p2), (r1 + r2)),
);
output::set_pixel_grey(output::RAYS_PER_PIXEL, x, y, rays as f32);
(pixel / scene.subsamples as f32, rays)
};
// Gamma correct, use gamma 2 correction, which is 1/gamma where gamma=2 which is 1/2 or
// sqrt.
(
Vec3::new(pixel[0].sqrt(), pixel[1].sqrt(), pixel[2].sqrt()),
rays,
)
}
fn render_worker(
tid: usize,
scene: &Scene,
input_chan: Arc<Mutex<Receiver<Request>>>,
output_chan: &SyncSender<Response>,
) {
loop {
let job = { input_chan.lock().unwrap().recv() };
match job {
Err(err) => {
trace!("Shutting down render_worker {}: {}", tid, err);
return;
}
Ok(req) => match req {
Request::Line { width, y } => {
trace!("tid {} width {} y {}", tid, width, y);
let batch = false;
if batch {
let (pixels, rays): (Vec<Vec3>, Vec<usize>) = (0..width)
.map(|x| render_pixel(scene, x, y))
.collect::<Vec<(_, _)>>()
.into_iter()
.unzip();
let rays = rays.iter().sum();
output_chan
.send(Response::Line {
y,
pixels,
rs: RenderStats {
rays,
pixels: width,
},
})
.expect("failed to send pixel response");
} else {
(0..width).for_each(|x| {
let (pixel, rays) = render_pixel(scene, x, y);
output_chan
.send(Response::Pixel {
x,
y,
pixel,
rs: RenderStats { rays, pixels: 1 },
})
.expect("failed to send pixel response");
});
}
}
Request::Pixel { x, y } => {
trace!("tid {} x {} y {}", tid, x, y);
let (pixel, rays) = render_pixel(scene, x, y);
output_chan
.send(Response::Pixel {
x,
y,
pixel,
rs: RenderStats { rays, pixels: 1 },
})
.expect("failed to send line response");
}
},
}
}
}
pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::io::Error> {
let num_threads = scene.num_threads.unwrap_or_else(num_cpus::get);
let (pixel_req_tx, pixel_req_rx) = sync_channel(2 * num_threads);
let (pixel_resp_tx, pixel_resp_rx) = sync_channel(2 * num_threads);
let scene = Arc::new(scene);
let pixel_req_rx = Arc::new(Mutex::new(pixel_req_rx));
info!("Adaptive subsampling: {:?}", scene.adaptive_subsampling);
// Retrieve the IDs of all active CPU cores.
let core_ids = core_affinity::get_core_ids().unwrap();
let core_ids = if core_ids.len() > num_threads {
core_ids[..num_threads].to_vec()
} else {
core_ids
};
info!("Creating {} render threads", core_ids.len());
output::register_image(
output::MAIN_IMAGE.to_string(),
(scene.width, scene.height),
output::ImageType::RGB01,
);
if scene.adaptive_subsampling.is_some() {
output::register_image(
output::ADAPTIVE_DEPTH.to_string(),
(scene.width, scene.height),
output::ImageType::RGB01,
);
}
output::register_image(
output::RAYS_PER_PIXEL.to_string(),
(scene.width, scene.height),
output::ImageType::GreyNormalized,
);
// Create a thread for each active CPU core.
let mut handles = core_ids
.into_iter()
.enumerate()
.filter(|(i, _id)| *i < num_threads)
.map(|(i, id)| {
let s = sync::Arc::clone(&scene);
let pixel_req_rx = pixel_req_rx.clone();
let pixel_resp_tx = pixel_resp_tx.clone();
thread::spawn(move || {
core_affinity::set_for_current(id);
render_worker(i, &s, pixel_req_rx, &pixel_resp_tx);
})
})
.collect::<Vec<_>>();
drop(pixel_req_rx);
drop(pixel_resp_tx);
let start_time = time::Instant::now();
let (w, h) = (scene.width, scene.height);
handles.push(thread::spawn(move || {
let batch_line_requests = true;
if batch_line_requests {
for y in 0..h {
pixel_req_tx
.send(Request::Line { width: w, y })
.expect("failed to send line request");
}
} else {
for y in 0..h {
for x in 0..w {
pixel_req_tx
.send(Request::Pixel { x, y })
.expect("failed to send pixel request");
}
}
}
drop(pixel_req_tx);
}));
info!("Rendering with {} subsamples", scene.subsamples);
let pixel_total = scene.width * scene.height;
let mut last_time = time::Instant::now();
let mut last_stat: RenderStats = Default::default();
let mut current_stat: RenderStats = Default::default();
for resp in pixel_resp_rx {
match resp {
Response::Pixel { x, y, pixel, rs } => {
current_stat += rs;
output::set_pixel(output::MAIN_IMAGE, x, y, pixel);
}
Response::Line { y, pixels, rs } => {
current_stat += rs;
for (x, pixel) in pixels.iter().enumerate() {
output::set_pixel(output::MAIN_IMAGE, x, y, *pixel);
}
}
}
let now = time::Instant::now();
let time_diff = now - last_time;
if time_diff > time::Duration::from_secs(1) {
info!(
"{}",
progress(&last_stat, &current_stat, time_diff, pixel_total)
);
last_stat = current_stat;
last_time = now;
}
}
for thr in handles {
thr.join().expect("thread join");
}
let time_diff = time::Instant::now() - start_time;
info!(
"Runtime {} seconds {}",
time_diff.as_secs_f32(),
progress(&Default::default(), &current_stat, time_diff, pixel_total)
);
output::write_images(&scene, time_diff, output_dir)
}

View File

@@ -0,0 +1,99 @@
use std::f32::consts::PI;
use std::f32::MAX;
use std::f32::MIN;
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::ray::Ray;
use crate::vec3::Vec3;
pub struct RotateY<H>
where
H: Hit,
{
hitable: H,
sin_theta: f32,
cos_theta: f32,
bbox: Option<AABB>,
}
impl<H> RotateY<H>
where
H: Hit,
{
pub fn new(hitable: H, angle: f32) -> RotateY<H> {
let radians = PI / 180. * angle;
let sin_theta = radians.sin();
let cos_theta = radians.cos();
let mut min = vec![MAX, MAX, MAX];
let mut max = vec![MIN, MIN, MIN];
let bbox = hitable.bounding_box(0., 1.).unwrap();
for i in 0..2 {
for j in 0..2 {
for k in 0..2 {
let x = i as f32 * bbox.max().x + (1 - i) as f32 * bbox.min().x;
let y = j as f32 * bbox.max().y + (1 - j) as f32 * bbox.min().y;
let z = k as f32 * bbox.max().z + (1 - k) as f32 * bbox.min().z;
let new_x = cos_theta * x + sin_theta * z;
let new_z = -sin_theta * x + cos_theta * z;
let tester = Vec3::new(new_x, y, new_z);
for c in 0..3 {
if tester[c] > max[c] {
max[c] = tester[c];
}
if tester[c] < min[c] {
min[c] = tester[c];
}
}
}
}
}
RotateY {
hitable,
sin_theta,
cos_theta,
bbox: Some(AABB::new(
Vec3::new(min[0], min[1], min[2]),
Vec3::new(max[0], max[1], max[2]),
)),
}
}
}
impl<H> Hit for RotateY<H>
where
H: Hit,
{
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
let origin = Vec3::new(
self.cos_theta * r.origin[0] - self.sin_theta * r.origin[2],
r.origin.y,
self.sin_theta * r.origin[0] + self.cos_theta * r.origin[2],
);
let direction = Vec3::new(
self.cos_theta * r.direction[0] - self.sin_theta * r.direction[2],
r.direction.y,
self.sin_theta * r.direction[0] + self.cos_theta * r.direction[2],
);
let rotated_r = Ray::new(origin, direction, r.time);
if let Some(rec) = self.hitable.hit(rotated_r, t_min, t_max) {
let p = Vec3::new(
self.cos_theta * rec.p[0] + self.sin_theta * rec.p[2],
rec.p[1],
-self.sin_theta * rec.p[0] + self.cos_theta * rec.p[2],
);
let normal = Vec3::new(
self.cos_theta * rec.normal[0] + self.sin_theta * rec.normal[2],
rec.normal[1],
-self.sin_theta * rec.normal[0] + self.cos_theta * rec.normal[2],
);
return Some(HitRecord { p, normal, ..rec });
}
None
}
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
self.bbox.clone()
}
}

View File

@@ -0,0 +1,78 @@
use log::trace;
use rand;
use rand::Rng;
use crate::bvh::BVH;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::Lambertian;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(20., 20., 20.);
let lookat = Vec3::new(0., 0., 0.);
let dist_to_focus = (lookfrom - lookat).length();
let aperture = 0.1;
let time_min = 0.;
let time_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
70.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let mut rng = rand::thread_rng();
let mut grid: Vec<Box<dyn Hit>> = Vec::new();
let len = 1000;
for x in 0..len {
for z in 0..len {
let r = rng.gen_range(0., 1.);
let g = rng.gen_range(0., 1.);
let b = rng.gen_range(0., 1.);
let x_pos = (x - len / 2) as f32 + rng.gen_range(-0.1, 0.1);
let z_pos = (z - len / 2) as f32 + rng.gen_range(-0.1, 0.1);
grid.push(Box::new(Sphere::new(
Vec3::new(x_pos, 0., z_pos),
0.5,
Lambertian::new(ConstantTexture::new(Vec3::new(r, g, b))),
)));
}
}
let world: Box<dyn Hit>;
if opt.use_accel {
let use_kd = true;
if use_kd {
let kdtree = KDTree::new(grid, time_min, time_max);
trace!("k-d tree world {}", kdtree);
world = Box::new(kdtree);
} else {
let bvh = BVH::new(grid, time_min, time_max);
trace!("BVH world {}", bvh);
world = Box::new(bvh);
}
} else {
world = Box::new(HitableList::new(grid));
}
Scene {
camera,
world,
subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
..Default::default()
}
}

View File

@@ -0,0 +1,144 @@
use rand;
use rand::Rng;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::Dielectric;
use crate::material::Lambertian;
use crate::material::Material;
use crate::material::Metal;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::CheckerTexture;
use crate::texture::ConstantTexture;
use crate::texture::EnvMap;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(13., 2., 3.);
let lookat = Vec3::new(0., 0., 0.);
let dist_to_focus = 10.;
let aperture = 0.1;
let time_min = 0.;
let time_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
20.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let ground_color = if opt.use_accel {
[1.0, 0.4, 0.4]
} else {
[0.4, 1.0, 0.4]
};
let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(random_scene(ground_color), time_min, time_max))
} else {
Box::new(HitableList::new(random_scene(ground_color)))
};
let skybox_bytes = include_bytes!("../../images/envmap.jpg");
let skybox = image::load_from_memory(skybox_bytes).unwrap().to_rgb();
Scene {
camera,
world,
subsamples: opt.subsamples,
adaptive_subsampling: Some(0.5),
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
global_illumination: true,
env_map: Some(EnvMap::new(skybox)),
..Default::default()
}
}
fn random_scene<V>(ground_color: V) -> Vec<Box<dyn Hit>>
where
V: Into<Vec3>,
{
let mut rng = rand::thread_rng();
let checker = true;
let ground_material: Box<dyn Material> = if checker {
Box::new(Lambertian::new(CheckerTexture::new(
ConstantTexture::new([0., 0., 0.]),
ConstantTexture::new(ground_color),
)))
} else {
Box::new(Lambertian::new(ConstantTexture::new(ground_color)))
};
let mut objects: Vec<Box<dyn Hit>> = vec![Box::new(Sphere::new(
[0., -1000., 0.],
1000.,
ground_material,
))];
let mut random = || rng.gen_range(0., 1.);
for a in -11..11 {
for b in -11..11 {
let choose_mat = random();
let center = Vec3::new(a as f32 + 0.9 * random(), 0.2, b as f32 + 0.9 * random());
if (center - Vec3::new(4., 0.2, 0.)).length() > 0.9 {
let sphere: Box<dyn Hit> = if choose_mat < 0.8 {
// diffuse
Box::new(Sphere::new(
center,
0.2,
Lambertian::new(ConstantTexture::new([
random() * random(),
random() * random(),
random() * random(),
])),
))
} else if choose_mat < 0.95 {
// metal
Box::new(Sphere::new(
center,
0.2,
Metal::new(
Vec3::new(
0.5 * (1. + random()),
0.5 * (1. + random()),
0.5 * (1. + random()),
),
0.5 * random(),
),
))
} else {
// glass
Box::new(Sphere::new(center, 0.2, Dielectric::new(1.5)))
};
objects.push(sphere);
};
}
}
let more: Vec<Box<dyn Hit>> = vec![
Box::new(Sphere::new(
Vec3::new(0., 1., 0.),
1.0,
Dielectric::new(1.5),
)),
Box::new(Sphere::new(
Vec3::new(-4., 1., 0.),
1.0,
Lambertian::new(ConstantTexture::new(Vec3::new(0.4, 0.2, 0.1))),
)),
Box::new(Sphere::new(
Vec3::new(4., 1., 0.),
1.0,
Metal::new(Vec3::new(0.7, 0.6, 0.5), 0.0),
)),
];
objects.extend(more);
objects
}

View File

@@ -0,0 +1,73 @@
use log::trace;
use crate::bvh::BVH;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::material::Lambertian;
use crate::material::Metal;
use crate::moving_sphere::MovingSphere;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(3., 3., 2.);
let lookat = Vec3::new(0., 0., -1.);
let dist_to_focus = (lookfrom - lookat).length();
let aperture = 0.1;
let time_min = 0.;
let time_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
45.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let b = BVH::new(
vec![
Box::new(Sphere::new(
Vec3::new(0., 0., -1.),
0.5,
Lambertian::new(ConstantTexture::new(Vec3::new(0.1, 0.2, 0.5))),
)),
Box::new(Sphere::new(
Vec3::new(0., -100.5, -1.),
100.,
Lambertian::new(ConstantTexture::new(Vec3::new(0.8, 0.8, 0.8))),
)),
Box::new(Sphere::new(
Vec3::new(1., 0., -1.),
0.5,
Metal::new(Vec3::new(0.6, 0.6, 0.6), 0.2),
)),
Box::new(MovingSphere::new(
Vec3::new(-1., 0., -1.25),
Vec3::new(-1., 0., -0.75),
0.5,
time_min,
time_max,
Lambertian::new(ConstantTexture::new(Vec3::new(0.2, 0.8, 0.2))),
)),
],
time_min,
time_max,
);
trace!(target: "bvh", "World {}", b);
let world: Box<dyn Hit> = Box::new(b);
Scene {
camera,
world,
subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
..Default::default()
}
}

View File

@@ -0,0 +1,139 @@
use std::sync::Arc;
use crate::camera::Camera;
use crate::cuboid::Cuboid;
use crate::flip_normals::FlipNormals;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::DiffuseLight;
use crate::material::Lambertian;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::rect::YZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::rotate::RotateY;
use crate::texture::ConstantTexture;
use crate::translate::Translate;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(278., 278., -800.);
let lookat = Vec3::new(278., 278., 0.);
let dist_to_focus = 10.0;
let aperture = 0.0;
let time_min = 0.;
let time_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
40.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let objects: Vec<Box<dyn Hit>> = vec![
// Box1
Box::new(Translate::new(
RotateY::new(
Cuboid::new(
Vec3::new(0., 0., 0.),
Vec3::new(165., 165., 165.),
Arc::new(Lambertian::new(ConstantTexture::new(Vec3::new(
0.73, 0.73, 0.73,
)))),
),
-18.,
),
Vec3::new(100., 0., 0.),
)),
// Box2
Box::new(Translate::new(
RotateY::new(
Cuboid::new(
Vec3::new(0., 0., 0.),
Vec3::new(165., 330., 165.),
Arc::new(Lambertian::new(ConstantTexture::new(Vec3::new(
0.73, 0.73, 0.73,
)))),
),
15.,
),
Vec3::new(265., 0., 295.),
)),
// Green wall left
Box::new(FlipNormals::new(YZRect::new(
0.,
555.,
0.,
555.,
555.,
Lambertian::new(ConstantTexture::new(Vec3::new(0.12, 0.45, 0.15))),
))),
// Red floor right
Box::new(YZRect::new(
0.,
555.,
0.,
555.,
0.,
Lambertian::new(ConstantTexture::new(Vec3::new(0.65, 0.05, 0.05))),
)),
// Light in ceiling
Box::new(XZRect::new(
213.,
343.,
227.,
332.,
554.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(15., 15., 15.))),
)),
// Grey ceiling
Box::new(FlipNormals::new(XZRect::new(
0.,
555.,
0.,
555.,
555.,
Lambertian::new(ConstantTexture::new(Vec3::new(0.73, 0.73, 0.73))),
))),
// Grey floor
Box::new(XZRect::new(
0.,
555.,
0.,
555.,
0.,
Lambertian::new(ConstantTexture::new(Vec3::new(0.73, 0.73, 0.73))),
)),
// Grey back wall
Box::new(FlipNormals::new(XYRect::new(
0.,
555.,
0.,
555.,
555.,
Lambertian::new(ConstantTexture::new(Vec3::new(0.73, 0.73, 0.73))),
))),
];
let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(objects, time_min, time_max))
} else {
Box::new(HitableList::new(objects))
};
Scene {
camera,
world,
subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
global_illumination: false,
..Default::default()
}
}

View File

@@ -0,0 +1,124 @@
use std::sync::Arc;
use crate::camera::Camera;
use crate::constant_medium::ConstantMedium;
use crate::cuboid::Cuboid;
use crate::flip_normals::FlipNormals;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::DiffuseLight;
use crate::material::Lambertian;
use crate::material::Material;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::rect::YZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::rotate::RotateY;
use crate::texture::ConstantTexture;
use crate::translate::Translate;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(278., 278., -800.);
let lookat = Vec3::new(278., 278., 0.);
let dist_to_focus = 10.0;
let aperture = 0.0;
let time_min = 0.;
let time_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
40.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let red = Lambertian::new(ConstantTexture::new([0.65, 0.05, 0.05]));
let white: Arc<dyn Material> =
Arc::new(Lambertian::new(ConstantTexture::new([0.73, 0.73, 0.73])));
let green = Lambertian::new(ConstantTexture::new([0.12, 0.45, 0.15]));
let light = DiffuseLight::new(ConstantTexture::new([7., 7., 7.]));
let objects: Vec<Box<dyn Hit>> = vec![
// White smoke box on the right
Box::new(ConstantMedium::new(
Translate::new(
RotateY::new(
Cuboid::new(
Vec3::new(0., 0., 0.),
Vec3::new(165., 165., 165.),
Arc::clone(&white),
),
-18.,
),
Vec3::new(130., 0., 65.),
),
0.01,
ConstantTexture::new([1., 1., 1.]),
)),
// Black smoke box on the left
Box::new(ConstantMedium::new(
Translate::new(
RotateY::new(
Cuboid::new(
Vec3::new(0., 0., 0.),
Vec3::new(165., 330., 165.),
Arc::clone(&white),
),
15.,
),
Vec3::new(265., 0., 295.),
),
0.01,
ConstantTexture::new([0., 0., 0.]),
)),
// Green wall left
Box::new(FlipNormals::new(YZRect::new(
0., 555., 0., 555., 555., green,
))),
// Red floor right
Box::new(YZRect::new(0., 555., 0., 555., 0., red)),
// Light in ceiling
Box::new(XZRect::new(113., 443., 127., 432., 554., light)),
// Grey ceiling
Box::new(FlipNormals::new(XZRect::new(
0.,
555.,
0.,
555.,
555.,
Arc::clone(&white),
))),
// Grey floor
Box::new(XZRect::new(0., 555., 0., 555., 0., Arc::clone(&white))),
// Grey back wall
Box::new(FlipNormals::new(XYRect::new(
0.,
555.,
0.,
555.,
555.,
Arc::clone(&white),
))),
];
let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(objects, time_min, time_max))
} else {
Box::new(HitableList::new(objects))
};
Scene {
camera,
world,
subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
..Default::default()
}
}

View File

@@ -0,0 +1,177 @@
use std::sync::Arc;
use image;
use rand;
use rand::Rng;
use crate::camera::Camera;
use crate::constant_medium::ConstantMedium;
use crate::cuboid::Cuboid;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::Dielectric;
use crate::material::DiffuseLight;
use crate::material::Lambertian;
use crate::material::Material;
use crate::material::Metal;
use crate::moving_sphere::MovingSphere;
use crate::noise::perlin::Perlin;
use crate::noise::NoiseType;
use crate::rect::XZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::rotate::RotateY;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::texture::ImageTexture;
use crate::texture::NoiseTexture;
use crate::translate::Translate;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(478., 278., -600.);
let lookat = Vec3::new(278., 278., 0.);
let dist_to_focus = 10.;
let aperture = 0.0;
let t_min = 0.;
let t_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
40.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
t_min,
t_max,
);
let nb = 20;
let rng = &mut rand::thread_rng();
let mut boxlist: Vec<Box<dyn Hit>> = Vec::with_capacity(10000);
let mut list: Vec<Box<dyn Hit>> = Vec::new();
let white: Arc<dyn Material> =
Arc::new(Lambertian::new(ConstantTexture::new([0.73, 0.73, 0.73])));
let ground: Arc<dyn Material> =
Arc::new(Lambertian::new(ConstantTexture::new([0.48, 0.83, 0.53])));
for i in 0..nb {
for j in 0..nb {
let w = 100.;
let x0 = -1000. + i as f32 * w;
let z0 = -1000. + j as f32 * w;
let y0 = 0.;
let x1 = x0 + w;
let y1 = 100. * (rng.gen_range(0., 1.) + 0.01);
let z1 = z0 + w;
boxlist.push(Box::new(Cuboid::new(
Vec3::new(x0, y0, z0),
Vec3::new(x1, y1, z1),
Arc::clone(&ground),
)));
}
}
list.push(Box::new(KDTree::new(boxlist, t_min, t_max)));
let light = DiffuseLight::new(ConstantTexture::new([7., 7., 7.]));
// Light in ceiling
list.push(Box::new(XZRect::new(123., 423., 147., 412., 554., light)));
let center = Vec3::new(400., 400., 200.);
// Moving brownish ball
list.push(Box::new(MovingSphere::new(
center,
center + Vec3::new(30., 0., 0.),
50.,
t_min,
t_max,
Lambertian::new(ConstantTexture::new([0.7, 0.3, 0.1])),
)));
// Glass ball, lower-left corner
list.push(Box::new(Sphere::new(
[260., 150., 45.],
50.,
Dielectric::new(1.5),
)));
// Metal ball lower-right corner
list.push(Box::new(Sphere::new(
[0., 150., 145.],
50.,
Metal::new([0.8, 0.8, 0.9], 10.0),
)));
// Blue smokey glass ball lower-left
let boundary: Arc<dyn Hit> =
Arc::new(Sphere::new([360., 150., 145.], 70., Dielectric::new(1.5)));
list.push(Box::new(Arc::clone(&boundary)));
list.push(Box::new(ConstantMedium::new(
Arc::clone(&boundary),
0.2,
ConstantTexture::new([0.2, 0.4, 0.9]),
)));
// General white mist over whole scene
let boundary: Arc<dyn Hit> = Arc::new(Sphere::new([0., 0., 0.], 5000., Dielectric::new(1.5)));
list.push(Box::new(Arc::clone(&boundary)));
list.push(Box::new(ConstantMedium::new(
Arc::clone(&boundary),
0.0001,
ConstantTexture::new([1.0, 1.0, 1.0]),
)));
// Earth sphere
let world_image_bytes = include_bytes!("../../images/world.jpg");
let world = ImageTexture::new(image::load_from_memory(world_image_bytes).unwrap().to_rgb());
list.push(Box::new(Sphere::new(
Vec3::new(400., 200., 400.),
100.,
Lambertian::new(world),
)));
// Perlin noise sphere
let noise_source = Perlin::new(rng);
let noise_type = NoiseType::Marble {
period: Vec3::new(0., 1., 0.),
power: 4.,
size: 32,
scale: 0.5,
};
let pertext = NoiseTexture::new(noise_source, noise_type);
list.push(Box::new(Sphere::new(
[220., 280., 300.],
80.,
Lambertian::new(pertext),
)));
// White 'cube' made of 1000 spheres
let ns = 1000;
let mut boxlist: Vec<Box<dyn Hit>> = Vec::with_capacity(1000);
for _ in 0..ns {
boxlist.push(Box::new(Sphere::new(
[
165. * rng.gen_range(0., 1.),
165. * rng.gen_range(0., 1.),
165. * rng.gen_range(0., 1.),
],
10.,
Arc::clone(&white),
)));
}
list.push(Box::new(Translate::new(
RotateY::new(KDTree::new(boxlist, t_min, t_max), 15.),
[-100., 270., 395.],
)));
Scene {
camera,
world: Box::new(HitableList::new(list)),
subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
..Default::default()
}
}

View File

@@ -0,0 +1,157 @@
use rand;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::DiffuseLight;
use crate::material::Lambertian;
use crate::noise::perlin::Perlin;
use crate::noise::NoiseType;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::rect::YZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::texture::ImageTexture;
use crate::texture::Mandelbrot;
use crate::texture::NoiseTexture;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(20., 20., 20.);
let lookat = Vec3::new(0., 1., 0.);
let dist_to_focus = 10.0;
let aperture = 0.0;
let time_min = 0.;
let time_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
20.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let rng = &mut rand::thread_rng();
let _ground_color = if opt.use_accel {
Box::new(ConstantTexture::new(Vec3::new(1.0, 0.4, 0.4)))
} else {
Box::new(ConstantTexture::new(Vec3::new(0.4, 1.0, 0.4)))
};
let world_image_bytes = include_bytes!("../../images/world.jpg");
let it = ImageTexture::new(image::load_from_memory(world_image_bytes).unwrap().to_rgb());
let noise_source = Perlin::new(rng);
let noise_type = NoiseType::Scale(10.);
let objects: Vec<Box<dyn Hit>> = vec![
// Textured globe
// Box::new(Sphere::new(Vec3::new(0., 2., 0.), 2.0, Lambertian::new(it))),
Box::new(Sphere::new(
Vec3::new(0., 2., 0.),
2.0,
DiffuseLight::new(it),
)),
// Ground
Box::new(XZRect::new(
-100.,
100.,
-100.,
1000.,
-60.,
DiffuseLight::new(Mandelbrot::default()),
)),
Box::new(XZRect::new(
-100.,
100.,
-100.,
200.,
60.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(1., 1., 1.))),
)),
Box::new(YZRect::new(
1.,
3.,
-1.,
1.,
4.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(4., 0., 4.))),
)),
Box::new(YZRect::new(
1.,
3.,
-1.,
1.,
-4.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(0., 4., 0.))),
)),
Box::new(XZRect::new(
-1.,
1.,
-1.,
1.,
6.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(4., 4., 0.))),
)),
Box::new(XYRect::new(
-1.,
1.,
1.,
3.,
-4.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(0., 0., 4.))),
)),
Box::new(XYRect::new(
-1.75,
1.75,
1.,
3.,
4.,
Lambertian::new(NoiseTexture::new(noise_source, noise_type)),
)),
/*
Box::new(Sphere::new(
Vec3::new(0., 0., 0.),
0.5,
Box::new(Lambertian::new(ConstantTexture::new(Vec3::new(
0.1, 0.2, 0.5,
)))),
)),
// Shiny sphere
Box::new(Sphere::new(
Vec3::new(1., 0., 0.),
0.5,
Metal::new(Vec3::new(0.8, 0.8, 0.8), 0.2),
)),
Box::new(MovingSphere::new(
Vec3::new(-1., 0., -0.25),
Vec3::new(-1., 0., 0.25),
0.5,
0.,
1.,
Lambertian::new(ConstantTexture::new(Vec3::new(
0.2, 0.8, 0.2,
))),
)),
*/
];
let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(objects, time_min, time_max))
} else {
Box::new(HitableList::new(objects))
};
Scene {
camera,
world,
subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
..Default::default()
}
}

View File

@@ -0,0 +1,11 @@
pub mod bench;
pub mod book;
pub mod bvh;
pub mod cornell_box;
pub mod cornell_smoke;
pub mod final_scene;
pub mod mandelbrot;
pub mod perlin_debug;
pub mod spheramid;
pub mod test;
pub mod tutorial;

View File

@@ -0,0 +1,74 @@
use std::sync::Arc;
use rand;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::Lambertian;
use crate::noise::perlin::Perlin;
use crate::noise::NoiseType;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::NoiseTexture;
use crate::texture::Texture;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(13., 2., 3.);
let lookat = Vec3::new(0., 0., 0.);
let dist_to_focus = 10.0;
let aperture = 0.0;
let time_min = 0.;
let time_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
20.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let rng = &mut rand::thread_rng();
let noise_source = Perlin::new(rng);
let noise_type = NoiseType::Marble {
period: Vec3::new(0., 1., 0.),
power: 4.,
size: 32,
scale: 0.05,
};
let pertext: Arc<dyn Texture> = Arc::new(NoiseTexture::new(noise_source, noise_type));
let objects: Vec<Box<dyn Hit>> = vec![
Box::new(Sphere::new(
Vec3::new(0., -1000., 0.),
1000.,
Lambertian::new(Arc::clone(&pertext)),
)),
Box::new(Sphere::new(
Vec3::new(0., 2., 0.),
2.0,
Lambertian::new(Arc::clone(&pertext)),
)),
];
let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(objects, time_min, time_max))
} else {
Box::new(HitableList::new(objects))
};
Scene {
camera,
world,
subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
global_illumination: true,
..Default::default()
}
}

View File

@@ -0,0 +1,123 @@
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::Dielectric;
use crate::material::Lambertian;
use crate::material::Metal;
use crate::moving_sphere::MovingSphere;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::CheckerTexture;
use crate::texture::ConstantTexture;
use crate::texture::EnvMap;
use crate::texture::Texture;
use crate::vec3::Vec3;
// Draws many spheres along each positive axis.
// Blue X-positive
// Green Y-positive
// Red Z-positive
fn draw_axises() -> Vec<Box<dyn Hit>> {
let half_diameter = 0.1;
let x = (0..100).map(|i| -> Box<dyn Hit> {
// X-axis
Box::new(Sphere::new(
Vec3::new(i as f32 * 2. * half_diameter, 0., 0.),
half_diameter,
Lambertian::new(ConstantTexture::new([0., 0., 1.])),
))
});
let y = (0..100).map(|i| -> Box<dyn Hit> {
// Y-axis
Box::new(Sphere::new(
Vec3::new(0., i as f32 * 2. * half_diameter, 0.),
half_diameter,
Lambertian::new(ConstantTexture::new([0., 1., 0.])),
))
});
let z = (0..100).map(|i| -> Box<dyn Hit> {
// Z-axis
Box::new(Sphere::new(
Vec3::new(0., 0., i as f32 * 2. * half_diameter),
half_diameter,
Lambertian::new(ConstantTexture::new([1., 0., 0.])),
))
});
x.chain(y.chain(z)).collect()
}
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(10., 10., 20.);
let lookat = Vec3::new(0., 0., 0.);
let dist_to_focus = (lookfrom - lookat).length();
let aperture = 0.1;
let time_min = 0.;
let time_max = 0.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 0., 1.),
45.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let ground_color = CheckerTexture::new(
ConstantTexture::new([0., 0., 0.]),
ConstantTexture::new([1.0, 1.0, 1.0]),
);
let mut objects: Vec<Box<dyn Hit>> = vec![
Box::new(XYRect::new(
-10.,
10.,
-10.,
10.,
0.,
Lambertian::new(ground_color),
)),
Box::new(Sphere::new([1., 0.5, 1.], 0.5, Dielectric::new(1.5))),
Box::new(Sphere::new(
Vec3::new(0., 0., 0.5),
0.5,
Lambertian::new(ConstantTexture::new(Vec3::new(0.1, 0.2, 0.5))),
)),
Box::new(Sphere::new(
Vec3::new(1., 0., -1.),
0.5,
Metal::new(Vec3::new(0.8, 0.6, 0.2), 0.2),
)),
Box::new(MovingSphere::new(
Vec3::new(-1., 0., -1.25),
Vec3::new(-1., 0., -0.75),
0.5,
0.,
1.,
Lambertian::new(ConstantTexture::new(Vec3::new(0.2, 0.8, 0.2))),
)),
];
objects.extend(draw_axises());
//let world = Box::new(KDTree::new(objects, time_min, time_max));
let world = Box::new(HitableList::new(objects));
let skybox_bytes = include_bytes!("../../images/envmap.jpg");
let skybox = image::load_from_memory(skybox_bytes).unwrap().to_rgb();
Scene {
camera,
world,
subsamples: opt.subsamples,
adaptive_subsampling: None,
// adaptive_subsampling: Some(0.5),
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
global_illumination: true,
env_map: Some(EnvMap::new(skybox)),
..Default::default()
}
}

View File

@@ -0,0 +1,150 @@
use image;
use rand;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::DiffuseLight;
use crate::material::Lambertian;
use crate::noise::perlin::Perlin;
use crate::noise::NoiseType;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::rect::YZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::texture::ImageTexture;
use crate::texture::NoiseTexture;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(20., 20., 20.);
let lookat = Vec3::new(0., 1., 0.);
let dist_to_focus = 10.0;
let aperture = 0.0;
let time_min = 0.;
let time_max = 1.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
20.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let rng = &mut rand::thread_rng();
let _ground_color = if opt.use_accel {
Box::new(ConstantTexture::new(Vec3::new(1.0, 0.4, 0.4)))
} else {
Box::new(ConstantTexture::new(Vec3::new(0.4, 1.0, 0.4)))
};
let world_image_bytes = include_bytes!("../../images/world.jpg");
let it = ImageTexture::new(image::load_from_memory(world_image_bytes).unwrap().to_rgb());
let noise_source = Perlin::new(rng);
let noise_type = NoiseType::Scale(10.);
let objects: Vec<Box<dyn Hit>> = vec![
// Big sphere
Box::new(Sphere::new(Vec3::new(0., 2., 0.), 2.0, Lambertian::new(it))),
// Earth sized sphere
Box::new(Sphere::new(
Vec3::new(0., -1000., 0.),
1000.,
// Box::new(Lambertian::new(ground_color)),
Lambertian::new(NoiseTexture::new(noise_source, noise_type)),
)),
Box::new(XZRect::new(
-100.,
100.,
-100.,
1000.,
60.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(1., 1., 1.))),
)),
Box::new(YZRect::new(
1.,
3.,
-1.,
1.,
4.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(4., 0., 4.))),
)),
Box::new(YZRect::new(
1.,
3.,
-1.,
1.,
-4.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(0., 4., 0.))),
)),
Box::new(XZRect::new(
-1.,
1.,
-1.,
1.,
6.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(4., 4., 0.))),
)),
Box::new(XYRect::new(
-1.,
1.,
1.,
3.,
-4.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(0., 0., 4.))),
)),
Box::new(XYRect::new(
-1.,
1.,
1.,
3.,
4.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(0., 4., 4.))),
)),
/*
Box::new(Sphere::new(
Vec3::new(0., 0., 0.),
0.5,
Box::new(Lambertian::new(ConstantTexture::new(Vec3::new(
0.1, 0.2, 0.5,
)))),
)),
// Shiny sphere
Box::new(Sphere::new(
Vec3::new(1., 0., 0.),
0.5,
Metal::new(Vec3::new(0.8, 0.8, 0.8), 0.2),
)),
Box::new(MovingSphere::new(
Vec3::new(-1., 0., -0.25),
Vec3::new(-1., 0., 0.25),
0.5,
0.,
1.,
Lambertian::new(ConstantTexture::new(Vec3::new(
0.2, 0.8, 0.2,
))),
)),
*/
];
let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(objects, time_min, time_max))
} else {
Box::new(HitableList::new(objects))
};
Scene {
camera,
world,
subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
..Default::default()
}
}

View File

@@ -0,0 +1,88 @@
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::Dielectric;
use crate::material::Lambertian;
use crate::material::Metal;
use crate::moving_sphere::MovingSphere;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::CheckerTexture;
use crate::texture::ConstantTexture;
use crate::texture::Texture;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(3., 4., 2.);
let lookat = Vec3::new(0., 0., -1.);
let dist_to_focus = (lookfrom - lookat).length();
let aperture = 0.1;
let time_min = 0.;
let time_max = 0.;
let camera = Camera::new(
lookfrom,
lookat,
Vec3::new(0., 1., 0.),
45.,
opt.width as f32 / opt.height as f32,
aperture,
dist_to_focus,
time_min,
time_max,
);
let ground_color: Box<dyn Texture> = if opt.use_accel {
Box::new(CheckerTexture::new(
ConstantTexture::new([0., 0., 0.]),
ConstantTexture::new([1.0, 0.4, 0.4]),
))
} else {
Box::new(ConstantTexture::new(Vec3::new(0.4, 1.0, 0.4)))
};
let objects: Vec<Box<dyn Hit>> = vec![
//let world: Box<Hit> = Box::new(HitableList::new(vec![
Box::new(Sphere::new([1., 0.5, 1.], 0.5, Dielectric::new(1.5))),
Box::new(Sphere::new(
Vec3::new(0., 0., -1.),
0.5,
Lambertian::new(ConstantTexture::new(Vec3::new(0.1, 0.2, 0.5))),
)),
Box::new(Sphere::new(
Vec3::new(0., -1000.5, -1.),
1000.,
Lambertian::new(ground_color),
)),
Box::new(Sphere::new(
Vec3::new(1., 0., -1.),
0.5,
Metal::new(Vec3::new(0.8, 0.6, 0.2), 0.2),
)),
Box::new(MovingSphere::new(
Vec3::new(-1., 0., -1.25),
Vec3::new(-1., 0., -0.75),
0.5,
0.,
1.,
Lambertian::new(ConstantTexture::new(Vec3::new(0.2, 0.8, 0.2))),
)),
];
let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(objects, time_min, time_max))
} else {
Box::new(HitableList::new(objects))
};
Scene {
camera,
world,
subsamples: opt.subsamples,
adaptive_subsampling: None,
// adaptive_subsampling: Some(0.5),
num_threads: opt.num_threads,
width: opt.width,
height: opt.height,
global_illumination: true,
..Default::default()
}
}

View File

@@ -0,0 +1,89 @@
use std::f32::consts::PI;
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::material::Material;
use crate::ray::Ray;
use crate::vec3::dot;
use crate::vec3::Vec3;
pub struct Sphere<M>
where
M: Material,
{
center: Vec3,
radius: f32,
material: M,
}
pub fn get_sphere_uv(p: Vec3) -> (f32, f32) {
let phi = p.z.atan2(p.x);
let theta = p.y.asin();
let u = 1. - (phi + PI) / (2. * PI);
let v = (theta + PI / 2.) / PI;
(u, v)
}
impl<M> Sphere<M>
where
M: Material,
{
pub fn new<V>(center: V, radius: f32, material: M) -> Sphere<M>
where
V: Into<Vec3>,
{
Sphere {
center: center.into(),
radius,
material,
}
}
}
impl<M> Hit for Sphere<M>
where
M: Material,
{
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
let oc = r.origin - self.center;
let a = dot(r.direction, r.direction);
let b = dot(oc, r.direction);
let c = dot(oc, oc) - self.radius * self.radius;
let discriminant = b * b - a * c;
if discriminant > 0. {
let temp = (-b - (b * b - a * c).sqrt()) / a;
if temp < t_max && temp > t_min {
let point = r.point_at_parameter(temp);
let uv = get_sphere_uv((point - self.center) / self.radius);
return Some(HitRecord {
t: temp,
uv,
p: point,
normal: (point - self.center) / self.radius,
material: &self.material,
});
}
let temp = (-b + (b * b - a * c).sqrt()) / a;
if temp < t_max && temp > t_min {
let point = r.point_at_parameter(temp);
let uv = get_sphere_uv((point - self.center) / self.radius);
return Some(HitRecord {
t: temp,
uv,
p: point,
normal: (point - self.center) / self.radius,
material: &self.material,
});
}
}
None
}
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
Some(AABB::new(
self.center - Vec3::new(self.radius, self.radius, self.radius),
self.center + Vec3::new(self.radius, self.radius, self.radius),
))
}
}

View File

@@ -0,0 +1,33 @@
use crate::texture::Texture;
use crate::vec3::Vec3;
pub struct CheckerTexture<T>
where
T: Texture,
{
odd: T,
even: T,
}
impl<T> CheckerTexture<T>
where
T: Texture,
{
pub fn new(odd: T, even: T) -> CheckerTexture<T> {
CheckerTexture { odd, even }
}
}
impl<T> Texture for CheckerTexture<T>
where
T: Texture,
{
fn value(&self, u: f32, v: f32, p: Vec3) -> Vec3 {
let sines = (10. * p.x).sin() * (10. * p.y).sin() * (10. * p.z).sin();
if sines < 0. {
self.odd.value(u, v, p)
} else {
self.even.value(u, v, p)
}
}
}

View File

@@ -0,0 +1,24 @@
use crate::texture::Texture;
use crate::vec3::Vec3;
#[derive(Debug, PartialEq)]
pub struct ConstantTexture {
color: Vec3,
}
impl ConstantTexture {
pub fn new<V>(color: V) -> ConstantTexture
where
V: Into<Vec3>,
{
ConstantTexture {
color: color.into(),
}
}
}
impl Texture for ConstantTexture {
fn value(&self, _u: f32, _v: f32, _p: Vec3) -> Vec3 {
self.color
}
}

View File

@@ -0,0 +1,26 @@
use std::f32;
use image::RgbImage;
use crate::texture::ImageTexture;
use crate::texture::Texture;
use crate::vec3::Vec3;
#[derive(Debug)]
pub struct EnvMap {
img_tex: ImageTexture,
}
impl EnvMap {
pub fn new(img: RgbImage) -> EnvMap {
EnvMap {
img_tex: ImageTexture::new(img),
}
}
pub fn color(&self, ray: Vec3) -> Vec3 {
let zero = Vec3::new(0., 0., 0.);
let u = ray.x.atan2(ray.z) / f32::consts::PI /2.0 + 0.5;
let v = ray.y / 2.0 + 0.5;
self.img_tex.value(u, v, zero)
}
}

View File

@@ -0,0 +1,48 @@
use image::RgbImage;
use crate::texture::Texture;
use crate::vec3::Vec3;
#[derive(Debug)]
pub struct ImageTexture {
img: RgbImage,
width: f32,
height: f32,
}
impl ImageTexture {
pub fn new(img: RgbImage) -> ImageTexture {
let (w, h) = img.dimensions();
ImageTexture {
img,
width: f32::from(w.min(64000) as u16),
height: f32::from(h.min(64000) as u16),
}
}
}
impl Texture for ImageTexture {
fn value(&self, u: f32, v: f32, _p: Vec3) -> Vec3 {
// Wrap texcoords by default.
let x = (u % 1. * (self.width - 1.)) as u32;
let y = ((1. - v % 1.) * (self.height - 1.)) as u32;
if x >= self.width as u32 {
panic!(format!(
"u {} v {} x {} y {} w {} h {}",
u, v, x, y, self.width, self.height
));
}
if y >= self.height as u32 {
panic!(format!(
"u {} v {} x {} y {} w {} h {}",
u, v, x, y, self.width, self.height
));
}
let pixel = self.img.get_pixel(x, y);
Vec3::new(
f32::from(pixel[0]) / 255.,
f32::from(pixel[1]) / 255.,
f32::from(pixel[2]) / 255.,
)
}
}

View File

@@ -0,0 +1,79 @@
#![allow(clippy::many_single_char_names)]
use rand;
use rand::Rng;
use crate::texture::Texture;
use crate::vec3::Vec3;
#[derive(Debug)]
pub struct Mandelbrot {
scale: f32,
palette: Vec<Vec3>,
}
// HSV values in [0..1]
// returns [r, g, b] values from 0 to 255
//From https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Vec3 {
let h_i = (h * 6.) as i32;
let f = h * 6. - h_i as f32;
let p = v * (1. - s);
let q = v * (1. - f * s);
let t = v * (1. - (1. - f) * s);
match h_i {
0 => Vec3::new(v, t, p),
1 => Vec3::new(q, v, p),
2 => Vec3::new(p, v, t),
3 => Vec3::new(p, q, v),
4 => Vec3::new(t, p, v),
5 => Vec3::new(v, p, q),
_ => panic!(format!("Unknown H value {}", h_i)),
}
}
fn generate_palette(num: usize) -> Vec<Vec3> {
let mut rng = rand::thread_rng();
let mut random = || rng.gen_range(0.0, 0.1);
// use golden ratio
let golden_ratio_conjugate = 0.618_034;
let mut h = random();
(0..num)
.map(|_| {
h += golden_ratio_conjugate;
h %= 1.0;
hsv_to_rgb(h, 0.99, 0.99)
})
.collect()
}
impl Default for Mandelbrot {
fn default() -> Self {
Mandelbrot {
scale: 2.0,
palette: generate_palette(10),
}
}
}
impl Texture for Mandelbrot {
fn value(&self, u: f32, v: f32, _p: Vec3) -> Vec3 {
// scaled x coordinate of pixel (scaled to lie in the Mandelbrot X scale (-2.5, 1))
let x0 = u * 3.5 - 2.5;
// scaled y coordinate of pixel (scaled to lie in the Mandelbrot Y scale (-1, 1))
let y0 = v * 2.0 - 1.0;
let mut x = 0.0;
let mut y = 0.0;
let mut iteration = 0;
let max_iteration = 1000;
while (x * x + y * y) <= 2. * 2. && iteration < max_iteration {
let xtemp = x * x - y * y + x0;
y = 2. * x * y + y0;
x = xtemp;
iteration += 1;
}
if iteration == max_iteration {
return Vec3::default();
}
self.palette[iteration % self.palette.len()]
}
}

View File

@@ -0,0 +1,45 @@
mod checker;
mod constant;
mod envmap;
mod image;
mod mandelbrot;
mod noise;
pub use crate::texture::checker::CheckerTexture;
pub use crate::texture::constant::ConstantTexture;
pub use crate::texture::envmap::EnvMap;
pub use crate::texture::image::ImageTexture;
pub use crate::texture::mandelbrot::Mandelbrot;
pub use crate::texture::noise::NoiseTexture;
use std::sync::Arc;
use crate::vec3::Vec3;
pub trait Texture: Send + Sync {
fn value(&self, u: f32, v: f32, p: Vec3) -> Vec3;
}
impl Texture for Arc<dyn Texture> {
fn value(&self, u: f32, v: f32, p: Vec3) -> Vec3 {
(**self).value(u, v, p)
}
}
impl Texture for Box<dyn Texture> {
fn value(&self, u: f32, v: f32, p: Vec3) -> Vec3 {
(**self).value(u, v, p)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn constant_texture_from_array() {
assert_eq!(
ConstantTexture::new(Vec3::new(1., 2., 3.)),
ConstantTexture::new([1., 2., 3.])
);
}
}

View File

@@ -0,0 +1,46 @@
use crate::noise::NoiseSource;
use crate::noise::NoiseType;
use crate::texture::Texture;
use crate::vec3::Vec3;
pub struct NoiseTexture<N>
where
N: NoiseSource,
{
noise_source: N,
noise_type: NoiseType,
}
impl<N> NoiseTexture<N>
where
N: NoiseSource,
{
pub fn new(noise_source: N, noise_type: NoiseType) -> NoiseTexture<N> {
NoiseTexture {
noise_source,
noise_type,
}
}
}
impl<N> Texture for NoiseTexture<N>
where
N: NoiseSource,
{
fn value(&self, _u: f32, _v: f32, p: Vec3) -> Vec3 {
let v = match self.noise_type {
NoiseType::Scale(scale) => self.noise_source.scaled(p, scale),
NoiseType::Turbulence(turbulence) => self.noise_source.turbulence(p, turbulence),
NoiseType::Marble {
period,
power,
size,
scale,
} => self.noise_source.marble(p, period, power, size, scale),
};
debug_assert!(v >= 0., "Cold pixel @ {}: {}", p, v);
debug_assert!(v <= 1., "Hot pixel @ {}: {}", p, v);
Vec3::new(1., 1., 1.) * v
}
}

View File

@@ -0,0 +1,54 @@
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::ray::Ray;
use crate::vec3::Vec3;
pub struct Translate<H>
where
H: Hit,
{
hitable: H,
offset: Vec3,
}
impl<H> Translate<H>
where
H: Hit,
{
pub fn new<V>(hitable: H, offset: V) -> Translate<H>
where
V: Into<Vec3>,
{
Translate {
hitable,
offset: offset.into(),
}
}
}
impl<H> Hit for Translate<H>
where
H: Hit,
{
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
let moved_r = Ray::new(r.origin - self.offset, r.direction, r.time);
if let Some(rec) = self.hitable.hit(moved_r, t_min, t_max) {
return Some(HitRecord {
p: rec.p + self.offset,
..rec
});
}
None
}
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
if let Some(bbox) = self.hitable.bounding_box(t_min, t_max) {
return Some(AABB::new(
bbox.min() + self.offset,
bbox.max() + self.offset,
));
}
None
}
}

279
rtiow/renderer/src/vec3.rs Normal file
View File

@@ -0,0 +1,279 @@
use std::convert::From;
use std::fmt;
use std::num::ParseFloatError;
use std::ops::Add;
use std::ops::Div;
use std::ops::Index;
use std::ops::Mul;
use std::ops::Neg;
use std::ops::Sub;
use std::str;
use serde_derive::Deserialize;
use serde_derive::Serialize;
#[derive(Default, Debug, Deserialize, Serialize, PartialEq, Copy, Clone)]
pub struct Vec3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
pub fn cross(v1: Vec3, v2: Vec3) -> Vec3 {
Vec3 {
x: v1.y * v2.z - v1.z * v2.y,
y: v1.z * v2.x - v1.x * v2.z,
z: v1.x * v2.y - v1.y * v2.x,
}
}
pub fn dot(v1: Vec3, v2: Vec3) -> f32 {
v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
}
impl Vec3 {
pub fn new(x: f32, y: f32, z: f32) -> Vec3 {
Vec3 { x, y, z }
}
pub fn min(self) -> f32 {
self.x.min(self.y).min(self.z)
}
pub fn max(self) -> f32 {
self.x.max(self.y).max(self.z)
}
pub fn length(self) -> f32 {
self.squared_length().sqrt()
}
pub fn squared_length(self) -> f32 {
self.x * self.x + self.y * self.y + self.z * self.z
}
pub fn unit_vector(self) -> Vec3 {
self / self.length()
}
pub fn make_unit_vector(&mut self) {
*self = self.unit_vector();
}
}
impl From<[f32; 3]> for Vec3 {
fn from(v: [f32; 3]) -> Self {
Vec3 {
x: v[0],
y: v[1],
z: v[2],
}
}
}
impl fmt::Display for Vec3 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {} {}", self.x, self.y, self.z)
}
}
impl str::FromStr for Vec3 {
type Err = ParseFloatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let coords: Vec<&str> = s.split(' ').collect();
Ok(Vec3 {
x: coords[0].parse::<f32>()?,
y: coords[1].parse::<f32>()?,
z: coords[2].parse::<f32>()?,
})
}
}
impl Add<f32> for Vec3 {
type Output = Vec3;
fn add(self, r: f32) -> Vec3 {
Vec3 {
x: self.x + r,
y: self.y + r,
z: self.z + r,
}
}
}
impl Add for Vec3 {
type Output = Vec3;
fn add(self, r: Vec3) -> Vec3 {
Vec3 {
x: self.x + r.x,
y: self.y + r.y,
z: self.z + r.z,
}
}
}
impl Div<Vec3> for f32 {
type Output = Vec3;
fn div(self, r: Vec3) -> Vec3 {
Vec3 {
x: self / r.x,
y: self / r.y,
z: self / r.z,
}
}
}
impl Div<f32> for Vec3 {
type Output = Vec3;
fn div(self, r: f32) -> Vec3 {
Vec3 {
x: self.x / r,
y: self.y / r,
z: self.z / r,
}
}
}
impl Index<usize> for Vec3 {
type Output = f32;
fn index(&self, idx: usize) -> &f32 {
match idx {
0 => &self.x,
1 => &self.y,
2 => &self.z,
_ => panic!(format!("idx {} out of range for vec3", idx)),
}
}
}
impl Mul for Vec3 {
type Output = Vec3;
fn mul(self, r: Vec3) -> Vec3 {
Vec3 {
x: self.x * r.x,
y: self.y * r.y,
z: self.z * r.z,
}
}
}
impl Mul<Vec3> for f32 {
type Output = Vec3;
fn mul(self, v: Vec3) -> Vec3 {
Vec3 {
x: v.x * self,
y: v.y * self,
z: v.z * self,
}
}
}
impl Mul<f32> for Vec3 {
type Output = Vec3;
fn mul(self, r: f32) -> Vec3 {
Vec3 {
x: self.x * r,
y: self.y * r,
z: self.z * r,
}
}
}
impl Neg for Vec3 {
type Output = Vec3;
fn neg(self) -> Vec3 {
-1. * self
}
}
impl Sub for Vec3 {
type Output = Vec3;
fn sub(self, r: Vec3) -> Vec3 {
Vec3 {
x: self.x - r.x,
y: self.y - r.y,
z: self.z - r.z,
}
}
}
#[cfg(test)]
mod tests {
use std::f32::consts::PI;
use std::str::FromStr;
use super::*;
#[test]
fn vec_add() {
let v0 = Vec3::new(1., 2., 3.);
let v1 = Vec3::new(1., 1., 1.);
assert_eq!(v0 + v1, Vec3::new(2., 3., 4.));
}
#[test]
fn vec_div() {
let v0 = Vec3::new(1., 2., 4.);
assert_eq!(v0 / 2., Vec3::new(0.5, 1., 2.));
}
#[test]
fn vec_mul() {
let v0 = Vec3::new(1., 2., 4.);
assert_eq!(v0 * 0.5, Vec3::new(0.5, 1., 2.));
assert_eq!(v0 * v0, Vec3::new(1., 4., 16.));
}
#[test]
fn vec_sub() {
let v0 = Vec3::new(1., 2., 3.);
let v1 = Vec3::new(1., 1., 1.);
assert_eq!(v0 - v1, Vec3::new(0., 1., 2.));
}
#[test]
fn vec_idx() {
let v0 = Vec3::new(1., 2., 3.);
assert_eq!(v0[2], 3.);
}
#[test]
fn vec_display() {
let v0 = Vec3::new(1., 2., 3.);
assert_eq!(format!("{}", v0), "1 2 3".to_owned());
}
#[test]
fn vec_from_str() {
assert_eq!(Vec3::from_str("1. 2. 3.").unwrap(), Vec3::new(1., 2., 3.));
}
#[test]
fn vec_str_roundtrip() {
let v = Vec3::from_str("1 2 3").unwrap();
let s = format!("{}", v);
assert_eq!(v, Vec3::from_str(&s).unwrap());
}
#[test]
fn vec_dot() {
let v0 = Vec3::new(1., 0., 0.);
let v1 = Vec3::new(-1., 0., 0.);
assert_eq!(dot(v0, v1), v0.length() * v1.length() * PI.cos());
}
#[test]
fn vec_cross() {
let v0 = Vec3::new(1., 0., 0.);
let v1 = Vec3::new(0., 1., 0.);
assert_eq!(cross(v0, v1), Vec3::new(0., 0., 1.));
}
}