diff --git a/Cargo.toml b/Cargo.toml index 4f032612a2dee..d6482b935d4d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1358,6 +1358,17 @@ category = "3D Rendering" # Requires compute shaders, which are not supported by WebGL. wasm = false +[[example]] +name = "monochromatic_lights" +path = "examples/3d/monochromatic_lights.rs" +doc-scrape-examples = true + +[package.metadata.example.monochromatic_lights] +name = "Monochromatic Lights" +description = "Showcases support for monochromatic lights in a 3D scene" +category = "3D Rendering" +wasm = true + [[example]] name = "spotlight" path = "examples/3d/spotlight.rs" diff --git a/crates/bevy_camera/src/components.rs b/crates/bevy_camera/src/components.rs index b346cc945b8d8..f4eb797dacba8 100644 --- a/crates/bevy_camera/src/components.rs +++ b/crates/bevy_camera/src/components.rs @@ -100,3 +100,20 @@ pub enum CompositingSpace { /// Perceptually uniform blending. Often smoother gradients. Requires [`Hdr`] because its value can be outside [0, 1]. Oklab, } + +/// Model used to represent the light spectrum while rendering. +/// +/// Defaults to [`SpectralModel::Tristimulus`] +#[derive(Component, Copy, Clone, Reflect, PartialEq, Eq, Hash, Debug, Default)] +#[reflect(Component, PartialEq, Hash, Debug, Default)] +pub enum SpectralModel { + /// The “traditional” computer graphics model, using three color channels (i.e. RGB) for both materials and lights. + #[default] + Tristimulus, + + /// Extends [`SpectralModel::Tristimulus`] while also supporting monochromatic (single wavelength) light sources + /// via a fast HSV + triangle function-based gaussian approximation. + /// + /// Allows things like Violet or Sodium Vapor lights, with a small performance impact. + MonochromaticLights, +} diff --git a/crates/bevy_light/src/directional_light.rs b/crates/bevy_light/src/directional_light.rs index 1857d57bc4ed0..040c96d450322 100644 --- a/crates/bevy_light/src/directional_light.rs +++ b/crates/bevy_light/src/directional_light.rs @@ -142,6 +142,15 @@ pub struct DirectionalLight { /// is scaled to the shadow map's texel size so that it is automatically /// adjusted to the orthographic projection. pub shadow_normal_bias: f32, + + /// Whether this directional light is a source of [monochromatic light]. + /// + /// Must be paired with [`SpectralModel::MonochromaticLights`](bevy_camera::SpectralModel::MonochromaticLights) on the camera to have any effect. + /// + /// When combined with light colors that are non-spectral (e.g. white, magenta) produces non-physical results. + /// + /// [monochromatic light]:https://en.wikipedia.org/wiki/Monochromatic_radiation + pub monochromatic: bool, } impl Default for DirectionalLight { @@ -156,6 +165,7 @@ impl Default for DirectionalLight { affects_lightmapped_mesh_diffuse: true, #[cfg(feature = "experimental_pbr_pcss")] soft_shadow_size: None, + monochromatic: false, } } } diff --git a/crates/bevy_light/src/point_light.rs b/crates/bevy_light/src/point_light.rs index 6f3fcf390ced3..b852039a4c064 100644 --- a/crates/bevy_light/src/point_light.rs +++ b/crates/bevy_light/src/point_light.rs @@ -123,6 +123,15 @@ pub struct PointLight { /// /// This only has an effect if shadows are enabled. pub shadow_map_near_z: f32, + + /// Whether this point light is a source of [monochromatic light]. + /// + /// Must be paired with [`SpectralModel::MonochromaticLights`](bevy_camera::SpectralModel::MonochromaticLights) on the camera to have any effect. + /// + /// When combined with light colors that are non-spectral (e.g. white, magenta) produces non-physical results. + /// + /// [monochromatic light]:https://en.wikipedia.org/wiki/Monochromatic_radiation + pub monochromatic: bool, } impl Default for PointLight { @@ -140,6 +149,7 @@ impl Default for PointLight { shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z, #[cfg(feature = "experimental_pbr_pcss")] soft_shadows_enabled: false, + monochromatic: false, } } } @@ -154,7 +164,7 @@ impl PointLight { } /// Add to a [`PointLight`] to add a light texture effect. -/// A texture mask is applied to the light source to modulate its intensity, +/// A texture mask is applied to the light source to modulate its intensity, /// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. #[derive(Clone, Component, Debug, Reflect, FromTemplate)] #[reflect(Component, Debug)] diff --git a/crates/bevy_light/src/spot_light.rs b/crates/bevy_light/src/spot_light.rs index f770e696ca788..b822b27371c32 100644 --- a/crates/bevy_light/src/spot_light.rs +++ b/crates/bevy_light/src/spot_light.rs @@ -134,6 +134,15 @@ 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, + + /// Whether this spot light is a source of [monochromatic light]. + /// + /// Must be paired with [`SpectralModel::MonochromaticLights`](bevy_camera::SpectralModel::MonochromaticLights) on the camera to have any effect. + /// + /// When combined with light colors that are non-spectral (e.g. white, magenta) produces non-physical results. + /// + /// [monochromatic light]:https://en.wikipedia.org/wiki/Monochromatic_radiation + pub monochromatic: bool, } impl SpotLight { @@ -166,6 +175,7 @@ impl Default for SpotLight { outer_angle: core::f32::consts::FRAC_PI_4, #[cfg(feature = "experimental_pbr_pcss")] soft_shadows_enabled: false, + monochromatic: false, } } } @@ -206,7 +216,7 @@ pub fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 { } /// Add to a [`SpotLight`] to add a light texture effect. -/// A texture mask is applied to the light source to modulate its intensity, +/// A texture mask is applied to the light source to modulate its intensity, /// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. #[derive(Clone, Component, Debug, Reflect, FromTemplate)] #[reflect(Component, Debug)] diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index e2b9450774bd9..5ef9c9abe06df 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -6,6 +6,7 @@ use crate::{ }; use bevy_app::prelude::*; use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use bevy_camera::SpectralModel; use bevy_core_pipeline::{ core_3d::main_opaque_pass_3d, deferred::{ @@ -287,9 +288,15 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { if key.contains(MeshPipelineKey::DISTANCE_FOG) { shader_defs.push("DISTANCE_FOG".into()); } + if key.contains(MeshPipelineKey::ATMOSPHERE) { shader_defs.push("ATMOSPHERE".into()); } + + if key.contains(MeshPipelineKey::MONOCHROMATIC_LIGHTS) { + shader_defs.push("MONOCHROMATIC_LIGHTS".into()); + } + shader_defs.push("STANDARD_MATERIAL_CLEARCOAT".into()); // Always true, since we're in the deferred lighting pipeline @@ -502,6 +509,10 @@ pub fn prepare_deferred_lighting_pipelines( } } + if let Some(SpectralModel::MonochromaticLights) = camera.spectral_model { + view_key |= MeshPipelineKey::MONOCHROMATIC_LIGHTS; + } + if ssao { view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; } diff --git a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index 4160cdfd0a6b5..842a3446ab385 100644 --- a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -3,7 +3,7 @@ use super::{ resource_manager::ResourceManager, }; use crate::*; -use bevy_camera::{Camera3d, Projection}; +use bevy_camera::{Camera3d, Projection, SpectralModel}; use bevy_core_pipeline::{ prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, tonemapping::{DebandDither, Tonemapping}, @@ -138,9 +138,14 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( } } + if let Some(SpectralModel::MonochromaticLights) = camera.spectral_model { + view_key |= MeshPipelineKey::MONOCHROMATIC_LIGHTS; + } + if ssao { view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; } + if distance_fog { view_key |= MeshPipelineKey::DISTANCE_FOG; } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index fa35677f0f558..e80ca5b3a765a 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -93,6 +93,7 @@ pub struct ExtractedPointLight { pub soft_shadows_enabled: bool, /// whether this point light contributes diffuse light to lightmapped meshes pub affects_lightmapped_mesh_diffuse: bool, + pub monochromatic: bool, } #[derive(Component, Debug)] @@ -127,6 +128,7 @@ pub struct ExtractedDirectionalLight { pub occlusion_culling: bool, pub sun_disk_angular_size: f32, pub sun_disk_intensity: f32, + pub monochromatic: bool, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -139,6 +141,7 @@ bitflags::bitflags! { const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 3; const CONTACT_SHADOWS_ENABLED = 1 << 4; const SPOT_LIGHT = 1 << 5; + const MONOCHROMATIC = 1 << 6; const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -176,6 +179,7 @@ bitflags::bitflags! { const VOLUMETRIC = 1 << 1; const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 2; const CONTACT_SHADOWS_ENABLED = 1 << 3; + const MONOCHROMATIC = 1 << 4; const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -546,6 +550,7 @@ pub fn extract_lights( soft_shadows_enabled: point_light.soft_shadows_enabled, #[cfg(not(feature = "experimental_pbr_pcss"))] soft_shadows_enabled: false, + monochromatic: point_light.monochromatic, }; entity_commands.insert(( extracted_point_light, @@ -659,6 +664,7 @@ pub fn extract_lights( soft_shadows_enabled: spot_light.soft_shadows_enabled, #[cfg(not(feature = "experimental_pbr_pcss"))] soft_shadows_enabled: false, + monochromatic: spot_light.monochromatic, }; entity_commands.insert(( extracted_spot_light, @@ -816,6 +822,7 @@ pub fn extract_lights( occlusion_culling, sun_disk_angular_size: sun_disk.unwrap_or_default().angular_size, sun_disk_intensity: sun_disk.unwrap_or_default().intensity, + monochromatic: directional_light.monochromatic, }; let mut entity_commands = commands @@ -1248,6 +1255,10 @@ pub fn prepare_lights( flags |= PointLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; } + if light.monochromatic { + flags |= PointLightFlags::MONOCHROMATIC; + } + let (light_custom_data, spot_light_tan_angle) = match light.spot_light_angles { Some((inner, outer)) => { flags |= PointLightFlags::SPOT_LIGHT; @@ -1828,6 +1839,10 @@ pub fn prepare_lights( flags |= DirectionalLightFlags::VOLUMETRIC; } + if light.monochromatic { + flags |= DirectionalLightFlags::MONOCHROMATIC; + } + // Shadow enabled lights are second let mut num_cascades = 0; if light.shadow_maps_enabled { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 1938a6e3a9f37..cd58536ac884f 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -11,7 +11,7 @@ use bevy_camera::visibility::NoCpuCulling; use bevy_camera::{ primitives::Aabb, visibility::{NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange}, - Camera, Projection, + Camera, Projection, SpectralModel, }; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, @@ -484,6 +484,13 @@ pub fn check_views_need_specialization( view_key |= MeshPipelineKey::DEBAND_DITHER; } } + + if let Some(camera) = camera + && let Some(SpectralModel::MonochromaticLights) = camera.spectral_model + { + view_key |= MeshPipelineKey::MONOCHROMATIC_LIGHTS; + } + if ssao { view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; } @@ -3022,7 +3029,8 @@ bitflags::bitflags! { const INVERT_CULLING = 1 << 22; const PREPASS_READS_MATERIAL = 1 << 23; const CONTACT_SHADOWS = 1 << 24; - const LAST_FLAG = Self::CONTACT_SHADOWS.bits(); + const MONOCHROMATIC_LIGHTS = 1 << 25; + const LAST_FLAG = Self::MONOCHROMATIC_LIGHTS.bits(); const ALL_PREPASS_BITS = Self::DEPTH_PREPASS.bits() | Self::NORMAL_PREPASS.bits() @@ -3542,6 +3550,7 @@ impl SpecializedMeshPipeline for MeshPipeline { if key.contains(MeshPipelineKey::LIGHTMAPPED) { shader_defs.push("LIGHTMAP".into()); } + if key.contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) { shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into()); } @@ -3550,6 +3559,10 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("TEMPORAL_JITTER".into()); } + if key.contains(MeshPipelineKey::MONOCHROMATIC_LIGHTS) { + shader_defs.push("MONOCHROMATIC_LIGHTS".into()); + } + let shadow_filter_method = key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS); if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 { diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 9e5a7f132f975..668c895cadc4d 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -23,6 +23,7 @@ const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 2u; const POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 3u; const POINT_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT: u32 = 1u << 4u; const POINT_LIGHT_FLAGS_SPOT_LIGHT_BIT: u32 = 1u << 5u; +const POINT_LIGHT_FLAGS_MONOCHROMATIC_BIT: u32 = 1u << 6u; struct DirectionalCascade { clip_from_world: mat4x4, @@ -51,6 +52,7 @@ const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << const DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 1u; const DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 2u; const DIRECTIONAL_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT: u32 = 1u << 3u; +const DIRECTIONAL_LIGHT_FLAGS_MONOCHROMATIC_BIT: u32 = 1u << 4u; struct RectLight { color: vec4, diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index e9fb29ee34d20..8f7de6bcc89fe 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -1,13 +1,24 @@ #define_import_path bevy_pbr::lighting #import bevy_pbr::{ - mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, + mesh_view_types::{ + POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, + POINT_LIGHT_FLAGS_MONOCHROMATIC_BIT, + DIRECTIONAL_LIGHT_FLAGS_MONOCHROMATIC_BIT, + }, mesh_view_bindings as view_bindings, atmosphere::functions::{calculate_visible_sun_ratio, clamp_to_surface}, atmosphere::bruneton_functions::transmittance_lut_r_mu_to_uv, } #import bevy_render::maths::{PI, orthonormalize} +#ifdef MONOCHROMATIC_LIGHTS +#import bevy_render::color_operations::{ + hsv_to_rgb, + rgb_to_hsv, +} +#endif + const LAYER_BASE: u32 = 0; const LAYER_CLEARCOAT: u32 = 1; @@ -781,6 +792,13 @@ fn point_light( } #endif +#ifdef MONOCHROMATIC_LIGHTS + if ((*light).flags & POINT_LIGHT_FLAGS_MONOCHROMATIC_BIT) != 0u { + let base_color = color_times_NdotL; + let light_color = (*light).color_inverse_square_range.rgb * rangeAttenuation * texture_sample; + return monochromatic_response(color_times_NdotL, (*light).color_inverse_square_range.rgb * rangeAttenuation * texture_sample); + } +#endif return color_times_NdotL * (*light).color_inverse_square_range.rgb * rangeAttenuation * texture_sample; } @@ -914,7 +932,17 @@ fn directional_light( } #endif -color *= (*light).color.rgb * texture_sample; +#ifdef MONOCHROMATIC_LIGHTS + if ((*light).flags & DIRECTIONAL_LIGHT_FLAGS_MONOCHROMATIC_BIT) != 0u { + let base_color = color; + let light_color = (*light).color.rgb * texture_sample; + color = monochromatic_response(base_color, light_color); + } else { + color *= (*light).color.rgb * texture_sample; + } +#else + color *= (*light).color.rgb * texture_sample; +#endif #ifdef ATMOSPHERE let P = (*input).P; @@ -1109,3 +1137,28 @@ fn sample_transmittance_lut(r: f32, mu: f32) -> vec3 { view_bindings::atmosphere_transmittance_sampler, uv, 0.0).rgb; } #endif // ATMOSPHERE + +#ifdef MONOCHROMATIC_LIGHTS +// Efficiently approximates the response of a base color to a monochromatic (i.e. single frequency) +// light source, using RGB -> HSV conversion and a triangle function +fn monochromatic_response(base: vec3, light: vec3) -> 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 intensity = 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 intensity * light; +} +#endif // MONOCHROMATIC_LIGHTS diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index 29d15a00b0b97..c4924dc3351ac 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -24,7 +24,8 @@ use bevy_camera::{ visibility::{self, RenderLayers, VisibleEntities}, Camera, Camera2d, Camera3d, CameraMainTextureUsages, CameraOutputMode, CameraUpdateSystems, ClearColor, ClearColorConfig, CompositingSpace, Exposure, Hdr, ManualTextureViewHandle, - MsaaWriteback, NormalizedRenderTarget, Projection, RenderTarget, RenderTargetInfo, Viewport, + MsaaWriteback, NormalizedRenderTarget, Projection, RenderTarget, RenderTargetInfo, + SpectralModel, Viewport, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -468,6 +469,7 @@ pub struct ExtractedCamera { /// When [`CompositingSpace::Srgb`], the main texture uses linear storage (`Rgba8Unorm`) /// and shaders output sRGB-encoded values for gamma-encoded blending. pub compositing_space: Option, + pub spectral_model: Option, } pub fn extract_cameras( @@ -492,6 +494,7 @@ pub fn extract_cameras( Option<&MipBias>, Option<&RenderLayers>, Option<&Projection>, + Option<&SpectralModel>, Has, ), )>, @@ -539,6 +542,7 @@ pub fn extract_cameras( mip_bias, render_layers, projection, + spectral_model, no_indirect_drawing, ), ) in query.iter() @@ -639,6 +643,7 @@ pub fn extract_cameras( .unwrap_or_else(|| Exposure::default().exposure()), hdr, compositing_space: compositing_space.copied(), + spectral_model: spectral_model.copied(), }, ExtractedView { retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0), diff --git a/examples/3d/monochromatic_lights.rs b/examples/3d/monochromatic_lights.rs new file mode 100644 index 0000000000000..d894a2e16e062 --- /dev/null +++ b/examples/3d/monochromatic_lights.rs @@ -0,0 +1,196 @@ +//! Showcases support for monochromatic lights in a 3D scene. +//! +//! ## Controls +//! +//! | Key Binding | Action | +//! |:-------------------|:-----------------------------------------------------| +//! | 1–6 | Switch light preset | + +use bevy::{camera::SpectralModel, prelude::*}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, update) + .insert_resource(GlobalAmbientLight { + brightness: 0.0, + ..default() + }) + .insert_resource(ClearColor(Color::srgb(0.0, 0.0, 0.0))) + .run(); +} + +const AMBER: Color = Color::linear_rgb(1.0, 0.216, 0.0); +const PURPLE: Color = Color::linear_rgb(0.164, 0.0, 1.0); + +#[derive(Debug, Default)] +enum LightPreset { + #[default] + White, + Amber, + SodiumVapor, + Purple, + Violet, + MonochromaticWhite, // Non-physical! +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // camera + commands.spawn(( + Camera3d::default(), + SpectralModel::MonochromaticLights, // Important: Enables monochromatic lights support + Transform::from_xyz(-3.5, 7.0, 12.0).looking_at(Vec3::new(0.0, 2.5, 0.), Vec3::Y), + )); + + // circular base + commands.spawn(( + Mesh3d(meshes.add(Circle::new(4.0))), + MeshMaterial3d(materials.add(Color::WHITE)), + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )); + + // red sphere + commands.spawn(( + Mesh3d(meshes.add(Sphere::new(1.0))), + MeshMaterial3d(materials.add(Color::srgb(1.0, 0.0, 0.0))), + Transform::from_xyz(-2.0, 1.0, 0.0), + )); + + // green sphere + commands.spawn(( + Mesh3d(meshes.add(Sphere::new(1.0))), + MeshMaterial3d(materials.add(Color::srgb(0.0, 1.0, 0.0))), + Transform::from_xyz(0.0, 1.0, 0.0), + )); + + // blue sphere + commands.spawn(( + Mesh3d(meshes.add(Sphere::new(1.0))), + MeshMaterial3d(materials.add(Color::srgb(0.0, 0.0, 1.0))), + Transform::from_xyz(2.0, 1.0, 0.0), + )); + + // HSV cubes + for j in 0..=5 { + for i in 0..=17 { + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: if j == 5 { + // Grayscale band at the top + Color::hsv(0.0, 0.0, i as f32 / 17.0) + } else { + Color::hsv(i as f32 * 15.0, j as f32 / 4.0, 1.0) + }, + perceptual_roughness: 1.0, + reflectance: 0.0, + ..default() + })), + Transform::from_xyz(-4.0 + i as f32 * 0.5, 2.5 + j as f32 * 0.5, 0.0), + )); + } + } + + // point light + commands.spawn(( + PointLight { + shadow_maps_enabled: true, + + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0), + )); + + // UI + commands.spawn(( + Node { + position_type: PositionType::Absolute, + padding: UiRect::all(px(5)), + ..default() + }, + children![(Text::default(), children![TextSpan::new("")])], + )); +} + +fn update( + mut text_query: Query<&mut TextSpan>, + mut light_query: Query<&mut PointLight>, + mut light_type: Local, + keyboard: Res>, +) -> Result { + let mut light = light_query.single_mut()?; + let mut text = text_query.single_mut()?; + + if keyboard.just_pressed(KeyCode::Digit1) { + *light_type = LightPreset::White; + } + + if keyboard.just_pressed(KeyCode::Digit2) { + *light_type = LightPreset::Amber; + } + + if keyboard.just_pressed(KeyCode::Digit3) { + *light_type = LightPreset::SodiumVapor; + } + + if keyboard.just_pressed(KeyCode::Digit4) { + *light_type = LightPreset::Purple; + } + + if keyboard.just_pressed(KeyCode::Digit5) { + *light_type = LightPreset::Violet; + } + + if keyboard.just_pressed(KeyCode::Digit6) { + *light_type = LightPreset::MonochromaticWhite; + } + + match *light_type { + LightPreset::Amber => { + light.color = AMBER; + light.monochromatic = false; + } + LightPreset::SodiumVapor => { + light.color = AMBER; + light.monochromatic = true; + } + LightPreset::Purple => { + light.color = PURPLE; + light.monochromatic = false; + } + LightPreset::Violet => { + // See: https://en.wikipedia.org/wiki/Violet_(color)#Relationship_to_purple + light.color = PURPLE; + light.monochromatic = true; + } + LightPreset::White => { + light.color = Color::WHITE; + light.monochromatic = false; + } + LightPreset::MonochromaticWhite => { + light.color = Color::WHITE; + light.monochromatic = true; + } + } + + let linear_rgb = light.color.to_linear(); + + text.0 = format!( + "Preset:\n{} 1. White Light (e.g. Sun or Cool LED Light)\n{} 2. Amber Polychromatic Light (e.g. Incandescent or Warm LED Light)\n{} 3. Amber Monochromatic Light (e.g. Sodium Vapor Light)\n{} 4. Purple Light\n{} 5. Violet Light\n{} 6. White Monochromatic Light (Non-Physical!)\n\nMonochromatic: {}\nR: {}\nG: {}\nB: {}", + if let LightPreset::White = *light_type { "*" } else { " " }, + if let LightPreset::Amber = *light_type { "*" } else { " " }, + if let LightPreset::SodiumVapor = *light_type { "*" } else { " " }, + if let LightPreset::Purple = *light_type { "*" } else { " " }, + if let LightPreset::Violet = *light_type { "*" } else { " " }, + if let LightPreset::MonochromaticWhite = *light_type { "*" } else { " " }, + light.monochromatic, linear_rgb.red, linear_rgb.green, linear_rgb.blue + ); + + Ok(()) +} diff --git a/examples/README.md b/examples/README.md index dc8c5650f375e..4a7a81a5f8ad4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -174,6 +174,7 @@ Example | Description [Meshlet](../examples/3d/meshlet.rs) | Meshlet rendering for dense high-poly scenes (experimental) [Mirror](../examples/3d/mirror.rs) | Demonstrates how to create a mirror with a second camera [Mixed lighting](../examples/3d/mixed_lighting.rs) | Demonstrates how to combine baked and dynamic lighting +[Monochromatic Lights](../examples/3d/monochromatic_lights.rs) | Showcases support for monochromatic lights in a 3D scene [Motion Blur](../examples/3d/motion_blur.rs) | Demonstrates per-pixel motion blur [Occlusion Culling](../examples/3d/occlusion_culling.rs) | Demonstration of Occlusion Culling [Order Independent Transparency](../examples/3d/order_independent_transparency.rs) | Demonstrates how to use OIT