use crate::camera::camera::{Camera, Projection};
use crate::renderer::{ImageAnchor, ScreenImageItem};
use glam::Vec3;
use rayon::prelude::*;
#[derive(Clone, Debug)]
pub struct ImplicitRenderOptions {
pub width: u32,
pub height: u32,
pub max_steps: u32,
pub step_scale: f32,
pub hit_threshold: f32,
pub max_distance: f32,
pub surface_colour: [u8; 4],
pub background: [u8; 4],
}
impl Default for ImplicitRenderOptions {
fn default() -> Self {
Self {
width: 512,
height: 512,
max_steps: 128,
step_scale: 0.9,
hit_threshold: 5e-4,
max_distance: 1000.0,
surface_colour: [200, 200, 200, 255],
background: [0, 0, 0, 0],
}
}
}
pub fn march_implicit_surface<F>(
camera: &Camera,
options: &ImplicitRenderOptions,
sdf: F,
) -> ScreenImageItem
where
F: Fn(Vec3) -> f32 + Sync,
{
let colour = options.surface_colour;
march_impl(camera, options, move |p| (sdf(p), colour))
}
pub fn march_implicit_surface_colour<F>(
camera: &Camera,
options: &ImplicitRenderOptions,
sdf_colour: F,
) -> ScreenImageItem
where
F: Fn(Vec3) -> (f32, [u8; 4]) + Sync,
{
march_impl(camera, options, sdf_colour)
}
fn march_impl<F>(camera: &Camera, options: &ImplicitRenderOptions, sdf_colour: F) -> ScreenImageItem
where
F: Fn(Vec3) -> (f32, [u8; 4]) + Sync,
{
let w = options.width.max(1);
let h = options.height.max(1);
let eye = camera.eye_position();
let forward = {
let diff = camera.center - eye;
if diff.length_squared() > 1e-10 {
diff.normalize()
} else {
-(camera.orientation * Vec3::Z)
}
};
let right = camera.orientation * Vec3::X;
let up = camera.orientation * Vec3::Y;
let half_h_persp = (camera.fov_y / 2.0).tan();
let half_w_persp = half_h_persp * camera.aspect;
let orth_half_h = camera.distance * half_h_persp;
let orth_half_w = camera.distance * half_w_persp;
let is_ortho = matches!(camera.projection, Projection::Orthographic);
let znear = camera.effective_znear();
let effective_zfar = camera.effective_zfar();
let eps = (options.hit_threshold * 100.0).max(1e-5_f32);
const LIGHT: Vec3 = Vec3::new(0.577_350_26, 0.577_350_26, 0.577_350_26);
const AMBIENT: f32 = 0.25_f32;
let count = (w * h) as usize;
let mut pixels = vec![[0u8; 4]; count];
let mut depths = vec![1.0_f32; count];
pixels
.par_iter_mut()
.zip(depths.par_iter_mut())
.enumerate()
.for_each(|(idx, (pix, dep))| {
let px = (idx as u32) % w;
let py = (idx as u32) / w;
let ndc_x = (px as f32 + 0.5) / w as f32 * 2.0 - 1.0;
let ndc_y = 1.0 - (py as f32 + 0.5) / h as f32 * 2.0;
let (ray_o, ray_d): (Vec3, Vec3) = if is_ortho {
let o = eye + right * (ndc_x * orth_half_w) + up * (ndc_y * orth_half_h);
(o, forward)
} else {
let d = (forward + right * (ndc_x * half_w_persp) + up * (ndc_y * half_h_persp))
.normalize();
(eye, d)
};
let mut t = znear;
let mut hit = false;
let mut hit_pos = Vec3::ZERO;
let mut hit_colour = options.surface_colour;
for _ in 0..options.max_steps {
let pos = ray_o + ray_d * t;
let (d, colour) = sdf_colour(pos);
if d.abs() < options.hit_threshold {
hit = true;
hit_pos = pos;
hit_colour = colour;
break;
}
t += d * options.step_scale;
if t > options.max_distance {
break;
}
}
if hit {
let gx =
sdf_colour(hit_pos + Vec3::X * eps).0 - sdf_colour(hit_pos - Vec3::X * eps).0;
let gy =
sdf_colour(hit_pos + Vec3::Y * eps).0 - sdf_colour(hit_pos - Vec3::Y * eps).0;
let gz =
sdf_colour(hit_pos + Vec3::Z * eps).0 - sdf_colour(hit_pos - Vec3::Z * eps).0;
let normal = Vec3::new(gx, gy, gz).normalize_or_zero();
let diffuse = normal.dot(LIGHT).max(0.0);
let shade = (AMBIENT + (1.0 - AMBIENT) * diffuse).min(1.0);
*pix = [
(hit_colour[0] as f32 * shade) as u8,
(hit_colour[1] as f32 * shade) as u8,
(hit_colour[2] as f32 * shade) as u8,
hit_colour[3],
];
let view_depth = (hit_pos - eye).dot(forward);
*dep = if view_depth > znear {
(effective_zfar * (view_depth - znear)
/ (view_depth * (effective_zfar - znear)))
.clamp(0.0, 1.0)
} else {
0.0
};
} else {
*pix = options.background;
*dep = 1.0;
}
});
ScreenImageItem {
pixels,
width: w,
height: h,
anchor: ImageAnchor::TopLeft,
scale: 1.0,
alpha: 1.0,
depth: Some(depths),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Camera;
fn default_cam() -> Camera {
Camera {
center: glam::Vec3::ZERO,
distance: 6.0,
orientation: glam::Quat::IDENTITY,
fov_y: std::f32::consts::FRAC_PI_4,
aspect: 1.0,
znear: Some(0.1),
zfar: 100.0,
..Camera::default()
}
}
#[test]
fn march_sphere_hits_center() {
let cam = default_cam();
let opts = ImplicitRenderOptions {
width: 64,
height: 64,
max_steps: 256,
hit_threshold: 1e-4,
max_distance: 200.0,
surface_colour: [255, 0, 0, 255],
..Default::default()
};
let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
assert_eq!(img.pixels.len(), 64 * 64);
assert_eq!(img.depth.as_ref().map(|d| d.len()), Some(64 * 64));
let cx = 32usize;
let cy = 32usize;
let center_px = img.pixels[cy * 64 + cx];
assert!(
center_px[3] == 255,
"centre pixel should have alpha=255 (sphere hit), got {:?}",
center_px
);
}
#[test]
fn march_sphere_depth_in_range() {
let cam = default_cam();
let opts = ImplicitRenderOptions {
width: 32,
height: 32,
max_steps: 256,
hit_threshold: 1e-4,
max_distance: 200.0,
..Default::default()
};
let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
let depths = img.depth.as_ref().unwrap();
let cx = 16usize;
let cy = 16usize;
let d = depths[cy * 32 + cx];
assert!(
d > 0.0 && d < 1.0,
"centre depth should be in (0,1), got {d}"
);
}
#[test]
fn march_miss_returns_background() {
let cam = default_cam();
let opts = ImplicitRenderOptions {
width: 8,
height: 8,
max_steps: 64,
max_distance: 0.01, background: [0, 0, 0, 0],
..Default::default()
};
let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
for (i, px) in img.pixels.iter().enumerate() {
assert_eq!(*px, [0, 0, 0, 0], "pixel {i} should be background colour");
}
let depths = img.depth.as_ref().unwrap();
for d in depths.iter() {
assert!(
(d - 1.0).abs() < 1e-6,
"missed pixels should have far-plane depth (1.0), got {d}"
);
}
}
#[test]
fn march_colour_closure_applies_colour() {
let cam = default_cam();
let opts = ImplicitRenderOptions {
width: 32,
height: 32,
max_steps: 256,
hit_threshold: 1e-4,
max_distance: 200.0,
..Default::default()
};
let target_alpha = 200u8;
let img = march_implicit_surface_colour(&cam, &opts, |p| {
(p.length() - 1.0, [0, 255, 0, target_alpha])
});
let cx = 16usize;
let cy = 16usize;
let px = img.pixels[cy * 32 + cx];
assert_eq!(px[3], target_alpha, "alpha should pass through unchanged");
assert!(px[1] > 0, "green channel should survive shading");
assert_eq!(px[0], 0, "red should be 0");
assert_eq!(px[2], 0, "blue should be 0");
}
#[test]
fn output_dimensions_match_options() {
let cam = default_cam();
let opts = ImplicitRenderOptions {
width: 17,
height: 11,
..Default::default()
};
let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
assert_eq!(img.width, 17);
assert_eq!(img.height, 11);
assert_eq!(img.pixels.len(), 17 * 11);
assert_eq!(img.depth.as_ref().unwrap().len(), 17 * 11);
}
#[test]
fn orthographic_camera_hits_sphere() {
let mut cam = default_cam();
cam.projection = Projection::Orthographic;
let opts = ImplicitRenderOptions {
width: 32,
height: 32,
max_steps: 256,
hit_threshold: 1e-4,
max_distance: 200.0,
surface_colour: [255, 255, 255, 255],
..Default::default()
};
let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
let cx = 16usize;
let cy = 16usize;
assert_eq!(
img.pixels[cy * 32 + cx][3],
255,
"orthographic centre pixel should hit the sphere"
);
}
}