Skip to content

Commit

Permalink
math: Make Rgba use ZeroOne for alpha.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kpreid committed Oct 21, 2024
1 parent 99e56a2 commit 7cff74a
Show file tree
Hide file tree
Showing 22 changed files with 182 additions and 100 deletions.
99 changes: 75 additions & 24 deletions all-is-cubes-base/src/math/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -43,7 +43,7 @@ macro_rules! rgba_const {
$crate::math::PositiveSign::<f32>::new_strict($r),
$crate::math::PositiveSign::<f32>::new_strict($g),
$crate::math::PositiveSign::<f32>::new_strict($b),
$crate::math::PositiveSign::<f32>::new_strict($a),
$crate::math::ZeroOne::<f32>::new_strict($a),
)
}
};
Expand All @@ -70,11 +70,12 @@ pub struct Rgb(Vector3D<PositiveSign<f32>, 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<f32>,
alpha: ZeroOne<f32>,
}

/// Unit-of-measure type for vectors that contain color channels.
Expand Down Expand Up @@ -151,13 +152,13 @@ impl Rgb {

/// Adds an alpha component to produce an [Rgba] color.
#[inline]
pub const fn with_alpha(self, alpha: PositiveSign<f32>) -> Rgba {
pub const fn with_alpha(self, alpha: ZeroOne<f32>) -> 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.
Expand All @@ -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).
Expand Down Expand Up @@ -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.
Expand All @@ -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::<f32>::new_strict(a))
Rgb::new(r, g, b).with_alpha(ZeroOne::<f32>::new_strict(a))
}

/// Constructs a color from components that have already been checked for not being
Expand All @@ -270,7 +271,7 @@ impl Rgba {
r: PositiveSign<f32>,
g: PositiveSign<f32>,
b: PositiveSign<f32>,
alpha: PositiveSign<f32>,
alpha: ZeroOne<f32>,
) -> Self {
Self {
rgb: Rgb::new_ps(r, g, b),
Expand Down Expand Up @@ -312,25 +313,23 @@ 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<f32> {
pub const fn alpha(self) -> ZeroOne<f32> {
self.alpha
}

/// Returns whether this color is fully transparent, or has an alpha component of
/// 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
Expand Down Expand Up @@ -412,7 +411,7 @@ impl Rgba {
pub fn clamp(self) -> Self {
Self {
rgb: self.rgb.clamp(),
alpha: self.alpha.min(PS1),
alpha: self.alpha,
}
}

Expand All @@ -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)
}
}

Expand All @@ -445,16 +444,46 @@ impl From<[PositiveSign<f32>; 3]> for Rgb {
Self(value.into())
}
}
impl From<[PositiveSign<f32>; 4]> for Rgba {
impl From<[ZeroOne<f32>; 3]> for Rgb {
#[inline]
fn from(value: [PositiveSign<f32>; 4]) -> Self {
fn from(value: [ZeroOne<f32>; 3]) -> Self {
Self::from(value.map(PositiveSign::from))
}
}
impl From<[ZeroOne<f32>; 4]> for Rgba {
#[inline]
fn from(value: [ZeroOne<f32>; 4]) -> Self {
let [r, g, b, alpha] = value;
Self {
rgb: Rgb::from([r, g, b]),
alpha,
}
}
}
impl
From<(
PositiveSign<f32>,
PositiveSign<f32>,
PositiveSign<f32>,
ZeroOne<f32>,
)> for Rgba
{
#[inline]
fn from(
value: (
PositiveSign<f32>,
PositiveSign<f32>,
PositiveSign<f32>,
ZeroOne<f32>,
),
) -> Self {
let (r, g, b, alpha) = value;
Self {
rgb: Rgb::from([r, g, b]),
alpha,
}
}
}

impl From<Rgb> for Vector3D<f32, Intensity> {
#[inline]
Expand All @@ -473,7 +502,21 @@ impl From<Rgba> for [PositiveSign<f32>; 4] {
#[inline]
fn from(value: Rgba) -> Self {
let [r, g, b]: [PositiveSign<f32>; 3] = value.rgb.into();
[r, g, b, value.alpha]
[r, g, b, value.alpha.into()]
}
}
impl From<Rgba>
for (
PositiveSign<f32>,
PositiveSign<f32>,
PositiveSign<f32>,
ZeroOne<f32>,
)
{
#[inline]
fn from(value: Rgba) -> Self {
let [r, g, b]: [PositiveSign<f32>; 3] = value.rgb.into();
(r, g, b, value.alpha)
}
}

Expand Down Expand Up @@ -547,6 +590,14 @@ impl Mul<PositiveSign<f32>> for Rgb {
Self(self.0 * scalar)
}
}
impl Mul<ZeroOne<f32>> for Rgb {
type Output = Self;
/// Multiplies this color value by a scalar.
#[inline]
fn mul(self, scalar: ZeroOne<f32>) -> 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.
Expand Down Expand Up @@ -699,9 +750,9 @@ fn component_from_linear8_arithmetic(c: u8) -> f32 {
}

#[inline]
const fn component_from_linear8_const(c: u8) -> PositiveSign<f32> {
// 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<f32> {
// 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.
Expand Down
21 changes: 21 additions & 0 deletions all-is-cubes-base/src/math/serde_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,24 @@ where
Self::try_from(T::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}

impl<T: Serialize> Serialize for math::ZeroOne<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.as_ref().serialize(serializer)
}
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for math::ZeroOne<T>
where
Self: TryFrom<T, Error: core::error::Error>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::try_from(T::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}
2 changes: 1 addition & 1 deletion all-is-cubes-content/src/city/exhibits/move_modifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
))
Expand Down
5 changes: 3 additions & 2 deletions all-is-cubes-content/src/city/exhibits/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
2 changes: 1 addition & 1 deletion all-is-cubes-content/src/city/exhibits/transparency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
4 changes: 2 additions & 2 deletions all-is-cubes-content/src/clouds.rs
Original file line number Diff line number Diff line change
@@ -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 _;
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions all-is-cubes-content/src/landscape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -109,7 +109,7 @@ impl DefaultProvision<Block> 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()
}
Expand Down
6 changes: 2 additions & 4 deletions all-is-cubes-gpu/src/in_wgpu/space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -981,8 +980,7 @@ impl ParticleSet {
crate::wireframe_vertices::<WgpuLinesVertex, _, _>(
&mut tmp,
Rgb::ONE.with_alpha(
PositiveSign::<f32>::try_from(0.9f32.powf(self.age as f32))
.unwrap_or(PositiveSign::ZERO),
ZeroOne::<f32>::try_from(0.9f32.powf(self.age as f32)).unwrap_or(ZeroOne::ZERO),
),
&self.fluff.position.aab().expand(0.004 * (self.age as f64)),
);
Expand Down
4 changes: 2 additions & 2 deletions all-is-cubes-mesh/src/dynamic/chunked_mesh/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions all-is-cubes-mesh/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -69,7 +69,7 @@ fn test_block_mesh_threshold(block: Block) -> BlockMesh<TextureMt> {
&block.evaluate().unwrap(),
&Allocator::new(),
&MeshOptions {
transparency: TransparencyOption::Threshold(ps32(0.5)),
transparency: TransparencyOption::Threshold(zo32(0.5)),
..MeshOptions::dont_care_for_test()
},
)
Expand Down
Loading

0 comments on commit 7cff74a

Please sign in to comment.