use glam::Vec3;
use super::gamut;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransferFunction {
Srgb,
Rec2020,
Linear,
}
impl TransferFunction {
#[must_use]
pub fn encode(self, linear: f32) -> f32 {
match self {
Self::Srgb => {
if linear <= 0.003_130_8 {
linear * 12.92
} else {
1.055f32.mul_add(linear.powf(1.0 / 2.4), -0.055)
}
}
Self::Rec2020 => {
const ALPHA: f32 = 1.099_296_8;
const BETA: f32 = 0.018_053_97;
if linear < BETA {
linear * 4.5
} else {
ALPHA.mul_add(linear.powf(0.45), -(ALPHA - 1.0))
}
}
Self::Linear => linear,
}
}
#[must_use]
pub fn decode(self, encoded: f32) -> f32 {
match self {
Self::Srgb => {
if encoded <= 0.040_45 {
encoded / 12.92
} else {
((encoded + 0.055) / 1.055).powf(2.4)
}
}
Self::Rec2020 => {
const ALPHA: f32 = 1.099_296_8;
const BETA: f32 = 0.018_053_97;
let breakpoint = 4.5 * BETA;
if encoded < breakpoint {
encoded / 4.5
} else {
((encoded + (ALPHA - 1.0)) / ALPHA).powf(1.0 / 0.45)
}
}
Self::Linear => encoded,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ToneMap {
None,
AcesFilmic {
exposure: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorSpace {
Srgb,
DisplayP3,
Rec2020,
AcesCg,
}
impl ColorSpace {
#[must_use]
pub const fn xyz_to_rgb_matrix(self) -> [[f32; 3]; 3] {
match self {
Self::Srgb => [
[3.240_454_2, -1.537_138_5, -0.498_531_4],
[-0.969_266, 1.876_010_8, 0.041_556_0],
[0.055_643_4, -0.204_025_9, 1.057_225_2],
],
Self::DisplayP3 => [
[2.493_497, -0.931_383_6, -0.402_710_78],
[-0.829_489, 1.762_664, 0.023_624_686],
[0.035_845_83, -0.076_172_39, 0.956_884_5],
],
Self::Rec2020 => [
[1.716_651_2, -0.355_670_78, -0.253_366_3],
[-0.666_684_3, 1.616_481_2, 0.015_768_546],
[0.017_639_857, -0.042_770_613, 0.942_103_1],
],
Self::AcesCg => [
[1.641_023_4, -0.324_803_3, -0.236_424_7],
[-0.663_662_86, 1.615_331_6, 0.016_756_348],
[0.011_721_894, -0.008_284_442, 0.988_394_86],
],
}
}
#[must_use]
pub const fn white_point_xy(self) -> (f32, f32) {
match self {
Self::AcesCg => (0.321_68, 0.337_67),
Self::Srgb | Self::DisplayP3 | Self::Rec2020 => (0.3127, 0.3290),
}
}
#[must_use]
pub const fn transfer_function(self) -> TransferFunction {
match self {
Self::Srgb | Self::DisplayP3 => TransferFunction::Srgb,
Self::Rec2020 => TransferFunction::Rec2020,
Self::AcesCg => TransferFunction::Linear,
}
}
#[must_use]
pub fn xyz_to_linear(self, xyz: Vec3) -> Vec3 {
let m = self.xyz_to_rgb_matrix();
Vec3::new(
m[0][0].mul_add(xyz.x, m[0][1].mul_add(xyz.y, m[0][2] * xyz.z)),
m[1][0].mul_add(xyz.x, m[1][1].mul_add(xyz.y, m[1][2] * xyz.z)),
m[2][0].mul_add(xyz.x, m[2][1].mul_add(xyz.y, m[2][2] * xyz.z)),
)
}
#[must_use]
pub fn encode(self, xyz: Vec3, tonemap: ToneMap) -> [u8; 4] {
let sum = xyz.x + xyz.y + xyz.z;
if !sum.is_finite() || sum <= 1e-6 {
return [0, 0, 0, 255];
}
let toned_rgb = match tonemap {
ToneMap::None => gamut::project_to_gamut(xyz, self),
ToneMap::AcesFilmic { exposure } => {
let luminance = xyz.y.max(0.0);
let exposed_luminance = (luminance * exposure).max(0.0);
let y_tm = crate::optics::raytracer::aces_tonemap(exposed_luminance);
let luminance_scale = (y_tm / exposed_luminance.max(1e-5)) * exposure;
let toned_xyz = xyz * luminance_scale;
gamut::project_to_gamut_bounded(toned_xyz, self, 1.0)
}
};
let tf = self.transfer_function();
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the encoded value is clamped to 0..=1, so encoded * 255 + 0.5 lies in 0.5..=255.5 and truncates to 0..=255"
)]
let encode_channel = |c: f32| -> u8 {
let linear = if c.is_finite() {
c.clamp(0.0, 1.0)
} else {
0.0
};
let encoded = tf.encode(linear).clamp(0.0, 1.0);
encoded.mul_add(255.0, 0.5) as u8
};
[
encode_channel(toned_rgb.x),
encode_channel(toned_rgb.y),
encode_channel(toned_rgb.z),
255,
]
}
}