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_color: [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_color: [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 color = options.surface_color;
124 march_impl(camera, options, move |p| (sdf(p), color))
125}
126
127pub fn march_implicit_surface_color<F>(
141 camera: &Camera,
142 options: &ImplicitRenderOptions,
143 sdf_color: F,
144) -> ScreenImageItem
145where
146 F: Fn(Vec3) -> (f32, [u8; 4]) + Sync,
147{
148 march_impl(camera, options, sdf_color)
149}
150
151fn march_impl<F>(camera: &Camera, options: &ImplicitRenderOptions, sdf_color: 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
217 + right * (ndc_x * half_w_persp)
218 + up * (ndc_y * half_h_persp))
219 .normalize();
220 (eye, d)
221 };
222
223 let mut t = znear;
225 let mut hit = false;
226 let mut hit_pos = Vec3::ZERO;
227 let mut hit_color = options.surface_color;
228
229 for _ in 0..options.max_steps {
230 let pos = ray_o + ray_d * t;
231 let (d, color) = sdf_color(pos);
232 if d.abs() < options.hit_threshold {
233 hit = true;
234 hit_pos = pos;
235 hit_color = color;
236 break;
237 }
238 t += d * options.step_scale;
239 if t > options.max_distance {
240 break;
241 }
242 }
243
244 if hit {
245 let gx = sdf_color(hit_pos + Vec3::X * eps).0
247 - sdf_color(hit_pos - Vec3::X * eps).0;
248 let gy = sdf_color(hit_pos + Vec3::Y * eps).0
249 - sdf_color(hit_pos - Vec3::Y * eps).0;
250 let gz = sdf_color(hit_pos + Vec3::Z * eps).0
251 - sdf_color(hit_pos - Vec3::Z * eps).0;
252 let normal = Vec3::new(gx, gy, gz).normalize_or_zero();
253
254 let diffuse = normal.dot(LIGHT).max(0.0);
256 let shade = (AMBIENT + (1.0 - AMBIENT) * diffuse).min(1.0);
257
258 *pix = [
259 (hit_color[0] as f32 * shade) as u8,
260 (hit_color[1] as f32 * shade) as u8,
261 (hit_color[2] as f32 * shade) as u8,
262 hit_color[3],
263 ];
264
265 let view_depth = (hit_pos - eye).dot(forward);
269 *dep = if view_depth > znear {
270 (effective_zfar * (view_depth - znear)
271 / (view_depth * (effective_zfar - znear)))
272 .clamp(0.0, 1.0)
273 } else {
274 0.0
275 };
276 } else {
277 *pix = options.background;
278 *dep = 1.0;
280 }
281 });
282
283 ScreenImageItem {
284 pixels,
285 width: w,
286 height: h,
287 anchor: ImageAnchor::TopLeft,
288 scale: 1.0,
289 alpha: 1.0,
290 depth: Some(depths),
291 }
292}
293
294#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::Camera;
302
303 fn default_cam() -> Camera {
304 Camera {
305 center: glam::Vec3::ZERO,
306 distance: 6.0,
307 orientation: glam::Quat::IDENTITY,
308 fov_y: std::f32::consts::FRAC_PI_4,
309 aspect: 1.0,
310 znear: Some(0.1),
311 zfar: 100.0,
312 ..Camera::default()
313 }
314 }
315
316 #[test]
317 fn march_sphere_hits_center() {
318 let cam = default_cam();
319 let opts = ImplicitRenderOptions {
320 width: 64,
321 height: 64,
322 max_steps: 256,
323 hit_threshold: 1e-4,
324 max_distance: 200.0,
325 surface_color: [255, 0, 0, 255],
326 ..Default::default()
327 };
328 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
330
331 assert_eq!(img.pixels.len(), 64 * 64);
332 assert_eq!(img.depth.as_ref().map(|d| d.len()), Some(64 * 64));
333
334 let cx = 32usize;
336 let cy = 32usize;
337 let center_px = img.pixels[cy * 64 + cx];
338 assert!(
339 center_px[3] == 255,
340 "centre pixel should have alpha=255 (sphere hit), got {:?}",
341 center_px
342 );
343 }
344
345 #[test]
346 fn march_sphere_depth_in_range() {
347 let cam = default_cam();
348 let opts = ImplicitRenderOptions {
349 width: 32,
350 height: 32,
351 max_steps: 256,
352 hit_threshold: 1e-4,
353 max_distance: 200.0,
354 ..Default::default()
355 };
356 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
357
358 let depths = img.depth.as_ref().unwrap();
359 let cx = 16usize;
360 let cy = 16usize;
361 let d = depths[cy * 32 + cx];
362 assert!(d > 0.0 && d < 1.0, "centre depth should be in (0,1), got {d}");
363 }
364
365 #[test]
366 fn march_miss_returns_background() {
367 let cam = default_cam();
368 let opts = ImplicitRenderOptions {
369 width: 8,
370 height: 8,
371 max_steps: 64,
372 max_distance: 0.01, background: [0, 0, 0, 0],
374 ..Default::default()
375 };
376 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
377
378 for (i, px) in img.pixels.iter().enumerate() {
379 assert_eq!(
380 *px,
381 [0, 0, 0, 0],
382 "pixel {i} should be background colour"
383 );
384 }
385 let depths = img.depth.as_ref().unwrap();
386 for d in depths.iter() {
387 assert!(
388 (d - 1.0).abs() < 1e-6,
389 "missed pixels should have far-plane depth (1.0), got {d}"
390 );
391 }
392 }
393
394 #[test]
395 fn march_color_closure_applies_color() {
396 let cam = default_cam();
397 let opts = ImplicitRenderOptions {
398 width: 32,
399 height: 32,
400 max_steps: 256,
401 hit_threshold: 1e-4,
402 max_distance: 200.0,
403 ..Default::default()
404 };
405 let target_alpha = 200u8;
406 let img = march_implicit_surface_color(&cam, &opts, |p| {
407 (p.length() - 1.0, [0, 255, 0, target_alpha])
408 });
409
410 let cx = 16usize;
412 let cy = 16usize;
413 let px = img.pixels[cy * 32 + cx];
414 assert_eq!(px[3], target_alpha, "alpha should pass through unchanged");
415 assert!(px[1] > 0, "green channel should survive shading");
417 assert_eq!(px[0], 0, "red should be 0");
419 assert_eq!(px[2], 0, "blue should be 0");
420 }
421
422 #[test]
423 fn output_dimensions_match_options() {
424 let cam = default_cam();
425 let opts = ImplicitRenderOptions {
426 width: 17,
427 height: 11,
428 ..Default::default()
429 };
430 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
431 assert_eq!(img.width, 17);
432 assert_eq!(img.height, 11);
433 assert_eq!(img.pixels.len(), 17 * 11);
434 assert_eq!(img.depth.as_ref().unwrap().len(), 17 * 11);
435 }
436
437 #[test]
438 fn orthographic_camera_hits_sphere() {
439 let mut cam = default_cam();
440 cam.projection = Projection::Orthographic;
441 let opts = ImplicitRenderOptions {
442 width: 32,
443 height: 32,
444 max_steps: 256,
445 hit_threshold: 1e-4,
446 max_distance: 200.0,
447 surface_color: [255, 255, 255, 255],
448 ..Default::default()
449 };
450 let img = march_implicit_surface(&cam, &opts, |p| p.length() - 1.0);
451 let cx = 16usize;
452 let cy = 16usize;
453 assert_eq!(
454 img.pixels[cy * 32 + cx][3],
455 255,
456 "orthographic centre pixel should hit the sphere"
457 );
458 }
459}