#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
#[inline]
fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
#[inline]
fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn length3(v: [f64; 3]) -> f64 {
dot3(v, v).sqrt()
}
#[inline]
fn normalize3(v: [f64; 3]) -> [f64; 3] {
let len = length3(v);
if len < 1e-15 {
return [0.0; 3];
}
scale3(v, 1.0 / len)
}
#[inline]
fn clamp(v: f64, lo: f64, hi: f64) -> f64 {
v.max(lo).min(hi)
}
#[inline]
fn max3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0].max(b[0]), a[1].max(b[1]), a[2].max(b[2])]
}
#[inline]
fn abs3(v: [f64; 3]) -> [f64; 3] {
[v[0].abs(), v[1].abs(), v[2].abs()]
}
#[derive(Debug, Clone, Copy)]
pub struct Ray {
pub origin: [f64; 3],
pub direction: [f64; 3],
}
impl Ray {
pub fn new(origin: [f64; 3], direction: [f64; 3]) -> Self {
Self {
origin,
direction: normalize3(direction),
}
}
pub fn at(&self, t: f64) -> [f64; 3] {
[
self.origin[0] + t * self.direction[0],
self.origin[1] + t * self.direction[1],
self.origin[2] + t * self.direction[2],
]
}
}
pub trait Sdf {
fn distance(&self, p: [f64; 3]) -> f64;
fn normal(&self, p: [f64; 3]) -> [f64; 3] {
let e = 1e-5;
let dx = self.distance([p[0] + e, p[1], p[2]]) - self.distance([p[0] - e, p[1], p[2]]);
let dy = self.distance([p[0], p[1] + e, p[2]]) - self.distance([p[0], p[1] - e, p[2]]);
let dz = self.distance([p[0], p[1], p[2] + e]) - self.distance([p[0], p[1], p[2] - e]);
normalize3([dx, dy, dz])
}
}
#[derive(Debug, Clone, Copy)]
pub struct SphereSdf {
pub center: [f64; 3],
pub radius: f64,
}
impl SphereSdf {
pub fn new(center: [f64; 3], radius: f64) -> Self {
Self { center, radius }
}
}
impl Sdf for SphereSdf {
fn distance(&self, p: [f64; 3]) -> f64 {
let d = sub3(p, self.center);
length3(d) - self.radius
}
}
#[derive(Debug, Clone, Copy)]
pub struct BoxSdf {
pub half_extents: [f64; 3],
}
impl BoxSdf {
pub fn new(half_extents: [f64; 3]) -> Self {
Self { half_extents }
}
}
impl Sdf for BoxSdf {
fn distance(&self, p: [f64; 3]) -> f64 {
let q = [
p[0].abs() - self.half_extents[0],
p[1].abs() - self.half_extents[1],
p[2].abs() - self.half_extents[2],
];
let outside = length3(max3(q, [0.0; 3]));
let inside = q[0].max(q[1]).max(q[2]).min(0.0);
outside + inside
}
}
#[derive(Debug, Clone, Copy)]
pub struct CapsuleSdf {
pub a: [f64; 3],
pub b: [f64; 3],
pub r: f64,
}
impl CapsuleSdf {
pub fn new(a: [f64; 3], b: [f64; 3], r: f64) -> Self {
Self { a, b, r }
}
}
impl Sdf for CapsuleSdf {
fn distance(&self, p: [f64; 3]) -> f64 {
let pa = sub3(p, self.a);
let ba = sub3(self.b, self.a);
let h = clamp(dot3(pa, ba) / dot3(ba, ba), 0.0, 1.0);
let closest = sub3(pa, scale3(ba, h));
length3(closest) - self.r
}
}
#[derive(Debug, Clone, Copy)]
pub struct TorusSdf {
pub r_major: f64,
pub r_minor: f64,
}
impl TorusSdf {
pub fn new(r_major: f64, r_minor: f64) -> Self {
Self { r_major, r_minor }
}
}
impl Sdf for TorusSdf {
fn distance(&self, p: [f64; 3]) -> f64 {
let q_x = (p[0] * p[0] + p[2] * p[2]).sqrt() - self.r_major;
let q_y = p[1];
(q_x * q_x + q_y * q_y).sqrt() - self.r_minor
}
}
pub fn sdf_union(a: f64, b: f64) -> f64 {
a.min(b)
}
pub fn sdf_intersection(a: f64, b: f64) -> f64 {
a.max(b)
}
pub fn sdf_subtraction(a: f64, b: f64) -> f64 {
a.max(-b)
}
pub fn sdf_smooth_union(a: f64, b: f64, k: f64) -> f64 {
if k < 1e-15 {
return sdf_union(a, b);
}
let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
let mix = b + h * (a - b);
mix - k * h * (1.0 - h)
}
#[derive(Debug, Clone, Copy)]
pub struct RayMarchResult {
pub hit: bool,
pub t: f64,
pub steps: usize,
pub normal: [f64; 3],
}
impl RayMarchResult {
pub fn miss(t: f64, steps: usize) -> Self {
Self {
hit: false,
t,
steps,
normal: [0.0; 3],
}
}
pub fn hit(t: f64, steps: usize, normal: [f64; 3]) -> Self {
Self {
hit: true,
t,
steps,
normal,
}
}
}
pub fn ray_march(
ray: &Ray,
sdf: &dyn Sdf,
max_steps: usize,
max_dist: f64,
eps: f64,
) -> RayMarchResult {
let mut t = 0.0_f64;
for step in 0..max_steps {
let p = ray.at(t);
let d = sdf.distance(p);
if d.abs() < eps {
let normal = sdf.normal(p);
return RayMarchResult::hit(t, step + 1, normal);
}
t += d;
if t >= max_dist {
return RayMarchResult::miss(max_dist, step + 1);
}
}
RayMarchResult::miss(t, max_steps)
}
#[derive(Debug, Clone, Copy)]
pub struct AmbientOcclusion {
pub samples: usize,
pub radius: f64,
pub falloff: f64,
}
impl AmbientOcclusion {
pub fn new(samples: usize, radius: f64, falloff: f64) -> Self {
Self {
samples,
radius,
falloff,
}
}
pub fn compute(&self, pos: [f64; 3], normal: [f64; 3], sdf: &dyn Sdf) -> f64 {
if self.samples == 0 {
return 1.0;
}
let mut occ = 0.0_f64;
for i in 1..=self.samples {
let t = self.radius * (i as f64) / (self.samples as f64);
let sample_pos = add3(pos, scale3(normal, t));
let d = sdf.distance(sample_pos);
occ += (t - d).max(0.0) / t.powf(self.falloff);
}
(1.0 - occ / (self.samples as f64)).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone, Copy)]
pub struct SoftShadow {
pub light_dir: [f64; 3],
pub k: f64,
}
impl SoftShadow {
pub fn new(light_dir: [f64; 3], k: f64) -> Self {
Self {
light_dir: normalize3(light_dir),
k,
}
}
pub fn compute(&self, pos: [f64; 3], sdf: &dyn Sdf, max_dist: f64) -> f64 {
let eps = 1e-4;
let mut res = 1.0_f64;
let mut t = eps;
while t < max_dist {
let p = add3(pos, scale3(self.light_dir, t));
let d = sdf.distance(p);
if d < eps {
return 0.0;
}
res = res.min(self.k * d / t);
t += d;
}
res.clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone)]
pub struct RayMarchRenderer {
pub width: usize,
pub height: usize,
pub fov: f64,
pub camera_pos: [f64; 3],
pub camera_target: [f64; 3],
}
impl RayMarchRenderer {
pub fn new(
width: usize,
height: usize,
fov: f64,
camera_pos: [f64; 3],
camera_target: [f64; 3],
) -> Self {
Self {
width,
height,
fov,
camera_pos,
camera_target,
}
}
fn camera_basis(&self) -> ([f64; 3], [f64; 3], [f64; 3]) {
let world_up = [0.0_f64, 1.0, 0.0];
let forward = normalize3(sub3(self.camera_target, self.camera_pos));
let right = normalize3(cross3_fn(forward, world_up));
let up = cross3_fn(right, forward);
(right, up, forward)
}
pub fn generate_ray(&self, px: usize, py: usize) -> Ray {
let (right, up, forward) = self.camera_basis();
let aspect = self.width as f64 / self.height.max(1) as f64;
let half_h = (self.fov * 0.5).tan();
let half_w = half_h * aspect;
let u = (2.0 * (px as f64 + 0.5) / self.width as f64 - 1.0) * half_w;
let v = (1.0 - 2.0 * (py as f64 + 0.5) / self.height as f64) * half_h;
let dir = add3(add3(forward, scale3(right, u)), scale3(up, v));
Ray::new(self.camera_pos, dir)
}
pub fn render_depth(&self, sdf: &dyn Sdf) -> Vec<f64> {
let n = self.width * self.height;
let mut depth = Vec::with_capacity(n);
for py in 0..self.height {
for px in 0..self.width {
let ray = self.generate_ray(px, py);
let result = ray_march(&ray, sdf, 256, 1000.0, 1e-5);
depth.push(if result.hit { result.t } else { f64::INFINITY });
}
}
depth
}
pub fn render_normals(&self, sdf: &dyn Sdf) -> Vec<[f64; 3]> {
let n = self.width * self.height;
let mut normals = Vec::with_capacity(n);
for py in 0..self.height {
for px in 0..self.width {
let ray = self.generate_ray(px, py);
let result = ray_march(&ray, sdf, 256, 1000.0, 1e-5);
normals.push(result.normal);
}
}
normals
}
}
#[inline]
fn cross3_fn(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
pub fn subsurface_scattering_approx(depth: f64, scatter_coeff: f64, absorption: f64) -> f64 {
let extinction = scatter_coeff + absorption;
(-extinction * depth.max(0.0)).exp()
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn ray_at_origin() {
let ray = Ray::new([0.0; 3], [0.0, 0.0, 1.0]);
let p = ray.at(0.0);
assert_eq!(p, [0.0; 3]);
}
#[test]
fn ray_at_t_positive() {
let ray = Ray::new([1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
let p = ray.at(5.0);
assert!((p[0] - 1.0).abs() < 1e-12);
assert!((p[2] - 5.0).abs() < 1e-12);
}
#[test]
fn ray_direction_normalized() {
let ray = Ray::new([0.0; 3], [3.0, 4.0, 0.0]);
let len = length3(ray.direction);
assert!((len - 1.0).abs() < 1e-12);
}
#[test]
fn ray_at_negative_t() {
let ray = Ray::new([0.0; 3], [1.0, 0.0, 0.0]);
let p = ray.at(-2.0);
assert!((p[0] + 2.0).abs() < 1e-12);
}
#[test]
fn sphere_distance_outside() {
let s = SphereSdf::new([0.0; 3], 1.0);
let d = s.distance([2.0, 0.0, 0.0]);
assert!((d - 1.0).abs() < 1e-10);
}
#[test]
fn sphere_distance_inside() {
let s = SphereSdf::new([0.0; 3], 1.0);
let d = s.distance([0.0; 3]);
assert!((d + 1.0).abs() < 1e-10);
}
#[test]
fn sphere_distance_surface() {
let s = SphereSdf::new([0.0; 3], 1.0);
let d = s.distance([1.0, 0.0, 0.0]);
assert!(d.abs() < 1e-12);
}
#[test]
fn sphere_normal_at_x_axis() {
let s = SphereSdf::new([0.0; 3], 1.0);
let n = s.normal([1.0, 0.0, 0.0]);
assert!((n[0] - 1.0).abs() < 1e-4);
assert!(n[1].abs() < 1e-4);
assert!(n[2].abs() < 1e-4);
}
#[test]
fn sphere_translated_center() {
let s = SphereSdf::new([5.0, 0.0, 0.0], 2.0);
let d = s.distance([7.0, 0.0, 0.0]);
assert!(d.abs() < 1e-12);
}
#[test]
fn box_distance_outside_along_x() {
let b = BoxSdf::new([1.0; 3]);
let d = b.distance([3.0, 0.0, 0.0]);
assert!((d - 2.0).abs() < 1e-12);
}
#[test]
fn box_distance_inside() {
let b = BoxSdf::new([1.0; 3]);
let d = b.distance([0.0; 3]);
assert!(d < 0.0);
}
#[test]
fn box_distance_on_face() {
let b = BoxSdf::new([1.0, 1.0, 1.0]);
let d = b.distance([1.0, 0.0, 0.0]);
assert!(d.abs() < 1e-12);
}
#[test]
fn box_normal_x_face() {
let b = BoxSdf::new([1.0; 3]);
let n = b.normal([1.0, 0.0, 0.0]);
assert!((n[0] - 1.0).abs() < 1e-3);
}
#[test]
fn capsule_distance_at_midpoint() {
let c = CapsuleSdf::new([0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
let d = c.distance([0.5, 0.0, 0.0]);
assert!(d.abs() < 1e-10);
}
#[test]
fn capsule_distance_outside() {
let c = CapsuleSdf::new([0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
let d = c.distance([2.0, 0.0, 0.0]);
assert!((d - 1.5).abs() < 1e-10);
}
#[test]
fn capsule_distance_at_cap() {
let c = CapsuleSdf::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.5);
let d = c.distance([0.0, 2.0, 0.0]);
assert!((d - 0.5).abs() < 1e-10);
}
#[test]
fn torus_distance_on_ring() {
let t = TorusSdf::new(2.0, 0.5);
let d = t.distance([2.0, 0.5, 0.0]);
assert!(d.abs() < 1e-10);
}
#[test]
fn torus_distance_centre_negative() {
let t = TorusSdf::new(2.0, 0.5);
let d = t.distance([0.0; 3]);
assert!((d - 1.5).abs() < 1e-10);
}
#[test]
fn torus_distance_outside_far() {
let t = TorusSdf::new(1.0, 0.2);
let d = t.distance([10.0, 0.0, 0.0]);
assert!(d > 0.0);
}
#[test]
fn sdf_union_picks_min() {
assert_eq!(sdf_union(3.0, 1.0), 1.0);
assert_eq!(sdf_union(-1.0, 2.0), -1.0);
}
#[test]
fn sdf_intersection_picks_max() {
assert_eq!(sdf_intersection(3.0, 1.0), 3.0);
}
#[test]
fn sdf_subtraction_correctness() {
assert_eq!(sdf_subtraction(1.0, -2.0), 2.0);
assert_eq!(sdf_subtraction(1.0, 3.0), 1.0);
}
#[test]
fn sdf_smooth_union_degenerate() {
let su = sdf_smooth_union(3.0, 1.0, 0.0);
assert!((su - sdf_union(3.0, 1.0)).abs() < 1e-10);
}
#[test]
fn sdf_smooth_union_blends() {
let su = sdf_smooth_union(0.0, 0.0, 1.0);
assert!(su <= 0.0 + 1e-10);
}
#[test]
fn sdf_smooth_union_between_values() {
let a = 2.0_f64;
let b = 3.0_f64;
let su = sdf_smooth_union(a, b, 0.5);
assert!(su <= a + 1e-10);
}
#[test]
fn ray_march_result_miss() {
let r = RayMarchResult::miss(100.0, 10);
assert!(!r.hit);
assert!((r.t - 100.0).abs() < 1e-12);
}
#[test]
fn ray_march_result_hit() {
let r = RayMarchResult::hit(5.0, 20, [1.0, 0.0, 0.0]);
assert!(r.hit);
assert!((r.normal[0] - 1.0).abs() < 1e-12);
}
#[test]
fn ray_march_hits_sphere() {
let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
let ray = Ray::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
let result = ray_march(&ray, &sphere, 256, 100.0, 1e-5);
assert!(result.hit);
assert!((result.t - 4.0).abs() < 1e-3);
}
#[test]
fn ray_march_misses_sphere() {
let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
let ray = Ray::new([10.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
let result = ray_march(&ray, &sphere, 256, 100.0, 1e-5);
assert!(!result.hit);
}
#[test]
fn ray_march_steps_bounded() {
let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
let ray = Ray::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
let result = ray_march(&ray, &sphere, 128, 100.0, 1e-5);
assert!(result.steps <= 128);
}
#[test]
fn ray_march_hit_normal_approx_minus_z() {
let sphere = SphereSdf::new([0.0, 0.0, 5.0], 1.0);
let ray = Ray::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
let result = ray_march(&ray, &sphere, 256, 100.0, 1e-5);
assert!(result.hit);
assert!(result.normal[2] < 0.0);
}
#[test]
fn ao_open_space_returns_one() {
let sphere = SphereSdf::new([0.0; 3], 1.0);
let ao = AmbientOcclusion::new(8, 1.0, 1.0);
let pos = [100.0, 0.0, 0.0];
let normal = [1.0, 0.0, 0.0];
let v = ao.compute(pos, normal, &sphere);
assert!(v > 0.5);
}
#[test]
fn ao_samples_zero_returns_one() {
let sphere = SphereSdf::new([0.0; 3], 1.0);
let ao = AmbientOcclusion::new(0, 1.0, 1.0);
let v = ao.compute([0.0; 3], [0.0, 1.0, 0.0], &sphere);
assert!((v - 1.0).abs() < 1e-12);
}
#[test]
fn ao_result_clamped() {
let sphere = SphereSdf::new([0.0; 3], 1.0);
let ao = AmbientOcclusion::new(4, 0.5, 1.0);
let pos = [1.0, 0.0, 0.0];
let normal = [1.0, 0.0, 0.0];
let v = ao.compute(pos, normal, &sphere);
assert!((0.0..=1.0).contains(&v));
}
#[test]
fn soft_shadow_lit_no_occluder() {
let sphere = SphereSdf::new([0.0, 0.0, -100.0], 0.1);
let ss = SoftShadow::new([0.0, 1.0, 0.0], 8.0);
let pos = [0.0, 0.0, 0.0];
let v = ss.compute(pos, &sphere, 50.0);
assert!(v > 0.9);
}
#[test]
fn soft_shadow_shadow_with_occluder() {
let sphere = SphereSdf::new([0.0, 5.0, 0.0], 1.0);
let ss = SoftShadow::new([0.0, 1.0, 0.0], 8.0);
let pos = [0.0, 0.0, 0.0];
let v = ss.compute(pos, &sphere, 20.0);
assert!(v < 0.5);
}
#[test]
fn soft_shadow_result_in_range() {
let sphere = SphereSdf::new([0.0; 3], 1.0);
let ss = SoftShadow::new([1.0, 1.0, 1.0], 4.0);
let v = ss.compute([3.0, 0.0, 0.0], &sphere, 20.0);
assert!((0.0..=1.0).contains(&v));
}
#[test]
fn renderer_depth_buffer_size() {
let renderer = RayMarchRenderer::new(4, 4, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
let sphere = SphereSdf::new([0.0; 3], 1.0);
let depth = renderer.render_depth(&sphere);
assert_eq!(depth.len(), 16);
}
#[test]
fn renderer_normals_buffer_size() {
let renderer = RayMarchRenderer::new(2, 2, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
let sphere = SphereSdf::new([0.0; 3], 1.0);
let normals = renderer.render_normals(&sphere);
assert_eq!(normals.len(), 4);
}
#[test]
fn renderer_center_ray_hits_sphere() {
let renderer = RayMarchRenderer::new(11, 11, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
let sphere = SphereSdf::new([0.0; 3], 1.0);
let depth = renderer.render_depth(&sphere);
let center_t = depth[5 * 11 + 5];
assert!(center_t < f64::INFINITY);
}
#[test]
fn renderer_corner_ray_misses_small_sphere() {
let renderer = RayMarchRenderer::new(11, 11, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
let sphere = SphereSdf::new([0.0; 3], 0.01);
let depth = renderer.render_depth(&sphere);
assert_eq!(depth[0], f64::INFINITY);
}
#[test]
fn renderer_generate_ray_center() {
let renderer = RayMarchRenderer::new(11, 11, PI / 3.0, [0.0, 0.0, -5.0], [0.0, 0.0, 0.0]);
let ray = renderer.generate_ray(5, 5);
assert!(ray.direction[2] > 0.0);
}
#[test]
fn sss_zero_depth_returns_one() {
let v = subsurface_scattering_approx(0.0, 1.0, 1.0);
assert!((v - 1.0).abs() < 1e-12);
}
#[test]
fn sss_large_depth_near_zero() {
let v = subsurface_scattering_approx(1000.0, 1.0, 0.0);
assert!(v < 1e-10);
}
#[test]
fn sss_negative_depth_clamped() {
let v = subsurface_scattering_approx(-5.0, 1.0, 1.0);
assert!((v - 1.0).abs() < 1e-12);
}
#[test]
fn sss_result_in_range() {
let v = subsurface_scattering_approx(0.5, 2.0, 1.0);
assert!((0.0..=1.0).contains(&v));
}
#[test]
fn sss_monotone_decreasing() {
let v1 = subsurface_scattering_approx(1.0, 1.0, 0.5);
let v2 = subsurface_scattering_approx(2.0, 1.0, 0.5);
assert!(v1 > v2);
}
#[test]
fn test_add3() {
let r = add3([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]);
assert_eq!(r, [5.0, 7.0, 9.0]);
}
#[test]
fn test_sub3() {
let r = sub3([4.0, 5.0, 6.0], [1.0, 2.0, 3.0]);
assert_eq!(r, [3.0, 3.0, 3.0]);
}
#[test]
fn test_scale3() {
let r = scale3([1.0, 2.0, 3.0], 2.0);
assert_eq!(r, [2.0, 4.0, 6.0]);
}
#[test]
fn test_abs3() {
let r = abs3([-1.0, 2.0, -3.0]);
assert_eq!(r, [1.0, 2.0, 3.0]);
}
#[test]
fn test_max3() {
let r = max3([1.0, -1.0, 2.0], [0.0, 0.0, 0.0]);
assert_eq!(r, [1.0, 0.0, 2.0]);
}
#[test]
fn test_normalize3_zero() {
let n = normalize3([0.0; 3]);
assert_eq!(n, [0.0; 3]);
}
#[test]
fn test_clamp() {
assert_eq!(clamp(-1.0, 0.0, 1.0), 0.0);
assert_eq!(clamp(0.5, 0.0, 1.0), 0.5);
assert_eq!(clamp(2.0, 0.0, 1.0), 1.0);
}
}