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}