From 7cff74a48fe3118c4fa344919c0fc163c5074048 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Sun, 20 Oct 2024 19:44:46 -0700 Subject: [PATCH] math: Make `Rgba` use `ZeroOne` for alpha. This means that color blending code need not consider alpha being less than 0 or greater than 1. Caveat: serialization has to use a tuple now. I expect that eventually, this will be addressed by also 0-1 restricting all the serialized RGBA colors which represent reflectance. --- all-is-cubes-base/src/math/color.rs | 99 ++++++++++++++----- all-is-cubes-base/src/math/serde_impls.rs | 21 ++++ .../src/city/exhibits/move_modifier.rs | 2 +- .../src/city/exhibits/prelude.rs | 5 +- .../src/city/exhibits/transparency.rs | 2 +- all-is-cubes-content/src/clouds.rs | 4 +- all-is-cubes-content/src/landscape.rs | 4 +- all-is-cubes-gpu/src/in_wgpu/space.rs | 6 +- .../src/dynamic/chunked_mesh/tests.rs | 4 +- all-is-cubes-mesh/src/tests.rs | 4 +- all-is-cubes-port/src/stl.rs | 4 +- all-is-cubes-ui/src/apps/input.rs | 4 +- all-is-cubes-ui/src/ui_content/options.rs | 4 +- all-is-cubes/src/block/eval/derived.rs | 6 +- all-is-cubes/src/block/modifier/composite.rs | 38 +++---- all-is-cubes/src/block/modifier/move.rs | 30 +++--- all-is-cubes/src/block/tests.rs | 14 +-- all-is-cubes/src/camera/graphics_options.rs | 8 +- all-is-cubes/src/raytracer.rs | 4 +- all-is-cubes/src/raytracer/accum.rs | 4 +- all-is-cubes/src/save/schema.rs | 11 ++- test-renderers/src/test_cases.rs | 4 +- 22 files changed, 182 insertions(+), 100 deletions(-) diff --git a/all-is-cubes-base/src/math/color.rs b/all-is-cubes-base/src/math/color.rs index 3a55bd280..de2d8bd38 100644 --- a/all-is-cubes-base/src/math/color.rs +++ b/all-is-cubes-base/src/math/color.rs @@ -12,7 +12,7 @@ use ordered_float::NotNan; #[allow(unused_imports)] use num_traits::float::Float as _; -use crate::math::{NotPositiveSign, PositiveSign}; +use crate::math::{NotPositiveSign, PositiveSign, ZeroOne}; /// Allows writing a constant [`Rgb`] color value, provided that its components are float /// literals. @@ -43,7 +43,7 @@ macro_rules! rgba_const { $crate::math::PositiveSign::::new_strict($r), $crate::math::PositiveSign::::new_strict($g), $crate::math::PositiveSign::::new_strict($b), - $crate::math::PositiveSign::::new_strict($a), + $crate::math::ZeroOne::::new_strict($a), ) } }; @@ -70,11 +70,12 @@ pub struct Rgb(Vector3D, Intensity>); /// * The alpha is not premultiplied. /// * Alpha values less than zero and greater than one will usually be treated equivalently to /// zero and one, respectively, but are preserved rather than clipped. -/// (TODO: Constrain it to the 0-1 range to reduce hazards.) #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Rgba { + // TODO: Split `Rgba` into two types: one premultiplied with unbounded RGB, and one with + // RGB restricted to 0-1 for reflectance-like use cases. rgb: Rgb, - alpha: PositiveSign, + alpha: ZeroOne, } /// Unit-of-measure type for vectors that contain color channels. @@ -151,13 +152,13 @@ impl Rgb { /// Adds an alpha component to produce an [Rgba] color. #[inline] - pub const fn with_alpha(self, alpha: PositiveSign) -> Rgba { + pub const fn with_alpha(self, alpha: ZeroOne) -> Rgba { Rgba { rgb: self, alpha } } /// Adds an alpha component of `1.0` (fully opaque) to produce an [Rgba] color. #[inline] pub const fn with_alpha_one(self) -> Rgba { - self.with_alpha(PS1) + self.with_alpha(ZeroOne::ONE) } /// Adds an alpha component of `1.0` (fully opaque) to produce an [Rgba] color. @@ -166,7 +167,7 @@ impl Rgb { #[inline] #[must_use] pub const fn with_alpha_one_if_has_no_alpha(self) -> Rgba { - self.with_alpha(PS1) + self.with_alpha(ZeroOne::ONE) } /// Returns the red color component. Values are linear (gamma = 1). @@ -246,7 +247,7 @@ impl Rgb { impl Rgba { /// Transparent black (all components zero); identical to /// `Rgba::new(0.0, 0.0, 0.0, 0.0)` except for being a constant. - pub const TRANSPARENT: Rgba = Rgb::ZERO.with_alpha(PS0); + pub const TRANSPARENT: Rgba = Rgb::ZERO.with_alpha(ZeroOne::ZERO); /// Black; identical to `Rgba::new(0.0, 0.0, 0.0, 1.0)` except for being a constant. pub const BLACK: Rgba = Rgb::ZERO.with_alpha_one(); /// White; identical to `Rgba::new(1.0, 1.0, 1.0, 1.0)` except for being a constant. @@ -257,7 +258,7 @@ impl Rgba { #[inline] #[track_caller] pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self { - Rgb::new(r, g, b).with_alpha(PositiveSign::::new_strict(a)) + Rgb::new(r, g, b).with_alpha(ZeroOne::::new_strict(a)) } /// Constructs a color from components that have already been checked for not being @@ -270,7 +271,7 @@ impl Rgba { r: PositiveSign, g: PositiveSign, b: PositiveSign, - alpha: PositiveSign, + alpha: ZeroOne, ) -> Self { Self { rgb: Rgb::new_ps(r, g, b), @@ -312,11 +313,9 @@ impl Rgba { } /// Returns the alpha component. /// - /// Alpha is not premultiplied. Alpha values less than zero and greater than one are - /// allowed and may be returned by this method, but alpha test methods will treat - // them equivalently to zero and one. + /// Note that the RGB components are not premultiplied by alpha. #[inline] - pub const fn alpha(self) -> PositiveSign { + pub const fn alpha(self) -> ZeroOne { self.alpha } @@ -324,13 +323,13 @@ impl Rgba { /// zero or less. #[inline] pub fn fully_transparent(self) -> bool { - self.alpha() <= PS0 + self.alpha().is_zero() } /// Returns whether this color is fully opaque, or has an alpha component of /// one or greater. #[inline] pub fn fully_opaque(self) -> bool { - self.alpha() >= PS1 + self.alpha().is_one() } /// Returns the [`OpacityCategory`] which this color's alpha fits into. /// This returns the same information as [`Rgba::fully_transparent`] combined with @@ -412,7 +411,7 @@ impl Rgba { pub fn clamp(self) -> Self { Self { rgb: self.rgb.clamp(), - alpha: self.alpha.min(PS1), + alpha: self.alpha, } } @@ -428,7 +427,7 @@ impl Rgba { pub fn reflect(self, illumination: Rgb) -> Rgb { // TODO: do this math without any NaN checks or negative/amplified values. // by introducing a dedicated RgbaReflectance type with constrained components? - self.to_rgb() * illumination * self.alpha + self.to_rgb() * illumination * PositiveSign::from(self.alpha) } } @@ -445,9 +444,15 @@ impl From<[PositiveSign; 3]> for Rgb { Self(value.into()) } } -impl From<[PositiveSign; 4]> for Rgba { +impl From<[ZeroOne; 3]> for Rgb { #[inline] - fn from(value: [PositiveSign; 4]) -> Self { + fn from(value: [ZeroOne; 3]) -> Self { + Self::from(value.map(PositiveSign::from)) + } +} +impl From<[ZeroOne; 4]> for Rgba { + #[inline] + fn from(value: [ZeroOne; 4]) -> Self { let [r, g, b, alpha] = value; Self { rgb: Rgb::from([r, g, b]), @@ -455,6 +460,30 @@ impl From<[PositiveSign; 4]> for Rgba { } } } +impl + From<( + PositiveSign, + PositiveSign, + PositiveSign, + ZeroOne, + )> for Rgba +{ + #[inline] + fn from( + value: ( + PositiveSign, + PositiveSign, + PositiveSign, + ZeroOne, + ), + ) -> Self { + let (r, g, b, alpha) = value; + Self { + rgb: Rgb::from([r, g, b]), + alpha, + } + } +} impl From for Vector3D { #[inline] @@ -473,7 +502,21 @@ impl From for [PositiveSign; 4] { #[inline] fn from(value: Rgba) -> Self { let [r, g, b]: [PositiveSign; 3] = value.rgb.into(); - [r, g, b, value.alpha] + [r, g, b, value.alpha.into()] + } +} +impl From + for ( + PositiveSign, + PositiveSign, + PositiveSign, + ZeroOne, + ) +{ + #[inline] + fn from(value: Rgba) -> Self { + let [r, g, b]: [PositiveSign; 3] = value.rgb.into(); + (r, g, b, value.alpha) } } @@ -547,6 +590,14 @@ impl Mul> for Rgb { Self(self.0 * scalar) } } +impl Mul> for Rgb { + type Output = Self; + /// Multiplies this color value by a scalar. + #[inline] + fn mul(self, scalar: ZeroOne) -> Self { + Self(self.0 * PositiveSign::from(scalar)) + } +} /// Multiplies this color value by a scalar. /// /// Panics if the scalar is NaN. Returns zero if the scalar is negative. @@ -699,9 +750,9 @@ fn component_from_linear8_arithmetic(c: u8) -> f32 { } #[inline] -const fn component_from_linear8_const(c: u8) -> PositiveSign { - // Safety: the table may be inspected to contain no NaNs or negatives. - unsafe { PositiveSign::new_unchecked(CONST_LINEAR_LOOKUP_TABLE[c as usize]) } +const fn component_from_linear8_const(c: u8) -> ZeroOne { + // Safety: the table may be inspected to contain no NaNs or out-of-bounds values. + unsafe { ZeroOne::new_unchecked(CONST_LINEAR_LOOKUP_TABLE[c as usize]) } } /// Implements sRGB decoding using the standard arithmetic. diff --git a/all-is-cubes-base/src/math/serde_impls.rs b/all-is-cubes-base/src/math/serde_impls.rs index 82b2f833d..511510519 100644 --- a/all-is-cubes-base/src/math/serde_impls.rs +++ b/all-is-cubes-base/src/math/serde_impls.rs @@ -106,3 +106,24 @@ where Self::try_from(T::deserialize(deserializer)?).map_err(serde::de::Error::custom) } } + +impl Serialize for math::ZeroOne { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_ref().serialize(serializer) + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for math::ZeroOne +where + Self: TryFrom, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Self::try_from(T::deserialize(deserializer)?).map_err(serde::de::Error::custom) + } +} diff --git a/all-is-cubes-content/src/city/exhibits/move_modifier.rs b/all-is-cubes-content/src/city/exhibits/move_modifier.rs index 34b41445f..661a9d310 100644 --- a/all-is-cubes-content/src/city/exhibits/move_modifier.rs +++ b/all-is-cubes-content/src/city/exhibits/move_modifier.rs @@ -56,7 +56,7 @@ fn PROJECTILE(ctx: Context<'_>) { // This will require getting `Move` tick actions to cooperate with `Composite`. let launcher = Block::builder() .display_name(literal!("Launcher")) - .color(Rgb::UNIFORM_LUMINANCE_RED.with_alpha(ps32(1.0))) + .color(Rgb::UNIFORM_LUMINANCE_RED.with_alpha(zo32(1.0))) .animation_hint(block::AnimationHint::replacement( block::AnimationChange::Shape, )) diff --git a/all-is-cubes-content/src/city/exhibits/prelude.rs b/all-is-cubes-content/src/city/exhibits/prelude.rs index b7043f755..1441560ab 100644 --- a/all-is-cubes-content/src/city/exhibits/prelude.rs +++ b/all-is-cubes-content/src/city/exhibits/prelude.rs @@ -28,8 +28,9 @@ pub(super) use all_is_cubes::euclid::{ pub(super) use all_is_cubes::linking::{BlockProvider, InGenError}; pub(super) use all_is_cubes::listen::ListenableSource; pub(super) use all_is_cubes::math::{ - ps32, rgb_const, rgba_const, Cube, Face6, FaceMap, FreeCoordinate, GridAab, GridCoordinate, - GridPoint, GridRotation, GridSize, GridVector, Gridgid, PositiveSign, Rgb, Rgba, + ps32, rgb_const, rgba_const, zo32, Cube, Face6, FaceMap, FreeCoordinate, GridAab, + GridCoordinate, GridPoint, GridRotation, GridSize, GridVector, Gridgid, PositiveSign, Rgb, + Rgba, }; pub(super) use all_is_cubes::op::Operation; pub(super) use all_is_cubes::space::{self, Space, SpacePhysics, SpaceTransaction}; diff --git a/all-is-cubes-content/src/city/exhibits/transparency.rs b/all-is-cubes-content/src/city/exhibits/transparency.rs index fd78e99d6..e27197a4d 100644 --- a/all-is-cubes-content/src/city/exhibits/transparency.rs +++ b/all-is-cubes-content/src/city/exhibits/transparency.rs @@ -17,7 +17,7 @@ fn TRANSPARENCY_LARGE(_: Context<'_>) { Rgb::new(0.5, 0.5, 1.0), Rgb::new(0.9, 0.9, 0.9), ]; - let alphas = [0.25, 0.5, 0.75, 0.95].map(ps32); + let alphas = [0.25, 0.5, 0.75, 0.95].map(zo32); for (rot, color) in GridRotation::CLOCKWISE.iterate().zip(&colors) { let windowpane = GridAab::from_lower_upper([-1, 0, 3], [2, alphas.len() as GridCoordinate, 4]); diff --git a/all-is-cubes-content/src/clouds.rs b/all-is-cubes-content/src/clouds.rs index 712da1a08..2fc9bd1ed 100644 --- a/all-is-cubes-content/src/clouds.rs +++ b/all-is-cubes-content/src/clouds.rs @@ -1,7 +1,7 @@ //! Cloud generation. use all_is_cubes::block::{Block, BlockCollision, AIR}; -use all_is_cubes::math::{ps32, GridAab, GridCoordinate, GridPoint, Rgb}; +use all_is_cubes::math::{zo32, GridAab, GridCoordinate, GridPoint, Rgb}; use all_is_cubes::space::{SetCubeError, Space}; use crate::alg::NoiseFnExt as _; @@ -24,7 +24,7 @@ pub fn clouds(region: GridAab, space: &mut Space, density: f32) -> Result<(), Se fn cloud_block(alpha: f32) -> Block { Block::builder() .display_name("Cloud") - .color(Rgb::ONE.with_alpha(ps32(alpha))) + .color(Rgb::ONE.with_alpha(zo32(alpha))) .collision(if alpha >= 1.0 { BlockCollision::Hard } else { diff --git a/all-is-cubes-content/src/landscape.rs b/all-is-cubes-content/src/landscape.rs index 4351ba0ea..e3761c4ad 100644 --- a/all-is-cubes-content/src/landscape.rs +++ b/all-is-cubes-content/src/landscape.rs @@ -17,7 +17,7 @@ use all_is_cubes::block::{ AIR, }; use all_is_cubes::linking::{BlockModule, BlockProvider, DefaultProvision, GenError, InGenError}; -use all_is_cubes::math::{ps32, Cube, FreeCoordinate, GridAab, GridCoordinate, GridVector, Rgb}; +use all_is_cubes::math::{zo32, Cube, FreeCoordinate, GridAab, GridCoordinate, GridVector, Rgb}; use all_is_cubes::space::Sky; use all_is_cubes::space::{SetCubeError, Space}; use all_is_cubes::universe::UniverseTransaction; @@ -109,7 +109,7 @@ impl DefaultProvision for LandscapeBlocks { fn blades() -> Block { Block::builder() .display_name("Grass Blades") - .color(palette::GRASS.with_alpha(ps32(0.1))) + .color(palette::GRASS.with_alpha(zo32(0.1))) .collision(BlockCollision::None) .build() } diff --git a/all-is-cubes-gpu/src/in_wgpu/space.rs b/all-is-cubes-gpu/src/in_wgpu/space.rs index 48d18fbdd..5f2f977ce 100644 --- a/all-is-cubes-gpu/src/in_wgpu/space.rs +++ b/all-is-cubes-gpu/src/in_wgpu/space.rs @@ -6,14 +6,13 @@ use std::sync::{atomic, mpsc, Arc, Mutex, Weak}; use std::time::Duration; use itertools::Itertools as _; -use num_traits::ConstZero as _; use all_is_cubes::chunking::ChunkPos; use all_is_cubes::content::palette; use all_is_cubes::listen::{Listen as _, Listener}; use all_is_cubes::math::{ rgba_const, Cube, Face6, FreeCoordinate, FreePoint, GridAab, GridCoordinate, GridPoint, - GridSize, GridVector, PositiveSign, Rgb, Wireframe as _, + GridSize, GridVector, Rgb, Wireframe as _, ZeroOne, }; use all_is_cubes::raycast::Ray; #[cfg(feature = "rerun")] @@ -981,8 +980,7 @@ impl ParticleSet { crate::wireframe_vertices::( &mut tmp, Rgb::ONE.with_alpha( - PositiveSign::::try_from(0.9f32.powf(self.age as f32)) - .unwrap_or(PositiveSign::ZERO), + ZeroOne::::try_from(0.9f32.powf(self.age as f32)).unwrap_or(ZeroOne::ZERO), ), &self.fluff.position.aab().expand(0.004 * (self.age as f64)), ); diff --git a/all-is-cubes-mesh/src/dynamic/chunked_mesh/tests.rs b/all-is-cubes-mesh/src/dynamic/chunked_mesh/tests.rs index 587df6dc4..2d45d374f 100644 --- a/all-is-cubes-mesh/src/dynamic/chunked_mesh/tests.rs +++ b/all-is-cubes-mesh/src/dynamic/chunked_mesh/tests.rs @@ -8,7 +8,7 @@ use all_is_cubes::chunking::ChunkPos; use all_is_cubes::color_block; use all_is_cubes::content::make_some_blocks; use all_is_cubes::listen::Listener as _; -use all_is_cubes::math::{ps32, GridPoint, NotNan}; +use all_is_cubes::math::{zo32, GridPoint, NotNan}; use all_is_cubes::math::{Cube, FreePoint, GridAab, GridCoordinate}; use all_is_cubes::space::{BlockIndex, Space, SpaceChange, SpaceTransaction}; use all_is_cubes::time; @@ -284,7 +284,7 @@ fn graphics_options_change() { assert_eq!(vertices, Some(24)); // Change options so that the mesh should disappear - options.transparency = TransparencyOption::Threshold(ps32(0.5)); + options.transparency = TransparencyOption::Threshold(zo32(0.5)); tester.camera.set_options(options.clone()); vertices = None; diff --git a/all-is-cubes-mesh/src/tests.rs b/all-is-cubes-mesh/src/tests.rs index cfb44d219..af026ec33 100644 --- a/all-is-cubes-mesh/src/tests.rs +++ b/all-is-cubes-mesh/src/tests.rs @@ -8,7 +8,7 @@ use all_is_cubes::block::{self, Block, Resolution::*, AIR}; use all_is_cubes::color_block; use all_is_cubes::content::{make_some_blocks, make_some_voxel_blocks}; use all_is_cubes::euclid::{point3, Point3D, Vector3D}; -use all_is_cubes::math::{ps32, Cube, Rgb}; +use all_is_cubes::math::{zo32, Cube, Rgb}; use all_is_cubes::math::{ Face6::{self, *}, FaceMap, FreeCoordinate, GridAab, GridRotation, Rgba, @@ -69,7 +69,7 @@ fn test_block_mesh_threshold(block: Block) -> BlockMesh { &block.evaluate().unwrap(), &Allocator::new(), &MeshOptions { - transparency: TransparencyOption::Threshold(ps32(0.5)), + transparency: TransparencyOption::Threshold(zo32(0.5)), ..MeshOptions::dont_care_for_test() }, ) diff --git a/all-is-cubes-port/src/stl.rs b/all-is-cubes-port/src/stl.rs index 409109583..dff639457 100644 --- a/all-is-cubes-port/src/stl.rs +++ b/all-is-cubes-port/src/stl.rs @@ -7,7 +7,7 @@ use stl_io::Triangle; use all_is_cubes::block; use all_is_cubes::euclid::Vector3D; -use all_is_cubes::math::{ps32, Cube, FreeCoordinate}; +use all_is_cubes::math::{zo32, Cube, FreeCoordinate}; use all_is_cubes::space::Space; use all_is_cubes::universe::PartialUniverse; use all_is_cubes::util::YieldProgress; @@ -81,7 +81,7 @@ pub(crate) fn block_to_stl_triangles(block: &block::EvaluatedBlock) -> Vec mesh::MeshOptions { let mut g = GraphicsOptions::default(); - g.transparency = all_is_cubes_render::camera::TransparencyOption::Threshold(ps32(0.01)); + g.transparency = all_is_cubes_render::camera::TransparencyOption::Threshold(zo32(0.01)); mesh::MeshOptions::new(&g) } diff --git a/all-is-cubes-ui/src/apps/input.rs b/all-is-cubes-ui/src/apps/input.rs index 21b59602d..c4207b0fd 100644 --- a/all-is-cubes-ui/src/apps/input.rs +++ b/all-is-cubes-ui/src/apps/input.rs @@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet}; use all_is_cubes::character::Character; use all_is_cubes::euclid::{Point2D, Vector2D}; use all_is_cubes::listen::{ListenableCell, ListenableSource}; -use all_is_cubes::math::{ps32, FreeCoordinate, FreeVector}; +use all_is_cubes::math::{zo32, FreeCoordinate, FreeVector}; use all_is_cubes::time::Tick; use all_is_cubes::universe::{Handle, Universe}; use all_is_cubes_render::camera::{ @@ -374,7 +374,7 @@ impl InputProcessor { options.transparency = match options.transparency { TransparencyOption::Surface => TransparencyOption::Volumetric, TransparencyOption::Volumetric => { - TransparencyOption::Threshold(ps32(0.5)) + TransparencyOption::Threshold(zo32(0.5)) } TransparencyOption::Threshold(_) => TransparencyOption::Surface, _ => TransparencyOption::Surface, // TODO: either stop doing cycle-commands or put it on the enum so it can be exhaustive diff --git a/all-is-cubes-ui/src/ui_content/options.rs b/all-is-cubes-ui/src/ui_content/options.rs index b5eeb68a2..45b23890d 100644 --- a/all-is-cubes-ui/src/ui_content/options.rs +++ b/all-is-cubes-ui/src/ui_content/options.rs @@ -4,7 +4,7 @@ use alloc::vec::Vec; use core::fmt; use all_is_cubes::arcstr::{self, literal}; -use all_is_cubes::math::{ps32, Face6}; +use all_is_cubes::math::{zo32, Face6}; use all_is_cubes_render::camera::{self, AntialiasingOption, GraphicsOptions}; use crate::apps::ControlMessage; @@ -71,7 +71,7 @@ pub(crate) fn graphics_options_widgets( [ camera::TransparencyOption::Surface, camera::TransparencyOption::Volumetric, - camera::TransparencyOption::Threshold(ps32(0.5)), + camera::TransparencyOption::Threshold(zo32(0.5)), ], ), ]); diff --git a/all-is-cubes/src/block/eval/derived.rs b/all-is-cubes/src/block/eval/derived.rs index 793de1380..89a3cab08 100644 --- a/all-is-cubes/src/block/eval/derived.rs +++ b/all-is-cubes/src/block/eval/derived.rs @@ -1,8 +1,8 @@ use alloc::sync::Arc; use core::ops; -use itertools::Itertools; use euclid::Vector3D; +use itertools::Itertools; /// Acts as polyfill for float methods #[cfg(not(feature = "std"))] @@ -14,7 +14,7 @@ use crate::block::{ Resolution::{self, R1}, }; use crate::math::{ - Cube, Face6, FaceMap, GridAab, Intensity, OpacityCategory, PositiveSign, Rgb, Rgba, Vol, + Cube, Face6, FaceMap, GridAab, Intensity, OpacityCategory, Rgb, Rgba, Vol, ZeroOne, }; use crate::raytracer; @@ -235,7 +235,7 @@ impl VoxSum { // Note that by dividing the alpha by the full surface area, not the count, // we handle the case where the voxel data doesn't cover the full block and // uncounted pixels should act as if they are transparent. - PositiveSign::::new_clamped(self.alpha_sum / (surface_area)), + ZeroOne::::new_clamped(self.alpha_sum / (surface_area)), ) } } diff --git a/all-is-cubes/src/block/modifier/composite.rs b/all-is-cubes/src/block/modifier/composite.rs index 96d196947..01c1960ce 100644 --- a/all-is-cubes/src/block/modifier/composite.rs +++ b/all-is-cubes/src/block/modifier/composite.rs @@ -6,12 +6,12 @@ use alloc::vec; use core::mem; -use num_traits::Zero; - use crate::block::{ self, Block, BlockCollision, Evoxel, Evoxels, MinEval, Modifier, Resolution::R1, AIR, }; -use crate::math::{Cube, GridAab, GridCoordinate, GridRotation, GridSize, PositiveSign, Rgb, Vol}; +use crate::math::{ + Cube, GridAab, GridCoordinate, GridRotation, GridSize, PositiveSign, Rgb, Vol, ZeroOne, +}; use crate::op::Operation; use crate::universe; @@ -444,24 +444,27 @@ impl CompositeOperator { fn alpha_blend( self, source: Rgb, - sa: PositiveSign, + sa: ZeroOne, destination: Rgb, - da: PositiveSign, - ) -> (Rgb, PositiveSign) { + da: ZeroOne, + ) -> (Rgb, ZeroOne) { match self { Self::Over => { // TODO: Surely this is not the only place we have implemented rgba blending? // Note that this math would be simpler if we used premultiplied alpha. - let sa_complement = PositiveSign::::new_clamped(1. - sa.into_inner()); + let sa_complement = sa.complement(); let rgb = source * sa + destination * sa_complement; - (rgb, sa + sa_complement * da) + ( + rgb, + // TODO: express this alpha calculation in a correct-by-construction way instead + ZeroOne::::new_clamped( + sa.into_inner() + (sa_complement * da).into_inner(), + ), + ) } Self::In => (source, sa * da), - Self::Out => { - let da_complement = PositiveSign::::new_clamped(1. - da.into_inner()); - (source, sa * da_complement) - } + Self::Out => (source, sa * da.complement()), Self::Atop => { let sa_complement = PositiveSign::::new_clamped(1. - sa.into_inner()); @@ -683,7 +686,7 @@ mod tests { use super::*; use crate::block::{EvKey, EvaluatedBlock, Resolution::*}; use crate::content::{make_slab, make_some_blocks}; - use crate::math::{ps32, Rgba}; + use crate::math::{zo32, Rgba}; use crate::space::Space; use crate::time; use crate::universe::Universe; @@ -720,7 +723,7 @@ mod tests { Evoxel { // color doesn't matter, except that at zero alpha it should be the canonical zero // for convenience of testing. (TODO: maybe `Rgba` should enforce that or be premultiplied.) - color: Rgb::ZERO.with_alpha(ps32(alpha)), + color: Rgb::ZERO.with_alpha(zo32(alpha)), emission, selectable: true, collision: Hard, @@ -853,10 +856,11 @@ mod tests { #[test] fn over_silly_floats() { - // We just want to see this does not panic on NaN. + // We just want to see this does not panic on math errors. + // TODO: this test should eventually become obsolete by using constrained numeric types. Over.blend_evoxel( - evcolor(Rgba::new(2e25, 2e25, 2e25, 2e25)), - evcolor(Rgba::new(2e25, 2e25, 2e25, 2e25)), + evcolor(Rgba::new(2e25, 2e25, 2e25, 1.0)), + evcolor(Rgba::new(2e25, 2e25, 2e25, 1.0)), ); } diff --git a/all-is-cubes/src/block/modifier/move.rs b/all-is-cubes/src/block/modifier/move.rs index 0ec4e8490..fccbdb13f 100644 --- a/all-is-cubes/src/block/modifier/move.rs +++ b/all-is-cubes/src/block/modifier/move.rs @@ -231,7 +231,7 @@ mod tests { use super::*; use crate::block::{Composite, EvaluatedBlock, Resolution::*, VoxelOpacityMask}; use crate::content::make_some_blocks; - use crate::math::{ps32, rgba_const, FaceMap, GridPoint, OpacityCategory, Rgb, Rgba}; + use crate::math::{rgba_const, zo32, FaceMap, GridPoint, OpacityCategory, Rgb, Rgba}; use crate::space::Space; use crate::universe::Universe; use pretty_assertions::assert_eq; @@ -265,14 +265,14 @@ mod tests { recursion: 0 }, derived: block::Derived { - color: color.to_rgb().with_alpha(ps32(2. / 3.)), + color: color.to_rgb().with_alpha(zo32(2. / 3.)), face_colors: FaceMap { - nx: color.to_rgb().with_alpha(ps32(0.5)), - ny: color.to_rgb().with_alpha(ps32(1.0)), - nz: color.to_rgb().with_alpha(ps32(0.5)), - px: color.to_rgb().with_alpha(ps32(0.5)), - py: color.to_rgb().with_alpha(ps32(1.0)), - pz: color.to_rgb().with_alpha(ps32(0.5)), + nx: color.to_rgb().with_alpha(zo32(0.5)), + ny: color.to_rgb().with_alpha(zo32(1.0)), + nz: color.to_rgb().with_alpha(zo32(0.5)), + px: color.to_rgb().with_alpha(zo32(0.5)), + py: color.to_rgb().with_alpha(zo32(1.0)), + pz: color.to_rgb().with_alpha(zo32(0.5)), }, light_emission: Rgb::ZERO, opaque: FaceMap::splat(false).with(Face6::PY, true), @@ -322,14 +322,14 @@ mod tests { Vol::repeat(expected_bounds, Evoxel::from_block(&ev_original)) ), derived: block::Derived { - color: color.to_rgb().with_alpha(ps32(2. / 3.)), + color: color.to_rgb().with_alpha(zo32(2. / 3.)), face_colors: FaceMap { - nx: color.to_rgb().with_alpha(ps32(0.5)), - ny: color.to_rgb().with_alpha(ps32(1.0)), - nz: color.to_rgb().with_alpha(ps32(0.5)), - px: color.to_rgb().with_alpha(ps32(0.5)), - py: color.to_rgb().with_alpha(ps32(1.0)), - pz: color.to_rgb().with_alpha(ps32(0.5)), + nx: color.to_rgb().with_alpha(zo32(0.5)), + ny: color.to_rgb().with_alpha(zo32(1.0)), + nz: color.to_rgb().with_alpha(zo32(0.5)), + px: color.to_rgb().with_alpha(zo32(0.5)), + py: color.to_rgb().with_alpha(zo32(1.0)), + pz: color.to_rgb().with_alpha(zo32(0.5)), }, light_emission: Rgb::ZERO, opaque: FaceMap::splat(false).with(Face6::PY, true), diff --git a/all-is-cubes/src/block/tests.rs b/all-is-cubes/src/block/tests.rs index ccff03dc4..c951baf69 100644 --- a/all-is-cubes/src/block/tests.rs +++ b/all-is-cubes/src/block/tests.rs @@ -16,7 +16,7 @@ use crate::block::{ use crate::content::make_some_blocks; use crate::listen::{self, NullListener, Sink}; use crate::math::{ - ps32, Cube, Face6, FaceMap, GridAab, GridPoint, GridRotation, GridVector, Intensity, + zo32, Cube, Face6, FaceMap, GridAab, GridPoint, GridRotation, GridVector, Intensity, OpacityCategory, Rgb, Rgba, Vol, }; use crate::space::{Space, SpaceTransaction}; @@ -315,9 +315,9 @@ mod eval { let block = Block::builder() .voxels_fn(resolution, |point| { Block::from(voxel_color.with_alpha(if point.x == 0 && point.z == 0 { - ps32(alpha) + zo32(alpha) } else { - ps32(1.0) + zo32(1.0) })) }) .unwrap() @@ -329,11 +329,11 @@ mod eval { // the light paths with opaque surfaces. assert_eq!( e.color(), - voxel_color.with_alpha(ps32(1.0 - (alpha / (f32::from(resolution).powi(2) * 3.0)))) + voxel_color.with_alpha(zo32(1.0 - (alpha / (f32::from(resolution).powi(2) * 3.0)))) ); // This is the sum of the transparency of one voxel on one of the six faces let one_face_transparency = - voxel_color.with_alpha(ps32(1.0 - (alpha / f32::from(resolution).powi(2)))); + voxel_color.with_alpha(zo32(1.0 - (alpha / f32::from(resolution).powi(2)))); assert_eq!( e.face_colors(), FaceMap { @@ -369,7 +369,7 @@ mod eval { let mut universe = Universe::new(); let c1 = Rgb::new(1.0, 0.0, 0.0); let c2 = Rgb::new(0.0, 1.0, 0.0); - let colors = [c1.with_alpha_one(), c2.with_alpha(ps32(0.5))]; + let colors = [c1.with_alpha_one(), c2.with_alpha(zo32(0.5))]; let block = Block::builder() .voxels_fn(R2, |cube| Block::from(colors[cube.y as usize])) .unwrap() @@ -600,7 +600,7 @@ mod eval { #[test] fn color_evaluation_regression() { let block = Block::builder() - .color(Rgba::new(1e28, 1e28, 1e28, 1e28)) + .color(Rgba::new(1e28, 1e28, 1e28, 1.0)) // Modifier matters because it causes the block to become voxels .modifier(Modifier::Move(modifier::Move::new(Face6::NX, 0, 0))) .build(); diff --git a/all-is-cubes/src/camera/graphics_options.rs b/all-is-cubes/src/camera/graphics_options.rs index 86dbba05b..1aa6289ae 100644 --- a/all-is-cubes/src/camera/graphics_options.rs +++ b/all-is-cubes/src/camera/graphics_options.rs @@ -3,7 +3,7 @@ use core::fmt; use num_traits::ConstOne as _; use ordered_float::NotNan; -use crate::math::{notnan, FreeCoordinate, PositiveSign, Rgb, Rgba}; +use crate::math::{notnan, FreeCoordinate, PositiveSign, Rgb, Rgba, ZeroOne}; use crate::util::ShowStatus; #[cfg(doc)] @@ -398,7 +398,7 @@ pub enum TransparencyOption { Volumetric, /// Alpha above or below the given threshold value will be rounded to fully opaque /// or fully transparent, respectively. - Threshold(PositiveSign), + Threshold(ZeroOne), } impl TransparencyOption { @@ -476,7 +476,7 @@ impl AntialiasingOption { #[cfg(test)] mod tests { use super::*; - use crate::math::{ps32, rgba_const, OpacityCategory}; + use crate::math::{rgba_const, zo32, OpacityCategory}; use pretty_assertions::assert_eq; #[test] @@ -550,7 +550,7 @@ mod tests { for transparency in &[ TransparencyOption::Surface, TransparencyOption::Volumetric, - TransparencyOption::Threshold(ps32(0.5)), + TransparencyOption::Threshold(zo32(0.5)), ] { assert_eq!( transparency.will_output_alpha(), diff --git a/all-is-cubes/src/raytracer.rs b/all-is-cubes/src/raytracer.rs index 55c9ebf51..addeecd5f 100644 --- a/all-is-cubes/src/raytracer.rs +++ b/all-is-cubes/src/raytracer.rs @@ -36,7 +36,7 @@ use crate::camera::{Camera, GraphicsOptions, TransparencyOption}; use crate::math::Euclid as _; use crate::math::{ rgb_const, smoothstep, Cube, Face6, Face7, FreeCoordinate, FreePoint, FreeVector, GridAab, - GridMatrix, Intensity, PositiveSign, Rgb, Rgba, Vol, + GridMatrix, Intensity, Rgb, Rgba, Vol, ZeroOne, }; use crate::raycast::{self, Ray, RayIsh}; use crate::space::{BlockIndex, BlockSky, PackedLight, Sky, Space, SpaceBlockData}; @@ -628,7 +628,7 @@ fn apply_transmittance(color: Rgba, thickness: f32) -> (Rgba, f32) { // Convert back to alpha. // TODO: skip NaN check ... this may require refactoring Surface usage. // We might also benefit from an "UncheckedRgba" concept. - let alpha = PositiveSign::::new_clamped(1.0 - depth_transmittance); + let alpha = ZeroOne::::new_clamped(1.0 - depth_transmittance); let modified_color = color.to_rgb().with_alpha(alpha); // Compute how the emission should be scaled to account for internal absorption and thickness. diff --git a/all-is-cubes/src/raytracer/accum.rs b/all-is-cubes/src/raytracer/accum.rs index 10ca797c6..def7c1071 100644 --- a/all-is-cubes/src/raytracer/accum.rs +++ b/all-is-cubes/src/raytracer/accum.rs @@ -4,7 +4,7 @@ use euclid::Vector3D; use crate::block::Resolution; use crate::camera::GraphicsOptions; -use crate::math::{ps32, rgb_const, Intensity, PositiveSign, Rgb, Rgba}; +use crate::math::{rgb_const, zo32, Intensity, Rgb, Rgba, ZeroOne}; use crate::space::SpaceBlockData; /// Borrowed data which may be used to customize the result of raytracing. @@ -251,7 +251,7 @@ impl From for Rgba { let non_premultiplied_color = buf.light / color_alpha; Rgb::try_from(non_premultiplied_color) .unwrap_or(rgb_const!(1.0, 0.0, 0.0)) - .with_alpha(PositiveSign::::try_from(color_alpha).unwrap_or(ps32(1.0))) + .with_alpha(ZeroOne::::try_from(color_alpha).unwrap_or(zo32(1.0))) } } } diff --git a/all-is-cubes/src/save/schema.rs b/all-is-cubes/src/save/schema.rs index 6d7d82972..52079aeed 100644 --- a/all-is-cubes/src/save/schema.rs +++ b/all-is-cubes/src/save/schema.rs @@ -23,7 +23,9 @@ use arcstr::ArcStr; use serde::{Deserialize, Serialize}; use crate::block::Block; -use crate::math::{Aab, Face6, GridAab, GridCoordinate, GridRotation, NotNan, PositiveSign}; +use crate::math::{ + Aab, Face6, GridAab, GridCoordinate, GridRotation, NotNan, PositiveSign, ZeroOne, +}; use crate::save::compress::{GzSerde, Leu16}; use crate::time::Schedule; use crate::universe::Handle; @@ -367,7 +369,12 @@ pub(crate) struct IconRowSerV1 { type RgbSer = [PositiveSign; 3]; -type RgbaSer = [PositiveSign; 4]; +type RgbaSer = ( + PositiveSign, + PositiveSign, + PositiveSign, + ZeroOne, +); //------------------------------------------------------------------------------------------------// // Schema corresponding to the `op` module diff --git a/test-renderers/src/test_cases.rs b/test-renderers/src/test_cases.rs index c2d1ef32d..7dd8d8a3b 100644 --- a/test-renderers/src/test_cases.rs +++ b/test-renderers/src/test_cases.rs @@ -15,7 +15,7 @@ use all_is_cubes::color_block; use all_is_cubes::euclid::{point3, size2, size3, vec2, vec3, Point2D, Size2D, Size3D, Vector3D}; use all_is_cubes::listen::{ListenableCell, ListenableSource}; use all_is_cubes::math::{ - ps32, rgb_const, rgba_const, Axis, Cube, Face6, FreeCoordinate, GridAab, GridCoordinate, + ps32, rgb_const, rgba_const, zo32, Axis, Cube, Face6, FreeCoordinate, GridAab, GridCoordinate, GridPoint, GridRotation, GridVector, NotNan, Rgb, Rgba, Vol, }; use all_is_cubes::space::{self, LightPhysics, Space}; @@ -502,7 +502,7 @@ async fn follow_options_change(mut context: RenderTestContext) { let mut options_2 = options_1.clone(); options_2.fov_y = NotNan::from(70); options_2.exposure = ExposureOption::Fixed(ps32(1.5)); - options_2.transparency = TransparencyOption::Threshold(ps32(0.1)); + options_2.transparency = TransparencyOption::Threshold(zo32(0.1)); let options_cell = ListenableCell::new(options_1); let cameras: StandardCameras = StandardCameras::new(