1use crate::camera::camera::{Camera, Projection};
37use crate::renderer::{ImageAnchor, ScreenImageItem};
38use glam::Vec3;
39use rayon::prelude::*;
40
41#[derive(Clone, Debug)]
52pub struct ImplicitRenderOptions {
53 pub width: u32,
55 pub height: u32,
57 pub max_steps: u32,
61 pub step_scale: f32,
66 pub hit_threshold: f32,
71 pub max_distance: f32,
74 pub surface_colour: [u8; 4],
79 pub background: [u8; 4],
82}
83
84impl Default for ImplicitRenderOptions {
85 fn default() -> Self {
86 Self {
87 width: 512,
88 height: 512,
89 max_steps: 128,
90 step_scale: 0.9,
91 hit_threshold: 5e-4,
92 max_distance: 1000.0,
93 surface_colour: [200, 200, 200, 255],
94 background: [0, 0, 0, 0],
95 }
96 }
97}
98
99pub fn march_implicit_surface<F>(
116 camera: &Camera,
117 options: &ImplicitRenderOptions,
118 sdf: F,
119) -> ScreenImageItem
120where
121 F: Fn(Vec3) -> f32 + Sync,
122{
123 let colour = options.surface_colour;
124 march_impl(camera, options, move |p| (sdf(p), colour))
125}
126
127pub fn march_implicit_surface_colour<F>(
141 camera: &Camera,
142 options: &ImplicitRenderOptions,
143 sdf_colour: F,
144) -> ScreenImageItem
145where
146 F: Fn(Vec3) -> (f32, [u8; 4]) + Sync,
147{
148 march_impl(camera, options, sdf_colour)
149}
150
151fn march_impl<F>(camera: &Camera, options: &ImplicitRenderOptions, sdf_colour: F) -> ScreenImageItem
156where
157 F: Fn(Vec3) -> (f32, [u8; 4]) + Sync,
158{
159 let w = options.width.max(1);
160 let h = options.height.max(1);
161
162 let eye = camera.eye_position();
163 let forward = {
165 let diff = camera.center - eye;
166 if diff.length_squared() > 1e-10 {
167 diff.normalize()
168 } else {
169 -(camera.orientation * Vec3::Z)
170 }
171 };
172 let right = camera.orientation * Vec3::X;
173 let up = camera.orientation * Vec3::Y;
174
175 let half_h_persp = (camera.fov_y / 2.0).tan();
177 let half_w_persp = half_h_persp * camera.aspect;
178
179 let orth_half_h = camera.distance * half_h_persp;
181 let orth_half_w = camera.distance * half_w_persp;
182
183 let is_ortho = matches!(camera.projection, Projection::Orthographic);
184
185 let znear = camera.effective_znear();
186 let effective_zfar = camera.effective_zfar();
188
189 let eps = (options.hit_threshold * 100.0).max(1e-5_f32);
191
192 const LIGHT: Vec3 = Vec3::new(0.577_350_26, 0.577_350_26, 0.577_350_26);
194 const AMBIENT: f32 = 0.25_f32;
195
196 let count = (w * h) as usize;
197 let mut pixels = vec![[0u8; 4]; count];
198 let mut depths = vec![1.0_f32; count];
199
200 pixels
201 .par_iter_mut()
202 .zip(depths.par_iter_mut())
203 .enumerate()
204 .for_each(|(idx, (pix, dep))| {
205 let px = (idx as u32) % w;
206 let py = (idx as u32) / w;
207
208 let ndc_x = (px as f32 + 0.5) / w as f32 * 2.0 - 1.0;
210 let ndc_y = 1.0 - (py as f32 + 0.5) / h as f32 * 2.0;
211
212 let (ray_o, ray_d): (Vec3, Vec3) = if is_ortho {
213 let o = eye + right * (ndc_x * orth_half_w) + up * (ndc_y * orth_half_h);
214 (o, forward)
215 } else {
216 let d = (forward + right * (ndc_x * half_w_persp) + up * (ndc_y * half_h_persp))
217 .normalize();
218 (eye, d)
219 };
220
221 let mut t = znear;
223 let mut hit = false;
224 let mut hit_pos = Vec3::ZERO;
225 let mut hit_colour = options.surface_colour;
226
227 for _ in 0..options.max_steps {
228 let pos = ray_o + ray_d * t;
229 let (d, colour) = sdf_colour(pos);
230 if d.abs() < options.hit_threshold {
231 hit = true;
232 hit_pos = pos;
233 hit_colour = colour;
234 break;
235 }
236 t += d * options.step_scale;
237 if t > options.max_distance {
238 break;
239 }
240 }
241
242 if hit {
243 let gx =
245 sdf_colour(hit_pos + Vec3::X * eps).0 - sdf_colour(hit_pos - Vec3::X * eps).0;
246 let gy =
247 sdf_colour(hit_pos + Vec3::Y * eps).0 - sdf_colour(hit_pos - Vec3::Y * eps).0;
248 let gz =
249 sdf_colour(hit_pos + Vec3::Z * eps).0 - sdf_colour(hit_pos - Vec3::Z * eps).0;
250 let normal = Vec3::new(gx, gy, gz).normalize_or_zero();
251
252 let diffuse = normal.dot(LIGHT).max(0.0);
254 let shade = (AMBIENT + (1.0 - AMBIENT) * diffuse).min(1.0);
255
256 *pix = [
257 (hit_colour[0] as f32 * shade) as u8,
258 (hit_colour[1] as f32 * shade) as u8,
259 (hit_colour[2] as f32 * shade) as u8,
260 hit_colour[3],
261 ];
262
263 let view_depth = (hit_pos - eye).dot(forward);
267 *dep = if view_depth > znear {
268 (effective_zfar * (view_depth - znear)
269 / (view_depth * (effective_zfar - znear)))
270 .clamp(0.0, 1.0)
271 } else {
272 0.0
273 };
274 } else {
275 *pix = options.background;
276 *dep = 1.0;
278 }
279 });
280
281 ScreenImageItem {
282 pixels,
283 width: w,
284 height: h,
285 anchor: ImageAnchor::TopLeft,
286 scale: 1.0,
287 alpha: 1.0,
288 depth: Some(depths),
289 ..Default::default()
290 }
291}
292
293#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::Camera;
301
302 fn default_cam() -> Camera {
303 Camera {
304 center: glam::Vec3::ZERO,
305 distance: 6.0,
306 orientation: glam::Quat::IDENTITY,
307 fov_y: std::f32::consts::FRAC_PI_4,
308 aspect: 1.0,
309 znear: Some(0.1),
310 zfar: 100.0,
311 ..Camera::default()
312 }
313 }
314
315 #[test]
316 fn march_sphere_hits_center() {
317 let cam = default_cam();
318 let opts = ImplicitRenderOptions {
319 width: 64,
320 height: 64,
321 max_steps: 256,
322 hit_threshold: 1e-4,
323 max_distance: 200.0,
324 surface_colour: [255, 0, 0, 255],
325 ..Default::default()
326 };
327 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
329
330 assert_eq!(img.pixels.len(), 64 * 64);
331 assert_eq!(img.depth.as_ref().map(|d| d.len()), Some(64 * 64));
332
333 let cx = 32usize;
335 let cy = 32usize;
336 let center_px = img.pixels[cy * 64 + cx];
337 assert!(
338 center_px[3] == 255,
339 "centre pixel should have alpha=255 (sphere hit), got {:?}",
340 center_px
341 );
342 }
343
344 #[test]
345 fn march_sphere_depth_in_range() {
346 let cam = default_cam();
347 let opts = ImplicitRenderOptions {
348 width: 32,
349 height: 32,
350 max_steps: 256,
351 hit_threshold: 1e-4,
352 max_distance: 200.0,
353 ..Default::default()
354 };
355 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
356
357 let depths = img.depth.as_ref().unwrap();
358 let cx = 16usize;
359 let cy = 16usize;
360 let d = depths[cy * 32 + cx];
361 assert!(
362 d > 0.0 && d < 1.0,
363 "centre depth should be in (0,1), got {d}"
364 );
365 }
366
367 #[test]
368 fn march_miss_returns_background() {
369 let cam = default_cam();
370 let opts = ImplicitRenderOptions {
371 width: 8,
372 height: 8,
373 max_steps: 64,
374 max_distance: 0.01, background: [0, 0, 0, 0],
376 ..Default::default()
377 };
378 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
379
380 for (i, px) in img.pixels.iter().enumerate() {
381 assert_eq!(*px, [0, 0, 0, 0], "pixel {i} should be background colour");
382 }
383 let depths = img.depth.as_ref().unwrap();
384 for d in depths.iter() {
385 assert!(
386 (d - 1.0).abs() < 1e-6,
387 "missed pixels should have far-plane depth (1.0), got {d}"
388 );
389 }
390 }
391
392 #[test]
393 fn march_colour_closure_applies_colour() {
394 let cam = default_cam();
395 let opts = ImplicitRenderOptions {
396 width: 32,
397 height: 32,
398 max_steps: 256,
399 hit_threshold: 1e-4,
400 max_distance: 200.0,
401 ..Default::default()
402 };
403 let target_alpha = 200u8;
404 let img = march_implicit_surface_colour(&cam, &opts, |p| {
405 (p.length() - 1.0, [0, 255, 0, target_alpha])
406 });
407
408 let cx = 16usize;
410 let cy = 16usize;
411 let px = img.pixels[cy * 32 + cx];
412 assert_eq!(px[3], target_alpha, "alpha should pass through unchanged");
413 assert!(px[1] > 0, "green channel should survive shading");
415 assert_eq!(px[0], 0, "red should be 0");
417 assert_eq!(px[2], 0, "blue should be 0");
418 }
419
420 #[test]
421 fn output_dimensions_match_options() {
422 let cam = default_cam();
423 let opts = ImplicitRenderOptions {
424 width: 17,
425 height: 11,
426 ..Default::default()
427 };
428 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
429 assert_eq!(img.width, 17);
430 assert_eq!(img.height, 11);
431 assert_eq!(img.pixels.len(), 17 * 11);
432 assert_eq!(img.depth.as_ref().unwrap().len(), 17 * 11);
433 }
434
435 #[test]
436 fn orthographic_camera_hits_sphere() {
437 let mut cam = default_cam();
438 cam.projection = Projection::Orthographic;
439 let opts = ImplicitRenderOptions {
440 width: 32,
441 height: 32,
442 max_steps: 256,
443 hit_threshold: 1e-4,
444 max_distance: 200.0,
445 surface_colour: [255, 255, 255, 255],
446 ..Default::default()
447 };
448 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
449 let cx = 16usize;
450 let cy = 16usize;
451 assert_eq!(
452 img.pixels[cy * 32 + cx][3],
453 255,
454 "orthographic centre pixel should hit the sphere"
455 );
456 }
457}