-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Add SpectralColor type
#24447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add SpectralColor type
#24447
Changes from 2 commits
099eb70
defabda
01cc975
3c4f4e0
0bf4c25
1e1f81e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,357 @@ | ||||
| use crate::{ | ||||
| Alpha, Color, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Luminance, Oklaba, Oklcha, Srgba, Xyza, | ||||
| }; | ||||
| use bevy_math::FloatExt; | ||||
| #[cfg(feature = "bevy_reflect")] | ||||
| use bevy_reflect::prelude::*; | ||||
|
|
||||
| /// A color produced by monochromatic light. (of a single wavelength) | ||||
| /// | ||||
| /// Since not every color is a spectral color, (e.g. magenta, white) | ||||
| /// this type can be converted to `Color`, but not the other way around. | ||||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||||
| #[cfg_attr( | ||||
| feature = "bevy_reflect", | ||||
| derive(Reflect), | ||||
| reflect(Clone, PartialEq, Default) | ||||
| )] | ||||
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] | ||||
| #[cfg_attr( | ||||
| all(feature = "serialize", feature = "bevy_reflect"), | ||||
| reflect(Serialize, Deserialize) | ||||
| )] | ||||
| pub struct SpectralColor { | ||||
| /// Wavelength in nanometers. | ||||
| pub wavelength: f32, | ||||
|
|
||||
| /// Luminance in candelas per square meter. | ||||
| pub luminance: f32, | ||||
| } | ||||
|
|
||||
| impl SpectralColor { | ||||
| /// Monochromatic light in the 830 nm wavelength. | ||||
| pub const INFRARED: Self = Self { | ||||
| wavelength: 830.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Monochromatic light in the 700 nm wavelength. | ||||
| pub const RED: Self = Self { | ||||
| wavelength: 700.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Monochromatic light in the 600 nm wavelength. | ||||
| pub const ORANGE: Self = Self { | ||||
| wavelength: 600.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Monochromatic light in the 570 nm wavelength. | ||||
| pub const YELLOW: Self = Self { | ||||
| wavelength: 570.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Monochromatic light in the 540 nm wavelength. | ||||
| pub const GREEN: Self = Self { | ||||
| wavelength: 540.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Monochromatic light in the 510 nm wavelength. | ||||
| pub const CYAN: Self = Self { | ||||
| wavelength: 510.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Monochromatic light in the 460 nm wavelength. | ||||
| pub const BLUE: Self = Self { | ||||
| wavelength: 460.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Monochromatic light in the 400 nm wavelength. | ||||
| pub const VIOLET: Self = Self { | ||||
| wavelength: 400.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Monochromatic light in the 380 nm wavelength. | ||||
| pub const ULTRAVIOLET: Self = Self { | ||||
| wavelength: 380.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Monochromatic light in the 589 nm wavelength, typically produced by sodium vapor lamps. | ||||
| pub const SODIUM_VAPOR: Self = Self { | ||||
| wavelength: 589.0, | ||||
| luminance: 1.0, | ||||
| }; | ||||
|
|
||||
| /// Create a new spectral color with the given wavelength, luminance. | ||||
|
coreh marked this conversation as resolved.
Outdated
|
||||
| pub const fn new(wavelength: f32, luminance: f32) -> Self { | ||||
| Self { | ||||
| wavelength, | ||||
| luminance, | ||||
| } | ||||
| } | ||||
|
|
||||
| /// Create a new spectral color with the given wavelength and luminance of 1.0 | ||||
| pub const fn wavelength(wavelength: f32) -> Self { | ||||
| Self { | ||||
| wavelength, | ||||
| luminance: 1.0, | ||||
| } | ||||
| } | ||||
|
|
||||
| /// Returns a new spectral color with the given wavelength. | ||||
| pub fn with_wavelength(&self, wavelength: f32) -> Self { | ||||
| Self { | ||||
| wavelength, | ||||
| ..*self | ||||
| } | ||||
| } | ||||
|
|
||||
| /// The wavelength where the look up table starts. | ||||
| const CIE_1931_NM_CMF_LOOKUP_TABLE_START: f32 = 355.0; | ||||
|
|
||||
| /// The wavelength where the look up table ends. | ||||
| const CIE_1931_NM_CMF_LOOKUP_TABLE_END: f32 = 835.0; | ||||
|
|
||||
| /// The increment between each row in the look up table. | ||||
| const CIE_1931_NM_CMF_LOOKUP_TABLE_INCREMENT: f32 = 5.0; | ||||
|
|
||||
| /// CIE 1931 2-deg, XYZ color matching functions, in lookup table form. | ||||
| /// Each row is a 5nm step from 360nm to 830nm (inclusive), with two | ||||
| /// rows of sentinel values, at the start and end of the table. | ||||
| /// (For interpolation to zero.) | ||||
| /// | ||||
| /// Source: <http://cvrl.ioo.ucl.ac.uk> | ||||
| #[expect(clippy::excessive_precision, reason = "Reference values")] | ||||
| const CIE_1931_XYZ_CMF_LOOKUP_TABLE: [[f32; 3]; 97] = [ | ||||
| [0.000000000000, 0.000000000000, 0.000000000000], // Sentinel value | ||||
| [0.000129900000, 0.000003917000, 0.000606100000], // 360 nm | ||||
| [0.000232100000, 0.000006965000, 0.001086000000], // 365 nm | ||||
| [0.000414900000, 0.000012390000, 0.001946000000], // 370 nm | ||||
| [0.000741600000, 0.000022020000, 0.003486000000], // 375 nm | ||||
| [0.001368000000, 0.000039000000, 0.006450001000], // 380 nm | ||||
| [0.002236000000, 0.000064000000, 0.010549990000], // 385 nm | ||||
| [0.004243000000, 0.000120000000, 0.020050010000], // 390 nm | ||||
| [0.007650000000, 0.000217000000, 0.036210000000], // 395 nm | ||||
| [0.014310000000, 0.000396000000, 0.067850010000], // 400 nm | ||||
| [0.023190000000, 0.000640000000, 0.110200000000], // 405 nm | ||||
| [0.043510000000, 0.001210000000, 0.207400000000], // 410 nm | ||||
| [0.077630000000, 0.002180000000, 0.371300000000], // 415 nm | ||||
| [0.134380000000, 0.004000000000, 0.645600000000], // 420 nm | ||||
| [0.214770000000, 0.007300000000, 1.039050100000], // 425 nm | ||||
| [0.283900000000, 0.011600000000, 1.385600000000], // 430 nm | ||||
| [0.328500000000, 0.016840000000, 1.622960000000], // 435 nm | ||||
| [0.348280000000, 0.023000000000, 1.747060000000], // 440 nm | ||||
| [0.348060000000, 0.029800000000, 1.782600000000], // 445 nm | ||||
| [0.336200000000, 0.038000000000, 1.772110000000], // 450 nm | ||||
| [0.318700000000, 0.048000000000, 1.744100000000], // 455 nm | ||||
| [0.290800000000, 0.060000000000, 1.669200000000], // 460 nm | ||||
| [0.251100000000, 0.073900000000, 1.528100000000], // 465 nm | ||||
| [0.195360000000, 0.090980000000, 1.287640000000], // 470 nm | ||||
| [0.142100000000, 0.112600000000, 1.041900000000], // 475 nm | ||||
| [0.095640000000, 0.139020000000, 0.812950100000], // 480 nm | ||||
| [0.057950010000, 0.169300000000, 0.616200000000], // 485 nm | ||||
| [0.032010000000, 0.208020000000, 0.465180000000], // 490 nm | ||||
| [0.014700000000, 0.258600000000, 0.353300000000], // 495 nm | ||||
| [0.004900000000, 0.323000000000, 0.272000000000], // 500 nm | ||||
| [0.002400000000, 0.407300000000, 0.212300000000], // 505 nm | ||||
| [0.009300000000, 0.503000000000, 0.158200000000], // 510 nm | ||||
| [0.029100000000, 0.608200000000, 0.111700000000], // 515 nm | ||||
| [0.063270000000, 0.710000000000, 0.078249990000], // 520 nm | ||||
| [0.109600000000, 0.793200000000, 0.057250010000], // 525 nm | ||||
| [0.165500000000, 0.862000000000, 0.042160000000], // 530 nm | ||||
| [0.225749900000, 0.914850100000, 0.029840000000], // 535 nm | ||||
| [0.290400000000, 0.954000000000, 0.020300000000], // 540 nm | ||||
| [0.359700000000, 0.980300000000, 0.013400000000], // 545 nm | ||||
| [0.433449900000, 0.994950100000, 0.008749999000], // 550 nm | ||||
| [0.512050100000, 1.000000000000, 0.005749999000], // 555 nm | ||||
| [0.594500000000, 0.995000000000, 0.003900000000], // 560 nm | ||||
| [0.678400000000, 0.978600000000, 0.002749999000], // 565 nm | ||||
| [0.762100000000, 0.952000000000, 0.002100000000], // 570 nm | ||||
| [0.842500000000, 0.915400000000, 0.001800000000], // 575 nm | ||||
| [0.916300000000, 0.870000000000, 0.001650001000], // 580 nm | ||||
| [0.978600000000, 0.816300000000, 0.001400000000], // 585 nm | ||||
| [1.026300000000, 0.757000000000, 0.001100000000], // 590 nm | ||||
| [1.056700000000, 0.694900000000, 0.001000000000], // 595 nm | ||||
| [1.062200000000, 0.631000000000, 0.000800000000], // 600 nm | ||||
| [1.045600000000, 0.566800000000, 0.000600000000], // 605 nm | ||||
| [1.002600000000, 0.503000000000, 0.000340000000], // 610 nm | ||||
| [0.938400000000, 0.441200000000, 0.000240000000], // 615 nm | ||||
| [0.854449900000, 0.381000000000, 0.000190000000], // 620 nm | ||||
| [0.751400000000, 0.321000000000, 0.000100000000], // 625 nm | ||||
| [0.642400000000, 0.265000000000, 0.000049999990], // 630 nm | ||||
| [0.541900000000, 0.217000000000, 0.000030000000], // 635 nm | ||||
| [0.447900000000, 0.175000000000, 0.000020000000], // 640 nm | ||||
| [0.360800000000, 0.138200000000, 0.000010000000], // 645 nm | ||||
| [0.283500000000, 0.107000000000, 0.000000000000], // 650 nm | ||||
| [0.218700000000, 0.081600000000, 0.000000000000], // 655 nm | ||||
| [0.164900000000, 0.061000000000, 0.000000000000], // 660 nm | ||||
| [0.121200000000, 0.044580000000, 0.000000000000], // 665 nm | ||||
| [0.087400000000, 0.032000000000, 0.000000000000], // 670 nm | ||||
| [0.063600000000, 0.023200000000, 0.000000000000], // 675 nm | ||||
| [0.046770000000, 0.017000000000, 0.000000000000], // 680 nm | ||||
| [0.032900000000, 0.011920000000, 0.000000000000], // 685 nm | ||||
| [0.022700000000, 0.008210000000, 0.000000000000], // 690 nm | ||||
| [0.015840000000, 0.005723000000, 0.000000000000], // 695 nm | ||||
| [0.011359160000, 0.004102000000, 0.000000000000], // 700 nm | ||||
| [0.008110916000, 0.002929000000, 0.000000000000], // 705 nm | ||||
| [0.005790346000, 0.002091000000, 0.000000000000], // 710 nm | ||||
| [0.004109457000, 0.001484000000, 0.000000000000], // 715 nm | ||||
| [0.002899327000, 0.001047000000, 0.000000000000], // 720 nm | ||||
| [0.002049190000, 0.000740000000, 0.000000000000], // 725 nm | ||||
| [0.001439971000, 0.000520000000, 0.000000000000], // 730 nm | ||||
| [0.000999949300, 0.000361100000, 0.000000000000], // 735 nm | ||||
| [0.000690078600, 0.000249200000, 0.000000000000], // 740 nm | ||||
| [0.000476021300, 0.000171900000, 0.000000000000], // 745 nm | ||||
| [0.000332301100, 0.000120000000, 0.000000000000], // 750 nm | ||||
| [0.000234826100, 0.000084800000, 0.000000000000], // 755 nm | ||||
| [0.000166150500, 0.000060000000, 0.000000000000], // 760 nm | ||||
| [0.000117413000, 0.000042400000, 0.000000000000], // 765 nm | ||||
| [0.000083075270, 0.000030000000, 0.000000000000], // 770 nm | ||||
| [0.000058706520, 0.000021200000, 0.000000000000], // 775 nm | ||||
| [0.000041509940, 0.000014990000, 0.000000000000], // 780 nm | ||||
| [0.000029353260, 0.000010600000, 0.000000000000], // 785 nm | ||||
| [0.000020673830, 0.000007465700, 0.000000000000], // 790 nm | ||||
| [0.000014559770, 0.000005257800, 0.000000000000], // 795 nm | ||||
| [0.000010253980, 0.000003702900, 0.000000000000], // 800 nm | ||||
| [0.000007221456, 0.000002607800, 0.000000000000], // 805 nm | ||||
| [0.000005085868, 0.000001836600, 0.000000000000], // 810 nm | ||||
| [0.000003581652, 0.000001293400, 0.000000000000], // 815 nm | ||||
| [0.000002522525, 0.000000910930, 0.000000000000], // 820 nm | ||||
| [0.000001776509, 0.000000641530, 0.000000000000], // 825 nm | ||||
| [0.000001251141, 0.000000451810, 0.000000000000], // 830 nm | ||||
| [0.000000000000, 0.000000000000, 0.000000000000], // Sentinel value | ||||
| ]; | ||||
| } | ||||
|
|
||||
| impl Default for SpectralColor { | ||||
| fn default() -> Self { | ||||
| Self::SODIUM_VAPOR | ||||
| } | ||||
| } | ||||
|
|
||||
| impl Luminance for SpectralColor { | ||||
| fn luminance(&self) -> f32 { | ||||
| self.luminance | ||||
| } | ||||
|
|
||||
| fn with_luminance(&self, value: f32) -> Self { | ||||
| Self { | ||||
| luminance: value, | ||||
| ..*self | ||||
| } | ||||
| } | ||||
|
|
||||
| fn darker(&self, amount: f32) -> Self { | ||||
| self.with_luminance((self.luminance - amount).max(0.0)) | ||||
| } | ||||
|
|
||||
| fn lighter(&self, amount: f32) -> Self { | ||||
| self.with_luminance(self.luminance + amount) | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for LinearRgba { | ||||
| /// Convert the spectral color to a Linear Rgba color, using the CIE 1931 2-deg XYZ color matching functions. | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| if value.wavelength < SpectralColor::CIE_1931_NM_CMF_LOOKUP_TABLE_START | ||||
| || value.wavelength >= SpectralColor::CIE_1931_NM_CMF_LOOKUP_TABLE_END | ||||
| { | ||||
| // If the wavelength is outside the range of the lookup table, return black | ||||
| return LinearRgba::BLACK; | ||||
| } | ||||
|
|
||||
| let index = ((value.wavelength - SpectralColor::CIE_1931_NM_CMF_LOOKUP_TABLE_START) | ||||
| / SpectralColor::CIE_1931_NM_CMF_LOOKUP_TABLE_INCREMENT) | ||||
| .floor() as usize; | ||||
|
|
||||
| let lerp = (value.wavelength - SpectralColor::CIE_1931_NM_CMF_LOOKUP_TABLE_START) | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as a fan of spec colors just looking through the lookup table interpolation here... wouldn't it be a bit cleaner to avoid the floating point modulo (%) and redundant math by leveraging .fract()? I use the same CIE 1931 table in a WGSL shader and ended up with this form (an experiment): 🙂
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm yeah. I ended up using There's also apparently a closed formula approximation (without lookup table) that @valaphee had mentioned on the original PR, but the link is now gone. |
||||
| % SpectralColor::CIE_1931_NM_CMF_LOOKUP_TABLE_INCREMENT | ||||
| / SpectralColor::CIE_1931_NM_CMF_LOOKUP_TABLE_INCREMENT; | ||||
|
|
||||
| let row = SpectralColor::CIE_1931_XYZ_CMF_LOOKUP_TABLE[index]; | ||||
| let next_row = SpectralColor::CIE_1931_XYZ_CMF_LOOKUP_TABLE[index + 1]; | ||||
|
|
||||
| let x = row[0].lerp(next_row[0], lerp); | ||||
| let y = row[1].lerp(next_row[1], lerp); | ||||
| let z = row[2].lerp(next_row[2], lerp); | ||||
|
|
||||
| let xyza = Xyza::new(x, y, z, 1.0); | ||||
|
|
||||
| let mut linear = Color::from(xyza).to_linear(); | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked, and for rows 43 to 57 this generates a linear color with the red value above 1.0, with a max of 2.5166698. Is that okay?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think so? Internally the shaders support and heavily use colors above 1 for highlights, etc, and for things like highlights, emissive, etc. Might be more of a convention than a strict rule? The conversion of Some examples currently use use values Line 43 in 01cc975
|
||||
|
|
||||
| // Clamp negative values to zero | ||||
| linear.red = linear.red.max(0.0); | ||||
| linear.green = linear.green.max(0.0); | ||||
| linear.blue = linear.blue.max(0.0); | ||||
|
|
||||
| // Apply luminance scaling, without clamping to white | ||||
| (linear * value.luminance).with_alpha(1.0) | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Hwba { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Hsla { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Hsva { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Srgba { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Oklaba { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Oklcha { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Lcha { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Laba { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Xyza { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
|
|
||||
| impl From<SpectralColor> for Color { | ||||
| fn from(value: SpectralColor) -> Self { | ||||
| LinearRgba::from(value).into() | ||||
| } | ||||
| } | ||||
Uh oh!
There was an error while loading. Please reload this page.