Skip to main content

all_is_cubes/camera/
graphics_options.rs

1use core::fmt;
2
3use num_traits::ConstOne as _;
4
5use crate::math::{FreeCoordinate, PositiveSign, Rgb, Rgba, ZeroOne, ps64, zo32};
6use crate::util::ShowStatus;
7
8#[cfg(doc)]
9use crate::{block::Block, camera::Camera, space::Space};
10
11/// Options for controlling rendering (not affecting gameplay except informationally).
12///
13/// Some options may be ignored by some renderers, such as when they request a particular
14/// implementation approach or debug visualization. Renderers should make an effort to
15/// report such failings, such as via the `all_is_cubes_render::Flaws` type.
16//---
17// (Due to crate splitting that can't be a doc-link.)
18#[doc = include_str!("../save/serde-warning.md")]
19#[derive(Clone, Eq, PartialEq)]
20#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
21#[cfg_attr(feature = "save", serde(default))]
22#[non_exhaustive]
23pub struct GraphicsOptions {
24    /// Overall rendering technique to use.
25    ///
26    /// May be ignored if the method requested is not supported in the current
27    /// environment.
28    pub render_method: RenderMethod,
29
30    /// Whether and how to draw fog obscuring the view distance limit.
31    ///
32    /// TODO: Implement fog in raytracer.
33    pub fog: FogOption,
34
35    /// Field of view, in degrees from top to bottom edge of the viewport.
36    ///
37    /// Values ≥ 180° are ignored.
38    // --
39    // TODO: make deserialization not break on infinity
40    pub fov_y: PositiveSign<FreeCoordinate>,
41
42    /// Method to use to remap colors to fit within the displayable range.
43    ///
44    /// In order for tone mapping to take effect, [`maximum_intensity`](Self::maximum_intensity)
45    /// must be set to an appropriate finite value.
46    pub tone_mapping: ToneMappingOperator,
47
48    /// Maximum value to allow in the output image’s color channels.
49    ///
50    /// * If the output format/device supports HDR (high dynamic range; color channel values greater
51    ///   than 1.0),
52    ///   this should be set to the maximum representable (or desired) value.
53    /// * If the output format is HDR and no specific limit is known or desired, use ∞.
54    ///   Note that this disables tone mapping.
55    /// * If the output format is not HDR (SDR), use 1.0.
56    /// * If no information is available, use ∞.
57    ///
58    /// The chosen [`tone_mapping`](ToneMappingOperator) will use this information to map to the
59    /// available range.
60    ///
61    /// The default value is ∞.
62    #[cfg_attr(feature = "save", serde(with = "serialize_infinity_as_none"))]
63    pub maximum_intensity: PositiveSign<f32>,
64
65    /// “Camera exposure” value: a scaling factor from scene luminance to displayed
66    /// luminance. Note that the exact interpretation of this depends on the chosen
67    /// [`tone_mapping`](ToneMappingOperator).
68    pub exposure: ExposureOption,
69
70    /// Proportion of bloom (blurred image) to mix into the original image.
71    /// 0.0 is no bloom and 1.0 is no original image.
72    pub bloom_intensity: ZeroOne<f32>,
73
74    /// Distance, in unit cubes, from the camera to the farthest visible point.
75    //---
76    /// TODO: Implement view distance limit (and fog) in raytracer.
77    /// TODO: make deserialization not break on infinity
78    pub view_distance: PositiveSign<FreeCoordinate>,
79
80    /// Style in which to draw the lighting of [`Space`](crate::space::Space)s.
81    /// This does not affect the *computation* of lighting.
82    pub lighting_display: LightingOption,
83
84    /// Method/fidelity to use for transparency.
85    pub transparency: TransparencyOption,
86
87    /// Whether to show the HUD or other UI elements.
88    ///
89    /// This does not affect UI state or clickability; it purely controls display.
90    /// It is intended for the purpose of asking a renderer to produce an image
91    /// of the scene without any UI.
92    ///
93    /// The cursor is not currently considered part of the UI. This may be revisited
94    /// later. The “info text” is controlled separately by
95    /// [`debug_info_text`](Self::debug_info_text).
96    pub show_ui: bool,
97
98    /// Whether to apply antialiasing techniques.
99    pub antialiasing: AntialiasingOption,
100
101    /// Draw text overlay showing debug information.
102    pub debug_info_text: bool,
103
104    /// What information should be displayed in [`debug_info_text`](Self::debug_info_text).
105    //---
106    // TODO: It's inelegant that this is counted as part of the graphics options, but perhaps
107    // not less so than the other debug options...
108    pub debug_info_text_contents: ShowStatus,
109
110    /// Draw boxes around [`Behavior`]s attached to parts of [`Space`]s.
111    /// This may also eventually include further in-world diagnostic information.
112    ///
113    /// [`Behavior`]: crate::behavior::Behavior
114    /// [`Space`]: crate::space::Space
115    pub debug_behaviors: bool,
116
117    /// Draw boxes around chunk borders and some debug info.
118    pub debug_chunk_boxes: bool,
119
120    /// Draw collision boxes for some objects.
121    pub debug_collision_boxes: bool,
122
123    /// Draw the light rays that contribute to the selected block.
124    pub debug_light_rays_at_cursor: bool,
125
126    /// Visualize the cost of rendering each pixel, rather than the color of the scene.
127    ///
128    /// Note that the current implementations of this in All is Cubes’ standard renderers do not
129    /// measure actual execution cost, but quantities correlated with it.
130    /// The color channels are used as follows:
131    ///
132    /// * In the raytracer,
133    ///     * Red-yellow: number of steps taken
134    ///     * Blue: original scene luminance
135    /// * In the GPU mesh renderer:
136    ///     * Red: number of opaque fragment shader executions
137    ///     * Green: number of transparent fragment shader executions
138    ///     * Blue: number of triangles rasterized, including ones discarded by depth test
139    pub debug_pixel_cost: bool,
140
141    /// Causes [`Camera`] to compute a falsified view frustum which is 1/2 the width and height
142    /// it should be.
143    ///
144    /// This may be used to visualize the effect of frustum culling that is performed via
145    /// [`Camera::aab_in_view()`].
146    pub debug_reduce_view_frustum: bool,
147}
148
149impl GraphicsOptions {
150    /// A set of graphics options which differs from [`GraphicsOptions::default()`] in
151    /// that it disables all operations which change colors away from their obvious
152    /// values; that is, the [`Rgba`] colors you get from a rendering will be identical
153    /// (except for quantization error and background colors) to the [`Rgba`] colors
154    /// in the depicted [`Atom`](crate::block::Primitive::Atom)s.
155    ///
156    /// * [`Self::bloom_intensity`] = `0`
157    /// * [`Self::fog`] = [`FogOption::None`]
158    /// * [`Self::lighting_display`] = [`LightingOption::None`]
159    /// * [`Self::tone_mapping`] = [`ToneMappingOperator::Clamp`]
160    ///
161    /// Future versions may set other options as necessary to maintain the intended
162    /// property.
163    pub const UNALTERED_COLORS: Self = Self {
164        render_method: RenderMethod::Preferred,
165        fog: FogOption::None,
166        fov_y: ps64(90.),
167        // TODO: Change tone mapping default once we have a good implementation.
168        tone_mapping: ToneMappingOperator::Clamp,
169        maximum_intensity: PositiveSign::<f32>::INFINITY,
170        exposure: ExposureOption::Fixed(PositiveSign::<f32>::ONE),
171        bloom_intensity: zo32(0.),
172        view_distance: ps64(200.),
173        lighting_display: LightingOption::None,
174        transparency: TransparencyOption::Volumetric,
175        show_ui: true,
176        antialiasing: AntialiasingOption::None,
177        debug_info_text: true,
178        debug_info_text_contents: ShowStatus::DEFAULT,
179        debug_behaviors: false,
180        debug_chunk_boxes: false,
181        debug_collision_boxes: false,
182        debug_light_rays_at_cursor: false,
183        debug_pixel_cost: false,
184        debug_reduce_view_frustum: false,
185    };
186
187    /// Constrain fields to valid/practical values.
188    #[must_use]
189    pub fn repair(mut self) -> Self {
190        self.fov_y = self.fov_y.clamp(ps64(1.), ps64(189.));
191        self.view_distance = self.view_distance.clamp(ps64(1.), ps64(10000.));
192        self
193    }
194}
195
196impl fmt::Debug for GraphicsOptions {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        let Self {
199            render_method,
200            fog,
201            fov_y,
202            tone_mapping,
203            maximum_intensity,
204            exposure,
205            bloom_intensity,
206            view_distance,
207            lighting_display,
208            transparency,
209            show_ui,
210            antialiasing,
211            debug_info_text,
212            debug_info_text_contents,
213            debug_behaviors,
214            debug_chunk_boxes,
215            debug_collision_boxes,
216            debug_light_rays_at_cursor,
217            debug_pixel_cost,
218            debug_reduce_view_frustum,
219        } = self;
220        // This custom impl reduces unnecessary text by stripping off NotNan wrappers.
221        f.debug_struct("GraphicsOptions")
222            .field("render_method", render_method)
223            .field("fog", fog)
224            .field("fov_y", &fov_y.into_inner())
225            .field("tone_mapping", &tone_mapping)
226            .field("maximum_intensity", &maximum_intensity)
227            .field("exposure", &exposure)
228            .field("bloom_intensity", &bloom_intensity.into_inner())
229            .field("view_distance", &view_distance.into_inner())
230            .field("lighting_display", &lighting_display)
231            .field("transparency", &transparency)
232            .field("show_ui", &show_ui)
233            .field("antialiasing", &antialiasing)
234            .field("debug_info_text", &debug_info_text)
235            .field("debug_info_text_contents", &debug_info_text_contents)
236            .field("debug_behaviors", &debug_behaviors)
237            .field("debug_chunk_boxes", &debug_chunk_boxes)
238            .field("debug_collision_boxes", &debug_collision_boxes)
239            .field("debug_light_rays_at_cursor", &debug_light_rays_at_cursor)
240            .field("debug_pixel_cost", &debug_pixel_cost)
241            .field("debug_reduce_view_frustum", &debug_reduce_view_frustum)
242            .finish()
243    }
244}
245
246impl Default for GraphicsOptions {
247    /// Default graphics options broadly have “everything reasonable” turned on
248    /// (they may disable things that are not well-implemented yet).
249    ///
250    /// TODO: Explain exactly what the default is.
251    fn default() -> Self {
252        Self {
253            render_method: RenderMethod::Preferred,
254            fog: FogOption::Abrupt,
255            fov_y: ps64(90.),
256            // TODO: Change tone mapping default once we have a good implementation.
257            tone_mapping: ToneMappingOperator::Clamp,
258            maximum_intensity: PositiveSign::<f32>::INFINITY,
259            exposure: ExposureOption::default(),
260            bloom_intensity: zo32(0.125),
261            view_distance: ps64(200.),
262            lighting_display: LightingOption::Smooth,
263            transparency: TransparencyOption::Volumetric,
264            show_ui: true,
265            antialiasing: AntialiasingOption::default(),
266            debug_info_text: true,
267            debug_info_text_contents: ShowStatus::DEFAULT,
268            debug_behaviors: false,
269            debug_chunk_boxes: false,
270            debug_collision_boxes: false,
271            debug_light_rays_at_cursor: false,
272            debug_pixel_cost: false,
273            debug_reduce_view_frustum: false,
274        }
275    }
276}
277
278/// Choices for [`GraphicsOptions::render_method`].
279#[derive(Clone, Debug, Eq, PartialEq)]
280#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
281#[non_exhaustive]
282pub enum RenderMethod {
283    /// Use whichever method is presumed to be better for the current situation.
284    ///
285    /// Currently, this typically means [`RenderMethod::Reference`] for
286    /// non-interactive (headless) rendering and [`RenderMethod::Mesh`] for
287    /// interactive usage.
288    Preferred,
289
290    /// Make triangle meshes of [`Block`]s and of chunks of [`Space`]s and draw them
291    /// using the GPU (or software triangle rasterizer as a fallback).
292    ///
293    /// As of this documentation being written, the available implementation has trouble
294    /// with transparent volumes.
295    Mesh,
296
297    /// Use the reference implementation of All is Cubes content rendering.
298    ///
299    /// This means `all_is_cubes_render::raytracer`, a CPU-based raytracer.
300    /// It is typically too slow for high-resolution interactive use, though it could
301    /// have an advantage in rapidly changing content.
302    ///
303    /// See also [`LightingOption::Bounce`].
304    Reference,
305    // TODO: Someday, GpuRaytracing.
306}
307
308/// Choices for [`GraphicsOptions::fog`].
309///
310#[doc = include_str!("../save/serde-warning.md")]
311#[derive(Clone, Debug, Eq, PartialEq)]
312#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
313#[non_exhaustive]
314pub enum FogOption {
315    /// No fog: objects will maintain their color and disappear raggedly.
316    None,
317    /// Fog starts just before the view distance ends.
318    Abrupt,
319    /// Compromise between `Abrupt` and `Physical` options.
320    Compromise,
321    /// Almost physically realistic fog of constant density.
322    Physical,
323}
324
325/// Choices for [`GraphicsOptions::tone_mapping`].
326///
327#[doc = include_str!("../save/serde-warning.md")]
328#[derive(Clone, Debug, Eq, PartialEq)]
329#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
330#[non_exhaustive]
331pub enum ToneMappingOperator {
332    /// Limit values above the maximum (or below zero) to lie within that range.
333    ///
334    /// This is the trivial tone mapping operation, and most “correct” for
335    /// colors which do lie within the range, but will cause overly bright colors
336    /// to change hue (as the RGB components are clamped independently).
337    Clamp,
338
339    /// TODO: As currently implemented this is an inadequate placeholder which is
340    /// overly dark.
341    Reinhard,
342}
343
344impl ToneMappingOperator {
345    /// Apply this operator to the given high-dynamic-range color value.
346    #[inline]
347    pub fn apply(&self, maximum_intensity: PositiveSign<f32>, input: Rgb) -> Rgb {
348        if !maximum_intensity.is_finite() {
349            // Can't operate without an upper bound.
350            return input;
351        }
352        match self {
353            ToneMappingOperator::Clamp => input.clamp(maximum_intensity),
354            // From <https://64.github.io/tonemapping/>, this will cut brightness
355            // too much, but the better versions require a parameter of max scene brightness,
356            // or more likely for our use case, we'll hook this up to a model of eye
357            // adaptation to average brightness.
358            ToneMappingOperator::Reinhard => {
359                let scale = (1.0 + input.luminance() / maximum_intensity.into_inner()).recip();
360                input * scale
361            }
362        }
363    }
364}
365
366/// “Camera exposure” control: selection of algorithm to control the scaling factor from
367/// scene luminance to displayed luminance. Part of a [`GraphicsOptions`].
368///
369/// Note that the exact interpretation of this value also depends on on the chosen
370/// [`ToneMappingOperator`].
371///
372#[doc = include_str!("../save/serde-warning.md")]
373#[derive(Clone, Eq, PartialEq)]
374#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
375#[non_exhaustive]
376pub enum ExposureOption {
377    /// Constant exposure; light values in the scene are multiplied by this value
378    /// before the tone mapping operator is applied.
379    Fixed(PositiveSign<f32>),
380    /// Exposure adjusts to compensate for the actual brightness of the scene.
381    ///
382    /// Note: If [`GraphicsOptions::lighting_display`] is disabled,
383    /// then this currently will act as `Fixed(1.0)`.
384    Automatic,
385}
386
387impl fmt::Debug for ExposureOption {
388    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389        match self {
390            Self::Fixed(value) => write!(f, "Fixed({})", value.into_inner()),
391            Self::Automatic => write!(f, "Automatic"),
392        }
393    }
394}
395
396impl ExposureOption {
397    pub(crate) fn initial(&self) -> PositiveSign<f32> {
398        match *self {
399            ExposureOption::Fixed(value) => value,
400            ExposureOption::Automatic => PositiveSign::<f32>::ONE,
401        }
402    }
403}
404
405impl Default for ExposureOption {
406    fn default() -> Self {
407        ExposureOption::Fixed(PositiveSign::<f32>::ONE)
408    }
409}
410
411/// How to display light in a [`Space`]; part of a [`GraphicsOptions`].
412///
413#[doc = include_str!("../save/serde-warning.md")]
414///
415/// [`Space`]: crate::space::Space
416#[derive(Clone, Debug, Eq, PartialEq)]
417#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
418#[non_exhaustive]
419pub enum LightingOption {
420    /// No lighting: objects will be displayed with their intrinsically defined surface color,
421    /// as if illuminated by a white light with luminance 1.0 everywhere.
422    None,
423
424    /// Light is taken from the volume immediately above a cube face.
425    /// Edges between cubes are visible.
426    Flat,
427
428    /// Light is interpolated across surfaces, between adjacent cubes.
429    Smooth,
430
431    /// Compute per-pixel rather than per-block illumination.
432    ///
433    /// This option has the most physical accuracy, but may be very slow or unsupported.
434    /// If unsupported, the renderer should substitute [`Smooth`][Self::Smooth].
435    Bounce,
436}
437
438/// How to render transparent objects; part of a [`GraphicsOptions`].
439///
440/// Note: There is not yet a consistent interpretation of alpha between the `Surface`
441/// and `Volumetric` options; this will probably be changed in the future in favor
442/// of the volumetric interpretation.
443///
444#[doc = include_str!("../save/serde-warning.md")]
445#[derive(Clone, Debug, Eq, PartialEq)]
446#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
447#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
448#[non_exhaustive]
449pub enum TransparencyOption {
450    /// Conventional transparent surfaces.
451    Surface,
452    /// Accounts for the thickness of material passed through; colors' alpha values are
453    /// interpreted as the opacity of a unit thickness of the material.
454    Volumetric,
455    /// Alpha above or below the given threshold value will be rounded to fully opaque
456    /// or fully transparent, respectively.
457    Threshold(ZeroOne<f32>),
458}
459
460impl TransparencyOption {
461    /// Replace a color's alpha value according to the requested threshold,
462    /// if any.
463    #[inline]
464    pub fn limit_alpha(&self, color: Rgba) -> Rgba {
465        match *self {
466            Self::Threshold(t) => {
467                if color.alpha() > t {
468                    color.to_rgb().with_alpha_one()
469                } else {
470                    Rgba::TRANSPARENT
471                }
472            }
473            _ => color,
474        }
475    }
476
477    #[inline]
478    #[doc(hidden)] // TODO: make public/documented?
479    pub fn will_output_alpha(&self) -> bool {
480        !matches!(self, Self::Threshold(_))
481    }
482}
483
484/// Choices for [`GraphicsOptions::antialiasing`].
485///
486#[doc = include_str!("../save/serde-warning.md")]
487#[derive(Clone, Debug, Default, Eq, PartialEq)]
488#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
489#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
490#[non_exhaustive]
491pub enum AntialiasingOption {
492    /// Do not apply antialiasing. Every pixel of the rendered image will be the exact
493    /// color of some part of the world, rather than a combination of adjacent parts.
494    #[default]
495    None,
496    /// If [multisample anti-aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing)
497    /// or similar functionality is available, allowing relatively cheap antialiasing,
498    /// then enable it.
499    IfCheap,
500    /// Always perform antialiasing, even if it is expensive.
501    Always,
502}
503
504impl AntialiasingOption {
505    // TODO: These functions allow dependents to decide what to do even though the
506    // enum is non_exhaustive. Figure out if it really should be non_exhaustive or
507    // if we should make these really public.
508
509    /// True if GPU renderers should enable multisampling
510    #[doc(hidden)]
511    #[mutants::skip] // a test would only reiterate the code
512    pub fn is_msaa(&self) -> bool {
513        match self {
514            Self::None => false,
515            Self::IfCheap => true,
516            Self::Always => true,
517        }
518    }
519
520    /// True if renderers for which antialiasing is expensive should do it anyway.
521    #[doc(hidden)]
522    #[mutants::skip] // a test would only reiterate the code
523    pub fn is_strongly_enabled(&self) -> bool {
524        match self {
525            Self::None => false,
526            Self::IfCheap => false,
527            Self::Always => true,
528        }
529    }
530}
531
532/// Kludge: `serde_json`, which we generally use, serializes infinity as null, but then
533/// does not accept it in deserialization. Work around this by adding an `Option`.
534/// Arguably this should be done in `PositiveSign` itself, but I don’t want to do this to more
535/// general types.
536#[cfg(feature = "save")]
537mod serialize_infinity_as_none {
538    use crate::math::PositiveSign;
539    use serde::{Deserialize, Serialize};
540
541    #[allow(clippy::trivially_copy_pass_by_ref)]
542    pub(super) fn serialize<S: serde::Serializer>(
543        &value: &PositiveSign<f32>,
544        serializer: S,
545    ) -> Result<S::Ok, S::Error> {
546        let value: Option<PositiveSign<f32>> = value.is_finite().then_some(value);
547        value.serialize(serializer)
548    }
549
550    pub(super) fn deserialize<'de, D: serde::Deserializer<'de>>(
551        deserializer: D,
552    ) -> Result<PositiveSign<f32>, D::Error> {
553        match Option::<PositiveSign<f32>>::deserialize(deserializer)? {
554            Some(value) => Ok(value),
555            None => Ok(PositiveSign::<f32>::INFINITY),
556        }
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563    use crate::math::{OpacityCategory, rgba_const, zo32};
564    use pretty_assertions::assert_eq;
565
566    #[test]
567    fn debug() {
568        let options = GraphicsOptions::default();
569        assert_eq!(
570            format!("{options:#?}"),
571            indoc::indoc! {
572                r"GraphicsOptions {
573                    render_method: Preferred,
574                    fog: Abrupt,
575                    fov_y: 90.0,
576                    tone_mapping: Clamp,
577                    maximum_intensity: inf,
578                    exposure: Fixed(1),
579                    bloom_intensity: 0.125,
580                    view_distance: 200.0,
581                    lighting_display: Smooth,
582                    transparency: Volumetric,
583                    show_ui: true,
584                    antialiasing: None,
585                    debug_info_text: true,
586                    debug_info_text_contents: ShowStatus(
587                        WORLD | STEP | RENDER | CURSOR,
588                    ),
589                    debug_behaviors: false,
590                    debug_chunk_boxes: false,
591                    debug_collision_boxes: false,
592                    debug_light_rays_at_cursor: false,
593                    debug_pixel_cost: false,
594                    debug_reduce_view_frustum: false,
595                }"
596            }
597        );
598    }
599
600    #[test]
601    fn default_is_clean() {
602        assert_eq!(
603            GraphicsOptions::default(),
604            GraphicsOptions::default().repair()
605        );
606    }
607
608    #[test]
609    fn unaltered_colors_is_clean() {
610        assert_eq!(
611            GraphicsOptions::UNALTERED_COLORS,
612            GraphicsOptions::UNALTERED_COLORS.repair()
613        );
614    }
615
616    #[test]
617    fn unaltered_colors_differs_from_default_only_as_necessary() {
618        // Note that this assertion will pass whether or not the specified values are
619        // *also* in GraphicsOptions::default(). That's what we want.
620        assert_eq!(
621            GraphicsOptions::UNALTERED_COLORS,
622            GraphicsOptions {
623                fog: FogOption::None,
624                tone_mapping: ToneMappingOperator::Clamp,
625                exposure: ExposureOption::Fixed(PositiveSign::<f32>::ONE),
626                bloom_intensity: zo32(0.),
627                lighting_display: LightingOption::None,
628                antialiasing: AntialiasingOption::None,
629                ..GraphicsOptions::default()
630            }
631        )
632    }
633
634    #[test]
635    fn will_output_alpha() {
636        for transparency in &[
637            TransparencyOption::Surface,
638            TransparencyOption::Volumetric,
639            TransparencyOption::Threshold(zo32(0.5)),
640        ] {
641            assert_eq!(
642                transparency.will_output_alpha(),
643                transparency.limit_alpha(rgba_const!(1.0, 1.0, 1.0, 0.25)).opacity_category()
644                    == OpacityCategory::Partial
645            );
646        }
647    }
648}