added scenes
This commit is contained in:
parent
a3d44f55f4
commit
f6a4202875
8 changed files with 804 additions and 121 deletions
25
README.md
25
README.md
|
|
@ -72,17 +72,34 @@ Multiprocessing uses one worker per CPU core by default. Pass `workers=N` to the
|
||||||
|
|
||||||
## Rust version
|
## Rust version
|
||||||
|
|
||||||
A much faster port lives in [`rust/`](rust/) (renders 800x600 at 100 spp in seconds). It takes the same arguments:
|
A much faster port lives in [`rust/`](rust/) (renders 800x600 at 100 spp in seconds). It takes the same arguments, plus an optional `--scene`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd rust
|
cd rust
|
||||||
cargo build --release
|
cargo build --release
|
||||||
./target/release/pathtracer 800 600 100 15 cornell_hq.png
|
./target/release/pathtracer 800 600 100 15 cornell_hq.png
|
||||||
|
./target/release/pathtracer --scene caustics 800 600 1024 15 caustics.png
|
||||||
|
./target/release/pathtracer --list-scenes
|
||||||
```
|
```
|
||||||
|
|
||||||
It uses all cores via rayon, shows a progress bar and the region brightness table, but does not write `preview.png` or `render_metrics.png`.
|
It uses all cores via rayon, shows a progress bar and (for the `cornell` scene) the region brightness table, but does not write `preview.png` or `render_metrics.png`. Without an output name it writes `<scene>.png`.
|
||||||
|
|
||||||
Beyond the Python version it adds:
|
### Scenes
|
||||||
|
|
||||||
|
| Scene | What it shows |
|
||||||
|
|-------|---------------|
|
||||||
|
| `cornell` (default) | The scene from `raytracer.py` |
|
||||||
|
| `caustics` | Clear, amber and blue glass spheres and a mirror under one light, placed so each sphere focuses a sharp caustic into its shadow. Caustics are the hardest light paths for a path tracer, so use ~1000+ spp |
|
||||||
|
| `spheres` | Large scene: ~470 random diffuse/mirror/glass spheres under a sky (after *Ray Tracing in One Weekend*). The BVH makes it ~11x faster than testing every object |
|
||||||
|
| `night` | The same field at night, lit by 61 glowing spheres. Each shading point picks one light, weighted by power / distance², so many lights cost no more shadow rays than one |
|
||||||
|
| `classic` | The classic Cornell box: one-sided ceiling panel light, two boxes, colour bleeding from the walls |
|
||||||
|
|
||||||
|
Scenes are defined in [`rust/src/scenes.rs`](rust/src/scenes.rs) with a small builder (`plane`, `sphere`, `cuboid`, `box_light`, `sphere_light`, `rect_light`). Every emitter must be added with a `*_light` method so light sampling knows about it.
|
||||||
|
|
||||||
|
### Beyond the Python version
|
||||||
|
|
||||||
|
- **BVH** - bounded objects live in a bounding volume hierarchy (median split); planes are tested separately.
|
||||||
|
- **Many-light sampling** - box, sphere and one-sided rectangle lights, one picked per shading point in proportion to its estimated contribution.
|
||||||
|
|
||||||
- **Russian roulette** - after 3 bounces, dim paths are terminated randomly (survivors are re-weighted, so the image stays unbiased). About 2.5x more image quality per second of rendering in this scene.
|
- **Russian roulette** - after 3 bounces, dim paths are terminated randomly (survivors are re-weighted, so the image stays unbiased). About 2.5x more image quality per second of rendering in this scene.
|
||||||
- **Cheap cosine-weighted diffuse sampling** - two uniform random numbers and an orthonormal basis instead of Gaussian samples.
|
- **Cheap cosine-weighted diffuse sampling** - two uniform random numbers and an orthonormal basis instead of Gaussian samples.
|
||||||
|
|
@ -108,7 +125,7 @@ The scene is the classic **Cornell Box**:
|
||||||
|
|
||||||
- Position: (278, 278, -800)
|
- Position: (278, 278, -800)
|
||||||
- Look-at: (278, 278, 278) (box center)
|
- Look-at: (278, 278, 278) (box center)
|
||||||
- FOV: 40°
|
- FOV: 40° vertical (earlier versions effectively used half of this, zooming the image in 2x)
|
||||||
- Focus distance: 1078
|
- Focus distance: 1078
|
||||||
|
|
||||||
## Materials
|
## Materials
|
||||||
|
|
|
||||||
|
|
@ -283,8 +283,8 @@ class Camera:
|
||||||
w = normalize(self.origin - np.asarray(lookat, dtype=np.float64))
|
w = normalize(self.origin - np.asarray(lookat, dtype=np.float64))
|
||||||
u = normalize(np.cross(vup, w))
|
u = normalize(np.cross(vup, w))
|
||||||
v = np.cross(w, u)
|
v = np.cross(w, u)
|
||||||
self.horizontal = focus_dist * half_width * u
|
self.horizontal = 2.0 * focus_dist * half_width * u
|
||||||
self.vertical = focus_dist * half_height * v
|
self.vertical = 2.0 * focus_dist * half_height * v
|
||||||
self.lower_left_corner = self.origin - self.horizontal / 2.0 - self.vertical / 2.0 - focus_dist * w
|
self.lower_left_corner = self.origin - self.horizontal / 2.0 - self.vertical / 2.0 - focus_dist * w
|
||||||
self.aperture = aperture
|
self.aperture = aperture
|
||||||
|
|
||||||
|
|
|
||||||
185
rust/src/bvh.rs
Normal file
185
rust/src/bvh.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
//! Bounding volume hierarchy over the scene's bounded objects, so a ray only
|
||||||
|
//! tests the objects whose boxes it passes through instead of all of them.
|
||||||
|
|
||||||
|
use crate::scene::Ray;
|
||||||
|
use crate::vec3::Vec3;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Aabb {
|
||||||
|
pub min: Vec3,
|
||||||
|
pub max: Vec3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Aabb {
|
||||||
|
pub const EMPTY: Aabb = Aabb {
|
||||||
|
min: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
|
||||||
|
max: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn union(self, o: Aabb) -> Aabb {
|
||||||
|
Aabb {
|
||||||
|
min: Vec3::new(self.min.x.min(o.min.x), self.min.y.min(o.min.y), self.min.z.min(o.min.z)),
|
||||||
|
max: Vec3::new(self.max.x.max(o.max.x), self.max.y.max(o.max.y), self.max.z.max(o.max.z)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn centroid(&self) -> Vec3 {
|
||||||
|
(self.min + self.max) * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slab test. `inv_dir` components may be infinite for axis-parallel rays;
|
||||||
|
/// the resulting NaNs are discarded by f64::min/max.
|
||||||
|
fn hit(&self, origin: Vec3, inv_dir: Vec3, mut t_min: f64, mut t_max: f64) -> bool {
|
||||||
|
for axis in 0..3 {
|
||||||
|
let mut t0 = (self.min[axis] - origin[axis]) * inv_dir[axis];
|
||||||
|
let mut t1 = (self.max[axis] - origin[axis]) * inv_dir[axis];
|
||||||
|
if inv_dir[axis] < 0.0 {
|
||||||
|
std::mem::swap(&mut t0, &mut t1);
|
||||||
|
}
|
||||||
|
t_min = t_min.max(t0);
|
||||||
|
t_max = t_max.min(t1);
|
||||||
|
if t_max < t_min {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEAF_SIZE: usize = 4;
|
||||||
|
|
||||||
|
/// Flattened node: a leaf covers `prims[start..start + count]`; an interior
|
||||||
|
/// node (count == 0) has its left child at the next index and its right child at `right`.
|
||||||
|
struct Node {
|
||||||
|
bounds: Aabb,
|
||||||
|
start: u32,
|
||||||
|
count: u32,
|
||||||
|
right: u32,
|
||||||
|
axis: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Bvh {
|
||||||
|
nodes: Vec<Node>,
|
||||||
|
/// Object indices, reordered so each leaf's objects are contiguous.
|
||||||
|
prims: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bvh {
|
||||||
|
pub fn build(bounds: &[Aabb]) -> Bvh {
|
||||||
|
let mut bvh = Bvh {
|
||||||
|
nodes: Vec::with_capacity(2 * bounds.len()),
|
||||||
|
prims: (0..bounds.len() as u32).collect(),
|
||||||
|
};
|
||||||
|
if !bounds.is_empty() {
|
||||||
|
let centroids: Vec<Vec3> = bounds.iter().map(Aabb::centroid).collect();
|
||||||
|
bvh.build_range(0, bounds.len(), bounds, ¢roids);
|
||||||
|
}
|
||||||
|
bvh
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Median split along the axis where the object centroids spread widest.
|
||||||
|
fn build_range(&mut self, start: usize, end: usize, bounds: &[Aabb], centroids: &[Vec3]) -> usize {
|
||||||
|
let prims = &self.prims[start..end];
|
||||||
|
let node_bounds = prims.iter().fold(Aabb::EMPTY, |b, &p| b.union(bounds[p as usize]));
|
||||||
|
let centroid_bounds = prims.iter().fold(Aabb::EMPTY, |b, &p| {
|
||||||
|
let c = centroids[p as usize];
|
||||||
|
b.union(Aabb { min: c, max: c })
|
||||||
|
});
|
||||||
|
let extent = centroid_bounds.max - centroid_bounds.min;
|
||||||
|
let axis = if extent.x >= extent.y && extent.x >= extent.z {
|
||||||
|
0
|
||||||
|
} else if extent.y >= extent.z {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
};
|
||||||
|
|
||||||
|
let idx = self.nodes.len();
|
||||||
|
self.nodes.push(Node {
|
||||||
|
bounds: node_bounds,
|
||||||
|
start: start as u32,
|
||||||
|
count: (end - start) as u32,
|
||||||
|
right: 0,
|
||||||
|
axis: axis as u8,
|
||||||
|
});
|
||||||
|
if end - start <= LEAF_SIZE || extent[axis] <= 0.0 {
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
let half = (end - start) / 2;
|
||||||
|
self.prims[start..end].select_nth_unstable_by(half, |&a, &b| {
|
||||||
|
centroids[a as usize][axis].total_cmp(¢roids[b as usize][axis])
|
||||||
|
});
|
||||||
|
self.build_range(start, start + half, bounds, centroids);
|
||||||
|
let right = self.build_range(start + half, end, bounds, centroids);
|
||||||
|
self.nodes[idx].count = 0;
|
||||||
|
self.nodes[idx].right = right as u32;
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls `visit` for each object in a leaf whose box the ray passes through
|
||||||
|
/// within (t_min, t_max). `visit` returns Some(t) to shrink t_max after a
|
||||||
|
/// hit, or Err(()) to stop traversal entirely.
|
||||||
|
fn traverse(
|
||||||
|
&self,
|
||||||
|
r: &Ray,
|
||||||
|
t_min: f64,
|
||||||
|
mut t_max: f64,
|
||||||
|
mut visit: impl FnMut(usize, f64) -> Result<Option<f64>, ()>,
|
||||||
|
) {
|
||||||
|
if self.nodes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let inv_dir = Vec3::new(1.0 / r.direction.x, 1.0 / r.direction.y, 1.0 / r.direction.z);
|
||||||
|
let mut stack = [0u32; 64];
|
||||||
|
let mut sp = 1;
|
||||||
|
while sp > 0 {
|
||||||
|
sp -= 1;
|
||||||
|
let node = &self.nodes[stack[sp] as usize];
|
||||||
|
if !node.bounds.hit(r.origin, inv_dir, t_min, t_max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if node.count > 0 {
|
||||||
|
for &p in &self.prims[node.start as usize..(node.start + node.count) as usize] {
|
||||||
|
match visit(p as usize, t_max) {
|
||||||
|
Ok(Some(t)) => t_max = t,
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(()) => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Push the far child first so the near one is visited first,
|
||||||
|
// which shrinks t_max sooner.
|
||||||
|
let left = stack[sp] + 1;
|
||||||
|
let (near, far) = if r.direction[node.axis as usize] < 0.0 {
|
||||||
|
(node.right, left)
|
||||||
|
} else {
|
||||||
|
(left, node.right)
|
||||||
|
};
|
||||||
|
stack[sp] = far;
|
||||||
|
stack[sp + 1] = near;
|
||||||
|
sp += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closest hit: `hit(object, t_max)` returns the hit distance if the object
|
||||||
|
/// is hit closer than t_max.
|
||||||
|
pub fn closest(&self, r: &Ray, t_min: f64, t_max: f64, mut hit: impl FnMut(usize, f64) -> Option<f64>) {
|
||||||
|
self.traverse(r, t_min, t_max, |p, t_max| Ok(hit(p, t_max)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True as soon as `hit(object)` reports any hit.
|
||||||
|
pub fn any(&self, r: &Ray, t_min: f64, t_max: f64, mut hit: impl FnMut(usize) -> bool) -> bool {
|
||||||
|
let mut found = false;
|
||||||
|
self.traverse(r, t_min, t_max, |p, _| {
|
||||||
|
if hit(p) {
|
||||||
|
found = true;
|
||||||
|
Err(())
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,8 +15,8 @@ impl Camera {
|
||||||
let w = (lookfrom - lookat).normalize();
|
let w = (lookfrom - lookat).normalize();
|
||||||
let u = vup.cross(w).normalize();
|
let u = vup.cross(w).normalize();
|
||||||
let v = w.cross(u);
|
let v = w.cross(u);
|
||||||
let horizontal = focus_dist * half_width * u;
|
let horizontal = 2.0 * focus_dist * half_width * u;
|
||||||
let vertical = focus_dist * half_height * v;
|
let vertical = 2.0 * focus_dist * half_height * v;
|
||||||
let lower_left_corner = lookfrom - horizontal / 2.0 - vertical / 2.0 - focus_dist * w;
|
let lower_left_corner = lookfrom - horizontal / 2.0 - vertical / 2.0 - focus_dist * w;
|
||||||
Camera {
|
Camera {
|
||||||
origin: lookfrom,
|
origin: lookfrom,
|
||||||
|
|
|
||||||
142
rust/src/main.rs
142
rust/src/main.rs
|
|
@ -1,5 +1,7 @@
|
||||||
|
mod bvh;
|
||||||
mod camera;
|
mod camera;
|
||||||
mod scene;
|
mod scene;
|
||||||
|
mod scenes;
|
||||||
mod vec3;
|
mod vec3;
|
||||||
|
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
|
|
@ -11,43 +13,20 @@ use rand::{Rng, SeedableRng};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
use camera::Camera;
|
use camera::Camera;
|
||||||
use scene::{cornell_box, BoxLight, HitRecord, Material, Ray, Scene, EPSILON};
|
use scene::{HitRecord, Light, Material, Ray, Scene, EPSILON};
|
||||||
use vec3::Vec3;
|
use vec3::Vec3;
|
||||||
|
|
||||||
const SHADOW_EPSILON: f64 = 1e-3;
|
/// Shadow rays stop this fraction short of the light, so they don't hit the
|
||||||
|
/// light's own surface. Relative, because scenes range from ~1 to ~1000 units.
|
||||||
|
const SHADOW_RELATIVE_EPSILON: f64 = 1e-4;
|
||||||
|
|
||||||
/// Direct light from one uniformly sampled point on the box light's surface.
|
/// Direct light from one uniformly sampled point on `light`'s surface.
|
||||||
fn sample_box_light<R: Rng>(scene: &Scene, light: &BoxLight, rec: &HitRecord, albedo: Vec3, rng: &mut R) -> Vec3 {
|
fn sample_direct<R: Rng>(scene: &Scene, light: &Light, rec: &HitRecord, albedo: Vec3, rng: &mut R) -> Vec3 {
|
||||||
let (lo, hi) = (light.min, light.max);
|
let (lp, light_normal) = light.sample(rng);
|
||||||
let size = hi - lo;
|
|
||||||
let face_areas = [size.y * size.z, size.x * size.z, size.x * size.y];
|
|
||||||
let half_area: f64 = face_areas.iter().sum();
|
|
||||||
let total_area = 2.0 * half_area;
|
|
||||||
|
|
||||||
// Pick a face with probability proportional to its area, then a uniform
|
|
||||||
// point on it: overall pdf is 1 / total_area.
|
|
||||||
let u = rng.gen::<f64>() * half_area;
|
|
||||||
let axis = if u < face_areas[0] {
|
|
||||||
0
|
|
||||||
} else if u < face_areas[0] + face_areas[1] {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
2
|
|
||||||
};
|
|
||||||
let positive_side = rng.gen::<f64>() < 0.5;
|
|
||||||
let mut p = [
|
|
||||||
lo.x + size.x * rng.gen::<f64>(),
|
|
||||||
lo.y + size.y * rng.gen::<f64>(),
|
|
||||||
lo.z + size.z * rng.gen::<f64>(),
|
|
||||||
];
|
|
||||||
p[axis] = if positive_side { hi[axis] } else { lo[axis] };
|
|
||||||
let mut n = [0.0; 3];
|
|
||||||
n[axis] = if positive_side { 1.0 } else { -1.0 };
|
|
||||||
let (lp, light_normal) = (Vec3::new(p[0], p[1], p[2]), Vec3::new(n[0], n[1], n[2]));
|
|
||||||
|
|
||||||
let to_light = lp - rec.point;
|
let to_light = lp - rec.point;
|
||||||
let dist_sq = to_light.dot(to_light);
|
let dist_sq = to_light.dot(to_light);
|
||||||
if dist_sq < 1e-8 {
|
if dist_sq < 1e-12 {
|
||||||
return Vec3::ZERO;
|
return Vec3::ZERO;
|
||||||
}
|
}
|
||||||
let dist = dist_sq.sqrt();
|
let dist = dist_sq.sqrt();
|
||||||
|
|
@ -58,13 +37,14 @@ fn sample_box_light<R: Rng>(scene: &Scene, light: &BoxLight, rec: &HitRecord, al
|
||||||
return Vec3::ZERO;
|
return Vec3::ZERO;
|
||||||
}
|
}
|
||||||
|
|
||||||
if scene.blocked(rec.point + EPSILON * rec.normal, d_to_light, dist - SHADOW_EPSILON) {
|
if scene.blocked(rec.point + EPSILON * rec.normal, d_to_light, dist * (1.0 - SHADOW_RELATIVE_EPSILON)) {
|
||||||
return Vec3::ZERO;
|
return Vec3::ZERO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uniform area sampling has pdf 1 / area.
|
||||||
let brdf = albedo / PI;
|
let brdf = albedo / PI;
|
||||||
let geometry = cos_at_rec * cos_at_light / dist_sq;
|
let geometry = cos_at_rec * cos_at_light / dist_sq;
|
||||||
light.emission.mul_elem(brdf) * (geometry * total_area)
|
light.emission.mul_elem(brdf) * (geometry * light.area())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bounces before Russian roulette may terminate a path.
|
/// Bounces before Russian roulette may terminate a path.
|
||||||
|
|
@ -82,14 +62,13 @@ fn trace<R: Rng>(scene: &Scene, primary: &Ray, max_depth: u32, rng: &mut R) -> V
|
||||||
|
|
||||||
for depth in 0..max_depth {
|
for depth in 0..max_depth {
|
||||||
let Some(rec) = scene.hit(&ray, EPSILON, f64::INFINITY) else {
|
let Some(rec) = scene.hit(&ray, EPSILON, f64::INFINITY) else {
|
||||||
let t = 0.5 * (ray.direction.y + 1.0);
|
radiance += throughput.mul_elem(scene.background.radiance(ray.direction));
|
||||||
let sky = Vec3::ONE * (1.0 - t) + Vec3::new(0.5, 0.7, 1.0) * t;
|
|
||||||
radiance += throughput.mul_elem(sky);
|
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Material::Light { emission } = rec.material {
|
if let Material::Light { emission } = rec.material {
|
||||||
if count_emission {
|
// Lights emit from their front side only (matters for one-sided rects).
|
||||||
|
if count_emission && rec.front_face {
|
||||||
radiance += throughput.mul_elem(emission);
|
radiance += throughput.mul_elem(emission);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -100,8 +79,9 @@ fn trace<R: Rng>(scene: &Scene, primary: &Ray, max_depth: u32, rng: &mut R) -> V
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Material::Diffuse { albedo } = rec.material {
|
if let Material::Diffuse { albedo } = rec.material {
|
||||||
for light in &scene.lights {
|
if let Some((light, pick_prob)) = scene.pick_light(rec.point, rng) {
|
||||||
radiance += throughput.mul_elem(sample_box_light(scene, light, &rec, albedo, rng));
|
let direct = sample_direct(scene, light, &rec, albedo, rng) / pick_prob;
|
||||||
|
radiance += throughput.mul_elem(direct);
|
||||||
}
|
}
|
||||||
count_emission = false;
|
count_emission = false;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -142,10 +122,6 @@ const ADAPTIVE_MIN_SPP: u32 = 32;
|
||||||
/// ...then later passes grow each still-noisy pixel's count by this fraction.
|
/// ...then later passes grow each still-noisy pixel's count by this fraction.
|
||||||
const ADAPTIVE_GROWTH: f64 = 0.5;
|
const ADAPTIVE_GROWTH: f64 = 0.5;
|
||||||
|
|
||||||
fn luminance(c: Vec3) -> f64 {
|
|
||||||
0.2126 * c.x + 0.7152 * c.y + 0.0722 * c.z
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Running mean and variance of a pixel's luminance (Welford's algorithm).
|
/// Running mean and variance of a pixel's luminance (Welford's algorithm).
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct PixelStats {
|
struct PixelStats {
|
||||||
|
|
@ -194,7 +170,7 @@ impl Renderer<'_> {
|
||||||
let v = ((height - 1 - j) as f64 + px.rng.gen::<f64>()) / height as f64;
|
let v = ((height - 1 - j) as f64 + px.rng.gen::<f64>()) / height as f64;
|
||||||
let sample = trace(self.scene, &self.cam.get_ray(u, v), self.s.max_depth, &mut px.rng);
|
let sample = trace(self.scene, &self.cam.get_ray(u, v), self.s.max_depth, &mut px.rng);
|
||||||
px.sum += sample;
|
px.sum += sample;
|
||||||
px.stats.add(luminance(sample));
|
px.stats.add(sample.luminance());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,15 +210,16 @@ fn bar(len: u64, template: &str) -> ProgressBar {
|
||||||
/// Returns the gamma-encoded image and the total number of samples taken.
|
/// Returns the gamma-encoded image and the total number of samples taken.
|
||||||
fn render(scene: &Scene, s: &Settings) -> (Vec<Vec3>, u64) {
|
fn render(scene: &Scene, s: &Settings) -> (Vec<Vec3>, u64) {
|
||||||
let (width, height) = (s.width, s.height);
|
let (width, height) = (s.width, s.height);
|
||||||
|
let view = scene.view;
|
||||||
let r = Renderer {
|
let r = Renderer {
|
||||||
scene,
|
scene,
|
||||||
cam: Camera::new(
|
cam: Camera::new(
|
||||||
Vec3::new(278.0, 278.0, -800.0),
|
view.lookfrom,
|
||||||
Vec3::new(278.0, 278.0, 278.0),
|
view.lookat,
|
||||||
Vec3::new(0.0, 1.0, 0.0),
|
Vec3::new(0.0, 1.0, 0.0),
|
||||||
40.0,
|
view.vfov,
|
||||||
width as f64 / height as f64,
|
width as f64 / height as f64,
|
||||||
1078.0,
|
(view.lookfrom - view.lookat).length(),
|
||||||
),
|
),
|
||||||
s,
|
s,
|
||||||
};
|
};
|
||||||
|
|
@ -356,32 +333,73 @@ fn print_region_table(image: &[Vec3], width: usize, height: usize) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const USAGE: &str = "usage: pathtracer [--scene NAME] [--list-scenes] \
|
||||||
|
[width] [height] [samples_per_pixel] [max_bounces] [output] [noise_threshold]";
|
||||||
|
|
||||||
|
fn list_scenes() {
|
||||||
|
println!("Scenes:");
|
||||||
|
for s in scenes::SCENES {
|
||||||
|
println!(" {:<10} {}", s.name, s.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
// Pull out the --scene / --list-scenes options; the rest are positional.
|
||||||
|
let mut scene_name = String::from("cornell");
|
||||||
|
let mut args: Vec<String> = Vec::new();
|
||||||
|
let mut raw = std::env::args().skip(1);
|
||||||
|
while let Some(a) = raw.next() {
|
||||||
|
if a == "--list-scenes" {
|
||||||
|
list_scenes();
|
||||||
|
return;
|
||||||
|
} else if a == "--scene" {
|
||||||
|
scene_name = raw.next().unwrap_or_else(|| {
|
||||||
|
eprintln!("{USAGE}");
|
||||||
|
std::process::exit(2);
|
||||||
|
});
|
||||||
|
} else if let Some(name) = a.strip_prefix("--scene=") {
|
||||||
|
scene_name = name.to_string();
|
||||||
|
} else if a == "--help" || a == "-h" {
|
||||||
|
println!("{USAGE}");
|
||||||
|
list_scenes();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
args.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(scene_info) = scenes::find(&scene_name) else {
|
||||||
|
eprintln!("unknown scene '{scene_name}'");
|
||||||
|
list_scenes();
|
||||||
|
std::process::exit(2);
|
||||||
|
};
|
||||||
|
|
||||||
let arg = |i: usize, default: &str| args.get(i).cloned().unwrap_or_else(|| default.to_string());
|
let arg = |i: usize, default: &str| args.get(i).cloned().unwrap_or_else(|| default.to_string());
|
||||||
fn parse<T: std::str::FromStr>(s: String) -> T {
|
fn parse<T: std::str::FromStr>(s: String) -> T {
|
||||||
s.parse().unwrap_or_else(|_| {
|
s.parse().unwrap_or_else(|_| {
|
||||||
eprintln!("usage: pathtracer [width] [height] [samples_per_pixel] [max_bounces] [output] [noise_threshold]");
|
eprintln!("{USAGE}");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings = Settings {
|
let settings = Settings {
|
||||||
width: parse(arg(1, "400")),
|
width: parse(arg(0, "400")),
|
||||||
height: parse(arg(2, "300")),
|
height: parse(arg(1, "300")),
|
||||||
spp: parse(arg(3, "50")),
|
spp: parse(arg(2, "50")),
|
||||||
max_depth: parse(arg(4, "15")),
|
max_depth: parse(arg(3, "15")),
|
||||||
output: arg(5, "cornell_box.png"),
|
output: arg(4, &format!("{scene_name}.png")),
|
||||||
noise_threshold: parse(arg(6, "0")),
|
noise_threshold: parse(arg(5, "0")),
|
||||||
};
|
};
|
||||||
if settings.width == 0 || settings.height == 0 || settings.spp == 0 || settings.noise_threshold < 0.0 {
|
if settings.width == 0 || settings.height == 0 || settings.spp == 0 || settings.noise_threshold < 0.0 {
|
||||||
eprintln!("width, height and samples_per_pixel must be >= 1, noise_threshold >= 0");
|
eprintln!("width, height and samples_per_pixel must be >= 1, noise_threshold >= 0");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("=== Cornell Box Path Tracer (Rust) ===");
|
let scene = (scene_info.build)();
|
||||||
println!("Features: reflections, refraction/caustics, multiple bounces, NEE");
|
|
||||||
|
println!("=== Path Tracer (Rust) ===");
|
||||||
|
println!("Features: reflections, refraction/caustics, multiple bounces, NEE, BVH");
|
||||||
println!();
|
println!();
|
||||||
|
println!("Scene: {} ({} objects, {} lights)", scene_name, scene.object_count(), scene.light_count());
|
||||||
println!("Resolution: {}x{}", settings.width, settings.height);
|
println!("Resolution: {}x{}", settings.width, settings.height);
|
||||||
println!("Samples per pixel: {} (max)", settings.spp);
|
println!("Samples per pixel: {} (max)", settings.spp);
|
||||||
println!("Max bounces: {} (Russian roulette after {})", settings.max_depth, RR_MIN_DEPTH);
|
println!("Max bounces: {} (Russian roulette after {})", settings.max_depth, RR_MIN_DEPTH);
|
||||||
|
|
@ -394,7 +412,6 @@ fn main() {
|
||||||
println!("Output: {}", settings.output);
|
println!("Output: {}", settings.output);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let scene = cornell_box();
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let (image, total_samples) = render(&scene, &settings);
|
let (image, total_samples) = render(&scene, &settings);
|
||||||
let elapsed = start.elapsed().as_secs_f64();
|
let elapsed = start.elapsed().as_secs_f64();
|
||||||
|
|
@ -413,6 +430,9 @@ fn main() {
|
||||||
total_samples as f64 / elapsed / 1e6,
|
total_samples as f64 / elapsed / 1e6,
|
||||||
total_samples as f64 / pixels
|
total_samples as f64 / pixels
|
||||||
);
|
);
|
||||||
println!();
|
// The regions (left wall, floor, ...) only mean something for the Cornell box.
|
||||||
print_region_table(&image, settings.width, settings.height);
|
if scene_name == "cornell" {
|
||||||
|
println!();
|
||||||
|
print_region_table(&image, settings.width, settings.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
|
use crate::bvh::{Aabb, Bvh};
|
||||||
use crate::vec3::{reflect, refract, schlick, Vec3};
|
use crate::vec3::{reflect, refract, schlick, Vec3};
|
||||||
|
|
||||||
pub const EPSILON: f64 = 1e-6;
|
pub const EPSILON: f64 = 1e-6;
|
||||||
|
|
@ -114,6 +115,10 @@ pub enum Shape {
|
||||||
Sphere { center: Vec3, radius: f64 },
|
Sphere { center: Vec3, radius: f64 },
|
||||||
Plane { point: Vec3, normal: Vec3 },
|
Plane { point: Vec3, normal: Vec3 },
|
||||||
Box { min: Vec3, max: Vec3 },
|
Box { min: Vec3, max: Vec3 },
|
||||||
|
/// Axis-aligned rectangle spanning min..max, flat along the axis of
|
||||||
|
/// `normal` (which must be ±x, ±y or ±z). As a light it emits only on the
|
||||||
|
/// `normal` side.
|
||||||
|
Rect { min: Vec3, max: Vec3, normal: Vec3 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Shape {
|
impl Shape {
|
||||||
|
|
@ -183,6 +188,38 @@ impl Shape {
|
||||||
n[hit_axis] = sign;
|
n[hit_axis] = sign;
|
||||||
Some((t_lo, Vec3::new(n[0], n[1], n[2])))
|
Some((t_lo, Vec3::new(n[0], n[1], n[2])))
|
||||||
}
|
}
|
||||||
|
Shape::Rect { min, max, normal } => {
|
||||||
|
let denom = r.direction.dot(normal);
|
||||||
|
if denom.abs() < 1e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let t = (min - r.origin).dot(normal) / denom;
|
||||||
|
if t < t_min || t > t_max {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let p = r.at(t);
|
||||||
|
let tol = 1e-9;
|
||||||
|
let inside = (0..3).all(|a| p[a] >= min[a] - tol && p[a] <= max[a] + tol);
|
||||||
|
inside.then_some((t, normal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shape {
|
||||||
|
/// None for unbounded shapes (planes), which are tested outside the BVH.
|
||||||
|
fn bounds(&self) -> Option<Aabb> {
|
||||||
|
match *self {
|
||||||
|
Shape::Sphere { center, radius } => Some(Aabb {
|
||||||
|
min: center - Vec3::splat(radius),
|
||||||
|
max: center + Vec3::splat(radius),
|
||||||
|
}),
|
||||||
|
Shape::Box { min, max } => Some(Aabb { min, max }),
|
||||||
|
Shape::Rect { min, max, .. } => Some(Aabb {
|
||||||
|
min: min - Vec3::splat(1e-9),
|
||||||
|
max: max + Vec3::splat(1e-9),
|
||||||
|
}),
|
||||||
|
Shape::Plane { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,79 +230,278 @@ pub struct Object {
|
||||||
pub material: Material,
|
pub material: Material,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An axis-aligned box light, sampled explicitly for next event estimation.
|
/// An emitter that next event estimation can sample: a box, sphere or rectangle.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct BoxLight {
|
pub struct Light {
|
||||||
pub min: Vec3,
|
shape: Shape,
|
||||||
pub max: Vec3,
|
|
||||||
pub emission: Vec3,
|
pub emission: Vec3,
|
||||||
|
/// Total emitted power (up to a constant): luminance * area.
|
||||||
|
power: f64,
|
||||||
|
center: Vec3,
|
||||||
|
/// Half the bounding box diagonal: how close a point can be to the centre
|
||||||
|
/// while still outside the light.
|
||||||
|
extent: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Light {
|
||||||
|
fn new(shape: Shape, emission: Vec3) -> Self {
|
||||||
|
let bounds = shape.bounds().expect("planes cannot be lights");
|
||||||
|
let mut light = Light {
|
||||||
|
shape,
|
||||||
|
emission,
|
||||||
|
power: 0.0,
|
||||||
|
center: bounds.centroid(),
|
||||||
|
extent: 0.5 * (bounds.max - bounds.min).length(),
|
||||||
|
};
|
||||||
|
light.power = emission.luminance() * light.area();
|
||||||
|
light
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn area(&self) -> f64 {
|
||||||
|
match self.shape {
|
||||||
|
Shape::Box { min, max } => {
|
||||||
|
let d = max - min;
|
||||||
|
2.0 * (d.x * d.y + d.y * d.z + d.x * d.z)
|
||||||
|
}
|
||||||
|
Shape::Sphere { radius, .. } => 4.0 * std::f64::consts::PI * radius * radius,
|
||||||
|
Shape::Rect { min, max, normal } => {
|
||||||
|
let d = max - min;
|
||||||
|
if normal.x != 0.0 {
|
||||||
|
d.y * d.z
|
||||||
|
} else if normal.y != 0.0 {
|
||||||
|
d.x * d.z
|
||||||
|
} else {
|
||||||
|
d.x * d.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Shape::Plane { .. } => unreachable!("planes cannot be lights"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rough estimate of how much this light contributes at `point`: its
|
||||||
|
/// power over the squared distance, clamped so points near or inside the
|
||||||
|
/// light's bounds don't blow up.
|
||||||
|
fn importance(&self, point: Vec3) -> f64 {
|
||||||
|
let d = point - self.center;
|
||||||
|
self.power / d.dot(d).max(self.extent * self.extent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A uniformly distributed point on the surface (pdf 1 / area) and the
|
||||||
|
/// outward normal there.
|
||||||
|
pub fn sample<R: Rng>(&self, rng: &mut R) -> (Vec3, Vec3) {
|
||||||
|
match self.shape {
|
||||||
|
Shape::Box { min, max } => {
|
||||||
|
// Pick a face with probability proportional to its area, then
|
||||||
|
// a uniform point on it.
|
||||||
|
let size = max - min;
|
||||||
|
let face_areas = [size.y * size.z, size.x * size.z, size.x * size.y];
|
||||||
|
let u = rng.gen::<f64>() * (face_areas[0] + face_areas[1] + face_areas[2]);
|
||||||
|
let axis = if u < face_areas[0] {
|
||||||
|
0
|
||||||
|
} else if u < face_areas[0] + face_areas[1] {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
};
|
||||||
|
let positive_side = rng.gen::<f64>() < 0.5;
|
||||||
|
let mut p = [
|
||||||
|
min.x + size.x * rng.gen::<f64>(),
|
||||||
|
min.y + size.y * rng.gen::<f64>(),
|
||||||
|
min.z + size.z * rng.gen::<f64>(),
|
||||||
|
];
|
||||||
|
p[axis] = if positive_side { max[axis] } else { min[axis] };
|
||||||
|
let mut n = [0.0; 3];
|
||||||
|
n[axis] = if positive_side { 1.0 } else { -1.0 };
|
||||||
|
(Vec3::new(p[0], p[1], p[2]), Vec3::new(n[0], n[1], n[2]))
|
||||||
|
}
|
||||||
|
Shape::Sphere { center, radius } => {
|
||||||
|
let z = 1.0 - 2.0 * rng.gen::<f64>();
|
||||||
|
let r = (1.0 - z * z).max(0.0).sqrt();
|
||||||
|
let phi = 2.0 * std::f64::consts::PI * rng.gen::<f64>();
|
||||||
|
let n = Vec3::new(r * phi.cos(), r * phi.sin(), z);
|
||||||
|
(center + radius * n, n)
|
||||||
|
}
|
||||||
|
Shape::Rect { min, max, normal } => {
|
||||||
|
let size = max - min;
|
||||||
|
let p = Vec3::new(
|
||||||
|
min.x + size.x * rng.gen::<f64>(),
|
||||||
|
min.y + size.y * rng.gen::<f64>(),
|
||||||
|
min.z + size.z * rng.gen::<f64>(),
|
||||||
|
);
|
||||||
|
(p, normal)
|
||||||
|
}
|
||||||
|
Shape::Plane { .. } => unreachable!("planes cannot be lights"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a ray that leaves the scene sees.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Background {
|
||||||
|
/// White-to-blue gradient, as in raytracer.py.
|
||||||
|
Sky,
|
||||||
|
Color(Vec3),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Background {
|
||||||
|
pub fn radiance(&self, direction: Vec3) -> Vec3 {
|
||||||
|
match *self {
|
||||||
|
Background::Sky => {
|
||||||
|
let t = 0.5 * (direction.y + 1.0);
|
||||||
|
Vec3::ONE * (1.0 - t) + Vec3::new(0.5, 0.7, 1.0) * t
|
||||||
|
}
|
||||||
|
Background::Color(c) => c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct View {
|
||||||
|
pub lookfrom: Vec3,
|
||||||
|
pub lookat: Vec3,
|
||||||
|
pub vfov: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SceneBuilder {
|
||||||
|
objects: Vec<Object>,
|
||||||
|
planes: Vec<Object>,
|
||||||
|
lights: Vec<Light>,
|
||||||
|
background: Background,
|
||||||
|
view: View,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SceneBuilder {
|
||||||
|
pub fn new(view: View, background: Background) -> Self {
|
||||||
|
SceneBuilder {
|
||||||
|
objects: Vec::new(),
|
||||||
|
planes: Vec::new(),
|
||||||
|
lights: Vec::new(),
|
||||||
|
background,
|
||||||
|
view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, shape: Shape, material: Material) {
|
||||||
|
let obj = Object { shape, material };
|
||||||
|
match shape {
|
||||||
|
Shape::Plane { .. } => self.planes.push(obj),
|
||||||
|
_ => self.objects.push(obj),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plane(&mut self, point: Vec3, normal: Vec3, material: Material) {
|
||||||
|
self.add(Shape::Plane { point, normal: normal.normalize() }, material);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sphere(&mut self, center: Vec3, radius: f64, material: Material) {
|
||||||
|
self.add(Shape::Sphere { center, radius }, material);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cuboid(&mut self, min: Vec3, max: Vec3, material: Material) {
|
||||||
|
self.add(Shape::Box { min, max }, material);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every emitter must be added through a *_light method: after a diffuse
|
||||||
|
/// bounce, trace() only counts light found by sampling these.
|
||||||
|
pub fn box_light(&mut self, min: Vec3, max: Vec3, emission: Vec3) {
|
||||||
|
self.add_light(Shape::Box { min, max }, emission);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sphere_light(&mut self, center: Vec3, radius: f64, emission: Vec3) {
|
||||||
|
self.add_light(Shape::Sphere { center, radius }, emission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-sided rectangular light emitting towards `normal` (±x, ±y or ±z).
|
||||||
|
pub fn rect_light(&mut self, min: Vec3, max: Vec3, normal: Vec3, emission: Vec3) {
|
||||||
|
self.add_light(Shape::Rect { min, max, normal }, emission);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_light(&mut self, shape: Shape, emission: Vec3) {
|
||||||
|
self.lights.push(Light::new(shape, emission));
|
||||||
|
self.add(shape, Material::Light { emission });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Scene {
|
||||||
|
let bounds: Vec<Aabb> = self.objects.iter().map(|o| o.shape.bounds().unwrap()).collect();
|
||||||
|
Scene {
|
||||||
|
bvh: Bvh::build(&bounds),
|
||||||
|
objects: self.objects,
|
||||||
|
planes: self.planes,
|
||||||
|
lights: self.lights,
|
||||||
|
background: self.background,
|
||||||
|
view: self.view,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
pub objects: Vec<Object>,
|
objects: Vec<Object>,
|
||||||
pub lights: Vec<BoxLight>,
|
planes: Vec<Object>,
|
||||||
|
bvh: Bvh,
|
||||||
|
lights: Vec<Light>,
|
||||||
|
pub background: Background,
|
||||||
|
pub view: View,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
pub fn add(&mut self, shape: Shape, material: Material) {
|
pub fn object_count(&self) -> usize {
|
||||||
self.objects.push(Object { shape, material });
|
self.objects.len() + self.planes.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_box_light(&mut self, min: Vec3, max: Vec3, emission: Vec3) {
|
pub fn light_count(&self) -> usize {
|
||||||
self.lights.push(BoxLight { min, max, emission });
|
self.lights.len()
|
||||||
self.add(Shape::Box { min, max }, Material::Light { emission });
|
}
|
||||||
|
|
||||||
|
/// Picks one light for shading `point`, with probability proportional to
|
||||||
|
/// its estimated contribution there (power / distance^2), so a scene with
|
||||||
|
/// many lights costs one shadow ray per shading point and mostly samples
|
||||||
|
/// nearby lights. Returns the light and the probability of picking it.
|
||||||
|
/// This is O(lights) per call; thousands of lights would want a light tree.
|
||||||
|
pub fn pick_light<R: Rng>(&self, point: Vec3, rng: &mut R) -> Option<(&Light, f64)> {
|
||||||
|
match self.lights.len() {
|
||||||
|
0 => None,
|
||||||
|
1 => Some((&self.lights[0], 1.0)),
|
||||||
|
_ => {
|
||||||
|
let total: f64 = self.lights.iter().map(|l| l.importance(point)).sum();
|
||||||
|
let u = rng.gen::<f64>() * total;
|
||||||
|
let mut acc = 0.0;
|
||||||
|
for light in &self.lights {
|
||||||
|
let w = light.importance(point);
|
||||||
|
acc += w;
|
||||||
|
if u < acc {
|
||||||
|
return Some((light, w / total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Rounding left u just past the last bucket.
|
||||||
|
let last = self.lights.last().unwrap();
|
||||||
|
Some((last, last.importance(point) / total))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
|
pub fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
|
||||||
let mut closest: Option<(f64, Vec3, Material)> = None;
|
let mut closest: Option<(f64, Vec3, Material)> = None;
|
||||||
let mut min_t = t_max;
|
let mut min_t = t_max;
|
||||||
for obj in &self.objects {
|
for obj in &self.planes {
|
||||||
if let Some((t, outward)) = obj.shape.hit(r, t_min, min_t) {
|
if let Some((t, outward)) = obj.shape.hit(r, t_min, min_t) {
|
||||||
if t < min_t {
|
min_t = t;
|
||||||
min_t = t;
|
closest = Some((t, outward, obj.material));
|
||||||
closest = Some((t, outward, obj.material));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.bvh.closest(r, t_min, min_t, |i, t_max| {
|
||||||
|
let obj = &self.objects[i];
|
||||||
|
let (t, outward) = obj.shape.hit(r, t_min, t_max)?;
|
||||||
|
closest = Some((t, outward, obj.material));
|
||||||
|
Some(t)
|
||||||
|
});
|
||||||
closest.map(|(t, outward, material)| HitRecord::new(r, t, outward, material))
|
closest.map(|(t, outward, material)| HitRecord::new(r, t, outward, material))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blocked(&self, origin: Vec3, direction: Vec3, t_max: f64) -> bool {
|
pub fn blocked(&self, origin: Vec3, direction: Vec3, t_max: f64) -> bool {
|
||||||
let r = Ray::new(origin, direction);
|
let r = Ray::from_unit(origin, direction);
|
||||||
self.objects
|
self.planes.iter().any(|obj| obj.shape.hit(&r, EPSILON, t_max).is_some())
|
||||||
.iter()
|
|| self
|
||||||
.any(|obj| obj.shape.hit(&r, EPSILON, t_max).is_some())
|
.bvh
|
||||||
|
.any(&r, EPSILON, t_max, |i| self.objects[i].shape.hit(&r, EPSILON, t_max).is_some())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cornell_box() -> Scene {
|
|
||||||
let mut scene = Scene::default();
|
|
||||||
|
|
||||||
let white = Material::Diffuse { albedo: Vec3::new(0.73, 0.73, 0.73) };
|
|
||||||
let gray = Material::Diffuse { albedo: Vec3::new(0.5, 0.5, 0.5) };
|
|
||||||
let red = Material::Diffuse { albedo: Vec3::new(0.65, 0.05, 0.05) };
|
|
||||||
let green = Material::Diffuse { albedo: Vec3::new(0.12, 0.45, 0.15) };
|
|
||||||
let mirror = Material::Mirror { albedo: Vec3::new(0.95, 0.95, 0.95) };
|
|
||||||
let glass = Material::Dielectric { ref_idx: 1.5, albedo: Vec3::ONE };
|
|
||||||
|
|
||||||
let plane = |p: Vec3, n: Vec3| Shape::Plane { point: p, normal: n.normalize() };
|
|
||||||
scene.add(plane(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0)), gray);
|
|
||||||
scene.add(plane(Vec3::new(0.0, 500.0, 0.0), Vec3::new(0.0, -1.0, 0.0)), white);
|
|
||||||
scene.add(plane(Vec3::new(0.0, 0.0, 500.0), Vec3::new(0.0, 0.0, -1.0)), white);
|
|
||||||
scene.add(plane(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)), red);
|
|
||||||
scene.add(plane(Vec3::new(500.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0)), green);
|
|
||||||
|
|
||||||
scene.add_box_light(
|
|
||||||
Vec3::new(0.0, 180.0, 180.0),
|
|
||||||
Vec3::new(25.0, 220.0, 320.0),
|
|
||||||
Vec3::splat(50.0),
|
|
||||||
);
|
|
||||||
|
|
||||||
let sphere = |c: Vec3, r: f64| Shape::Sphere { center: c, radius: r };
|
|
||||||
scene.add(sphere(Vec3::new(250.0, 180.0, 250.0), 80.0), glass);
|
|
||||||
scene.add(sphere(Vec3::new(150.0, 180.0, 380.0), 80.0), mirror);
|
|
||||||
scene.add(sphere(Vec3::new(350.0, 160.0, 300.0), 60.0), red);
|
|
||||||
|
|
||||||
scene
|
|
||||||
}
|
|
||||||
|
|
|
||||||
220
rust/src/scenes.rs
Normal file
220
rust/src/scenes.rs
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
//! The built-in scenes, selectable with `--scene NAME`.
|
||||||
|
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
use rand::{Rng, SeedableRng};
|
||||||
|
|
||||||
|
use crate::scene::{Background, Material, Scene, SceneBuilder, View};
|
||||||
|
use crate::vec3::Vec3;
|
||||||
|
|
||||||
|
pub struct SceneInfo {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub description: &'static str,
|
||||||
|
pub build: fn() -> Scene,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const SCENES: &[SceneInfo] = &[
|
||||||
|
SceneInfo {
|
||||||
|
name: "cornell",
|
||||||
|
description: "Cornell box from raytracer.py: glass, mirror and diffuse spheres, wall light",
|
||||||
|
build: cornell,
|
||||||
|
},
|
||||||
|
SceneInfo {
|
||||||
|
name: "caustics",
|
||||||
|
description: "Clear, amber and blue glass plus a mirror under one light, focusing caustics onto the floor",
|
||||||
|
build: caustics,
|
||||||
|
},
|
||||||
|
SceneInfo {
|
||||||
|
name: "spheres",
|
||||||
|
description: "Large scene: ~480 random spheres under a sky (exercises the BVH)",
|
||||||
|
build: spheres,
|
||||||
|
},
|
||||||
|
SceneInfo {
|
||||||
|
name: "night",
|
||||||
|
description: "The same sphere field at night, lit by dozens of glowing spheres (many-light sampling)",
|
||||||
|
build: night,
|
||||||
|
},
|
||||||
|
SceneInfo {
|
||||||
|
name: "classic",
|
||||||
|
description: "The classic Cornell box: ceiling light, two boxes, colour bleeding",
|
||||||
|
build: classic,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn find(name: &str) -> Option<&'static SceneInfo> {
|
||||||
|
SCENES.iter().find(|s| s.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diffuse(r: f64, g: f64, b: f64) -> Material {
|
||||||
|
Material::Diffuse { albedo: Vec3::new(r, g, b) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirror(r: f64, g: f64, b: f64) -> Material {
|
||||||
|
Material::Mirror { albedo: Vec3::new(r, g, b) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Glass; `tint` is applied at every reflection/refraction, so colour
|
||||||
|
/// deepens with each pass through the surface.
|
||||||
|
fn glass(tint: Vec3) -> Material {
|
||||||
|
Material::Dielectric { ref_idx: 1.5, albedo: tint }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cornell() -> Scene {
|
||||||
|
let mut b = SceneBuilder::new(
|
||||||
|
View {
|
||||||
|
lookfrom: Vec3::new(278.0, 278.0, -800.0),
|
||||||
|
lookat: Vec3::new(278.0, 278.0, 278.0),
|
||||||
|
vfov: 40.0,
|
||||||
|
},
|
||||||
|
Background::Sky,
|
||||||
|
);
|
||||||
|
|
||||||
|
let white = diffuse(0.73, 0.73, 0.73);
|
||||||
|
b.plane(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), diffuse(0.5, 0.5, 0.5));
|
||||||
|
b.plane(Vec3::new(0.0, 500.0, 0.0), Vec3::new(0.0, -1.0, 0.0), white);
|
||||||
|
b.plane(Vec3::new(0.0, 0.0, 500.0), Vec3::new(0.0, 0.0, -1.0), white);
|
||||||
|
b.plane(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), diffuse(0.65, 0.05, 0.05));
|
||||||
|
b.plane(Vec3::new(500.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), diffuse(0.12, 0.45, 0.15));
|
||||||
|
|
||||||
|
b.box_light(Vec3::new(0.0, 180.0, 180.0), Vec3::new(25.0, 220.0, 320.0), Vec3::splat(50.0));
|
||||||
|
|
||||||
|
b.sphere(Vec3::new(250.0, 180.0, 250.0), 80.0, glass(Vec3::ONE));
|
||||||
|
b.sphere(Vec3::new(150.0, 180.0, 380.0), 80.0, mirror(0.95, 0.95, 0.95));
|
||||||
|
b.sphere(Vec3::new(350.0, 160.0, 300.0), 60.0, diffuse(0.65, 0.05, 0.05));
|
||||||
|
b.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn caustics() -> Scene {
|
||||||
|
let mut b = SceneBuilder::new(
|
||||||
|
View {
|
||||||
|
lookfrom: Vec3::new(0.0, 160.0, -280.0),
|
||||||
|
lookat: Vec3::new(20.0, 30.0, 120.0),
|
||||||
|
vfov: 38.0,
|
||||||
|
},
|
||||||
|
Background::Color(Vec3::ZERO),
|
||||||
|
);
|
||||||
|
|
||||||
|
b.plane(Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), diffuse(0.8, 0.8, 0.8));
|
||||||
|
b.plane(Vec3::new(0.0, 0.0, 520.0), Vec3::new(0.0, 0.0, -1.0), diffuse(0.6, 0.6, 0.6));
|
||||||
|
|
||||||
|
// One downward-facing panel above and well behind the spheres. A glass
|
||||||
|
// sphere focuses light about 1.5 radii from its centre; with the light
|
||||||
|
// ~48 degrees from vertical that focus lands on the floor just in front
|
||||||
|
// of each sphere, facing the camera. A smaller panel gives sharper
|
||||||
|
// caustics but makes them slower to converge.
|
||||||
|
b.rect_light(
|
||||||
|
Vec3::new(-30.0, 250.0, 330.0),
|
||||||
|
Vec3::new(30.0, 250.0, 390.0),
|
||||||
|
Vec3::new(0.0, -1.0, 0.0),
|
||||||
|
Vec3::splat(90.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
b.sphere(Vec3::new(-75.0, 45.0, 110.0), 45.0, glass(Vec3::ONE));
|
||||||
|
b.sphere(Vec3::new(40.0, 38.0, 80.0), 38.0, glass(Vec3::new(1.0, 0.7, 0.3)));
|
||||||
|
b.sphere(Vec3::new(125.0, 30.0, 150.0), 30.0, glass(Vec3::new(0.4, 0.65, 1.0)));
|
||||||
|
b.sphere(Vec3::new(-10.0, 35.0, 210.0), 35.0, mirror(0.9, 0.9, 0.9));
|
||||||
|
b.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A grid of small random spheres around three large feature spheres (after
|
||||||
|
/// "Ray Tracing in One Weekend"). A `light_fraction` of the small spheres glow.
|
||||||
|
fn sphere_field(b: &mut SceneBuilder, light_fraction: f64) {
|
||||||
|
let mut rng = SmallRng::seed_from_u64(7);
|
||||||
|
b.plane(Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), diffuse(0.5, 0.5, 0.5));
|
||||||
|
|
||||||
|
let big = [
|
||||||
|
(Vec3::new(0.0, 1.0, 0.0), glass(Vec3::ONE)),
|
||||||
|
(Vec3::new(-4.0, 1.0, 0.0), diffuse(0.4, 0.2, 0.1)),
|
||||||
|
(Vec3::new(4.0, 1.0, 0.0), mirror(0.7, 0.6, 0.5)),
|
||||||
|
];
|
||||||
|
for (center, material) in big {
|
||||||
|
b.sphere(center, 1.0, material);
|
||||||
|
}
|
||||||
|
|
||||||
|
for a in -11..11 {
|
||||||
|
for c in -11..11 {
|
||||||
|
let center = Vec3::new(
|
||||||
|
a as f64 + 0.9 * rng.gen::<f64>(),
|
||||||
|
0.2,
|
||||||
|
c as f64 + 0.9 * rng.gen::<f64>(),
|
||||||
|
);
|
||||||
|
if big.iter().any(|(bc, _)| (center - Vec3::new(bc.x, 0.2, bc.z)).length() < 1.2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let rnd_color = |rng: &mut SmallRng| Vec3::new(rng.gen(), rng.gen(), rng.gen());
|
||||||
|
if rng.gen::<f64>() < light_fraction {
|
||||||
|
// Warm-to-white glow of varying strength.
|
||||||
|
let warmth: f64 = rng.gen();
|
||||||
|
let color = Vec3::new(1.0, 0.55 + 0.4 * warmth, 0.2 + 0.6 * warmth);
|
||||||
|
b.sphere_light(center, 0.2, color * (6.0 + 10.0 * rng.gen::<f64>()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let choice: f64 = rng.gen();
|
||||||
|
let material = if choice < 0.75 {
|
||||||
|
let albedo = rnd_color(&mut rng).mul_elem(rnd_color(&mut rng));
|
||||||
|
Material::Diffuse { albedo }
|
||||||
|
} else if choice < 0.9 {
|
||||||
|
Material::Mirror { albedo: Vec3::splat(0.5) + 0.5 * rnd_color(&mut rng) }
|
||||||
|
} else {
|
||||||
|
glass(Vec3::ONE)
|
||||||
|
};
|
||||||
|
b.sphere(center, 0.2, material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spheres() -> Scene {
|
||||||
|
let mut b = SceneBuilder::new(
|
||||||
|
View {
|
||||||
|
lookfrom: Vec3::new(13.0, 2.0, 3.0),
|
||||||
|
lookat: Vec3::ZERO,
|
||||||
|
vfov: 20.0,
|
||||||
|
},
|
||||||
|
Background::Sky,
|
||||||
|
);
|
||||||
|
sphere_field(&mut b, 0.0);
|
||||||
|
b.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn night() -> Scene {
|
||||||
|
let mut b = SceneBuilder::new(
|
||||||
|
View {
|
||||||
|
lookfrom: Vec3::new(13.0, 2.0, 3.0),
|
||||||
|
lookat: Vec3::ZERO,
|
||||||
|
vfov: 20.0,
|
||||||
|
},
|
||||||
|
Background::Color(Vec3::new(0.004, 0.006, 0.015)),
|
||||||
|
);
|
||||||
|
sphere_field(&mut b, 0.12);
|
||||||
|
b.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classic() -> Scene {
|
||||||
|
let mut b = SceneBuilder::new(
|
||||||
|
View {
|
||||||
|
lookfrom: Vec3::new(278.0, 278.0, -800.0),
|
||||||
|
lookat: Vec3::new(278.0, 278.0, 0.0),
|
||||||
|
vfov: 40.0,
|
||||||
|
},
|
||||||
|
Background::Color(Vec3::ZERO),
|
||||||
|
);
|
||||||
|
|
||||||
|
let white = diffuse(0.73, 0.73, 0.73);
|
||||||
|
b.plane(Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), white);
|
||||||
|
b.plane(Vec3::new(0.0, 555.0, 0.0), Vec3::new(0.0, -1.0, 0.0), white);
|
||||||
|
b.plane(Vec3::new(0.0, 0.0, 555.0), Vec3::new(0.0, 0.0, -1.0), white);
|
||||||
|
// The camera looks down +z with +x to its left, so red ends up on the left as usual.
|
||||||
|
b.plane(Vec3::new(555.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), diffuse(0.65, 0.05, 0.05));
|
||||||
|
b.plane(Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), diffuse(0.12, 0.45, 0.15));
|
||||||
|
|
||||||
|
// A flat panel just under the ceiling, shining down only.
|
||||||
|
b.rect_light(
|
||||||
|
Vec3::new(213.0, 554.0, 227.0),
|
||||||
|
Vec3::new(343.0, 554.0, 332.0),
|
||||||
|
Vec3::new(0.0, -1.0, 0.0),
|
||||||
|
Vec3::splat(15.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
b.cuboid(Vec3::new(130.0, 0.0, 65.0), Vec3::new(295.0, 165.0, 230.0), white);
|
||||||
|
b.cuboid(Vec3::new(265.0, 0.0, 295.0), Vec3::new(430.0, 330.0, 460.0), white);
|
||||||
|
b.build()
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,11 @@ impl Vec3 {
|
||||||
pub fn mean(self) -> f64 {
|
pub fn mean(self) -> f64 {
|
||||||
(self.x + self.y + self.z) / 3.0
|
(self.x + self.y + self.z) / 3.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rec. 709 luminance of a linear RGB colour.
|
||||||
|
pub fn luminance(self) -> f64 {
|
||||||
|
0.2126 * self.x + 0.7152 * self.y + 0.0722 * self.z
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Index<usize> for Vec3 {
|
impl Index<usize> for Vec3 {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue