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);
+ /// ```
+ ///
+ ///
+ 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();
+ /// ```
+ ///
+ ///
+ 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();
+ /// ```
+ ///
+ ///
+ 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