1use crate::detector::Detector;
4use crate::material::Interaction;
5use crate::ray::Photon;
6use crate::scene::Scene;
7use nalgebra::Point3;
8use rand::SeedableRng;
9use rand_xoshiro::Xoshiro256PlusPlus;
10use std::time::{Duration, Instant};
11
12#[derive(Debug, Clone)]
14pub struct TracerConfig {
15 pub num_photons: u64,
17 pub max_bounces: u32,
19 pub russian_roulette_threshold: f64,
21 pub seed: u64,
23 pub detector_c_resolution: f64,
25 pub detector_g_resolution: f64,
27 pub max_trails: usize,
29}
30
31impl Default for TracerConfig {
32 fn default() -> Self {
33 Self {
34 num_photons: 1_000_000,
35 max_bounces: 50,
36 russian_roulette_threshold: 0.01,
37 seed: 42,
38 detector_c_resolution: 1.0,
39 detector_g_resolution: 1.0,
40 max_trails: 0,
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct TracerResult {
48 pub detector: Detector,
50 pub stats: TracerStats,
52 pub trails: Vec<PhotonTrail>,
54}
55
56#[derive(Debug, Clone, Default)]
58pub struct TracerStats {
59 pub photons_traced: u64,
60 pub photons_detected: u64,
61 pub photons_absorbed: u64,
62 pub photons_max_bounces: u64,
63 pub photons_russian_roulette: u64,
64 pub total_energy_emitted: f64,
65 pub total_energy_detected: f64,
66 pub elapsed: Duration,
67}
68
69#[derive(Debug, Clone)]
71pub struct ProgressInfo {
72 pub photons_done: u64,
73 pub photons_total: u64,
74 pub photons_per_second: f64,
75 pub current_stats: TracerStats,
76}
77
78#[derive(Debug, Clone)]
80pub struct PhotonTrail {
81 pub points: Vec<TrailPoint>,
82}
83
84#[derive(Debug, Clone)]
86pub struct TrailPoint {
87 pub position: Point3<f64>,
88 pub event: TrailEvent,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum TrailEvent {
94 Emitted,
95 Reflected,
96 Transmitted,
97 Scattered,
98 Absorbed,
99 Detected,
100}
101
102pub struct Tracer;
104
105impl Tracer {
106 pub fn trace(scene: &Scene, config: &TracerConfig) -> TracerResult {
108 Self::trace_with_progress(scene, config, |_| {})
109 }
110
111 pub fn trace_with_progress(
113 scene: &Scene,
114 config: &TracerConfig,
115 callback: impl Fn(ProgressInfo) + Send + Sync,
116 ) -> TracerResult {
117 let start = Instant::now();
118
119 #[cfg(feature = "parallel")]
120 let result = trace_parallel(scene, config, &callback, start);
121
122 #[cfg(not(feature = "parallel"))]
123 let result = trace_sequential(scene, config, &callback, start);
124
125 result
126 }
127}
128
129#[cfg(not(feature = "parallel"))]
131fn trace_sequential(
132 scene: &Scene,
133 config: &TracerConfig,
134 callback: &(impl Fn(ProgressInfo) + Send + Sync),
135 start: Instant,
136) -> TracerResult {
137 let mut rng = Xoshiro256PlusPlus::seed_from_u64(config.seed);
138 let mut detector = Detector::new(config.detector_c_resolution, config.detector_g_resolution);
139 let mut stats = TracerStats::default();
140 let mut trails = Vec::new();
141
142 let batch_size = 10_000u64;
143 let num_sources = scene.sources.len();
144 assert!(num_sources > 0, "Scene must have at least one source");
145
146 for i in 0..config.num_photons {
147 let source = &scene.sources[(i as usize) % num_sources];
149 let ray = source.sample(&mut rng);
150 let record_trail = trails.len() < config.max_trails;
151
152 let result = trace_one_photon(scene, config, ray, &mut rng, record_trail);
153
154 stats.total_energy_emitted += 1.0;
155 stats.photons_traced += 1;
156
157 match result.outcome {
158 PhotonOutcome::Detected { energy } => {
159 detector.record(result.final_direction.as_ref().unwrap(), energy);
160 stats.photons_detected += 1;
161 stats.total_energy_detected += energy;
162 }
163 PhotonOutcome::Absorbed => {
164 stats.photons_absorbed += 1;
165 }
166 PhotonOutcome::MaxBounces => {
167 stats.photons_max_bounces += 1;
168 }
169 PhotonOutcome::RussianRoulette => {
170 stats.photons_russian_roulette += 1;
171 }
172 }
173
174 if let Some(trail) = result.trail {
175 trails.push(trail);
176 }
177
178 if (i + 1) % batch_size == 0 || i + 1 == config.num_photons {
180 let elapsed = start.elapsed();
181 stats.elapsed = elapsed;
182 callback(ProgressInfo {
183 photons_done: i + 1,
184 photons_total: config.num_photons,
185 photons_per_second: (i + 1) as f64 / elapsed.as_secs_f64(),
186 current_stats: stats.clone(),
187 });
188 }
189 }
190
191 stats.elapsed = start.elapsed();
192
193 TracerResult {
194 detector,
195 stats,
196 trails,
197 }
198}
199
200#[cfg(feature = "parallel")]
202fn trace_parallel(
203 scene: &Scene,
204 config: &TracerConfig,
205 callback: &(impl Fn(ProgressInfo) + Send + Sync),
206 start: Instant,
207) -> TracerResult {
208 use rayon::prelude::*;
209 use std::sync::atomic::{AtomicU64, Ordering};
210
211 let num_threads = rayon::current_num_threads();
212 let photons_per_thread = config.num_photons / num_threads as u64;
213 let progress_counter = AtomicU64::new(0);
214
215 let thread_results: Vec<(Detector, TracerStats, Vec<PhotonTrail>)> = (0..num_threads)
216 .into_par_iter()
217 .map(|thread_idx| {
218 let mut rng =
219 Xoshiro256PlusPlus::seed_from_u64(config.seed.wrapping_add(thread_idx as u64));
220 let mut detector =
221 Detector::new(config.detector_c_resolution, config.detector_g_resolution);
222 let mut stats = TracerStats::default();
223 let mut trails = Vec::new();
224 let num_sources = scene.sources.len();
225
226 let n = if thread_idx == 0 {
228 photons_per_thread + (config.num_photons % num_threads as u64)
229 } else {
230 photons_per_thread
231 };
232
233 for i in 0..n {
234 let source_idx =
235 ((thread_idx as u64 * photons_per_thread + i) as usize) % num_sources;
236 let source = &scene.sources[source_idx];
237 let ray = source.sample(&mut rng);
238 let record_trail = thread_idx == 0 && trails.len() < config.max_trails;
239
240 let result = trace_one_photon(scene, config, ray, &mut rng, record_trail);
241
242 stats.total_energy_emitted += 1.0;
243 stats.photons_traced += 1;
244
245 match result.outcome {
246 PhotonOutcome::Detected { energy } => {
247 detector.record(result.final_direction.as_ref().unwrap(), energy);
248 stats.photons_detected += 1;
249 stats.total_energy_detected += energy;
250 }
251 PhotonOutcome::Absorbed => stats.photons_absorbed += 1,
252 PhotonOutcome::MaxBounces => stats.photons_max_bounces += 1,
253 PhotonOutcome::RussianRoulette => stats.photons_russian_roulette += 1,
254 }
255
256 if let Some(trail) = result.trail {
257 trails.push(trail);
258 }
259
260 if thread_idx == 0 && (i + 1) % 10_000 == 0 {
262 let total_done = progress_counter.load(Ordering::Relaxed) + i + 1;
263 let elapsed = start.elapsed();
264 callback(ProgressInfo {
265 photons_done: total_done,
266 photons_total: config.num_photons,
267 photons_per_second: total_done as f64 / elapsed.as_secs_f64(),
268 current_stats: stats.clone(),
269 });
270 }
271 }
272
273 progress_counter.fetch_add(n, Ordering::Relaxed);
274 (detector, stats, trails)
275 })
276 .collect();
277
278 let mut detector = Detector::new(config.detector_c_resolution, config.detector_g_resolution);
280 let mut stats = TracerStats::default();
281 let mut trails = Vec::new();
282
283 for (d, s, t) in thread_results {
284 detector.merge(&d);
285 stats.photons_traced += s.photons_traced;
286 stats.photons_detected += s.photons_detected;
287 stats.photons_absorbed += s.photons_absorbed;
288 stats.photons_max_bounces += s.photons_max_bounces;
289 stats.photons_russian_roulette += s.photons_russian_roulette;
290 stats.total_energy_emitted += s.total_energy_emitted;
291 stats.total_energy_detected += s.total_energy_detected;
292 trails.extend(t);
293 }
294
295 stats.elapsed = start.elapsed();
296
297 TracerResult {
298 detector,
299 stats,
300 trails,
301 }
302}
303
304enum PhotonOutcome {
309 Detected { energy: f64 },
310 Absorbed,
311 MaxBounces,
312 RussianRoulette,
313}
314
315struct SinglePhotonResult {
316 outcome: PhotonOutcome,
317 final_direction: Option<nalgebra::Vector3<f64>>,
318 trail: Option<PhotonTrail>,
319}
320
321fn trace_one_photon(
322 scene: &Scene,
323 config: &TracerConfig,
324 initial_ray: crate::ray::Ray,
325 rng: &mut Xoshiro256PlusPlus,
326 record_trail: bool,
327) -> SinglePhotonResult {
328 use rand::Rng;
329
330 let mut photon = Photon::new(initial_ray);
331 let mut trail_points = if record_trail {
332 vec![TrailPoint {
333 position: photon.ray.origin,
334 event: TrailEvent::Emitted,
335 }]
336 } else {
337 Vec::new()
338 };
339
340 loop {
341 let hit = scene.intersect(&photon.ray, 1e-6, 1e10);
343
344 match hit {
345 None => {
346 if record_trail {
348 let far_point = photon.ray.at(1.0);
349 trail_points.push(TrailPoint {
350 position: far_point,
351 event: TrailEvent::Detected,
352 });
353 }
354 return SinglePhotonResult {
355 outcome: PhotonOutcome::Detected {
356 energy: photon.energy,
357 },
358 final_direction: Some(*photon.ray.direction.as_ref()),
359 trail: if record_trail {
360 Some(PhotonTrail {
361 points: trail_points,
362 })
363 } else {
364 None
365 },
366 };
367 }
368
369 Some(hit) => {
370 let material = scene.material(hit.material);
371 let interaction = material.interact(&photon, &hit, rng);
372
373 match interaction {
374 Interaction::Absorbed => {
375 if record_trail {
376 trail_points.push(TrailPoint {
377 position: hit.point,
378 event: TrailEvent::Absorbed,
379 });
380 }
381 return SinglePhotonResult {
382 outcome: PhotonOutcome::Absorbed,
383 final_direction: None,
384 trail: if record_trail {
385 Some(PhotonTrail {
386 points: trail_points,
387 })
388 } else {
389 None
390 },
391 };
392 }
393
394 Interaction::Reflected {
395 new_ray,
396 attenuation,
397 } => {
398 if record_trail {
399 trail_points.push(TrailPoint {
400 position: hit.point,
401 event: TrailEvent::Reflected,
402 });
403 }
404 photon.ray = new_ray;
405 photon.energy *= attenuation;
406 }
407
408 Interaction::Transmitted {
409 new_ray,
410 attenuation,
411 } => {
412 if record_trail {
413 trail_points.push(TrailPoint {
414 position: hit.point,
415 event: TrailEvent::Transmitted,
416 });
417 }
418 photon.ray = new_ray;
419 photon.energy *= attenuation;
420 }
421 }
422
423 photon.bounces += 1;
424
425 if photon.bounces >= config.max_bounces {
427 return SinglePhotonResult {
428 outcome: PhotonOutcome::MaxBounces,
429 final_direction: None,
430 trail: if record_trail {
431 Some(PhotonTrail {
432 points: trail_points,
433 })
434 } else {
435 None
436 },
437 };
438 }
439
440 if photon.energy < config.russian_roulette_threshold {
442 let survive_prob = photon.energy / config.russian_roulette_threshold;
443 if rng.random::<f64>() > survive_prob {
444 return SinglePhotonResult {
445 outcome: PhotonOutcome::RussianRoulette,
446 final_direction: None,
447 trail: if record_trail {
448 Some(PhotonTrail {
449 points: trail_points,
450 })
451 } else {
452 None
453 },
454 };
455 }
456 photon.energy = config.russian_roulette_threshold;
458 }
459 }
460 }
461 }
462}