diff --git a/Cargo.toml b/Cargo.toml index 978c1b754f9fb..27c58a1ed8dec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -316,6 +316,9 @@ pbr_multi_layer_material_textures = [ # Enable support for anisotropy texture in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs pbr_anisotropy_texture = ["bevy_internal/pbr_anisotropy_texture"] +# Enable support for spectral colors (monochromatic light) in lights +spectral_lighting = ["bevy_internal/spectral_lighting"] + # Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU. webgl2 = ["bevy_internal/webgl"] @@ -786,6 +789,18 @@ description = "A scene showcasing screen space ambient occlusion" category = "3D Rendering" wasm = false +[[example]] +name = "spectral_lighting" +path = "examples/3d/spectral_lighting.rs" +doc-scrape-examples = true +required-features = ["spectral_lighting"] + +[package.metadata.example.spectral_lighting] +name = "Spectral Lighting" +description = "Showcases support for spectral color (monochromatic) lights in a 3D scene" +category = "3D Rendering" +wasm = true + [[example]] name = "spotlight" path = "examples/3d/spotlight.rs" diff --git a/assets/textures/Linear_visible_spectrum.png b/assets/textures/Linear_visible_spectrum.png new file mode 100644 index 0000000000000..78f4cf10f38b3 Binary files /dev/null and b/assets/textures/Linear_visible_spectrum.png differ diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index 92fa2b98fd559..eec1499c9cf1d 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -1,6 +1,6 @@ use crate::{ color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, - Luminance, Mix, Oklaba, Oklcha, Srgba, StandardColor, Xyza, + Luminance, Mix, Oklaba, Oklcha, SpectralColor, Srgba, StandardColor, Xyza, }; #[cfg(feature = "bevy_reflect")] use bevy_reflect::prelude::*; @@ -486,6 +486,12 @@ impl From for Color { } } +impl From for Color { + fn from(value: SpectralColor) -> Self { + Self::LinearRgba(value.to_linear()) + } +} + impl From for Srgba { fn from(value: Color) -> Self { match value { diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 95c51a494db14..b916b136af73a 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -110,6 +110,8 @@ mod test_colors; mod testing; mod xyza; +mod spectral; + /// Commonly used color types and traits. pub mod prelude { pub use crate::color::*; @@ -124,6 +126,8 @@ pub mod prelude { pub use crate::oklcha::*; pub use crate::srgba::*; pub use crate::xyza::*; + + pub use crate::spectral::*; } pub use color::*; @@ -140,6 +144,8 @@ pub use oklcha::*; pub use srgba::*; pub use xyza::*; +pub use spectral::*; + /// Describes the traits that a color should implement for consistency. #[allow(dead_code)] // This is an internal marker trait used to ensure that our color types impl the required traits pub(crate) trait StandardColor diff --git a/crates/bevy_color/src/spectral.rs b/crates/bevy_color/src/spectral.rs new file mode 100644 index 0000000000000..e981c53a37d15 --- /dev/null +++ b/crates/bevy_color/src/spectral.rs @@ -0,0 +1,279 @@ +use crate::{Alpha, Color, LinearRgba, Luminance, Xyza}; +use bevy_math::FloatExt; +use std::ops::Mul; + +/// 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(Copy, Clone, Debug)] +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 and luminance + pub const fn new(wavelength: f32, luminance: f32) -> Self { + Self { + wavelength, + luminance, + } + } + + /// Convert the spectral color to a Linear Rgba color, using the CIE 1931 2-deg XYZ color matching functions + pub fn to_linear(&self) -> LinearRgba { + if self.wavelength < Self::CIE_1931_NM_CMF_LOOKUP_TABLE_START + || self.wavelength >= Self::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 = ((self.wavelength - Self::CIE_1931_NM_CMF_LOOKUP_TABLE_START) + / Self::CIE_1931_NM_CMF_LOOKUP_TABLE_INCREMENT) + .floor() as usize; + + let lerp = (self.wavelength - Self::CIE_1931_NM_CMF_LOOKUP_TABLE_START) + % Self::CIE_1931_NM_CMF_LOOKUP_TABLE_INCREMENT + / Self::CIE_1931_NM_CMF_LOOKUP_TABLE_INCREMENT; + + let row = Self::CIE_1931_XYZ_CMF_LOOKUP_TABLE[index]; + let next_row = Self::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(); + + // 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 * self.luminance).with_alpha(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: + #[allow(clippy::excessive_precision)] + 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 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 Mul for SpectralColor { + type Output = Self; + + fn mul(self, rhs: f32) -> Self { + Self { + luminance: self.luminance * rhs, + ..self + } + } +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 7505c8f0be369..fee95a7548688 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -114,6 +114,9 @@ pbr_anisotropy_texture = [ "bevy_gltf?/pbr_anisotropy_texture", ] +# Spectral colors (monochromatic light) in lights: +spectral_lighting = ["bevy_pbr?/spectral_lighting"] + # Optimise for WebGL2 webgl = [ "bevy_core_pipeline?/webgl", diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index a4c46dfdbcc60..d3b5481ddb7be 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -14,6 +14,7 @@ webgpu = [] pbr_transmission_textures = [] pbr_multi_layer_material_textures = [] pbr_anisotropy_texture = [] +spectral_lighting = [] shader_format_glsl = ["bevy_render/shader_format_glsl"] trace = ["bevy_render/trace"] ios_simulator = ["bevy_render/ios_simulator"] diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index fe913a1d196bb..1ae71a6b88b11 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -152,6 +152,8 @@ pub struct GpuClusterableObject { pub(crate) shadow_depth_bias: f32, pub(crate) shadow_normal_bias: f32, pub(crate) spot_light_tan_angle: f32, + #[cfg(feature = "spectral_lighting")] + pub(crate) monochromaticity: f32, } pub enum GpuClusterableObjects { diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index eb4ba66cf0998..25b3d9c75139e 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -255,6 +255,9 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] shader_defs.push("WEBGL2".into()); + #[cfg(feature = "spectral_lighting")] + shader_defs.push("SPECTRAL_LIGHTING".into()); + if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push(ShaderDefVal::UInt( diff --git a/crates/bevy_pbr/src/light/ambient_light.rs b/crates/bevy_pbr/src/light/ambient_light.rs index 9bff67d5f836d..31229ec51dee4 100644 --- a/crates/bevy_pbr/src/light/ambient_light.rs +++ b/crates/bevy_pbr/src/light/ambient_light.rs @@ -25,6 +25,13 @@ pub struct AmbientLight { /// /// [cd/m^2]: https://en.wikipedia.org/wiki/Candela_per_square_metre pub brightness: f32, + /// The degree to which this light is monochromatic. A value of 0.0 means this light is perfectly polychromatic, + /// while a value of 1.0 means this light is perfectly monochromatic. + /// + /// Meant to be used with `SpectralColor` to render monochromatic lights. (e.g. Sodium vapor lamps) + /// Combining non-zero values with non-spectral colors is not physically correct, but can be used for artistic effect. + #[cfg(feature = "spectral_lighting")] + pub monochromaticity: f32, } impl Default for AmbientLight { @@ -32,6 +39,8 @@ impl Default for AmbientLight { Self { color: Color::WHITE, brightness: 80.0, + #[cfg(feature = "spectral_lighting")] + monochromaticity: 0.0, } } } @@ -39,5 +48,7 @@ impl AmbientLight { pub const NONE: AmbientLight = AmbientLight { color: Color::WHITE, brightness: 0.0, + #[cfg(feature = "spectral_lighting")] + monochromaticity: 0.0, }; } diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_pbr/src/light/directional_light.rs index 9c74971ca15a5..4f5a5463419c1 100644 --- a/crates/bevy_pbr/src/light/directional_light.rs +++ b/crates/bevy_pbr/src/light/directional_light.rs @@ -63,6 +63,13 @@ pub struct DirectionalLight { /// A bias applied along the direction of the fragment's surface normal. It is scaled to the /// shadow map's texel size so that it is automatically adjusted to the orthographic projection. pub shadow_normal_bias: f32, + /// The degree to which this light is monochromatic. A value of 0.0 means this light is perfectly polychromatic, + /// while a value of 1.0 means this light is perfectly monochromatic. + /// + /// Meant to be used with `SpectralColor` to render monochromatic lights. (e.g. Sodium vapor lamps) + /// Combining non-zero values with non-spectral colors is not physically correct, but can be used for artistic effect. + #[cfg(feature = "spectral_lighting")] + pub monochromaticity: f32, } impl Default for DirectionalLight { @@ -73,6 +80,8 @@ impl Default for DirectionalLight { shadows_enabled: false, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, + #[cfg(feature = "spectral_lighting")] + monochromaticity: 0.0, } } } diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs index 9ca85cd4db167..3d24720b8e31e 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -45,6 +45,13 @@ pub struct PointLight { /// shadow map's texel size so that it can be small close to the camera and gets larger further /// away. pub shadow_normal_bias: f32, + /// The degree to which this light is monochromatic. A value of 0.0 means this light is perfectly polychromatic, + /// while a value of 1.0 means this light is perfectly monochromatic. + /// + /// Meant to be used with `SpectralColor` to render monochromatic lights. (e.g. Sodium vapor lamps) + /// Combining non-zero values with non-spectral colors is not physically correct, but can be used for artistic effect. + #[cfg(feature = "spectral_lighting")] + pub monochromaticity: f32, } impl Default for PointLight { @@ -60,6 +67,8 @@ impl Default for PointLight { shadows_enabled: false, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, + #[cfg(feature = "spectral_lighting")] + monochromaticity: 0.0, } } } diff --git a/crates/bevy_pbr/src/light/spot_light.rs b/crates/bevy_pbr/src/light/spot_light.rs index ab34196ff03fb..e5d590e466abb 100644 --- a/crates/bevy_pbr/src/light/spot_light.rs +++ b/crates/bevy_pbr/src/light/spot_light.rs @@ -29,6 +29,13 @@ pub struct SpotLight { /// Light is attenuated from `inner_angle` to `outer_angle` to give a smooth falloff. /// `inner_angle` should be <= `outer_angle` pub inner_angle: f32, + /// The degree to which this light is monochromatic. A value of 0.0 means this light is perfectly polychromatic, + /// while a value of 1.0 means this light is perfectly monochromatic. + /// + /// Meant to be used with `SpectralColor` to render monochromatic lights. (e.g. Sodium vapor lamps) + /// Combining non-zero values with non-spectral colors is not physically correct, but can be used for artistic effect. + #[cfg(feature = "spectral_lighting")] + pub monochromaticity: f32, } impl SpotLight { @@ -52,6 +59,8 @@ impl Default for SpotLight { shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, inner_angle: 0.0, outer_angle: std::f32::consts::FRAC_PI_4, + #[cfg(feature = "spectral_lighting")] + monochromaticity: 0.0, } } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index a8ca69a41f176..b6e448fe08daa 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -350,6 +350,9 @@ where #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] shader_defs.push("WEBGL2".into()); + #[cfg(feature = "spectral_lighting")] + shader_defs.push("SPECTRAL_LIGHTING".into()); + shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into()); if key.mesh_key.contains(MeshPipelineKey::DEPTH_PREPASS) { diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 9ecf1b3616be4..0994c2843105a 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -38,6 +38,8 @@ pub struct ExtractedPointLight { pub shadow_depth_bias: f32, pub shadow_normal_bias: f32, pub spot_light_angles: Option<(f32, f32)>, + #[cfg(feature = "spectral_lighting")] + pub monochromaticity: f32, } #[derive(Component, Debug)] @@ -53,6 +55,8 @@ pub struct ExtractedDirectionalLight { pub cascades: EntityHashMap>, pub frusta: EntityHashMap>, pub render_layers: RenderLayers, + #[cfg(feature = "spectral_lighting")] + pub monochromaticity: f32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -85,6 +89,8 @@ pub struct GpuDirectionalLight { cascades_overlap_proportion: f32, depth_texture_base_index: u32, skip: u32, + #[cfg(feature = "spectral_lighting")] + monochromaticity: f32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -111,6 +117,8 @@ pub struct GpuLights { n_directional_lights: u32, // offset from spot light's light index to spot light's shadow map index spot_light_shadowmap_offset: i32, + #[cfg(feature = "spectral_lighting")] + ambient_monochromaticity: f32, } //NOTE: When running bevy on Adreno GPU chipsets in WebGL, any value above 1 will result in a crash @@ -258,6 +266,8 @@ pub fn extract_lights( * point_light_texel_size * std::f32::consts::SQRT_2, spot_light_angles: None, + #[cfg(feature = "spectral_lighting")] + monochromaticity: point_light.monochromaticity, }; point_lights_values.push(( entity, @@ -307,6 +317,8 @@ pub fn extract_lights( * texel_size * std::f32::consts::SQRT_2, spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)), + #[cfg(feature = "spectral_lighting")] + monochromaticity: spot_light.monochromaticity, }, render_visible_entities, *frustum, @@ -350,6 +362,8 @@ pub fn extract_lights( cascades: cascades.cascades.clone(), frusta: frusta.frusta.clone(), render_layers: maybe_layers.unwrap_or_default().clone(), + #[cfg(feature = "spectral_lighting")] + monochromaticity: directional_light.monochromaticity, }, render_visible_entities, )); @@ -730,6 +744,8 @@ pub fn prepare_lights( shadow_depth_bias: light.shadow_depth_bias, shadow_normal_bias: light.shadow_normal_bias, spot_light_tan_angle, + #[cfg(feature = "spectral_lighting")] + monochromaticity: light.monochromaticity, }); global_light_meta.entity_to_index.insert(entity, index); } @@ -776,6 +792,8 @@ pub fn prepare_lights( num_cascades: num_cascades as u32, cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, depth_texture_base_index: num_directional_cascades_enabled as u32, + #[cfg(feature = "spectral_lighting")] + monochromaticity: light.monochromaticity, }; if index < directional_shadow_enabled_count { num_directional_cascades_enabled += num_cascades; @@ -860,6 +878,8 @@ pub fn prepare_lights( // index to shadow map index, we need to subtract point light count and add directional shadowmap count. spot_light_shadowmap_offset: num_directional_cascades_enabled as i32 - point_light_count as i32, + #[cfg(feature = "spectral_lighting")] + ambient_monochromaticity: ambient_light.monochromaticity, }; // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index f3e52b3780640..c2ff06c3b1def 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1834,6 +1834,9 @@ impl SpecializedMeshPipeline for MeshPipeline { #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] shader_defs.push("WEBGL2".into()); + #[cfg(feature = "spectral_lighting")] + shader_defs.push("SPECTRAL_LIGHTING".into()); + if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push(ShaderDefVal::UInt( diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index c1d379e3b4c6e..512a02eff0a5a 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -11,6 +11,9 @@ struct ClusterableObject { shadow_depth_bias: f32, shadow_normal_bias: f32, spot_light_tan_angle: f32, +#ifdef SPECTRAL_LIGHTING + monochromaticity: f32, +#endif }; const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; @@ -34,6 +37,9 @@ struct DirectionalLight { cascades_overlap_proportion: f32, depth_texture_base_index: u32, skip: u32, +#ifdef SPECTRAL_LIGHTING + monochromaticity: f32, +#endif }; const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; @@ -58,8 +64,9 @@ struct Lights { cluster_factors: vec4, n_directional_lights: u32, spot_light_shadowmap_offset: i32, - environment_map_smallest_specular_mip_level: u32, - environment_map_intensity: f32, +#ifdef SPECTRAL_LIGHTING + ambient_monochromaticity: f32, +#endif }; struct Fog { @@ -152,4 +159,4 @@ struct ScreenSpaceReflectionsSettings { struct EnvironmentMapUniform { // Transformation matrix for the environment cubemaps in world space. transform: mat4x4, -}; \ No newline at end of file +}; diff --git a/crates/bevy_pbr/src/render/pbr_ambient.wgsl b/crates/bevy_pbr/src/render/pbr_ambient.wgsl index 7b174da35c9db..272c2b52e66c7 100644 --- a/crates/bevy_pbr/src/render/pbr_ambient.wgsl +++ b/crates/bevy_pbr/src/render/pbr_ambient.wgsl @@ -1,7 +1,7 @@ #define_import_path bevy_pbr::ambient #import bevy_pbr::{ - lighting::{EnvBRDFApprox, F_AB}, + lighting::{EnvBRDFApprox, F_AB, monochromaticity_blend}, mesh_view_bindings::lights, } @@ -25,5 +25,9 @@ fn ambient_light( // See: https://google.github.io/filament/Filament.html#specularocclusion let specular_occlusion = saturate(dot(specular_color, vec3(50.0 * 0.33))); +#ifdef SPECTRAL_LIGHTING + return monochromaticity_blend((diffuse_ambient + specular_ambient * specular_occlusion), lights.ambient_color.rgb * occlusion, lights.ambient_monochromaticity); +#else return (diffuse_ambient + specular_ambient * specular_occlusion) * lights.ambient_color.rgb * occlusion; +#endif } diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index 0e88642333643..a08a5065d83fa 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -6,6 +6,13 @@ } #import bevy_render::maths::PI +#ifdef SPECTRAL_LIGHTING +#import bevy_render::color_operations::{ + hsv_to_rgb, + rgb_to_hsv, +} +#endif + const LAYER_BASE: u32 = 0; const LAYER_CLEARCOAT: u32 = 1; @@ -532,8 +539,11 @@ fn point_light(light_id: u32, input: ptr) -> vec3 color = diffuse + specular_light; #endif // STANDARD_MATERIAL_CLEARCOAT - return color * (*light).color_inverse_square_range.rgb * - (rangeAttenuation * derived_input.NdotL); +#ifdef SPECTRAL_LIGHTING + return monochromaticity_blend(color, (*light).color_inverse_square_range.rgb * (rangeAttenuation * derived_input.NdotL), (*light).monochromaticity); +#else + return color * (*light).color_inverse_square_range.rgb * (rangeAttenuation * derived_input.NdotL); +#endif } fn spot_light(light_id: u32, input: ptr) -> vec3 { @@ -607,5 +617,37 @@ fn directional_light(light_id: u32, input: ptr) -> vec3 color = (diffuse + specular_light) * derived_input.NdotL; #endif // STANDARD_MATERIAL_CLEARCOAT +#ifdef SPECTRAL_LIGHTING + return monochromaticity_blend(color, (*light).color.rgb, (*light).monochromaticity); +#else return color * (*light).color.rgb; +#endif +} + +#ifdef SPECTRAL_LIGHTING +// Blends base and light colors taking into account the light's monochromaticity +fn monochromaticity_blend(base: vec3, light: vec3, monochromaticity: f32) -> vec3 { + // Convert both colors to HSV + let base_hsv = rgb_to_hsv(base); + let light_hsv = rgb_to_hsv(light); + + // Approximate a gaussian using a triangle function + let deviation = 2.0 * PI / 3.0; // 120° + let triangular = (max(0.0, deviation - abs(base_hsv.x - light_hsv.x)) / deviation); + + let response = mix( + 1.0, + triangular, + ( + base_hsv.y * // As the base color gets less saturated, the response to the light color is more uniform + light_hsv.y // Any < 1.0 value means a non-spectral monochromatic color (non-physically accurate) + ), + ) * base_hsv.z; + + return mix( + base * light, // Polychromatic (Multiplicative blending) + response * light, // Mono-chromatic (Hue + value-based blending) + monochromaticity, + ); } +#endif // SPECTRAL_LIGHTING diff --git a/docs/cargo_features.md b/docs/cargo_features.md index d2cbf06a23b64..b847a8ec780d2 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -78,6 +78,7 @@ The default feature set enables most of the expected features of a game engine, |serialize|Enable serialization support through serde| |shader_format_glsl|Enable support for shaders in GLSL| |shader_format_spirv|Enable support for shaders in SPIR-V| +|spectral_lighting|Enable support for spectral colors (monochromatic light) in lights| |symphonia-aac|AAC audio format support (through symphonia)| |symphonia-all|AAC, FLAC, MP3, MP4, OGG/VORBIS, and WAV audio formats support (through symphonia)| |symphonia-flac|FLAC audio format support (through symphonia)| diff --git a/examples/2d/custom_gltf_vertex_attribute.rs b/examples/2d/custom_gltf_vertex_attribute.rs index bf5ce108e9f32..f88a6758f9169 100644 --- a/examples/2d/custom_gltf_vertex_attribute.rs +++ b/examples/2d/custom_gltf_vertex_attribute.rs @@ -24,8 +24,8 @@ const ATTRIBUTE_BARYCENTRIC: MeshVertexAttribute = fn main() { App::new() .insert_resource(AmbientLight { - color: Color::WHITE, brightness: 1.0 / 5.0f32, + ..default() }) .add_plugins(( DefaultPlugins.set( diff --git a/examples/3d/auto_exposure.rs b/examples/3d/auto_exposure.rs index 49ef4ea6197ce..75bbaf1a3fdce 100644 --- a/examples/3d/auto_exposure.rs +++ b/examples/3d/auto_exposure.rs @@ -106,8 +106,8 @@ fn setup( } commands.insert_resource(AmbientLight { - color: Color::WHITE, brightness: 0.0, + ..default() }); commands.spawn(PointLightBundle { diff --git a/examples/3d/irradiance_volumes.rs b/examples/3d/irradiance_volumes.rs index 0c4c44f00bf60..2a287164d276f 100644 --- a/examples/3d/irradiance_volumes.rs +++ b/examples/3d/irradiance_volumes.rs @@ -155,8 +155,8 @@ fn main() { .init_resource::() .init_resource::() .insert_resource(AmbientLight { - color: Color::WHITE, brightness: 0.0, + ..default() }) .add_systems(Startup, setup) .add_systems(PreUpdate, create_cubes) diff --git a/examples/3d/lighting.rs b/examples/3d/lighting.rs index 4fec6372a69d0..1722430d6b39c 100644 --- a/examples/3d/lighting.rs +++ b/examples/3d/lighting.rs @@ -126,6 +126,7 @@ fn setup( commands.insert_resource(AmbientLight { color: ORANGE_RED.into(), brightness: 0.02, + ..default() }); // red point light diff --git a/examples/3d/motion_blur.rs b/examples/3d/motion_blur.rs index 11297e05275f5..795abdaede905 100644 --- a/examples/3d/motion_blur.rs +++ b/examples/3d/motion_blur.rs @@ -62,8 +62,8 @@ fn setup_scene( mut materials: ResMut>, ) { commands.insert_resource(AmbientLight { - color: Color::WHITE, brightness: 300.0, + ..default() }); commands.insert_resource(CameraMode::Chase); commands.spawn(DirectionalLightBundle { diff --git a/examples/3d/skybox.rs b/examples/3d/skybox.rs index 115d378e2e2d6..ad967eedcad74 100644 --- a/examples/3d/skybox.rs +++ b/examples/3d/skybox.rs @@ -91,6 +91,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.insert_resource(AmbientLight { color: Color::srgb_u8(210, 220, 240), brightness: 1.0, + ..default() }); commands.insert_resource(Cubemap { diff --git a/examples/3d/spectral_lighting.rs b/examples/3d/spectral_lighting.rs new file mode 100644 index 0000000000000..0308ce7a52e7a --- /dev/null +++ b/examples/3d/spectral_lighting.rs @@ -0,0 +1,228 @@ +//! Showcases support for spectral color (monochromatic) lights in a 3D scene. +//! +//! ## Controls +//! +//! | Key Binding | Action | +//! |:-------------------|:-----------------------------------------------------| +//! | Left/Right Arrows | Change wavelength | +//! | Up/Down Arrows | Change monochromaticity | +//! | Space | Toggle monochromaticity | + +use bevy::{ + color::palettes::css::{ANTIQUE_WHITE, WHITE}, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, update) + .insert_resource(AmbientLight { + brightness: 0.0, + ..default() + }) + .insert_resource(ClearColor(Color::srgb(0.0, 0.0, 0.0))) + .insert_resource(Config { + spectral_color: SpectralColor::SODIUM_VAPOR, + }) + .run(); +} + +#[derive(Resource)] +struct Config { + spectral_color: SpectralColor, +} + +#[derive(Component)] +struct Polychromatic; + +#[derive(Component)] +struct Monochromatic; + +#[derive(Component)] +struct Indicator; + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, +) { + // circular base + commands.spawn(PbrBundle { + mesh: meshes.add(Circle::new(4.0)), + material: materials.add(Color::WHITE), + transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + ..default() + }); + // red sphere + commands.spawn(PbrBundle { + mesh: meshes.add(Sphere::new(1.0)), + material: materials.add(Color::srgb(1.0, 0.0, 0.0)), + transform: Transform::from_xyz(-2.0, 1.0, 0.0), + ..default() + }); + // green sphere + commands.spawn(PbrBundle { + mesh: meshes.add(Sphere::new(1.0)), + material: materials.add(Color::srgb(0.0, 1.0, 0.0)), + transform: Transform::from_xyz(0.0, 1.0, 0.0), + ..default() + }); + // blue sphere + commands.spawn(PbrBundle { + mesh: meshes.add(Sphere::new(1.0)), + material: materials.add(Color::srgb(0.0, 0.0, 1.0)), + transform: Transform::from_xyz(2.0, 1.0, 0.0), + ..default() + }); + // HSV cubes + for j in 0..=5 { + for i in 0..=17 { + commands.spawn(PbrBundle { + mesh: meshes.add(Cuboid::new(0.5, 0.5, 0.5)), + material: materials.add(StandardMaterial { + base_color: if j == 5 { + // Grayscale band at the top + Color::hsva(0.0, 0.0, i as f32 / 17.0, 1.0) + } else { + Color::hsva(i as f32 * 15.0, j as f32 / 4.0, 1.0, 1.0) + }, + perceptual_roughness: 1.0, + reflectance: 0.0, + ..default() + }), + transform: Transform::from_xyz(-4.0 + i as f32 * 0.5, 2.5 + j as f32 * 0.5, 0.0), + ..default() + }); + } + } + // monochromatic light + commands.spawn(( + PointLightBundle { + point_light: PointLight { + color: SpectralColor::SODIUM_VAPOR.into(), + shadows_enabled: true, + #[cfg(feature = "spectral_lighting")] + monochromaticity: 1.0, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }, + Monochromatic, + )); + // polychromatic light + commands.spawn(( + PointLightBundle { + point_light: PointLight { + shadows_enabled: true, + intensity: 1000000.0, + ..default() + }, + transform: Transform::from_xyz(-5.0, 2.5, 0.0), + ..default() + }, + Polychromatic, + )); + // camera + commands.spawn(Camera3dBundle { + camera: Camera { ..default() }, + transform: Transform::from_xyz(-3.5, 7.0, 12.0) + .looking_at(Vec3::new(0.0, 2.5, 0.), Vec3::Y), + ..default() + }); + // UI + commands.spawn( + TextBundle::from_section("", TextStyle::default()).with_style(Style { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }), + ); + commands + .spawn(ImageBundle { + style: Style { + width: Val::Px(360.), + height: Val::Px(68.5), + position_type: PositionType::Absolute, + top: Val::Px(70.), + left: Val::Px(12.), + ..default() + }, + image: UiImage::new(asset_server.load("textures/Linear_visible_spectrum.png")), + background_color: BackgroundColor(ANTIQUE_WHITE.into()), + ..default() + }) + .with_children(|children| { + children.spawn(( + NodeBundle { + style: Style { + width: Val::Px(4.), + height: Val::Px(40.), + position_type: PositionType::Absolute, + bottom: Val::Px(1.), + left: Val::Px(0.), + ..default() + }, + border_radius: BorderRadius::all(Val::Px(2.)), + background_color: BackgroundColor(WHITE.into()), + ..default() + }, + Indicator, + )); + }); +} + +fn update( + mut text_query: Query<&mut Text>, + mut polychromatic_light_query: Query<&mut Transform, With>, + mut indicator_query: Query<&mut Style, With>, + mut monochromatic_light_query: Query<&mut PointLight, With>, + mut config: ResMut, + time: Res