1use alloc::boxed::Box;
5use alloc::string::String;
6use alloc::vec::Vec;
7use core::fmt;
8use core::marker::PhantomData;
9
10use euclid::{Vector3D, vec3};
11use manyfmt::Fmt;
12#[cfg(not(any(feature = "std", test)))]
14#[allow(
15 unused_imports,
16 reason = "unclear why this warns even though it is needed"
17)]
18use num_traits::float::Float as _;
19use rand::SeedableRng;
20
21#[cfg(feature = "auto-threads")]
22use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _};
23
24use crate::block::{AIR, Evoxels, Resolution};
25use crate::camera::{Camera, GraphicsOptions, TransparencyOption};
26use crate::camera::{FogOption, NdcPoint2};
27#[cfg(not(any(feature = "std", test)))]
28#[allow(
29 unused_imports,
30 reason = "unclear why this warns even though it is needed"
31)]
32use crate::math::Euclid as _;
33use crate::math::{
34 Cube, Face6, Face7, FreeCoordinate, FreePoint, FreeVector, GridAab, GridMatrix, Intensity, Rgb,
35 Rgba, Vol, ZeroOne, rgb_const, smoothstep,
36};
37use crate::raycast::{self, Ray, RayIsh};
38use crate::space::{self, BlockIndex, BlockSky, PackedLight, Sky, SpaceBlockData};
39use crate::util::StatusText;
40
41#[cfg(doc)]
42use crate::space::Space;
43
44mod accum;
47pub use accum::*;
48mod surface;
49use surface::{DepthIter, DepthStep, Span, Surface, SurfaceIter, TraceStep};
50mod text;
52pub use text::*;
53pub use updating::*;
54mod updating;
55
56#[allow(clippy::module_name_repetitions, reason = "TODO: find better name")]
61pub struct SpaceRaytracer<D: RtBlockData> {
62 blocks: Vec<TracingBlock<D>>,
63 cubes: Vol<Box<[TracingCubeData]>>,
64
65 graphics_options: GraphicsOptions,
66 custom_options: D::Options,
67 sky: Sky,
68 sky_data: D,
69 block_sky: BlockSky,
70}
71
72impl<D: RtBlockData> SpaceRaytracer<D> {
73 pub fn new(
75 space: &space::Read<'_>,
76 graphics_options: GraphicsOptions,
77 custom_options: D::Options,
78 ) -> Self {
79 let options = RtOptionsRef {
80 graphics_options: &graphics_options,
81 custom_options: &custom_options,
82 };
83 let sky = space.physics().sky.clone();
84 SpaceRaytracer {
85 blocks: space
86 .block_data()
87 .iter()
88 .map(|sbd| TracingBlock::<D>::from_block(options, sbd))
89 .collect(),
90 cubes: prepare_cubes(space),
91 block_sky: sky.for_blocks(),
92 sky,
93 sky_data: D::exception(Exception::Sky, options),
94
95 graphics_options,
96 custom_options,
97 }
98 }
99
100 pub(crate) fn new_empty(
102 sky: Sky,
103 graphics_options: GraphicsOptions,
104 custom_options: D::Options,
105 ) -> Self {
106 let options = RtOptionsRef {
107 graphics_options: &graphics_options,
108 custom_options: &custom_options,
109 };
110 SpaceRaytracer {
111 blocks: Vec::new(),
112 cubes: Vol::from_elements(GridAab::ORIGIN_EMPTY, []).unwrap(),
113 block_sky: sky.for_blocks(),
114 sky,
115 sky_data: D::exception(Exception::Sky, options),
116
117 graphics_options,
118 custom_options,
119 }
120 }
121
122 pub fn trace_ray<P: Accumulate<BlockData = D>>(
124 &self,
125 ray: Ray,
126 accumulator: &mut P,
127 include_sky: bool,
128 ) -> RaytraceInfo {
129 self.trace_ray_impl::<P, Ray>(ray, accumulator, include_sky, true)
130 }
131
132 pub fn trace_axis_aligned_ray<P: Accumulate<BlockData = D>>(
137 &self,
138 ray: raycast::AaRay,
139 accumulator: &mut P,
140 include_sky: bool,
141 ) -> RaytraceInfo {
142 self.trace_ray_impl::<P, raycast::AaRay>(ray, accumulator, include_sky, true)
143 }
144
145 fn trace_ray_impl<P: Accumulate<BlockData = D>, R: RayIsh>(
146 &self,
147 ray: R,
148 accumulator: &mut P,
149 include_sky: bool,
150 allow_ray_bounce: bool,
151 ) -> RaytraceInfo {
152 let options = RtOptionsRef {
153 graphics_options: &self.graphics_options,
154 custom_options: &self.custom_options,
155 };
156 let ray_direction = ray.direction();
157
158 let sky_light = include_sky.then(|| self.sky.sample(ray.direction()));
159 let t_to_absolute_distance = ray.direction().length();
160 let mut state: TracingState<'_, P> = TracingState {
161 t_to_absolute_distance,
162 t_to_view_distance: (t_to_absolute_distance
163 / self.graphics_options.view_distance.into_inner())
164 as f32,
165 distance_fog_light: match self.graphics_options.fog {
166 FogOption::None => None,
167 _ => sky_light,
168 },
169 distance_fog_blend: match self.graphics_options.fog {
170 FogOption::Abrupt => 1.0,
171 FogOption::Compromise => 0.5,
172 FogOption::Physical => 0.0,
173 _ => 0.0,
174 },
175 primary_cubes_traced: 0,
176 secondary_info: RaytraceInfo::default(),
177 accumulator,
178 ray_bounce_rng: allow_ray_bounce.then(|| {
179 rand::rngs::SmallRng::seed_from_u64(
185 ray_direction
186 .x
187 .to_bits()
188 .wrapping_add(ray_direction.y.to_bits())
189 .wrapping_add(ray_direction.z.to_bits()),
190 )
191 }),
192 };
193 let surface_iter: SurfaceIter<'_, D, R::Caster> = SurfaceIter::new(self, ray);
194
195 match self.graphics_options.transparency {
197 TransparencyOption::Volumetric => {
198 for step in DepthIter::new(surface_iter) {
199 if state.count_step_should_stop(options) {
200 break;
201 }
202
203 match step {
204 DepthStep::Invisible { .. } => {
205 }
207 DepthStep::Span(span) => {
208 debug_assert!(span.surface.visible());
209 state.trace_through_span(span, self);
210 }
211 DepthStep::EnterBlock {
212 t_distance: _,
213 block_data,
214 } => state.accumulator.enter_block(block_data),
215 }
216 }
217 }
218 _ => {
219 for step in surface_iter {
220 if state.count_step_should_stop(options) {
221 break;
222 }
223
224 use TraceStep::*;
225 match step {
226 Invisible { .. } => {
227 }
229 EnterBlock {
230 t_distance: _,
231 block_data,
232 } => state.accumulator.enter_block(block_data),
233 EnterSurface(surface) => {
234 debug_assert!(surface.visible());
235 state.trace_through_surface(&surface, self);
236 }
237 }
238 }
239 }
240 }
241 state.finish(
242 if let Some(sky_light) = sky_light {
243 sky_light.with_alpha_one()
244 } else {
245 Rgba::TRANSPARENT
246 },
247 &self.sky_data,
248 self.graphics_options.debug_pixel_cost,
249 options,
250 )
251 }
252
253 #[inline]
254 fn get_packed_light(&self, cube: Cube) -> PackedLight {
255 match self.cubes.get(cube) {
256 Some(b) => b.lighting,
257 None => self.block_sky.light_outside(self.cubes.bounds(), cube),
258 }
259 }
260
261 fn get_interpolated_light(&self, point: FreePoint, face: Face7) -> Rgb {
262 let above_surface_epsilon = 0.5 / 256.0;
267
268 let origin = point.to_vector() + face.vector(above_surface_epsilon);
270
271 let reference_frame = match Face6::try_from(face) {
274 Ok(face) => face.face_transform(0).to_matrix(),
275 Err(_) => GridMatrix::ZERO,
276 }
277 .to_free();
278 let reference_frame_x = reference_frame.transform_vector3d(vec3(1., 0., 0.));
279 let reference_frame_y = reference_frame.transform_vector3d(vec3(0., 1., 0.));
280
281 let mut mix_1 = (origin.dot(reference_frame_x) - 0.5).rem_euclid(1.0);
282 let mut mix_2 = (origin.dot(reference_frame_y) - 0.5).rem_euclid(1.0);
283
284 fn flip_mix(mix: &mut FreeCoordinate, dir: FreeVector) -> FreeVector {
286 if *mix > 0.5 {
287 *mix = 1.0 - *mix;
288 -dir
289 } else {
290 dir
291 }
292 }
293 let dir_1 = flip_mix(&mut mix_1, reference_frame_x);
294 let dir_2 = flip_mix(&mut mix_2, reference_frame_y);
295
296 let mix_1 = smoothstep(mix_1);
301 let mix_2 = smoothstep(mix_2);
302
303 let get_light = |p: FreeVector| match Cube::containing(origin.to_point() + p) {
305 Some(cube) => self.get_packed_light(cube),
306 None => self.block_sky.mean(),
308 };
309 let lin_lo = -0.5;
310 let lin_hi = 0.5;
311 let near12 = get_light(dir_1 * lin_lo + dir_2 * lin_lo);
312 let near1far2 = get_light(dir_1 * lin_lo + dir_2 * lin_hi);
313 let near2far1 = get_light(dir_1 * lin_hi + dir_2 * lin_lo);
314 let mut far12 = get_light(dir_1 * lin_hi + dir_2 * lin_hi);
315
316 if !near1far2.valid() && !near2far1.valid() {
317 far12 = near12;
320 }
321
322 let near12 = near12.value_with_ambient_occlusion();
324 let near1far2 = near1far2.value_with_ambient_occlusion();
325 let near2far1 = near2far1.value_with_ambient_occlusion();
326 let far12 = far12.value_with_ambient_occlusion();
327
328 let [r, g, b, final_weight] = mix4(
331 mix4(near12, near1far2, mix_2 as f32),
332 mix4(near2far1, far12, mix_2 as f32),
333 mix_1 as f32,
334 );
335 Rgb::try_from(Vector3D::from([r, g, b]) / final_weight.max(0.1)).unwrap()
337 }
338}
339
340impl<D: RtBlockData> SpaceRaytracer<D> {
342 pub fn to_text<'a, P>(
346 &'a self,
347 camera: &'a Camera,
348 line_ending: &'a str,
349 ) -> impl fmt::Display + 'a
350 where
351 P: Accumulate<BlockData = D> + Into<String> + Default + 'a,
352 {
353 struct ToText<'a, D: RtBlockData, P> {
354 rt: &'a SpaceRaytracer<D>,
355 camera: &'a Camera,
356 line_ending: &'a str,
357 _p: PhantomData<fn() -> P>,
358 }
359
360 impl<D: RtBlockData, P: Accumulate<BlockData = D> + Into<String> + Default> fmt::Display
361 for ToText<'_, D, P>
362 {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 self.rt.trace_scene_to_text_impl::<P>(self.camera, self.line_ending, f)?;
365 Ok(())
366 }
367 }
368
369 ToText::<'a, D, P> {
370 rt: self,
371 camera,
372 line_ending,
373 _p: PhantomData,
374 }
375 }
376
377 #[cfg(feature = "auto-threads")]
378 fn trace_scene_to_text_impl<P>(
379 &self,
380 camera: &Camera,
381 line_ending: &str,
382 stream: &mut dyn fmt::Write,
383 ) -> Result<RaytraceInfo, fmt::Error>
384 where
385 P: Accumulate<BlockData = D> + Into<String> + Default,
386 {
387 let viewport = camera.viewport();
388 let viewport_size = viewport.framebuffer_size.to_usize();
389 let output_iterator = (0..viewport_size.height)
390 .into_par_iter()
391 .map(move |ych| {
392 let y = viewport.normalize_fb_y(ych);
393 (0..viewport_size.width)
394 .into_par_iter()
395 .map(move |xch| {
396 let x = viewport.normalize_fb_x(xch);
397 let mut buf = P::default();
398 let info = self.trace_ray::<P>(
399 camera.project_ndc_into_world(NdcPoint2::new(x, y)),
400 &mut buf,
401 true,
402 );
403 (buf.into(), info)
404 })
405 .chain(
406 Some((String::from(line_ending), RaytraceInfo::default())).into_par_iter(),
407 )
408 })
409 .flatten();
410
411 let (text, info_sum): (String, rayon_helper::ParExtSum<RaytraceInfo>) =
412 output_iterator.unzip();
413 stream.write_str(text.as_str())?;
414
415 Ok(info_sum.result())
416 }
417
418 #[cfg(not(feature = "auto-threads"))]
419 fn trace_scene_to_text_impl<P>(
420 &self,
421 camera: &Camera,
422 line_ending: &str,
423 stream: &mut dyn fmt::Write,
424 ) -> Result<RaytraceInfo, fmt::Error>
425 where
426 P: Accumulate<BlockData = D> + Into<String> + Default,
427 {
428 let mut total_info = RaytraceInfo::default();
429
430 let viewport = camera.viewport();
431 let viewport_size = viewport.framebuffer_size.to_usize();
432 for ych in 0..viewport_size.height {
433 let y = viewport.normalize_fb_y(ych);
434 for xch in 0..viewport_size.width {
435 let x = viewport.normalize_fb_x(xch);
436 let mut buf = P::default();
437 let info = self.trace_ray::<P>(
438 camera.project_ndc_into_world(NdcPoint2::new(x, y)),
439 &mut buf,
440 true,
441 );
442 total_info += info;
443 stream.write_str(buf.into().as_str())?;
444 }
445 stream.write_str(line_ending)?;
446 }
447
448 Ok(total_info)
449 }
450}
451
452impl<D> fmt::Debug for SpaceRaytracer<D>
454where
455 D: RtBlockData<Options: fmt::Debug>,
456{
457 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458 f.debug_struct("SpaceRaytracer")
459 .field("blocks.len", &self.blocks.len())
460 .field("cubes.bounds", &self.cubes.bounds())
461 .field("graphics_options", &self.graphics_options)
462 .field("custom_options", &self.custom_options)
463 .field("sky", &self.sky)
464 .finish_non_exhaustive()
465 }
466}
467
468fn mix4(a: [f32; 4], b: [f32; 4], amount: f32) -> [f32; 4] {
469 core::array::from_fn(|i| {
470 let a = a[i];
471 let b = b[i];
472 a + (b - a) * amount
473 })
474}
475
476#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
483#[non_exhaustive]
484pub struct RaytraceInfo {
485 cubes_traced: usize,
486}
487impl core::ops::Add for RaytraceInfo {
488 type Output = Self;
489 fn add(mut self, other: Self) -> Self {
490 self += other;
491 self
492 }
493}
494impl core::ops::AddAssign<RaytraceInfo> for RaytraceInfo {
495 fn add_assign(&mut self, other: Self) {
496 self.cubes_traced += other.cubes_traced;
497 }
498}
499impl core::iter::Sum for RaytraceInfo {
500 fn sum<I>(iter: I) -> Self
501 where
502 I: Iterator<Item = Self>,
503 {
504 let mut sum = Self::default();
505 for part in iter {
506 sum += part;
507 }
508 sum
509 }
510}
511
512impl Fmt<StatusText> for RaytraceInfo {
513 fn fmt(&self, fmt: &mut fmt::Formatter<'_>, _: &StatusText) -> fmt::Result {
514 let &Self { cubes_traced } = self;
515 write!(fmt, "Cubes traced: {cubes_traced}")
516 }
517}
518
519#[inline]
523fn prepare_cubes(space: &space::Read<'_>) -> Vol<Box<[TracingCubeData]>> {
524 space.extract(space.bounds(), |e| TracingCubeData {
525 block_index: e.block_index(),
526 lighting: e.light(),
527 always_invisible: e.block_data().block() == &AIR,
528 })
529}
530
531#[derive(Clone, Debug)]
532struct TracingCubeData {
533 block_index: BlockIndex,
534 lighting: PackedLight,
535 always_invisible: bool,
543}
544
545#[derive(Clone, Debug)]
548struct TracingBlock<D> {
549 block_data: D,
550 voxels: Evoxels,
552}
553
554impl<D: RtBlockData> TracingBlock<D> {
555 fn from_block(
556 options: RtOptionsRef<'_, D::Options>,
557 space_block_data: &SpaceBlockData,
558 ) -> Self {
559 TracingBlock {
560 block_data: D::from_block(options, space_block_data),
561 voxels: space_block_data.evaluated().voxels().clone(),
562 }
563 }
564}
565
566type BounceRng = rand::rngs::SmallRng;
569
570#[derive(Debug)]
573struct TracingState<'a, P: Accumulate> {
574 t_to_absolute_distance: f64,
577
578 t_to_view_distance: f32,
581
582 distance_fog_light: Option<Rgb>,
584
585 distance_fog_blend: f32,
587
588 primary_cubes_traced: usize,
591
592 secondary_info: RaytraceInfo,
594
595 accumulator: &'a mut P,
596
597 ray_bounce_rng: Option<BounceRng>,
600}
601impl<P: Accumulate> TracingState<'_, P> {
602 #[inline]
603 fn count_step_should_stop(
604 &mut self,
605 options: RtOptionsRef<'_, <P::BlockData as RtBlockData>::Options>,
606 ) -> bool {
607 if self.primary_cubes_traced == 0 {
608 self.accumulator.add(Hit {
609 exception: Some(Exception::EnterSpace),
610 surface: Rgba::TRANSPARENT.into(),
611 t_distance: None,
612 block: &P::BlockData::exception(Exception::EnterSpace, options),
613 position: None,
614 });
615 }
616
617 self.primary_cubes_traced += 1;
618
619 if self.primary_cubes_traced > 1000 {
622 self.accumulator.add(Hit {
624 exception: Some(Exception::Incomplete),
625 surface: Rgba::TRANSPARENT.into(),
626 t_distance: None,
627 block: &P::BlockData::exception(Exception::Incomplete, options),
628 position: None,
629 });
630 true
631 } else {
632 self.accumulator.opaque()
633 }
634 }
635
636 fn finish(
637 self,
638 sky_color: Rgba,
639 sky_data: &P::BlockData,
640 debug_steps: bool,
641 options: RtOptionsRef<'_, <P::BlockData as RtBlockData>::Options>,
642 ) -> RaytraceInfo {
643 self.accumulator.add(Hit {
648 exception: Some(Exception::Sky),
649 surface: sky_color.into(),
650 t_distance: Some(f64::INFINITY),
651 block: sky_data,
652 position: None,
653 });
654
655 if debug_steps {
657 self.accumulator.add(Hit {
658 exception: Some(Exception::DebugOverrideRg),
659 surface: (rgb_const!(0.02, 0.002, 0.0) * self.primary_cubes_traced as f32)
660 .with_alpha_one()
661 .into(),
662 t_distance: None,
663 block: &P::BlockData::exception(Exception::DebugOverrideRg, options),
664 position: None,
665 });
666 }
667
668 RaytraceInfo {
669 cubes_traced: self.primary_cubes_traced,
670 } + self.secondary_info
671 }
672
673 #[inline]
675 fn trace_through_surface(
676 &mut self,
677 surface: &Surface<'_, P::BlockData>,
678 rt: &SpaceRaytracer<P::BlockData>,
679 ) {
680 if let Some((light, info)) = surface.to_light(rt, self) {
681 self.accumulator.add(Hit {
682 exception: None,
683 surface: light,
684 t_distance: Some(surface.t_distance),
685 block: surface.block_data,
686 position: Some(Position {
687 cube: surface.cube,
688 resolution: surface.voxel.0,
689 voxel: surface.voxel.1,
690 face: surface.normal,
691 }),
692 });
693 self.secondary_info += info;
694 }
695 }
696
697 #[inline]
698 fn trace_through_span(
699 &mut self,
700 span: Span<'_, P::BlockData>,
701 rt: &SpaceRaytracer<P::BlockData>,
702 ) {
703 let Span {
704 mut surface,
705 exit_t_distance,
706 } = span;
707
708 let thickness =
709 ((exit_t_distance - surface.t_distance) * self.t_to_absolute_distance) as f32;
710
711 let (adjusted_color, emission_coeff) =
713 apply_transmittance(surface.diffuse_color, thickness);
714 surface.diffuse_color = adjusted_color;
715 surface.emission = surface.emission * emission_coeff;
716
717 self.trace_through_surface(&surface, rt);
718 }
719}
720
721#[inline]
727fn apply_transmittance(color: Rgba, thickness: f32) -> (Rgba, f32) {
728 let thickness = thickness.max(0.0);
730
731 if thickness == 0.0 {
735 return if color.fully_opaque() {
736 (color, 1.0)
737 } else {
738 (Rgba::TRANSPARENT, 0.0)
739 };
740 }
741
742 let unit_transmittance = 1.0 - color.clamp().alpha().into_inner();
744 let depth_transmittance = unit_transmittance.powf(thickness);
746 let alpha = ZeroOne::<f32>::new_clamped(1.0 - depth_transmittance);
750 let modified_color = color.to_rgb().with_alpha(alpha);
751
752 let emission_coeff = if unit_transmittance == 1.0 {
758 thickness
762 } else {
763 (depth_transmittance - 1.) / (unit_transmittance - 1.)
767 };
768
769 (modified_color, emission_coeff.max(0.0))
770}
771
772pub(crate) fn trace_for_eval(
782 voxels: &Evoxels,
783 origin: Cube,
784 direction: Face6,
785 resolution: Resolution,
786) -> EvalTrace {
787 let thickness = resolution.recip_f32();
788 let step = direction.normal_vector();
789
790 let mut cube = origin;
791 let mut color_buf = ColorBuf::default();
792 let mut emission = Vector3D::zero();
793
794 while let Some(voxel) = voxels.get(cube) {
795 let (adjusted_color, emission_coeff) = apply_transmittance(voxel.color, thickness);
796 emission += Vector3D::from(voxel.emission * emission_coeff) * color_buf.transmittance;
797 color_buf.add(Hit {
798 exception: None,
799 surface: adjusted_color.into(),
800 t_distance: None, block: &(),
802 position: None, });
804
805 if color_buf.opaque() {
806 break;
807 }
808 cube += step;
809 }
810 EvalTrace {
811 color: color_buf.into(),
812 emission,
813 }
814}
815
816#[derive(Clone, Copy)]
817pub(crate) struct EvalTrace {
818 pub color: Rgba,
819 pub emission: Vector3D<f32, Intensity>,
820}
821
822#[cfg(feature = "auto-threads")]
825mod rayon_helper {
826 use core::iter::{Sum, empty, once};
827 use rayon::iter::{IntoParallelIterator, ParallelExtend, ParallelIterator as _};
828
829 #[derive(Clone, Copy, Debug, Default)]
832 pub(crate) struct ParExtSum<T>(Option<T>);
833
834 impl<T: Sum> ParExtSum<T> {
835 pub fn result(self) -> T {
836 self.0.unwrap_or_else(|| empty().sum())
837 }
838 }
839
840 impl<T: Sum + Send> ParallelExtend<T> for ParExtSum<T> {
841 fn par_extend<I>(&mut self, par_iter: I)
842 where
843 I: IntoParallelIterator<Item = T>,
844 {
845 let new = par_iter.into_par_iter().sum();
846 self.0 = Some(match self.0.take() {
849 None => new,
850 Some(previous) => once(previous).chain(once(new)).sum(),
851 });
852 }
853 }
854}
855
856#[cfg(test)]
863mod tests {
864 use super::*;
865 use crate::math::rgba_const;
866
867 #[test]
868 fn apply_transmittance_identity() {
869 let color = rgba_const!(1.0, 0.5, 0.0, 0.5);
870 assert_eq!(apply_transmittance(color, 1.0), (color, 1.0));
871 }
872
873 #[test]
878 fn apply_transmittance_equivalence() {
879 fn case(color: Rgba, count: usize) {
880 let (modified_color, _emission_coeff) =
881 apply_transmittance(color, (count as f32).recip());
882 let mut color_buf = ColorBuf::default();
883 for _ in 0..count {
884 color_buf.add(Hit {
885 exception: None,
886 surface: modified_color.into(),
887 t_distance: None,
888 block: &(),
889 position: None,
890 });
891 }
892 let actual = Rgba::from(color_buf);
893 let error: Vec<f32> = <[f32; 4]>::from(actual)
894 .into_iter()
895 .zip(<[f32; 4]>::from(color))
896 .map(|(a, b)| a - b)
897 .collect();
898 assert!(
899 error.iter().sum::<f32>() < 0.00001,
900 "count {count}, color {color:?}, actual {actual:?}, error {error:?}"
901 );
902 }
903
904 let color = rgba_const!(1.0, 0.5, 0.0, 0.5);
905 case(color, 1);
906 case(color, 2);
907 case(color, 8);
908 }
909
910 #[test]
912 fn apply_transmittance_negative_thickness_transparent() {
913 assert_eq!(
914 apply_transmittance(rgba_const!(1.0, 0.5, 0.0, 0.5), -0.125),
915 (Rgba::TRANSPARENT, 0.0)
916 );
917 }
918 #[test]
919 fn apply_transmittance_negative_thickness_opaque() {
920 let color = rgba_const!(1.0, 0.5, 0.0, 1.0);
921 assert_eq!(apply_transmittance(color, -0.125), (color, 1.0));
922 }
923
924 #[test]
925 fn apply_transmittance_zero_thickness_transparent() {
926 assert_eq!(
927 apply_transmittance(rgba_const!(1.0, 0.5, 0.0, 0.5), 0.0),
928 (Rgba::TRANSPARENT, 0.0)
929 );
930 }
931 #[test]
932 fn apply_transmittance_zero_thickness_opaque() {
933 let color = rgba_const!(1.0, 0.5, 0.0, 1.0);
934 assert_eq!(apply_transmittance(color, 0.0), (color, 1.0));
935 }
936}