Compare commits

...

6 Commits

Author SHA1 Message Date
3619b97071 add README 2026-10-14 22:20:16 +03:00
21dec30715 add a screenshot 2026-10-14 22:04:35 +03:00
6f4ce25cc8 metallic reflections!
not exactly accurate, but look OK
2025-11-26 15:15:36 +03:00
a98eda1616 better colors for the show 2025-11-26 11:47:31 +03:00
13fb710dc7 full color! 2025-11-25 15:13:52 +03:00
ca460bc0d9 make colormap respect color 2025-11-25 14:51:49 +03:00
6 changed files with 113 additions and 32 deletions

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Forward ray tracing demo
![Screenshot](screenshot.jpg)
A simple forward ray tracing demo. “Forward” means the rays are traced starting from the light (unlike common “reverse” ray tracing where rays are traced from the camera).

BIN
screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -12,7 +12,7 @@ use crate::{
DEPTH_FORMAT, OUTPUT_FORMAT, DEPTH_FORMAT, OUTPUT_FORMAT,
lines::{LookParams, Mesh, Pipeline, Vertex}, lines::{LookParams, Mesh, Pipeline, Vertex},
}, },
trace::{Hit, Lambertian, Reflector, Scene, Source, Sphere}, trace::{DiscReflector, Hit, Lambertian, Reflector, Scene, Source, Sphere},
}; };
mod camera; mod camera;
@ -234,30 +234,45 @@ impl Core {
const BASE: Sphere = Sphere { const BASE: Sphere = Sphere {
position: vec3(0., 0., -BASE_R), position: vec3(0., 0., -BASE_R),
radius: BASE_R, radius: BASE_R,
color: vec3(0.01, 0.03, 0.30),
}; };
fn sphere(pos: Vec3) -> Sphere { fn sphere(pos: Vec3, color: Vec3) -> Sphere {
Sphere { Sphere {
position: pos, position: pos,
radius: BASE_POS.distance(pos) - BASE_R, radius: BASE_POS.distance(pos) - BASE_R,
color,
} }
} }
let scene = Scene { let scene = Scene {
objects: vec![ objects: vec![
BASE, BASE,
sphere(vec3(0., 0., 0.1)), sphere(vec3(0., 0., 0.1), vec3(1., 1., 1.)),
sphere(vec3(0.3, 0., 0.1)), sphere(vec3(0.3, 0., 0.1), vec3(1., 1., 1.)),
sphere(vec3(0.1, 0.3, 0.1)), sphere(vec3(0.1, 0.3, 0.1), vec3(0.5, 0.03, 0.01)),
], ],
}; };
#[derive(Debug, Clone, Copy)]
struct LightHit<'a> {
incident: Ray,
normal: Vec3,
light: Vec3,
object: &'a Sphere,
}
let mut prng = rand_pcg::Pcg64::new(42, 0); let mut prng = rand_pcg::Pcg64::new(42, 0);
let source_rays: Vec<Ray> = (0..10240).map(|_| source.make_ray(&mut prng)).collect(); let source_rays: Vec<Ray> = (0..10240).map(|_| source.make_ray(&mut prng)).collect();
let camera_rays: Vec<Ray> = (0..10240).map(|_| camera.make_ray(&mut prng)).collect(); let camera_rays: Vec<Ray> = (0..10240).map(|_| camera.make_ray(&mut prng)).collect();
let mut source_ray_display: Vec<Vertex> = Vec::with_capacity(source_rays.len()); let mut source_ray_display: Vec<Vertex> = Vec::with_capacity(source_rays.len());
let mut hits: Vec<Hit> = Vec::with_capacity(source_rays.len()); let mut hits: Vec<LightHit> = Vec::with_capacity(source_rays.len());
for ray in source_rays { for ray in source_rays {
let light = Vec3::splat(1.);
if let Some(hit) = scene.trace_ray(ray) { if let Some(hit) = scene.trace_ray(ray) {
hits.push(hit); hits.push(LightHit {
incident: hit.incident,
normal: hit.normal,
light,
object: hit.object,
});
if args.show_hit_emission { if args.show_hit_emission {
source_ray_display.extend([ source_ray_display.extend([
Vertex { Vertex {
@ -274,7 +289,7 @@ impl Core {
source_ray_display.extend([ source_ray_display.extend([
Vertex { Vertex {
pos: hit.incident.base - 0.02 * hit.incident.dir, pos: hit.incident.base - 0.02 * hit.incident.dir,
color: vec3(0., 0., 1.), color: light,
}, },
Vertex { Vertex {
pos: hit.incident.base, pos: hit.incident.base,
@ -297,23 +312,29 @@ impl Core {
} }
} }
} }
let reflector = DiscReflector { roughness: 0.25 };
if args.reflections > 0 { if args.reflections > 0 {
let mut hits1 = hits.clone(); let mut hits1 = hits.clone();
for _ in 0..args.reflections { for _ in 0..args.reflections {
let mut hits2: Vec<Hit> = Vec::with_capacity(hits1.len()); let mut hits2: Vec<LightHit> = Vec::with_capacity(hits1.len());
for hit in &hits1 { for hit in &hits1 {
let reflector = Lambertian;
let reflected = reflector.reflect(&mut prng, hit.normal, hit.incident.dir); let reflected = reflector.reflect(&mut prng, hit.normal, hit.incident.dir);
let light = hit.light * hit.object.color;
let ray = Ray::new(hit.incident.base, reflected); let ray = Ray::new(hit.incident.base, reflected);
let Some(hit2) = scene.trace_ray(ray) else { let Some(hit2) = scene.trace_ray(ray) else {
continue; continue;
}; };
hits2.push(hit2); hits2.push(LightHit {
incident: hit2.incident,
normal: hit2.normal,
light: light,
object: hit2.object,
});
if args.show_indirect_hit { if args.show_indirect_hit {
source_ray_display.extend([ source_ray_display.extend([
Vertex { Vertex {
pos: hit2.incident.base - 0.02 * hit2.incident.dir, pos: hit2.incident.base - 0.02 * hit2.incident.dir,
color: vec3(1., 0., 1.), color: light,
}, },
Vertex { Vertex {
pos: hit2.incident.base, pos: hit2.incident.base,
@ -330,8 +351,8 @@ impl Core {
let sigma2 = args.accum_sigma.powi(2); let sigma2 = args.accum_sigma.powi(2);
let accum_normalizator = (2. * PI * sigma2).recip(); let accum_normalizator = (2. * PI * sigma2).recip();
let hits = &hits; let hits = &hits;
move |hit: Hit| -> f32 { move |hit: Hit| -> Vec3 {
let mut total_cd = 0.0f32; let mut total_cd = Vec3::splat(0.);
for light_hit in hits { for light_hit in hits {
let d2 = hit.incident.base.distance_squared(light_hit.incident.base); let d2 = hit.incident.base.distance_squared(light_hit.incident.base);
if d2 > 9. * sigma2 { if d2 > 9. * sigma2 {
@ -343,17 +364,18 @@ impl Core {
}; };
assert!(normal.is_normalized()); assert!(normal.is_normalized());
assert!(hit.incident.dir.is_normalized()); assert!(hit.incident.dir.is_normalized());
let reflector = Lambertian; let in_lm = light_hit.light;
let in_lm = 1.0; let out_cd = in_lm
let out_cd = * reflector.brdf(normal, light_hit.incident.dir, -hit.incident.dir)
in_lm * reflector.brdf(normal, light_hit.incident.dir, -hit.incident.dir); * hit.object.color;
let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp(); let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp();
total_cd += weight * out_cd; total_cd += weight * out_cd;
} }
total_cd * args.accum_scale total_cd * args.accum_scale
} }
}; };
let colormap = |light: f32| { let colormap = |light: Vec3| {
let light = light.dot(Vec3::splat(1. / 3.));
let brightness = 3. * (1. - (1. + light).recip()); let brightness = 3. * (1. - (1. + light).recip());
vec3(brightness, brightness - 1., brightness - 2.) vec3(brightness, brightness - 1., brightness - 2.)
.clamp(Vec3::splat(0.), Vec3::splat(1.)) .clamp(Vec3::splat(0.), Vec3::splat(1.))
@ -397,11 +419,11 @@ impl Core {
if !dir.is_finite() { if !dir.is_finite() {
dir = camera.direction(); dir = camera.direction();
} }
let light = light_at(Hit { let color = light_at(Hit {
incident: Ray::new(pos, dir), incident: Ray::new(pos, dir),
normal, normal,
object: obj,
}); });
let color = Vec3::splat(light);
render::faces::Vertex { pos, color } render::faces::Vertex { pos, color }
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),

View File

@ -14,9 +14,10 @@ struct Varying {
@group(0) @binding(0) var<uniform> look: LookParams; @group(0) @binding(0) var<uniform> look: LookParams;
fn colormap(light: f32) -> vec3f { fn colormap(light: vec3f) -> vec3f {
let brightness = 3. * (1. - 1. / (1. + light)); let avg = dot(light, vec3f(0.2126, 0.7152, 0.0722));
return vec3(brightness, brightness - 1., brightness - 2.); let scale = 1. / (1. + avg);
return scale * light;
} }
@vertex @vertex
@ -28,5 +29,5 @@ fn on_vertex(in: Vertex) -> Varying {
@fragment @fragment
fn on_fragment(in: Varying) -> @location(0) vec4f { fn on_fragment(in: Varying) -> @location(0) vec4f {
return vec4f(colormap(in.color.x), 1.0); return vec4f(colormap(in.color.rgb), in.color.a);
} }

View File

@ -5,6 +5,10 @@ use rand_distr::{Distribution, UnitSphere};
use crate::{camera::OrbitalCamera, ray::Ray}; use crate::{camera::OrbitalCamera, ray::Ray};
pub use reflective::DiscReflector;
mod reflective;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Source { pub struct Source {
/// Horizontal position (angle), in radians from +X towards +Y. /// Horizontal position (angle), in radians from +X towards +Y.
@ -76,6 +80,7 @@ impl Source {
pub struct Sphere { pub struct Sphere {
pub position: Vec3, pub position: Vec3,
pub radius: f32, pub radius: f32,
pub color: Vec3,
} }
struct Hit1 { struct Hit1 {
@ -113,26 +118,28 @@ pub struct Scene {
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Hit { pub struct Hit<'a> {
pub incident: Ray, pub incident: Ray,
pub normal: Vec3, pub normal: Vec3,
pub object: &'a Sphere,
} }
impl Scene { impl Scene {
pub fn trace_ray(&self, ray: Ray) -> Option<Hit> { pub fn trace_ray(&self, ray: Ray) -> Option<Hit<'_>> {
const EPS: f32 = -1e-3; const EPS: f32 = -1e-3;
let hit = self let hit = self
.objects .objects
.iter() .iter()
.filter_map(|obj| obj.trace_ray(ray)) .filter_map(|obj| obj.trace_ray(ray).map(|hit| (obj, hit)))
.filter(|h| h.dist >= EPS) .filter(|(_obj, h)| h.dist >= EPS)
.min_by(|a, b| f32::total_cmp(&a.dist, &b.dist))?; .min_by(|a, b| f32::total_cmp(&a.1.dist, &b.1.dist))?;
Some(Hit { Some(Hit {
incident: Ray { incident: Ray {
base: hit.pos, base: hit.1.pos,
dir: ray.dir, dir: ray.dir,
}, },
normal: hit.normal, normal: hit.1.normal,
object: hit.0,
}) })
} }
} }

46
src/trace/reflective.rs Normal file
View File

@ -0,0 +1,46 @@
use std::f32::consts::PI;
use glam::Vec3;
use rand_distr::{Distribution, Uniform, UnitSphere};
use super::Reflector;
#[derive(Debug, Clone)]
pub struct DiscReflector {
pub roughness: f32,
}
impl Reflector for DiscReflector {
fn brdf(&self, normal: Vec3, incident: Vec3, reflected: Vec3) -> f32 {
let specular = incident.reflect(normal);
let convergence = reflected.dot(specular);
if convergence <= 1. - self.roughness {
return 0.;
}
let c = -incident.dot(normal) * reflected.dot(normal);
if c < 0. {
return 0.;
}
0.5 / (self.roughness * PI * (specular.dot(normal) * reflected.dot(normal)).sqrt())
}
fn reflect(&self, rgen: &mut impl rand::Rng, normal: Vec3, incident: Vec3) -> Vec3 {
let specular = incident.reflect(normal);
let dist_theta = Uniform::new(0., (1. - self.roughness).acos()).unwrap();
let dist_sphere = UnitSphere;
loop {
let theta: f32 = dist_theta.sample(rgen);
let (xy, z) = theta.sin_cos();
let spherical: Vec3 = dist_sphere.sample(rgen).into();
let off_dir = spherical.reject_from_normalized(specular).normalize();
let reflected = xy * off_dir + z * specular;
if reflected.dot(normal) >= 0. {
return reflected;
}
}
}
}