diff --git a/Cargo.toml b/Cargo.toml index 2b2d981196774..40cab1e3c107c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -297,6 +297,26 @@ description = "A scene showcasing the built-in 3D shapes" category = "3D Rendering" wasm = true +[[example]] +name = "atmospheric_fog" +path = "examples/3d/atmospheric_fog.rs" + +[package.metadata.example.atmospheric_fog] +name = "Atmospheric Fog" +description = "A scene showcasing the atmospheric fog effect" +category = "3D Rendering" +wasm = true + +[[example]] +name = "fog" +path = "examples/3d/fog.rs" + +[package.metadata.example.fog] +name = "Fog" +description = "A scene showcasing the distance fog effect" +category = "3D Rendering" +wasm = true + [[example]] name = "blend_modes" path = "examples/3d/blend_modes.rs" diff --git a/assets/models/terrain/Mountains.bin b/assets/models/terrain/Mountains.bin new file mode 100644 index 0000000000000..399b8de820320 Binary files /dev/null and b/assets/models/terrain/Mountains.bin differ diff --git a/assets/models/terrain/Mountains.gltf b/assets/models/terrain/Mountains.gltf new file mode 100644 index 0000000000000..e9a10389b8bee --- /dev/null +++ b/assets/models/terrain/Mountains.gltf @@ -0,0 +1,143 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v3.3.17", + "version" : "2.0" + }, + "extensionsUsed" : [ + "KHR_materials_specular", + "KHR_materials_ior" + ], + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0, + 1 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Grid" + }, + { + "mesh" : 0, + "name" : "Grid.001", + "translation" : [ + 0.0018983177142217755, + -2.7217100068810396e-05, + 0.0012765892315655947 + ] + } + ], + "materials" : [ + { + "doubleSided" : true, + "extensions" : { + "KHR_materials_specular" : { + "specularColorFactor" : [ + 0, + 0, + 0 + ] + }, + "KHR_materials_ior" : { + "ior" : 1.4500000476837158 + } + }, + "name" : "Material.001", + "pbrMetallicRoughness" : { + "baseColorFactor" : [ + 0.12338346652686596, + 0.35653680562973022, + 0.065849664583802223, + 1 + ], + "metallicFactor" : 0, + "roughnessFactor" : 0.9980237483978271 + } + } + ], + "meshes" : [ + { + "name" : "Grid", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3, + "material" : 0 + } + ] + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 6561, + "max" : [ + 1.0000003576278687, + 0.2493455857038498, + 1.0051095485687256 + ], + "min" : [ + -1.0000003576278687, + -0.08555418252944946, + -1.0000003576278687 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 6561, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 6561, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 38400, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 78732, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 78732, + "byteOffset" : 78732 + }, + { + "buffer" : 0, + "byteLength" : 52488, + "byteOffset" : 157464 + }, + { + "buffer" : 0, + "byteLength" : 76800, + "byteOffset" : 209952 + } + ], + "buffers" : [ + { + "byteLength" : 286752, + "uri" : "Mountains.bin" + } + ] +} diff --git a/crates/bevy_pbr/src/fog.rs b/crates/bevy_pbr/src/fog.rs new file mode 100644 index 0000000000000..c5ad092f5467a --- /dev/null +++ b/crates/bevy_pbr/src/fog.rs @@ -0,0 +1,486 @@ +use crate::ReflectComponent; +use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_math::Vec3; +use bevy_reflect::Reflect; +use bevy_render::{color::Color, extract_component::ExtractComponent, prelude::Camera}; + +/// Configures the “classic” computer graphics [distance fog](https://en.wikipedia.org/wiki/Distance_fog) effect, +/// in which objects appear progressively more covered in atmospheric haze the further away they are from the camera. +/// Affects meshes rendered via the PBR [`StandardMaterial`](crate::StandardMaterial). +/// +/// ## Falloff +/// +/// The rate at which fog intensity increases with distance is controlled by the falloff mode. +/// Currently, the following fog falloff modes are supported: +/// +/// - [`FogFalloff::Linear`] +/// - [`FogFalloff::Exponential`] +/// - [`FogFalloff::ExponentialSquared`] +/// - [`FogFalloff::Atmospheric`] +/// +/// ## Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::prelude::*; +/// # use bevy_core_pipeline::prelude::*; +/// # use bevy_pbr::prelude::*; +/// # fn system(mut commands: Commands) { +/// commands.spawn(( +/// // Setup your camera as usual +/// Camera3dBundle { +/// // ... camera options +/// # ..Default::default() +/// }, +/// // Add fog to the same entity +/// FogSettings { +/// color: Color::WHITE, +/// falloff: FogFalloff::Exponential { density: 1e-3 }, +/// ..Default::default() +/// }, +/// )); +/// # } +/// # bevy_ecs::system::assert_is_system(system); +/// ``` +/// +/// ## Material Override +/// +/// Once enabled for a specific camera, the fog effect can also be disabled for individual +/// [`StandardMaterial`](crate::StandardMaterial) instances via the `fog_enabled` flag. +#[derive(Debug, Clone, Component, Reflect)] +#[reflect(Component)] +pub struct FogSettings { + /// The color of the fog effect. + /// + /// **Tip:** The alpha channel of the color can be used to “modulate” the fog effect without + /// changing the fog falloff mode or parameters. + pub color: Color, + + /// Color used to modulate the influence of directional light colors on the + /// fog, where the view direction aligns with each directional light direction, + /// producing a “glow” or light dispersion effect. (e.g. around the sun) + /// + /// Use [`Color::NONE`] to disable the effect. + pub directional_light_color: Color, + + /// The exponent applied to the directional light alignment calculation. + /// A higher value means a more concentrated “glow”. + pub directional_light_exponent: f32, + + /// Determines which falloff mode to use, and its parameters. + pub falloff: FogFalloff, +} + +/// Allows switching between different fog falloff modes, and configuring their parameters. +/// +/// ## Convenience Methods +/// +/// When using non-linear fog modes it can be hard to determine the right parameter values +/// for a given scene. +/// +/// For easier artistic control, instead of creating the enum variants directly, you can use the +/// visibility-based convenience methods: +/// +/// - For `FogFalloff::Exponential`: +/// - [`FogFalloff::from_visibility()`] +/// - [`FogFalloff::from_visibility_contrast()`] +/// +/// - For `FogFalloff::ExponentialSquared`: +/// - [`FogFalloff::from_visibility_squared()`] +/// - [`FogFalloff::from_visibility_contrast_squared()`] +/// +/// - For `FogFalloff::Atmospheric`: +/// - [`FogFalloff::from_visibility_color()`] +/// - [`FogFalloff::from_visibility_colors()`] +/// - [`FogFalloff::from_visibility_contrast_color()`] +/// - [`FogFalloff::from_visibility_contrast_colors()`] +#[derive(Debug, Clone, Reflect)] +pub enum FogFalloff { + /// A linear fog falloff that grows in intensity between `start` and `end` distances. + /// + /// This falloff mode is simpler to control than other modes, however it can produce results that look “artificial”, depending on the scene. + /// + /// ## Formula + /// + /// The fog intensity for a given point in the scene is determined by the following formula: + /// + /// ```text + /// let fog_intensity = 1.0 - ((end - distance) / (end - start)).clamp(0.0, 1.0); + /// ``` + /// + /// + /// Plot showing how linear fog falloff behaves for start and end values of 0.8 and 2.2, respectively. + /// + /// 1 + /// 1 + /// 0 + /// 2 + /// 3 + /// distance + /// fog intensity + /// + /// + /// + /// start + /// end + /// + Linear { + /// Distance from the camera where fog is completely transparent, in world units. + start: f32, + + /// Distance from the camera where fog is completely opaque, in world units. + end: f32, + }, + + /// An exponential fog falloff with a given `density`. + /// + /// Initially gains intensity quickly with distance, then more slowly. Typically produces more natural results than [`FogFalloff::Linear`], + /// but is a bit harder to control. + /// + /// To move the fog “further away”, use lower density values. To move it “closer” use higher density values. + /// + /// ## Tips + /// + /// - Use the [`FogFalloff::from_visibility()`] convenience method to create an exponential falloff with the proper + /// density for a desired visibility distance in world units; + /// - It's not _unusual_ to have very large or very small values for the density, depending on the scene + /// scale. Typically, for scenes with objects in the scale of thousands of units, you might want density values + /// in the ballpark of `0.001`. Conversely, for really small scale scenes you might want really high values of + /// density; + /// - Combine the `density` parameter with the [`FogSettings`] `color`'s alpha channel for easier artistic control. + /// + /// ## Formula + /// + /// The fog intensity for a given point in the scene is determined by the following formula: + /// + /// ```text + /// let fog_intensity = 1.0 - 1.0 / (distance * density).exp(); + /// ``` + /// + /// + /// Plot showing how exponential fog falloff behaves for different density values + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// 1 + /// 1 + /// 0 + /// 2 + /// 3 + /// density = 2 + /// density = 1 + /// density = 0.5 + /// distance + /// fog intensity + /// + Exponential { + /// Multiplier applied to the world distance (within the exponential fog falloff calculation). + density: f32, + }, + + /// A squared exponential fog falloff with a given `density`. + /// + /// Similar to [`FogFalloff::Exponential`], but grows more slowly in intensity for closer distances + /// before “catching up”. + /// + /// To move the fog “further away”, use lower density values. To move it “closer” use higher density values. + /// + /// ## Tips + /// + /// - Use the [`FogFalloff::from_visibility_squared()`] convenience method to create an exponential squared falloff + /// with the proper density for a desired visibility distance in world units; + /// - Combine the `density` parameter with the [`FogSettings`] `color`'s alpha channel for easier artistic control. + /// + /// ## Formula + /// + /// The fog intensity for a given point in the scene is determined by the following formula: + /// + /// ```text + /// let fog_intensity = 1.0 - 1.0 / (distance * density).powi(2).exp(); + /// ``` + /// + /// + /// Plot showing how exponential squared fog falloff behaves for different density values + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// 1 + /// 1 + /// 0 + /// 2 + /// 3 + /// density = 2 + /// density = 1 + /// density = 0.5 + /// distance + /// fog intensity + /// + ExponentialSquared { + /// Multiplier applied to the world distance (within the exponential squared fog falloff calculation). + density: f32, + }, + + /// A more general form of the [`FogFalloff::Exponential`] mode. The falloff formula is separated into + /// two terms, `extinction` and `inscattering`, for a somewhat simplified atmospheric scattering model. + /// Additionally, individual color channels can have their own density values, resulting in a total of + /// six different configuration parameters. + /// + /// ## Tips + /// + /// - Use the [`FogFalloff::from_visibility_colors()`] or [`FogFalloff::from_visibility_color()`] convenience methods + /// to create an atmospheric falloff with the proper densities for a desired visibility distance in world units and + /// extinction and inscattering colors; + /// - Combine the atmospheric fog parameters with the [`FogSettings`] `color`'s alpha channel for easier artistic control. + /// + /// ## Formula + /// + /// Unlike other modes, atmospheric falloff doesn't use a simple intensity-based blend of fog color with + /// object color. Instead, it calculates per-channel extinction and inscattering factors, which are + /// then used to calculate the final color. + /// + /// ```text + /// let extinction_factor = 1.0 - 1.0 / (distance * extinction).exp(); + /// let inscattering_factor = 1.0 - 1.0 / (distance * inscattering).exp(); + /// let result = input_color * (1.0 - extinction_factor) + fog_color * inscattering_factor; + /// ``` + /// + /// ## Equivalence to [`FogFalloff::Exponential`] + /// + /// For a density value of `D`, the following two falloff modes will produce identical visual results: + /// + /// ``` + /// # use bevy_pbr::prelude::*; + /// # use bevy_math::prelude::*; + /// # const D: f32 = 0.5; + /// # + /// let exponential = FogFalloff::Exponential { + /// density: D, + /// }; + /// + /// let atmospheric = FogFalloff::Atmospheric { + /// extinction: Vec3::new(D, D, D), + /// inscattering: Vec3::new(D, D, D), + /// }; + /// ``` + /// + /// **Note:** While the results are identical, [`FogFalloff::Atmospheric`] is computationally more expensive. + Atmospheric { + /// Controls how much light is removed due to atmospheric “extinction”, i.e. loss of light due to + /// photons being absorbed by atmospheric particles. + /// + /// Each component can be thought of as an independent per `R`/`G`/`B` channel `density` factor from + /// [`FogFalloff::Exponential`]: Multiplier applied to the world distance (within the fog + /// falloff calculation) for that specific channel. + /// + /// **Note:** + /// This value is not a `Color`, since it affects the channels exponentially in a non-intuitive way. + /// For artistic control, use the [`FogFalloff::from_visibility_colors()`] convenience method. + extinction: Vec3, + + /// Controls how much light is added due to light scattering from the sun through the atmosphere. + /// + /// Each component can be thought of as an independent per `R`/`G`/`B` channel `density` factor from + /// [`FogFalloff::Exponential`]: A multiplier applied to the world distance (within the fog + /// falloff calculation) for that specific channel. + /// + /// **Note:** + /// This value is not a `Color`, since it affects the channels exponentially in a non-intuitive way. + /// For artistic control, use the [`FogFalloff::from_visibility_colors()`] convenience method. + inscattering: Vec3, + }, +} + +impl FogFalloff { + /// Creates a [`FogFalloff::Exponential`] value from the given visibility distance in world units, + /// using the revised Koschmieder contrast threshold, [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + pub fn from_visibility(visibility: f32) -> FogFalloff { + FogFalloff::from_visibility_contrast( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + ) + } + + /// Creates a [`FogFalloff::Exponential`] value from the given visibility distance in world units, + /// and a given contrast threshold in the range of `0.0` to `1.0`. + pub fn from_visibility_contrast(visibility: f32, contrast_threshold: f32) -> FogFalloff { + FogFalloff::Exponential { + density: FogFalloff::koschmieder(visibility, contrast_threshold), + } + } + + /// Creates a [`FogFalloff::ExponentialSquared`] value from the given visibility distance in world units, + /// using the revised Koschmieder contrast threshold, [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + pub fn from_visibility_squared(visibility: f32) -> FogFalloff { + FogFalloff::from_visibility_contrast_squared( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + ) + } + + /// Creates a [`FogFalloff::ExponentialSquared`] value from the given visibility distance in world units, + /// and a given contrast threshold in the range of `0.0` to `1.0`. + pub fn from_visibility_contrast_squared( + visibility: f32, + contrast_threshold: f32, + ) -> FogFalloff { + FogFalloff::ExponentialSquared { + density: (FogFalloff::koschmieder(visibility, contrast_threshold) / visibility).sqrt(), + } + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// and a shared color for both extinction and inscattering, using the revised Koschmieder contrast threshold, + /// [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + pub fn from_visibility_color( + visibility: f32, + extinction_inscattering_color: Color, + ) -> FogFalloff { + FogFalloff::from_visibility_contrast_colors( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + extinction_inscattering_color, + extinction_inscattering_color, + ) + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// extinction and inscattering colors, using the revised Koschmieder contrast threshold, + /// [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + /// + /// ## Tips + /// - Alpha values of the provided colors can modulate the `extinction` and `inscattering` effects; + /// - Using an `extinction_color` of [`Color::WHITE`] or [`Color::NONE`] disables the extinction effect; + /// - Using an `inscattering_color` of [`Color::BLACK`] or [`Color::NONE`] disables the inscattering effect. + pub fn from_visibility_colors( + visibility: f32, + extinction_color: Color, + inscattering_color: Color, + ) -> FogFalloff { + FogFalloff::from_visibility_contrast_colors( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + extinction_color, + inscattering_color, + ) + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// a contrast threshold in the range of `0.0` to `1.0`, and a shared color for both extinction and inscattering. + pub fn from_visibility_contrast_color( + visibility: f32, + contrast_threshold: f32, + extinction_inscattering_color: Color, + ) -> FogFalloff { + FogFalloff::from_visibility_contrast_colors( + visibility, + contrast_threshold, + extinction_inscattering_color, + extinction_inscattering_color, + ) + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// a contrast threshold in the range of `0.0` to `1.0`, extinction and inscattering colors. + /// + /// ## Tips + /// - Alpha values of the provided colors can modulate the `extinction` and `inscattering` effects; + /// - Using an `extinction_color` of [`Color::WHITE`] or [`Color::NONE`] disables the extinction effect; + /// - Using an `inscattering_color` of [`Color::BLACK`] or [`Color::NONE`] disables the inscattering effect. + pub fn from_visibility_contrast_colors( + visibility: f32, + contrast_threshold: f32, + extinction_color: Color, + inscattering_color: Color, + ) -> FogFalloff { + use std::f32::consts::E; + + let [r_e, g_e, b_e, a_e] = extinction_color.as_linear_rgba_f32(); + let [r_i, g_i, b_i, a_i] = inscattering_color.as_linear_rgba_f32(); + + FogFalloff::Atmospheric { + extinction: Vec3::new( + // Values are subtracted from 1.0 here to preserve the intuitive/artistic meaning of + // colors, since they're later subtracted. (e.g. by giving a blue extinction color, you + // get blue and _not_ yellow results) + (1.0 - r_e).powf(E), + (1.0 - g_e).powf(E), + (1.0 - b_e).powf(E), + ) * FogFalloff::koschmieder(visibility, contrast_threshold) + * a_e.powf(E), + + inscattering: Vec3::new(r_i.powf(E), g_i.powf(E), b_i.powf(E)) + * FogFalloff::koschmieder(visibility, contrast_threshold) + * a_i.powf(E), + } + } + + /// A 2% contrast threshold was originally proposed by Koschmieder, being the + /// minimum visual contrast at which a human observer could detect an object. + /// We use a revised 5% contrast threshold, deemed more realistic for typical human observers. + pub const REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD: f32 = 0.05; + + /// Calculates the extinction coefficient β, from V and Cₜ, where: + /// + /// - Cₜ is the contrast threshold, in the range of `0.0` to `1.0` + /// - V is the visibility distance in which a perfectly black object is still identifiable + /// against the horizon sky within the contrast threshold + /// + /// We start with Koschmieder's equation: + /// + /// ```text + /// -ln(Cₜ) + /// V = ───────── + /// β + /// ``` + /// + /// Multiplying both sides by β/V, that gives us: + /// + /// ```text + /// -ln(Cₜ) + /// β = ───────── + /// V + /// ``` + /// + /// See: + /// - + /// - + pub fn koschmieder(v: f32, c_t: f32) -> f32 { + -c_t.ln() / v + } +} + +impl Default for FogSettings { + fn default() -> Self { + FogSettings { + color: Color::rgba(1.0, 1.0, 1.0, 1.0), + falloff: FogFalloff::Linear { + start: 0.0, + end: 100.0, + }, + directional_light_color: Color::NONE, + directional_light_exponent: 8.0, + } + } +} + +impl ExtractComponent for FogSettings { + type Query = &'static Self; + type Filter = With; + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(item.clone()) + } +} diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 1ee943b19ee97..0035a9ed273aa 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -2,6 +2,7 @@ pub mod wireframe; mod alpha; mod bundle; +mod fog; mod light; mod material; mod pbr_material; @@ -11,6 +12,7 @@ mod render; pub use alpha::*; use bevy_utils::default; pub use bundle::*; +pub use fog::*; pub use light::*; pub use material::*; pub use pbr_material::*; @@ -27,6 +29,7 @@ pub mod prelude { DirectionalLightBundle, MaterialMeshBundle, PbrBundle, PointLightBundle, SpotLightBundle, }, + fog::{FogFalloff, FogSettings}, light::{AmbientLight, DirectionalLight, PointLight, SpotLight}, material::{Material, MaterialPlugin}, pbr_material::StandardMaterial, @@ -169,6 +172,7 @@ impl Plugin for PbrPlugin { .init_resource::() .init_resource::() .add_plugin(ExtractResourcePlugin::::default()) + .add_plugin(FogPlugin) .add_system_to_stage( CoreStage::PostUpdate, // NOTE: Clusters need to have been added before update_clusters is run so diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 9e95a3ffabe2b..998f728c11f14 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -207,6 +207,9 @@ pub struct StandardMaterial { /// shadows, alpha mode and ambient light are ignored if this is set to `true`. pub unlit: bool, + /// Whether to enable fog for this material. + pub fog_enabled: bool, + /// How to apply the alpha channel of the `base_color_texture`. /// /// See [`AlphaMode`] for details. Defaults to [`AlphaMode::Opaque`]. @@ -257,6 +260,7 @@ impl Default for StandardMaterial { double_sided: false, cull_mode: Some(Face::Back), unlit: false, + fog_enabled: true, alpha_mode: AlphaMode::Opaque, depth_bias: 0.0, } @@ -300,6 +304,7 @@ bitflags::bitflags! { const UNLIT = (1 << 5); const TWO_COMPONENT_NORMAL_MAP = (1 << 6); const FLIP_NORMAL_MAP_Y = (1 << 7); + const FOG_ENABLED = (1 << 8); const ALPHA_MODE_RESERVED_BITS = (Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS); // ← Bitmask reserving bits for the `AlphaMode` const ALPHA_MODE_OPAQUE = (0 << Self::ALPHA_MODE_SHIFT_BITS); // ← Values are just sequential values bitshifted into const ALPHA_MODE_MASK = (1 << Self::ALPHA_MODE_SHIFT_BITS); // the bitmask, and can range from 0 to 7. @@ -362,6 +367,9 @@ impl AsBindGroupShaderType for StandardMaterial { if self.unlit { flags |= StandardMaterialFlags::UNLIT; } + if self.fog_enabled { + flags |= StandardMaterialFlags::FOG_ENABLED; + } let has_normal_map = self.normal_map_texture.is_some(); if has_normal_map { if let Some(texture) = images.get(self.normal_map_texture.as_ref().unwrap()) { diff --git a/crates/bevy_pbr/src/render/fog.rs b/crates/bevy_pbr/src/render/fog.rs new file mode 100644 index 0000000000000..f2bb22a73205f --- /dev/null +++ b/crates/bevy_pbr/src/render/fog.rs @@ -0,0 +1,147 @@ +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, HandleUntyped}; +use bevy_ecs::{prelude::*, schedule::SystemLabel}; +use bevy_math::{Vec3, Vec4}; +use bevy_reflect::TypeUuid; +use bevy_render::{ + extract_component::ExtractComponentPlugin, + render_resource::{DynamicUniformBuffer, Shader, ShaderType}, + renderer::{RenderDevice, RenderQueue}, + view::ExtractedView, + RenderApp, RenderStage, +}; + +use crate::{FogFalloff, FogSettings}; + +/// The GPU-side representation of the fog configuration that's sent as a uniform to the shader +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuFog { + /// Fog color + base_color: Vec4, + /// The color used for the fog where the view direction aligns with directional lights + directional_light_color: Vec4, + /// Allocated differently depending on fog mode. + /// See `mesh_view_types.wgsl` for a detailed explanation + be: Vec3, + /// The exponent applied to the directional light alignment calculation + directional_light_exponent: f32, + /// Allocated differently depending on fog mode. + /// See `mesh_view_types.wgsl` for a detailed explanation + bi: Vec3, + /// Unsigned int representation of the active fog falloff mode + mode: u32, +} + +// Important: These must be kept in sync with `mesh_view_types.wgsl` +const GPU_FOG_MODE_OFF: u32 = 0; +const GPU_FOG_MODE_LINEAR: u32 = 1; +const GPU_FOG_MODE_EXPONENTIAL: u32 = 2; +const GPU_FOG_MODE_EXPONENTIAL_SQUARED: u32 = 3; +const GPU_FOG_MODE_ATMOSPHERIC: u32 = 4; + +/// Metadata for fog +#[derive(Default, Resource)] +pub struct FogMeta { + pub gpu_fogs: DynamicUniformBuffer, +} + +/// Prepares fog metadata and writes the fog-related uniform buffers to the GPU +pub fn prepare_fog( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut fog_meta: ResMut, + views: Query<(Entity, Option<&FogSettings>), With>, +) { + for (entity, fog) in &views { + let gpu_fog = if let Some(fog) = fog { + match &fog.falloff { + FogFalloff::Linear { start, end } => GpuFog { + mode: GPU_FOG_MODE_LINEAR, + base_color: fog.color.into(), + directional_light_color: fog.directional_light_color.into(), + directional_light_exponent: fog.directional_light_exponent, + be: Vec3::new(*start, *end, 0.0), + ..Default::default() + }, + FogFalloff::Exponential { density } => GpuFog { + mode: GPU_FOG_MODE_EXPONENTIAL, + base_color: fog.color.into(), + directional_light_color: fog.directional_light_color.into(), + directional_light_exponent: fog.directional_light_exponent, + be: Vec3::new(*density, 0.0, 0.0), + ..Default::default() + }, + FogFalloff::ExponentialSquared { density } => GpuFog { + mode: GPU_FOG_MODE_EXPONENTIAL_SQUARED, + base_color: fog.color.into(), + directional_light_color: fog.directional_light_color.into(), + directional_light_exponent: fog.directional_light_exponent, + be: Vec3::new(*density, 0.0, 0.0), + ..Default::default() + }, + FogFalloff::Atmospheric { + extinction, + inscattering, + } => GpuFog { + mode: GPU_FOG_MODE_ATMOSPHERIC, + base_color: fog.color.into(), + directional_light_color: fog.directional_light_color.into(), + directional_light_exponent: fog.directional_light_exponent, + be: *extinction, + bi: *inscattering, + }, + } + } else { + // If no fog is added to a camera, by default it's off + GpuFog { + mode: GPU_FOG_MODE_OFF, + ..Default::default() + } + }; + + // This is later read by `SetMeshViewBindGroup` + commands.entity(entity).insert(ViewFogUniformOffset { + offset: fog_meta.gpu_fogs.push(gpu_fog), + }); + } + + fog_meta + .gpu_fogs + .write_buffer(&render_device, &render_queue); +} + +/// Labels for fog-related systems +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] +pub enum RenderFogSystems { + PrepareFog, +} + +/// Inserted on each `Entity` with an `ExtractedView` to keep track of its offset +/// in the `gpu_fogs` `DynamicUniformBuffer` within `FogMeta` +#[derive(Component)] +pub struct ViewFogUniformOffset { + pub offset: u32, +} + +/// Handle for the fog WGSL Shader internal asset +pub const FOG_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 4913569193382610166); + +/// A plugin that consolidates fog extraction, preparation and related resources/assets +pub struct FogPlugin; + +impl Plugin for FogPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!(app, FOG_SHADER_HANDLE, "fog.wgsl", Shader::from_wgsl); + + app.add_plugin(ExtractComponentPlugin::::default()); + + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::().add_system_to_stage( + RenderStage::Prepare, + prepare_fog.label(RenderFogSystems::PrepareFog), + ); + } + } +} diff --git a/crates/bevy_pbr/src/render/fog.wgsl b/crates/bevy_pbr/src/render/fog.wgsl new file mode 100644 index 0000000000000..852265eefb36e --- /dev/null +++ b/crates/bevy_pbr/src/render/fog.wgsl @@ -0,0 +1,69 @@ +#define_import_path bevy_pbr::fog + +// Fog formulas adapted from: +// https://learn.microsoft.com/en-us/windows/win32/direct3d9/fog-formulas +// https://catlikecoding.com/unity/tutorials/rendering/part-14/ +// https://iquilezles.org/articles/fog/ (Atmospheric Fog and Scattering) + +fn scattering_adjusted_fog_color( + scattering: vec3, +) -> vec4 { + if (fog.directional_light_color.a > 0.0) { + return vec4( + fog.base_color.rgb + + scattering * fog.directional_light_color.rgb * fog.directional_light_color.a, + fog.base_color.a, + ); + } else { + return fog.base_color; + } +} + +fn linear_fog( + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(scattering); + let start = fog.be.x; + let end = fog.be.y; + fog_color.a *= 1.0 - clamp((end - distance) / (end - start), 0.0, 1.0); + return vec4(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a); +} + +fn exponential_fog( + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(scattering); + let density = fog.be.x; + fog_color.a *= 1.0 - 1.0 / exp(distance * density); + return vec4(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a); +} + +fn exponential_squared_fog( + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(scattering); + let distance_times_density = distance * fog.be.x; + fog_color.a *= 1.0 - 1.0 / exp(distance_times_density * distance_times_density); + return vec4(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a); +} + +fn atmospheric_fog( + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(scattering); + let extinction_factor = 1.0 - 1.0 / exp(distance * fog.be); + let inscattering_factor = 1.0 - 1.0 / exp(distance * fog.bi); + return vec4( + input_color.rgb * (1.0 - extinction_factor * fog_color.a) + + fog_color.rgb * inscattering_factor * fog_color.a, + input_color.a + ); +} diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 1b14b300ad9c5..c29429a031995 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,7 +1,8 @@ use crate::{ - GlobalLightMeta, GpuLights, GpuPointLights, LightMeta, NotShadowCaster, NotShadowReceiver, - ShadowPipeline, ViewClusterBindings, ViewLightsUniformOffset, ViewShadowBindings, - CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS, + FogMeta, GlobalLightMeta, GpuFog, GpuLights, GpuPointLights, LightMeta, NotShadowCaster, + NotShadowReceiver, ShadowPipeline, ViewClusterBindings, ViewFogUniformOffset, + ViewLightsUniformOffset, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, + MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS, }; use bevy_app::Plugin; use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; @@ -400,11 +401,22 @@ impl FromWorld for MeshPipeline { }, count: None, }, + // Fog + BindGroupLayoutEntry { + binding: 10, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(GpuFog::min_size()), + }, + count: None, + }, ]; if cfg!(not(feature = "webgl")) { // Depth texture entries.push(BindGroupLayoutEntry { - binding: 10, + binding: 11, visibility: ShaderStages::FRAGMENT, ty: BindingType::Texture { multisampled, @@ -415,7 +427,7 @@ impl FromWorld for MeshPipeline { }); // Normal texture entries.push(BindGroupLayoutEntry { - binding: 11, + binding: 12, visibility: ShaderStages::FRAGMENT, ty: BindingType::Texture { multisampled, @@ -887,6 +899,7 @@ pub fn queue_mesh_view_bind_groups( shadow_pipeline: Res, light_meta: Res, global_light_meta: Res, + fog_meta: Res, view_uniforms: Res, views: Query<( Entity, @@ -899,11 +912,18 @@ pub fn queue_mesh_view_bind_groups( msaa: Res, globals_buffer: Res, ) { - if let (Some(view_binding), Some(light_binding), Some(point_light_binding), Some(globals)) = ( + if let ( + Some(view_binding), + Some(light_binding), + Some(point_light_binding), + Some(globals), + Some(fog_binding), + ) = ( view_uniforms.uniforms.binding(), light_meta.view_gpu_lights.binding(), global_light_meta.gpu_point_lights.binding(), globals_buffer.buffer.binding(), + fog_meta.gpu_fogs.binding(), ) { for (entity, view_shadow_bindings, view_cluster_bindings, prepass_textures) in &views { let layout = if msaa.samples() > 1 { @@ -957,6 +977,10 @@ pub fn queue_mesh_view_bind_groups( binding: 9, resource: globals.clone(), }, + BindGroupEntry { + binding: 10, + resource: fog_binding.clone(), + }, ]; // When using WebGL with MSAA, we can't create the fallback textures required by the prepass @@ -971,7 +995,7 @@ pub fn queue_mesh_view_bind_groups( } }; entries.push(BindGroupEntry { - binding: 10, + binding: 11, resource: BindingResource::TextureView(depth_view), }); @@ -984,7 +1008,7 @@ pub fn queue_mesh_view_bind_groups( } }; entries.push(BindGroupEntry { - binding: 11, + binding: 12, resource: BindingResource::TextureView(normal_view), }); } @@ -1008,6 +1032,7 @@ impl RenderCommand

for SetMeshViewBindGroup type ViewWorldQuery = ( Read, Read, + Read, Read, ); type ItemWorldQuery = (); @@ -1015,7 +1040,10 @@ impl RenderCommand

for SetMeshViewBindGroup #[inline] fn render<'w>( _item: &P, - (view_uniform, view_lights, mesh_view_bind_group): ROQueryItem<'w, Self::ViewWorldQuery>, + (view_uniform, view_lights, view_fog, mesh_view_bind_group): ROQueryItem< + 'w, + Self::ViewWorldQuery, + >, _entity: (), _: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -1023,7 +1051,7 @@ impl RenderCommand

for SetMeshViewBindGroup pass.set_bind_group( I, &mesh_view_bind_group.value, - &[view_uniform.offset, view_lights.offset], + &[view_uniform.offset, view_lights.offset, view_fog.offset], ); RenderCommandResult::Success diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index 999d78152c2c1..9bade010b4c85 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -43,15 +43,17 @@ var cluster_offsets_and_counts: ClusterOffsetsAndCounts; @group(0) @binding(9) var globals: Globals; +@group(0) @binding(10) +var fog: Fog; #ifdef MULTISAMPLED -@group(0) @binding(10) -var depth_prepass_texture: texture_depth_multisampled_2d; @group(0) @binding(11) +var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(12) var normal_prepass_texture: texture_multisampled_2d; #else -@group(0) @binding(10) -var depth_prepass_texture: texture_depth_2d; @group(0) @binding(11) +var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(12) var normal_prepass_texture: texture_2d; -#endif \ No newline at end of file +#endif diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 1f6d1b2946696..3e797596fefa8 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -70,6 +70,31 @@ struct Lights { spot_light_shadowmap_offset: i32, }; +struct Fog { + base_color: vec4, + directional_light_color: vec4, + // `be` and `bi` are allocated differently depending on the fog mode + // + // For Linear Fog: + // be.x = start, be.y = end + // For Exponential and ExponentialSquared Fog: + // be.x = density + // For Atmospheric Fog: + // be = per-channel extinction density + // bi = per-channel inscattering density + be: vec3, + directional_light_exponent: f32, + bi: vec3, + mode: u32, +} + +// Important: These must be kept in sync with `fog.rs` +let FOG_MODE_OFF: u32 = 0u; +let FOG_MODE_LINEAR: u32 = 1u; +let FOG_MODE_EXPONENTIAL: u32 = 2u; +let FOG_MODE_EXPONENTIAL_SQUARED: u32 = 3u; +let FOG_MODE_ATMOSPHERIC: u32 = 4u; + #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 struct PointLights { data: array, diff --git a/crates/bevy_pbr/src/render/mod.rs b/crates/bevy_pbr/src/render/mod.rs index 353f05be1db94..1276b28c8e6ad 100644 --- a/crates/bevy_pbr/src/render/mod.rs +++ b/crates/bevy_pbr/src/render/mod.rs @@ -1,5 +1,7 @@ +mod fog; mod light; mod mesh; +pub use fog::*; pub use light::*; pub use mesh::*; diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 3f5774551535a..62837803bdb8b 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -6,6 +6,7 @@ #import bevy_pbr::clustered_forward #import bevy_pbr::lighting #import bevy_pbr::shadows +#import bevy_pbr::fog #import bevy_pbr::pbr_functions struct FragmentInput { @@ -95,6 +96,11 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { output_color = alpha_discard(material, output_color); } + // fog + if (fog.mode != FOG_MODE_OFF && (material.flags & STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) { + output_color = apply_fog(output_color, in.world_position.xyz, view.world_position.xyz); + } + #ifdef TONEMAP_IN_SHADER output_color = tone_mapping(output_color); #endif diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index f4a4f947310b6..57b5162028b47 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -269,6 +269,46 @@ fn dither(color: vec4, pos: vec2) -> vec4 { } #endif // DEBAND_DITHER +#ifndef NORMAL_PREPASS +fn apply_fog(input_color: vec4, fragment_world_position: vec3, view_world_position: vec3) -> vec4 { + let view_to_world = fragment_world_position.xyz - view_world_position.xyz; + + // `length()` is used here instead of just `view_to_world.z` since that produces more + // high quality results, especially for denser/smaller fogs. we get a "curved" + // fog shape that remains consistent with camera rotation, instead of a "linear" + // fog shape that looks a bit fake + let distance = length(view_to_world); + + var scattering = vec3(0.0); + if (fog.directional_light_color.a > 0.0) { + let view_to_world_normalized = view_to_world / distance; + let n_directional_lights = lights.n_directional_lights; + for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { + let light = lights.directional_lights[i]; + scattering += pow( + max( + dot(view_to_world_normalized, light.direction_to_light), + 0.0 + ), + fog.directional_light_exponent + ) * light.color.rgb; + } + } + + if (fog.mode == FOG_MODE_LINEAR) { + return linear_fog(input_color, distance, scattering); + } else if (fog.mode == FOG_MODE_EXPONENTIAL) { + return exponential_fog(input_color, distance, scattering); + } else if (fog.mode == FOG_MODE_EXPONENTIAL_SQUARED) { + return exponential_squared_fog(input_color, distance, scattering); + } else if (fog.mode == FOG_MODE_ATMOSPHERIC) { + return atmospheric_fog(input_color, distance, scattering); + } else { + return input_color; + } +} +#endif + #ifdef PREMULTIPLY_ALPHA fn premultiply_alpha(standard_material_flags: u32, color: vec4) -> vec4 { // `Blend`, `Premultiplied` and `Alpha` all share the same `BlendState`. Depending diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 8b9eb3297b292..c84ea79275c26 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -19,6 +19,7 @@ let STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 16u; let STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u; let STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 64u; let STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 128u; +let STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT: u32 = 256u; let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3758096384u; // (0b111u32 << 29) let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 29) let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 536870912u; // (1u32 << 29) diff --git a/examples/3d/atmospheric_fog.rs b/examples/3d/atmospheric_fog.rs new file mode 100644 index 0000000000000..d1024e83724c3 --- /dev/null +++ b/examples/3d/atmospheric_fog.rs @@ -0,0 +1,124 @@ +//! This example showcases atmospheric fog +//! +//! ## Controls +//! +//! | Key Binding | Action | +//! |:-------------------|:---------------------------------------| +//! | `Spacebar` | Toggle Atmospheric Fog | +//! | `S` | Toggle Directional Light Fog Influence | + +use bevy::{ + pbr::{CascadeShadowConfig, NotShadowCaster}, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup_camera_fog) + .add_startup_system(setup_terrain_scene) + .add_startup_system(setup_instructions) + .add_system(toggle_system) + .run(); +} + +fn setup_camera_fog(mut commands: Commands) { + commands.spawn(( + Camera3dBundle { + transform: Transform::from_xyz(-1.0, 0.1, 1.0) + .looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y), + ..default() + }, + FogSettings { + color: Color::rgba(0.1, 0.2, 0.4, 1.0), + directional_light_color: Color::rgba(1.0, 0.95, 0.75, 0.5), + directional_light_exponent: 30.0, + falloff: FogFalloff::from_visibility_colors( + 15.0, // distance in world units up to which objects retain visibility (>= 5% contrast) + Color::rgb(0.35, 0.5, 0.66), // atmospheric extinction color (after light is lost due to absorption by atmospheric particles) + Color::rgb(0.8, 0.844, 1.0), // atmospheric inscattering color (light gained due to scattering from the sun) + ), + }, + )); +} + +fn setup_terrain_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, +) { + // Configure a properly scaled cascade shadow map for this scene (defaults are too large, mesh units are in km) + // For WebGL we only support 1 cascade level for now + let cascade_shadow_config = + CascadeShadowConfig::new(if cfg!(feature = "webgl") { 1 } else { 4 }, 0.5, 2.5, 0.2); + + // Sun + commands.spawn(DirectionalLightBundle { + directional_light: DirectionalLight { + color: Color::rgb(0.98, 0.95, 0.82), + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(0.0, 0.0, 0.0) + .looking_at(Vec3::new(-0.15, -0.05, 0.25), Vec3::Y), + cascade_shadow_config, + ..default() + }); + + // Terrain + commands.spawn(SceneBundle { + scene: asset_server.load("models/terrain/Mountains.gltf#Scene0"), + ..default() + }); + + // Sky + commands.spawn(( + PbrBundle { + mesh: meshes.add(Mesh::from(shape::Box::default())), + material: materials.add(StandardMaterial { + base_color: Color::hex("888888").unwrap(), + unlit: true, + cull_mode: None, + ..default() + }), + transform: Transform::from_scale(Vec3::splat(20.0)), + ..default() + }, + NotShadowCaster, + )); +} + +fn setup_instructions(mut commands: Commands, asset_server: Res) { + commands.spawn((TextBundle::from_section( + "Press Spacebar to Toggle Atmospheric Fog.\nPress S to Toggle Directional Light Fog Influence.", + TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 15.0, + color: Color::WHITE, + }, + ) + .with_style(Style { + position_type: PositionType::Absolute, + position: UiRect { + bottom: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }, + ..default() + }),)); +} + +fn toggle_system(keycode: Res>, mut fog: Query<&mut FogSettings>) { + let mut fog_settings = fog.single_mut(); + + if keycode.just_pressed(KeyCode::Space) { + let a = fog_settings.color.a(); + fog_settings.color.set_a(1.0 - a); + } + + if keycode.just_pressed(KeyCode::S) { + let a = fog_settings.directional_light_color.a(); + fog_settings.directional_light_color.set_a(0.5 - a); + } +} diff --git a/examples/3d/fog.rs b/examples/3d/fog.rs new file mode 100644 index 0000000000000..ba9f844c124db --- /dev/null +++ b/examples/3d/fog.rs @@ -0,0 +1,317 @@ +//! This interactive example shows how to use distance fog, +//! and allows playing around with different fog settings. +//! +//! ## Controls +//! +//! | Key Binding | Action | +//! |:-------------------|:------------------------------------| +//! | `1` / `2` / `3` | Fog Falloff Mode | +//! | `A` / `S` | Move Start Distance (Linear Fog) | +//! | | Change Density (Exponential Fogs) | +//! | `Z` / `X` | Move End Distance (Linear Fog) | +//! | `-` / `=` | Adjust Fog Red Channel | +//! | `[` / `]` | Adjust Fog Green Channel | +//! | `;` / `'` | Adjust Fog Blue Channel | +//! | `.` / `?` | Adjust Fog Alpha Channel | + +use bevy::{ + pbr::{NotShadowCaster, NotShadowReceiver}, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup_camera_fog) + .add_startup_system(setup_pyramid_scene) + .add_startup_system(setup_instructions) + .add_system(update_system) + .run(); +} + +fn setup_camera_fog(mut commands: Commands) { + commands.spawn(( + Camera3dBundle::default(), + FogSettings { + color: Color::rgba(0.05, 0.05, 0.05, 1.0), + falloff: FogFalloff::Linear { + start: 5.0, + end: 20.0, + }, + ..default() + }, + )); +} + +fn setup_pyramid_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let stone = materials.add(StandardMaterial { + base_color: Color::hex("28221B").unwrap(), + perceptual_roughness: 1.0, + ..default() + }); + + // pillars + for (x, z) in &[(-1.5, -1.5), (1.5, -1.5), (1.5, 1.5), (-1.5, 1.5)] { + commands.spawn(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Box { + min_x: -0.5, + max_x: 0.5, + min_z: -0.5, + max_z: 0.5, + min_y: 0.0, + max_y: 3.0, + })), + material: stone.clone(), + transform: Transform::from_xyz(*x, 0.0, *z), + ..default() + }); + } + + // orb + commands.spawn(( + PbrBundle { + mesh: meshes.add(Mesh::try_from(shape::Icosphere::default()).unwrap()), + material: materials.add(StandardMaterial { + base_color: Color::hex("126212CC").unwrap(), + reflectance: 1.0, + perceptual_roughness: 0.0, + metallic: 0.5, + alpha_mode: AlphaMode::Blend, + ..default() + }), + transform: Transform::from_scale(Vec3::splat(1.75)) + .with_translation(Vec3::new(0.0, 4.0, 0.0)), + ..default() + }, + NotShadowCaster, + NotShadowReceiver, + )); + + // steps + for i in 0..50 { + let size = i as f32 / 2.0 + 3.0; + let y = -i as f32 / 2.0; + commands.spawn(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Box { + min_x: -size, + max_x: size, + min_z: -size, + max_z: size, + min_y: 0.0, + max_y: 0.5, + })), + material: stone.clone(), + transform: Transform::from_xyz(0.0, y, 0.0), + ..default() + }); + } + + // sky + commands.spawn(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Box::default())), + material: materials.add(StandardMaterial { + base_color: Color::hex("888888").unwrap(), + unlit: true, + cull_mode: None, + ..default() + }), + transform: Transform::from_scale(Vec3::splat(1_000_000.0)), + ..default() + }); + + // light + commands.spawn(PointLightBundle { + transform: Transform::from_xyz(0.0, 1.0, 0.0), + point_light: PointLight { + intensity: 1500., + range: 100., + shadows_enabled: true, + ..default() + }, + ..default() + }); +} + +fn setup_instructions(mut commands: Commands, asset_server: Res) { + commands.spawn((TextBundle::from_section( + "", + TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 15.0, + color: Color::WHITE, + }, + ) + .with_style(Style { + position_type: PositionType::Absolute, + position: UiRect { + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }, + ..default() + }),)); +} + +fn update_system( + mut camera: Query<(&mut FogSettings, &mut Transform)>, + mut text: Query<&mut Text>, + time: Res