Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
17 changes: 17 additions & 0 deletions crates/bevy_camera/src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
10 changes: 10 additions & 0 deletions crates/bevy_light/src/directional_light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -156,6 +165,7 @@ impl Default for DirectionalLight {
affects_lightmapped_mesh_diffuse: true,
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadow_size: None,
monochromatic: false,
}
}
}
Expand Down
12 changes: 11 additions & 1 deletion crates/bevy_light/src/point_light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
}
}
Expand All @@ -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)]
Expand Down
12 changes: 11 additions & 1 deletion crates/bevy_light/src/spot_light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
}
}
}
Expand Down Expand Up @@ -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)]
Expand Down
11 changes: 11 additions & 0 deletions crates/bevy_pbr/src/deferred/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
7 changes: 6 additions & 1 deletion crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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;
}
Expand Down
15 changes: 15 additions & 0 deletions crates/bevy_pbr/src/render/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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!
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 15 additions & 2 deletions crates/bevy_pbr/src/render/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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());
}
Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_pbr/src/render/mesh_view_types.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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<f32>,
Expand Down Expand Up @@ -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<f32>,
Expand Down
Loading
Loading