euv_engine/raytracing/fn.rs
1use super::*;
2
3/// Recursively traces a ray through the scene and returns the final shaded
4/// color.
5///
6/// The function first finds the closest hit via [`closest_hit`]. On a miss
7/// it returns the ambient color. On a hit it evaluates the surface material
8/// with [`LightingUniforms::shade`] and, when the hit material has a
9/// non-zero specular component, recurses with a reflected ray up to
10/// `max_bounces` times (decrementing the ray's `depth` field).
11///
12/// # Arguments
13///
14/// - `Ray` - The ray to trace.
15/// - `&[Occluder]` - All occluding surfaces in the scene.
16/// - `&LightingUniforms` - Lighting parameters used during shading.
17/// - `u32` - The maximum number of bounces allowed for this ray.
18///
19/// # Returns
20///
21/// - `Vector3D` - The final traced color.
22pub fn trace(
23 ray: Ray,
24 occluders: &[Occluder],
25 lights: &LightingUniforms,
26 max_bounces: u32,
27) -> Vector3D {
28 let ambient: Vector3D = lights.get_ambient();
29 match closest_hit(&ray, occluders) {
30 None => ambient,
31 Some(hit) => {
32 let occluder_points: Vec<(Vector3D, f64)> = collect_occluder_points(occluders);
33 let material: Material = hit.get_material().clone();
34 let mut color: Vector3D = lights.shade(
35 hit.get_position(),
36 hit.get_normal(),
37 &material,
38 &occluder_points,
39 );
40 if ray.get_depth() < max_bounces {
41 let spec: f64 = material.get_specular();
42 if spec > EPSILON {
43 let reflected: Ray = reflect_ray(&ray, &hit);
44 let bounced: Vector3D = trace(reflected, occluders, lights, max_bounces);
45 color += bounced.scaled(spec);
46 }
47 }
48 color
49 }
50 }
51}
52
53/// Convenience wrapper that traces a ray using the [`RAYTRACE_DEFAULT_MAX_BOUNCES`]
54/// constant as the bounce limit.
55///
56/// # Arguments
57///
58/// - `Ray` - The ray to trace.
59/// - `&[Occluder]` - All occluding surfaces in the scene.
60/// - `&LightingUniforms` - Lighting parameters used during shading.
61///
62/// # Returns
63///
64/// - `Vector3D` - The final traced color.
65pub fn trace_default(ray: Ray, occluders: &[Occluder], lights: &LightingUniforms) -> Vector3D {
66 trace(ray, occluders, lights, RAYTRACE_DEFAULT_MAX_BOUNCES)
67}
68
69/// Finds the closest intersection between a ray and a list of occluders.
70///
71/// # Arguments
72///
73/// - `&Ray` - The ray to test.
74/// - `&[Occluder]` - The occluders to test against.
75///
76/// # Returns
77///
78/// - `Option<Hit>` - The closest hit, or `None` if the ray misses.
79pub fn closest_hit(ray: &Ray, occluders: &[Occluder]) -> Option<Hit> {
80 let mut best: Option<Hit> = None;
81 let origin: Vector3D = ray.get_origin();
82 let dir: Vector3D = ray.get_direction();
83 let t_min: f64 = ray.get_t_min();
84 let t_max: f64 = ray.get_t_max();
85 for occ in occluders.iter() {
86 let candidate: Option<Hit> = match occ.get_kind() {
87 OccluderKind::Sphere => {
88 let center: Vector3D = occ.get_center();
89 let radius: f64 = occ.get_extent().get_x();
90 match ray_sphere_intersect(origin, dir, center, radius) {
91 Some((t, n)) => {
92 if t >= t_min && t <= t_max {
93 let hit_pos: Vector3D = origin + dir.scaled(t);
94 Some(Hit {
95 t,
96 position: hit_pos,
97 normal: n,
98 material: occ.get_material().clone(),
99 })
100 } else {
101 None
102 }
103 }
104 None => None,
105 }
106 }
107 OccluderKind::Aabb => {
108 let aabb_min: Vector3D = occ.get_center();
109 let aabb_max: Vector3D = occ.get_extent();
110 match ray_aabb_intersect(origin, dir, aabb_min, aabb_max) {
111 Some((t_near, _t_far, n)) => {
112 if t_near >= t_min && t_near <= t_max {
113 let hit_pos: Vector3D = origin + dir.scaled(t_near);
114 Some(Hit {
115 t: t_near,
116 position: hit_pos,
117 normal: n,
118 material: occ.get_material().clone(),
119 })
120 } else {
121 None
122 }
123 }
124 None => None,
125 }
126 }
127 };
128 if let Some(c) = candidate {
129 best = match best {
130 Some(prev) if prev.get_t() <= c.get_t() => Some(prev),
131 _ => Some(c),
132 };
133 }
134 }
135 best
136}
137
138/// Builds a reflected ray bouncing off the hit surface.
139///
140/// # Arguments
141///
142/// - `&Ray` - The incoming ray.
143/// - `&Hit` - The hit point with normal information.
144///
145/// # Returns
146///
147/// - `Ray` - A new ray originating at the hit point with the reflected
148/// direction, `t_min` reset to `RAYTRACE_DEFAULT_T_MIN`, `t_max` set to
149/// `RAYTRACE_DEFAULT_T_MAX`, and `depth` incremented by one.
150pub fn reflect_ray(ray: &Ray, hit: &Hit) -> Ray {
151 let dir: Vector3D = ray.get_direction();
152 let normal: Vector3D = hit.get_normal();
153 let dot: f64 = dir.dot(normal);
154 let reflected_dir: Vector3D = dir - normal.scaled(2.0 * dot);
155 Ray {
156 origin: hit.get_position(),
157 direction: reflected_dir,
158 t_min: RAYTRACE_DEFAULT_T_MIN,
159 t_max: RAYTRACE_DEFAULT_T_MAX,
160 depth: ray.get_depth() + 1,
161 }
162}
163
164/// Returns the AABB extents `(min, max)` of an [`Occluder`].
165///
166/// For AABB occluders this is `(center, extent)`. For sphere occluders
167/// the bounding box is computed from the center and the `.x` component of
168/// `extent` (the sphere radius).
169///
170/// # Arguments
171///
172/// - `&Occluder` - The occluder to bound.
173///
174/// # Returns
175///
176/// - `(Vector3D, Vector3D)` - The `(min, max)` corners of the AABB.
177pub fn occluder_aabb_extents(occluder: &Occluder) -> (Vector3D, Vector3D) {
178 match occluder.get_kind() {
179 OccluderKind::Aabb => (occluder.get_center(), occluder.get_extent()),
180 OccluderKind::Sphere => {
181 let center: Vector3D = occluder.get_center();
182 let radius: f64 = occluder.get_extent().get_x();
183 let r: Vector3D = Vector3D::new(radius, radius, radius);
184 (center - r, center + r)
185 }
186 }
187}
188
189/// Helper that flattens every occluder into `(center, radius)` sphere
190/// tuples used by [`soft_shadow_factor`].
191///
192/// For sphere occluders the tuple is `(center, radius)`. For AABB
193/// occluders a conservative bounding sphere is computed from the AABB.
194fn collect_occluder_points(occluders: &[Occluder]) -> Vec<(Vector3D, f64)> {
195 let mut out: Vec<(Vector3D, f64)> = Vec::new();
196 for occ in occluders.iter() {
197 let (mn, mx): (Vector3D, Vector3D) = occluder_aabb_extents(occ);
198 let cx: f64 = (mn.get_x() + mx.get_x()) * 0.5;
199 let cy: f64 = (mn.get_y() + mx.get_y()) * 0.5;
200 let cz: f64 = (mn.get_z() + mx.get_z()) * 0.5;
201 let ex: f64 = (mx.get_x() - mn.get_x()) * 0.5;
202 let ey: f64 = (mx.get_y() - mn.get_y()) * 0.5;
203 let ez: f64 = (mx.get_z() - mn.get_z()) * 0.5;
204 let r: f64 = (ex * ex + ey * ey + ez * ez).sqrt();
205 out.push((Vector3D::new(cx, cy, cz), r));
206 }
207 out
208}