diff --git a/Cargo.toml b/Cargo.toml index b8ebfd2326..c219ccdd41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3037,6 +3037,17 @@ description = "Demonstrates depth of field" category = "3D Rendering" wasm = false +[[example]] +name = "volumetric_fog" +path = "examples/3d/volumetric_fog.rs" +doc-scrape-examples = true + +[package.metadata.example.volumetric_fog] +name = "Volumetric fog" +description = "Demonstrates volumetric fog and lighting" +category = "3D Rendering" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/models/VolumetricFogExample/VolumetricFogExample.glb b/assets/models/VolumetricFogExample/VolumetricFogExample.glb new file mode 100644 index 0000000000..0f6179bba1 Binary files /dev/null and b/assets/models/VolumetricFogExample/VolumetricFogExample.glb differ diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index 9787146e11..cf062d340f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -65,7 +65,7 @@ impl Default for Camera3d { #[derive(Clone, Copy, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] -pub struct Camera3dDepthTextureUsage(u32); +pub struct Camera3dDepthTextureUsage(pub u32); impl From for Camera3dDepthTextureUsage { fn from(value: TextureUsages) -> Self { diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index bdcd75764b..90f045f63a 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -115,6 +115,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { let view_layout = self .mesh_pipeline + .view_layouts .get_view_layout(key.view_key.into()) .clone(); @@ -208,6 +209,7 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { let view_layout = self .mesh_pipeline + .view_layouts .get_view_layout(key.view_key.into()) .clone(); diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index 8bf4c70baa..00e09935f5 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -320,7 +320,10 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { RenderPipelineDescriptor { label: Some("deferred_lighting_pipeline".into()), layout: vec![ - self.mesh_pipeline.get_view_layout(key.into()).clone(), + self.mesh_pipeline + .view_layouts + .get_view_layout(key.into()) + .clone(), self.bind_group_layout_1.clone(), ], vertex: VertexState { diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 8c5078f1fe..efe9ae4aa2 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -34,6 +34,7 @@ mod pbr_material; mod prepass; mod render; mod ssao; +mod volumetric_fog; use bevy_color::{Color, LinearRgba}; use std::marker::PhantomData; @@ -50,6 +51,7 @@ pub use pbr_material::*; pub use prepass::*; pub use render::*; pub use ssao::*; +pub use volumetric_fog::*; pub mod prelude { #[doc(hidden)] @@ -81,6 +83,8 @@ pub mod graph { /// Label for the screen space ambient occlusion render node. ScreenSpaceAmbientOcclusion, DeferredLightingPass, + /// Label for the volumetric lighting pass. + VolumetricFog, /// Label for the compute shader instance data building pass. GpuPreprocess, } @@ -314,6 +318,7 @@ impl Plugin for PbrPlugin { GpuMeshPreprocessPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, }, + VolumetricFogPlugin, )) .configure_sets( PostUpdate, diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 00bf0fa81d..c0b8260245 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -1007,15 +1007,20 @@ pub(crate) fn point_light_order( } // Sort lights by -// - those with shadows enabled first, so that the index can be used to render at most `directional_light_shadow_maps_count` -// directional light shadows -// - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded. +// - those with volumetric (and shadows) enabled first, so that the volumetric +// lighting pass can quickly find the volumetric lights; +// - then those with shadows enabled second, so that the index can be used to +// render at most `directional_light_shadow_maps_count` directional light +// shadows; +// - then by entity as a stable key to ensure that a consistent set of lights +// are chosen if the light count limit is exceeded. pub(crate) fn directional_light_order( - (entity_1, shadows_enabled_1): (&Entity, &bool), - (entity_2, shadows_enabled_2): (&Entity, &bool), + (entity_1, volumetric_1, shadows_enabled_1): (&Entity, &bool, &bool), + (entity_2, volumetric_2, shadows_enabled_2): (&Entity, &bool, &bool), ) -> std::cmp::Ordering { - shadows_enabled_2 - .cmp(shadows_enabled_1) // shadow casters before non-casters + volumetric_2 + .cmp(volumetric_1) // volumetric before shadows + .then_with(|| shadows_enabled_2.cmp(shadows_enabled_1)) // shadow casters before non-casters .then_with(|| entity_1.cmp(entity_2)) // stable } diff --git a/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs b/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs index 65fa6272a2..d1c6042ecf 100644 --- a/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs @@ -165,7 +165,10 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( let pipeline_descriptor = RenderPipelineDescriptor { label: material_pipeline_descriptor.label, layout: vec![ - mesh_pipeline.get_view_layout(view_key.into()).clone(), + mesh_pipeline + .view_layouts + .get_view_layout(view_key.into()) + .clone(), gpu_scene.material_draw_bind_group_layout(), material_pipeline.material_layout.clone(), ], diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index ec654c22dd..1e9709ab00 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -46,6 +46,7 @@ pub struct ExtractedDirectionalLight { pub illuminance: f32, pub transform: GlobalTransform, pub shadows_enabled: bool, + pub volumetric: bool, pub shadow_depth_bias: f32, pub shadow_normal_bias: f32, pub cascade_shadow_config: CascadeShadowConfig, @@ -181,6 +182,7 @@ bitflags::bitflags! { #[repr(transparent)] struct DirectionalLightFlags: u32 { const SHADOWS_ENABLED = 1 << 0; + const VOLUMETRIC = 1 << 1; const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -346,6 +348,7 @@ pub fn extract_lights( &GlobalTransform, &ViewVisibility, Option<&RenderLayers>, + Option<&VolumetricLight>, ), Without, >, @@ -468,6 +471,7 @@ pub fn extract_lights( transform, view_visibility, maybe_layers, + volumetric_light, ) in &directional_lights { if !view_visibility.get() { @@ -481,6 +485,7 @@ pub fn extract_lights( color: directional_light.color.into(), illuminance: directional_light.illuminance, transform: *transform, + volumetric: volumetric_light.is_some(), shadows_enabled: directional_light.shadows_enabled, shadow_depth_bias: directional_light.shadow_depth_bias, // The factor of SQRT_2 is for the worst-case diagonal offset @@ -777,6 +782,13 @@ pub fn prepare_lights( .count() .min(max_texture_cubes); + let directional_volumetric_enabled_count = directional_lights + .iter() + .take(MAX_DIRECTIONAL_LIGHTS) + .filter(|(_, light)| light.volumetric) + .count() + .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); + let directional_shadow_enabled_count = directional_lights .iter() .take(MAX_DIRECTIONAL_LIGHTS) @@ -811,13 +823,17 @@ pub fn prepare_lights( }); // Sort lights by - // - those with shadows enabled first, so that the index can be used to render at most `directional_light_shadow_maps_count` - // directional light shadows - // - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded. + // - those with volumetric (and shadows) enabled first, so that the + // volumetric lighting pass can quickly find the volumetric lights; + // - then those with shadows enabled second, so that the index can be used + // to render at most `directional_light_shadow_maps_count` directional light + // shadows + // - then by entity as a stable key to ensure that a consistent set of + // lights are chosen if the light count limit is exceeded. directional_lights.sort_by(|(entity_1, light_1), (entity_2, light_2)| { directional_light_order( - (entity_1, &light_1.shadows_enabled), - (entity_2, &light_2.shadows_enabled), + (entity_1, &light_1.volumetric, &light_1.shadows_enabled), + (entity_2, &light_2.volumetric, &light_2.shadows_enabled), ) }); @@ -898,7 +914,14 @@ pub fn prepare_lights( { let mut flags = DirectionalLightFlags::NONE; - // Lights are sorted, shadow enabled lights are first + // Lights are sorted, volumetric and shadow enabled lights are first + if light.volumetric + && light.shadows_enabled + && (index < directional_volumetric_enabled_count) + { + flags |= DirectionalLightFlags::VOLUMETRIC; + } + // Shadow enabled lights are second if light.shadows_enabled && (index < directional_shadow_enabled_count) { flags |= DirectionalLightFlags::SHADOWS_ENABLED; } diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index db7f877a0c..de7b7b570f 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1031,6 +1031,7 @@ fn collect_meshes_for_gpu_building( } } +/// All data needed to construct a pipeline for rendering 3D meshes. #[derive(Resource, Clone)] pub struct MeshPipeline { /// A reference to all the mesh pipeline view layouts. @@ -1069,6 +1070,7 @@ impl FromWorld for MeshPipeline { )> = SystemState::new(world); let (render_device, default_sampler, render_queue, view_layouts) = system_state.get_mut(world); + let clustered_forward_buffer_binding_type = render_device .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); @@ -1555,7 +1557,7 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED".into()); } - let mut bind_group_layout = vec![self.get_view_layout(key.into()).clone()]; + let mut bind_group_layout = vec![self.view_layouts.get_view_layout(key.into()).clone()]; if key.msaa_samples() > 1 { shader_defs.push("MULTISAMPLED".into()); diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 89c5557fbe..c937d36493 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -506,7 +506,7 @@ pub fn prepare_mesh_view_bind_groups( .map(|t| &t.screen_space_ambient_occlusion_texture.default_view) .unwrap_or(&fallback_ssao); - let layout = &mesh_pipeline.get_view_layout( + let layout = &mesh_pipeline.view_layouts.get_view_layout( MeshPipelineViewLayoutKey::from(*msaa) | MeshPipelineViewLayoutKey::from(prepass_textures), ); diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 1b5d1b9076..f517daec4d 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -37,6 +37,7 @@ struct DirectionalLight { }; const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; +const DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 2u; struct Lights { // NOTE: this array size must be kept in sync with the constants defined in bevy_pbr/src/render/light.rs diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index eed6bf6a40..21b25f7f3a 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -113,24 +113,27 @@ fn get_cascade_index(light_id: u32, view_z: f32) -> u32 { return (*light).num_cascades; } -fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, surface_normal: vec3) -> f32 { +// Converts from world space to the uv position in the light's shadow map. +// +// The depth is stored in the return value's z coordinate. If the return value's +// w coordinate is 0.0, then we landed outside the shadow map entirely. +fn world_to_directional_light_local( + light_id: u32, + cascade_index: u32, + offset_position: vec4 +) -> vec4 { let light = &view_bindings::lights.directional_lights[light_id]; let cascade = &(*light).cascades[cascade_index]; - // The normal bias is scaled to the texel size. - let normal_offset = (*light).shadow_normal_bias * (*cascade).texel_size * surface_normal.xyz; - let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; - let offset_position = vec4(frag_position.xyz + normal_offset + depth_offset, frag_position.w); - let offset_position_clip = (*cascade).view_projection * offset_position; if (offset_position_clip.w <= 0.0) { - return 1.0; + return vec4(0.0); } let offset_position_ndc = offset_position_clip.xyz / offset_position_clip.w; // No shadow outside the orthographic projection volume if (any(offset_position_ndc.xy < vec2(-1.0)) || offset_position_ndc.z < 0.0 || any(offset_position_ndc > vec3(1.0))) { - return 1.0; + return vec4(0.0); } // compute texture coordinates for shadow lookup, compensating for the Y-flip difference @@ -140,8 +143,25 @@ fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: let depth = offset_position_ndc.z; + return vec4(light_local, depth, 1.0); +} + +fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, surface_normal: vec3) -> f32 { + let light = &view_bindings::lights.directional_lights[light_id]; + let cascade = &(*light).cascades[cascade_index]; + + // The normal bias is scaled to the texel size. + let normal_offset = (*light).shadow_normal_bias * (*cascade).texel_size * surface_normal.xyz; + let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; + let offset_position = vec4(frag_position.xyz + normal_offset + depth_offset, frag_position.w); + + let light_local = world_to_directional_light_local(light_id, cascade_index, offset_position); + if (light_local.w == 0.0) { + return 1.0; + } + let array_index = i32((*light).depth_texture_base_index + cascade_index); - return sample_shadow_map(light_local, depth, array_index, (*cascade).texel_size); + return sample_shadow_map(light_local.xy, light_local.z, array_index, (*cascade).texel_size); } fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3, view_z: f32) -> f32 { diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs new file mode 100644 index 0000000000..7d7b611a61 --- /dev/null +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -0,0 +1,647 @@ +//! Volumetric fog and volumetric lighting, also known as light shafts or god +//! rays. +//! +//! This module implements a more physically-accurate, but slower, form of fog +//! than the [`crate::fog`] module does. Notably, this *volumetric fog* allows +//! for light beams from directional lights to shine through, creating what is +//! known as *light shafts* or *god rays*. +//! +//! To add volumetric fog to a scene, add [`VolumetricFogSettings`] to the +//! camera, and add [`VolumetricLight`] to directional lights that you wish to +//! be volumetric. [`VolumetricFogSettings`] feature numerous settings that +//! allow you to define the accuracy of the simulation, as well as the look of +//! the fog. Currently, only interaction with directional lights that have +//! shadow maps is supported. Note that the overhead of the effect scales +//! directly with the number of directional lights in use, so apply +//! [`VolumetricLight`] sparingly for the best results. +//! +//! The overall algorithm, which is implemented as a postprocessing effect, is a +//! combination of the techniques described in [Scratchapixel] and [this blog +//! post]. It uses raymarching in screen space, transformed into shadow map +//! space for sampling and combined with physically-based modeling of absorption +//! and scattering. Bevy employs the widely-used [Henyey-Greenstein phase +//! function] to model asymmetry; this essentially allows light shafts to fade +//! into and out of existence as the user views them. +//! +//! [Scratchapixel]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html +//! +//! [this blog post]: https://www.alexandre-pestana.com/volumetric-lights/ +//! +//! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction + +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, Handle}; +use bevy_color::Color; +use bevy_core_pipeline::{ + core_3d::{ + graph::{Core3d, Node3d}, + prepare_core_3d_depth_textures, Camera3d, + }, + fullscreen_vertex_shader::fullscreen_shader_vertex_state, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Has, QueryItem, With}, + schedule::IntoSystemConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_math::Vec3; +use bevy_render::{ + render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_resource::{ + binding_types::{ + sampler, texture_2d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer, + }, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, + ColorTargetState, ColorWrites, DynamicUniformBuffer, FilterMode, FragmentState, + MultisampleState, Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, + RenderPassDescriptor, RenderPipelineDescriptor, Sampler, SamplerBindingType, + SamplerDescriptor, Shader, ShaderStages, ShaderType, SpecializedRenderPipeline, + SpecializedRenderPipelines, TextureFormat, TextureSampleType, TextureUsages, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::BevyDefault, + view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; +use bevy_utils::prelude::default; + +use crate::{ + graph::NodePbr, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, + ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset, +}; + +/// The volumetric fog shader. +pub const VOLUMETRIC_FOG_HANDLE: Handle = Handle::weak_from_u128(17400058287583986650); + +/// A plugin that implements volumetric fog. +pub struct VolumetricFogPlugin; + +/// Add this component to a [`DirectionalLight`] with a shadow map +/// (`shadows_enabled: true`) to make volumetric fog interact with it. +/// +/// This allows the light to generate light shafts/god rays. +#[derive(Clone, Copy, Component, Default, Debug)] +pub struct VolumetricLight; + +/// When placed on a [`Camera3d`], enables volumetric fog and volumetric +/// lighting, also known as light shafts or god rays. +#[derive(Clone, Copy, Component, Debug)] +pub struct VolumetricFogSettings { + /// The color of the fog. + /// + /// Note that the fog must be lit by a [`VolumetricLight`] or ambient light + /// in order for this color to appear. + /// + /// Defaults to white. + pub fog_color: Color, + + /// Color of the ambient light. + /// + /// This is separate from Bevy's [`crate::light::AmbientLight`] because an + /// [`EnvironmentMapLight`] is still considered an ambient light for the + /// purposes of volumetric fog. If you're using a + /// [`crate::EnvironmentMapLight`], for best results, this should be a good + /// approximation of the average color of the environment map. + /// + /// Defaults to white. + pub ambient_color: Color, + + /// The brightness of the ambient light. + /// + /// If there's no ambient light, set this to 0. + /// + /// Defaults to 0.1. + pub ambient_intensity: f32, + + /// The number of raymarching steps to perform. + /// + /// Higher values produce higher-quality results with less banding, but + /// reduce performance. + /// + /// The default value is 64. + pub step_count: u32, + + /// The maximum distance that Bevy will trace a ray for, in world space. + /// + /// You can think of this as the radius of a sphere of fog surrounding the + /// camera. It has to be capped to a finite value or else there would be an + /// infinite amount of fog, which would result in completely-opaque areas + /// where the skybox would be. + /// + /// The default value is 25. + pub max_depth: f32, + + /// The absorption coefficient, which measures what fraction of light is + /// absorbed by the fog at each step. + /// + /// Increasing this value makes the fog darker. + /// + /// The default value is 0.3. + pub absorption: f32, + + /// The scattering coefficient, which measures the fraction of light that's + /// scattered toward, and away from, the viewer. + /// + /// The default value is 0.3. + pub scattering: f32, + + /// The density of fog, which measures how dark the fog is. + /// + /// The default value is 0.1. + pub density: f32, + + /// Measures the fraction of light that's scattered *toward* the camera, as opposed to *away* from the camera. + /// + /// Increasing this value makes light shafts become more prominent when the + /// camera is facing toward their source and less prominent when the camera + /// is facing away. Essentially, a high value here means the light shafts + /// will fade into view as the camera focuses on them and fade away when the + /// camera is pointing away. + /// + /// The default value is 0.8. + pub scattering_asymmetry: f32, + + /// Applies a nonphysical color to the light. + /// + /// This can be useful for artistic purposes but is nonphysical. + /// + /// The default value is white. + pub light_tint: Color, + + /// Scales the light by a fixed fraction. + /// + /// This can be useful for artistic purposes but is nonphysical. + /// + /// The default value is 1.0, which results in no adjustment. + pub light_intensity: f32, +} + +/// The GPU pipeline for the volumetric fog postprocessing effect. +#[derive(Resource)] +pub struct VolumetricFogPipeline { + /// A reference to the shared set of mesh pipeline view layouts. + mesh_view_layouts: MeshPipelineViewLayouts, + /// The view bind group when multisample antialiasing isn't in use. + volumetric_view_bind_group_layout_no_msaa: BindGroupLayout, + /// The view bind group when multisample antialiasing is in use. + volumetric_view_bind_group_layout_msaa: BindGroupLayout, + /// The sampler that we use to sample the postprocessing input. + color_sampler: Sampler, +} + +#[derive(Component, Deref, DerefMut)] +pub struct ViewVolumetricFogPipeline(pub CachedRenderPipelineId); + +/// The node in the render graph, part of the postprocessing stack, that +/// implements volumetric fog. +#[derive(Default)] +pub struct VolumetricFogNode; + +/// Identifies a single specialization of the volumetric fog shader. +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct VolumetricFogPipelineKey { + /// The layout of the view, which is needed for the raymarching. + mesh_pipeline_view_key: MeshPipelineViewLayoutKey, + /// Whether the view has high dynamic range. + hdr: bool, +} + +/// The same as [`VolumetricFogSettings`], but formatted for the GPU. +#[derive(ShaderType)] +pub struct VolumetricFogUniform { + fog_color: Vec3, + light_tint: Vec3, + ambient_color: Vec3, + ambient_intensity: f32, + step_count: u32, + max_depth: f32, + absorption: f32, + scattering: f32, + density: f32, + scattering_asymmetry: f32, + light_intensity: f32, +} + +/// Specifies the offset within the [`VolumetricFogUniformBuffer`] of the +/// [`VolumetricFogUniform`] for a specific view. +#[derive(Component, Deref, DerefMut)] +pub struct ViewVolumetricFogUniformOffset(u32); + +/// The GPU buffer that stores the [`VolumetricFogUniform`] data. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct VolumetricFogUniformBuffer(pub DynamicUniformBuffer); + +impl Plugin for VolumetricFogPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + VOLUMETRIC_FOG_HANDLE, + "volumetric_fog.wgsl", + Shader::from_wgsl + ); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .init_resource::() + .add_systems(ExtractSchedule, extract_volumetric_fog) + .add_systems( + Render, + ( + prepare_volumetric_fog_pipelines.in_set(RenderSet::Prepare), + prepare_volumetric_fog_uniforms.in_set(RenderSet::Prepare), + prepare_view_depth_textures_for_volumetric_fog + .in_set(RenderSet::Prepare) + .before(prepare_core_3d_depth_textures), + ), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .add_render_graph_node::>( + Core3d, + NodePbr::VolumetricFog, + ) + .add_render_graph_edges( + Core3d, + // Volumetric fog is a postprocessing effect. Run it after the + // main pass but before bloom. + (Node3d::EndMainPass, NodePbr::VolumetricFog, Node3d::Bloom), + ); + } +} + +impl Default for VolumetricFogSettings { + fn default() -> Self { + Self { + step_count: 64, + max_depth: 25.0, + absorption: 0.3, + scattering: 0.3, + density: 0.1, + scattering_asymmetry: 0.5, + fog_color: Color::WHITE, + // Matches `AmbientLight` defaults. + ambient_color: Color::WHITE, + ambient_intensity: 0.1, + light_tint: Color::WHITE, + light_intensity: 1.0, + } + } +} + +impl FromWorld for VolumetricFogPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let mesh_view_layouts = world.resource::(); + + // Create the bind group layout entries common to both the MSAA and + // non-MSAA bind group layouts. + let base_bind_group_layout_entries = &*BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + // `volumetric_fog` + uniform_buffer::(true), + // `color_texture` + texture_2d(TextureSampleType::Float { filterable: true }), + // `color_sampler` + sampler(SamplerBindingType::Filtering), + ), + ); + + // Because `texture_depth_2d` and `texture_depth_2d_multisampled` are + // different types, we need to make separate bind group layouts for + // each. + + let mut bind_group_layout_entries_no_msaa = base_bind_group_layout_entries.to_vec(); + bind_group_layout_entries_no_msaa.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ((3, texture_depth_2d()),), + )); + let volumetric_view_bind_group_layout_no_msaa = render_device.create_bind_group_layout( + "volumetric lighting view bind group layout", + &bind_group_layout_entries_no_msaa, + ); + + let mut bind_group_layout_entries_msaa = base_bind_group_layout_entries.to_vec(); + bind_group_layout_entries_msaa.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ((3, texture_depth_2d_multisampled()),), + )); + let volumetric_view_bind_group_layout_msaa = render_device.create_bind_group_layout( + "volumetric lighting view bind group layout (multisampled)", + &bind_group_layout_entries_msaa, + ); + + let color_sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("volumetric lighting color sampler"), + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + compare: None, + ..default() + }); + + VolumetricFogPipeline { + mesh_view_layouts: mesh_view_layouts.clone(), + volumetric_view_bind_group_layout_no_msaa, + volumetric_view_bind_group_layout_msaa, + color_sampler, + } + } +} + +/// Extracts [`VolumetricFogSettings`] and [`VolumetricLight`]s from the main +/// world to the render world. +pub fn extract_volumetric_fog( + mut commands: Commands, + view_targets: Extract>, + volumetric_lights: Extract>, +) { + if volumetric_lights.is_empty() { + return; + } + + for (view_target, volumetric_fog_settings) in view_targets.iter() { + commands + .get_or_spawn(view_target) + .insert(*volumetric_fog_settings); + } + + for (entity, volumetric_light) in volumetric_lights.iter() { + commands.get_or_spawn(entity).insert(*volumetric_light); + } +} + +impl ViewNode for VolumetricFogNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + view_target, + view_depth_texture, + view_volumetric_lighting_pipeline, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_volumetric_lighting_uniform_buffer_offset, + view_bind_group, + ): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let volumetric_lighting_pipeline = world.resource::(); + let volumetric_lighting_uniform_buffer = world.resource::(); + let msaa = world.resource::(); + + // Fetch the uniform buffer and binding. + let (Some(pipeline), Some(volumetric_lighting_uniform_buffer_binding)) = ( + pipeline_cache.get_render_pipeline(**view_volumetric_lighting_pipeline), + volumetric_lighting_uniform_buffer.binding(), + ) else { + return Ok(()); + }; + + let postprocess = view_target.post_process_write(); + + // Create the bind group for the view. + // + // TODO: Cache this. + let volumetric_view_bind_group_layout = match *msaa { + Msaa::Off => &volumetric_lighting_pipeline.volumetric_view_bind_group_layout_no_msaa, + _ => &volumetric_lighting_pipeline.volumetric_view_bind_group_layout_msaa, + }; + let volumetric_view_bind_group = render_context.render_device().create_bind_group( + None, + volumetric_view_bind_group_layout, + &BindGroupEntries::sequential(( + volumetric_lighting_uniform_buffer_binding, + postprocess.source, + &volumetric_lighting_pipeline.color_sampler, + view_depth_texture.view(), + )), + ); + + let render_pass_descriptor = RenderPassDescriptor { + label: Some("volumetric lighting pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: postprocess.destination, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&render_pass_descriptor); + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &view_bind_group.value, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + ], + ); + render_pass.set_bind_group( + 1, + &volumetric_view_bind_group, + &[**view_volumetric_lighting_uniform_buffer_offset], + ); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} + +impl SpecializedRenderPipeline for VolumetricFogPipeline { + type Key = VolumetricFogPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mesh_view_layout = self + .mesh_view_layouts + .get_view_layout(key.mesh_pipeline_view_key); + + // We always use hardware 2x2 filtering for sampling the shadow map; the + // more accurate versions with percentage-closer filtering aren't worth + // the overhead. + let mut shader_defs = vec!["SHADOW_FILTER_METHOD_HARDWARE_2X2".into()]; + + // We need a separate layout for MSAA and non-MSAA. + let volumetric_view_bind_group_layout = if key + .mesh_pipeline_view_key + .contains(MeshPipelineViewLayoutKey::MULTISAMPLED) + { + shader_defs.push("MULTISAMPLED".into()); + self.volumetric_view_bind_group_layout_msaa.clone() + } else { + self.volumetric_view_bind_group_layout_no_msaa.clone() + }; + + RenderPipelineDescriptor { + label: Some("volumetric lighting pipeline".into()), + layout: vec![mesh_view_layout.clone(), volumetric_view_bind_group_layout], + push_constant_ranges: vec![], + vertex: fullscreen_shader_vertex_state(), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: VOLUMETRIC_FOG_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: if key.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + } + } +} + +/// Specializes volumetric fog pipelines for all views with that effect enabled. +pub fn prepare_volumetric_fog_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + volumetric_lighting_pipeline: Res, + view_targets: Query< + ( + Entity, + &ExtractedView, + Has, + Has, + Has, + Has, + ), + With, + >, + msaa: Res, +) { + for (entity, view, normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass) in + view_targets.iter() + { + // Create a mesh pipeline view layout key corresponding to the view. + let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(*msaa); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::NORMAL_PREPASS, normal_prepass); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::DEPTH_PREPASS, depth_prepass); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS, + motion_vector_prepass, + ); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::DEFERRED_PREPASS, + deferred_prepass, + ); + + // Specialize the pipeline. + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &volumetric_lighting_pipeline, + VolumetricFogPipelineKey { + mesh_pipeline_view_key, + hdr: view.hdr, + }, + ); + + commands + .entity(entity) + .insert(ViewVolumetricFogPipeline(pipeline_id)); + } +} + +/// A system that converts [`VolumetricFogSettings`] +pub fn prepare_volumetric_fog_uniforms( + mut commands: Commands, + mut volumetric_lighting_uniform_buffer: ResMut, + view_targets: Query<(Entity, &VolumetricFogSettings)>, + render_device: Res, + render_queue: Res, +) { + let Some(mut writer) = volumetric_lighting_uniform_buffer.get_writer( + view_targets.iter().len(), + &render_device, + &render_queue, + ) else { + return; + }; + + for (entity, volumetric_fog_settings) in view_targets.iter() { + let offset = writer.write(&VolumetricFogUniform { + fog_color: Vec3::from_slice( + &volumetric_fog_settings.fog_color.linear().to_f32_array()[0..3], + ), + light_tint: Vec3::from_slice( + &volumetric_fog_settings.light_tint.linear().to_f32_array()[0..3], + ), + ambient_color: Vec3::from_slice( + &volumetric_fog_settings + .ambient_color + .linear() + .to_f32_array()[0..3], + ), + ambient_intensity: volumetric_fog_settings.ambient_intensity, + step_count: volumetric_fog_settings.step_count, + max_depth: volumetric_fog_settings.max_depth, + absorption: volumetric_fog_settings.absorption, + scattering: volumetric_fog_settings.scattering, + density: volumetric_fog_settings.density, + scattering_asymmetry: volumetric_fog_settings.scattering_asymmetry, + light_intensity: volumetric_fog_settings.light_intensity, + }); + + commands + .entity(entity) + .insert(ViewVolumetricFogUniformOffset(offset)); + } +} + +/// A system that marks all view depth textures as readable in shaders. +/// +/// The volumetric lighting pass needs to do this, and it doesn't happen by +/// default. +pub fn prepare_view_depth_textures_for_volumetric_fog( + mut view_targets: Query<&mut Camera3d, With>, +) { + for mut camera in view_targets.iter_mut() { + camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits(); + } +} diff --git a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl new file mode 100644 index 0000000000..0ea6c18f7c --- /dev/null +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -0,0 +1,218 @@ +// A postprocessing shader that implements volumetric fog via raymarching and +// sampling directional light shadow maps. +// +// The overall approach is a combination of the volumetric rendering in [1] and +// the shadow map raymarching in [2]. First, we sample the depth buffer to +// determine how long our ray is. Then we do a raymarch, with physically-based +// calculations at each step to determine how much light was absorbed, scattered +// out, and scattered in. To determine in-scattering, we sample the shadow map +// for the light to determine whether the point was in shadow or not. +// +// [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html +// +// [2]: http://www.alexandre-pestana.com/volumetric-lights/ + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::mesh_view_bindings::{lights, view} +#import bevy_pbr::mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT +#import bevy_pbr::shadow_sampling::sample_shadow_map_hardware +#import bevy_pbr::shadows::{get_cascade_index, world_to_directional_light_local} +#import bevy_pbr::view_transformations::{ + frag_coord_to_ndc, + position_ndc_to_view, + position_ndc_to_world +} + +// The GPU version of [`VolumetricFogSettings`]. See the comments in +// `volumetric_fog/mod.rs` for descriptions of the fields here. +struct VolumetricFog { + fog_color: vec3, + light_tint: vec3, + ambient_color: vec3, + ambient_intensity: f32, + step_count: u32, + max_depth: f32, + absorption: f32, + scattering: f32, + density: f32, + scattering_asymmetry: f32, + light_intensity: f32, +} + +@group(1) @binding(0) var volumetric_fog: VolumetricFog; +@group(1) @binding(1) var color_texture: texture_2d; +@group(1) @binding(2) var color_sampler: sampler; + +#ifdef MULTISAMPLED +@group(1) @binding(3) var depth_texture: texture_depth_multisampled_2d; +#else +@group(1) @binding(3) var depth_texture: texture_depth_2d; +#endif + +// 1 / (4π) +const FRAC_4_PI: f32 = 0.07957747154594767; + +// The common Henyey-Greenstein asymmetric phase function [1] [2]. +// +// This determines how much light goes toward the viewer as opposed to away from +// the viewer. From a visual point of view, it controls how the light shafts +// appear and disappear as the camera looks at the light source. +// +// [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/ray-marching-get-it-right.html +// +// [2]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction +fn henyey_greenstein(neg_LdotV: f32) -> f32 { + let g = volumetric_fog.scattering_asymmetry; + let denom = 1.0 + g * g - 2.0 * g * neg_LdotV; + return FRAC_4_PI * (1.0 - g * g) / (denom * sqrt(denom)); +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + // Unpack the `volumetric_fog` settings. + let fog_color = volumetric_fog.fog_color; + let ambient_color = volumetric_fog.ambient_color; + let ambient_intensity = volumetric_fog.ambient_intensity; + let step_count = volumetric_fog.step_count; + let max_depth = volumetric_fog.max_depth; + let absorption = volumetric_fog.absorption; + let scattering = volumetric_fog.scattering; + let density = volumetric_fog.density; + let light_tint = volumetric_fog.light_tint; + let light_intensity = volumetric_fog.light_intensity; + + let exposure = view.exposure; + + // Sample the depth. If this is multisample, just use sample 0; this is + // approximate but good enough. + let frag_coord = in.position; + let depth = textureLoad(depth_texture, vec2(frag_coord.xy), 0); + + // Starting at the end depth, which we got above, figure out how long the + // ray we want to trace is and the length of each increment. + let end_depth = min( + max_depth, + -position_ndc_to_view(frag_coord_to_ndc(vec4(in.position.xy, depth, 1.0))).z + ); + let step_size = end_depth / f32(step_count); + + let directional_light_count = lights.n_directional_lights; + + // Calculate the ray origin (`Ro`) and the ray direction (`Rd`) in NDC, + // view, and world coordinates. + let Rd_ndc = vec3(frag_coord_to_ndc(in.position).xy, 1.0); + let Rd_view = normalize(position_ndc_to_view(Rd_ndc)); + let Ro_world = view.world_position; + let Rd_world = normalize(position_ndc_to_world(Rd_ndc) - Ro_world); + + // Use Beer's law [1] [2] to calculate the maximum amount of light that each + // directional light could contribute, and modulate that value by the light + // tint and fog color. (The actual value will in turn be modulated by the + // phase according to the Henyey-Greenstein formula.) + // + // We use a bit of a hack here. Conceptually, directional lights are + // infinitely far away. But, if we modeled exactly that, then directional + // lights would never contribute any light to the fog, because an + // infinitely-far directional light combined with an infinite amount of fog + // would result in complete absorption of the light. So instead we pretend + // that the directional light is `max_depth` units away and do the + // calculation in those terms. Because the fake distance to the directional + // light is a constant, this lets us perform the calculation once up here + // instead of marching secondary rays toward the light during the + // raymarching step, which improves performance dramatically. + // + // [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html + // + // [2]: https://en.wikipedia.org/wiki/Beer%E2%80%93Lambert_law + let light_attenuation = exp(-density * max_depth * (absorption + scattering)); + let light_factors_per_step = fog_color * light_tint * light_attenuation * scattering * + density * step_size * light_intensity * exposure; + + // Use Beer's law again to accumulate the ambient light all along the path. + var accumulated_color = exp(-end_depth * (absorption + scattering)) * ambient_color * + ambient_intensity; + + // Pre-calculate absorption (amount of light absorbed by the fog) and + // out-scattering (amount of light the fog scattered away). This is the same + // amount for every step. + let sample_attenuation = exp(-step_size * density * (absorption + scattering)); + + // This is the amount of the background that shows through. We're actually + // going to recompute this over and over again for each directional light, + // coming up with the same values each time. + var background_alpha = 1.0; + + for (var light_index = 0u; light_index < directional_light_count; light_index += 1u) { + // Volumetric lights are all sorted first, so the first time we come to + // a non-volumetric light, we know we've seen them all. + let light = &lights.directional_lights[light_index]; + if (((*light).flags & DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT) == 0) { + break; + } + + // Offset the depth value by the bias. + let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; + + // Compute phase, which determines the fraction of light that's + // scattered toward the camera instead of away from it. + let neg_LdotV = dot(normalize((*light).direction_to_light.xyz), Rd_world); + let phase = henyey_greenstein(neg_LdotV); + + // Modulate the factor we calculated above by the phase, fog color, + // light color, light tint. + let light_color_per_step = (*light).color.rgb * phase * light_factors_per_step; + + // Reset `background_alpha` for a new raymarch. + background_alpha = 1.0; + + // Start raymarching. + for (var step = 0u; step < step_count; step += 1u) { + // As an optimization, break if we've gotten too dark. + if (background_alpha < 0.001) { + break; + } + + // Calculate where we are in the ray. + let P_world = Ro_world + Rd_world * f32(step) * step_size; + let P_view = Rd_view * f32(step) * step_size; + + // Process absorption and out-scattering. + background_alpha *= sample_attenuation; + + // Compute in-scattering (amount of light other fog particles + // scattered into this ray). This is where any directional light is + // scattered in. + + // Prepare to sample the shadow map. + let cascade_index = get_cascade_index(light_index, P_view.z); + let light_local = world_to_directional_light_local( + light_index, + cascade_index, + vec4(P_world + depth_offset, 1.0) + ); + + // If we're outside the shadow map entirely, local light attenuation + // is zero. + var local_light_attenuation = f32(light_local.w != 0.0); + + // Otherwise, sample the shadow map to determine whether, and by how + // much, this sample is in the light. + if (local_light_attenuation != 0.0) { + let cascade = &(*light).cascades[cascade_index]; + let array_index = i32((*light).depth_texture_base_index + cascade_index); + local_light_attenuation = + sample_shadow_map_hardware(light_local.xy, light_local.z, array_index); + } + + if (local_light_attenuation != 0.0) { + // Accumulate the light. + accumulated_color += light_color_per_step * local_light_attenuation * + background_alpha; + } + } + } + + // We're done! Blend between the source color and the lit fog color. + let source = textureSample(color_texture, color_sampler, in.uv); + return vec4(source.rgb * background_alpha + accumulated_color, source.a); +} diff --git a/examples/3d/volumetric_fog.rs b/examples/3d/volumetric_fog.rs new file mode 100644 index 0000000000..8f53f81dcd --- /dev/null +++ b/examples/3d/volumetric_fog.rs @@ -0,0 +1,117 @@ +//! Demonstrates volumetric fog and lighting (light shafts or god rays). + +use bevy::{ + core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping, Skybox}, + math::vec3, + pbr::{VolumetricFogSettings, VolumetricLight}, + prelude::*, +}; + +const DIRECTIONAL_LIGHT_MOVEMENT_SPEED: f32 = 0.02; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(ClearColor(Color::Srgba(Srgba { + red: 0.02, + green: 0.02, + blue: 0.02, + alpha: 1.0, + }))) + .insert_resource(AmbientLight::NONE) + .add_systems(Startup, setup) + .add_systems(Update, tweak_scene) + .add_systems(Update, move_directional_light) + .run(); +} + +/// Initializes the scene. +fn setup(mut commands: Commands, asset_server: Res) { + // Spawn the glTF scene. + commands.spawn(SceneBundle { + scene: asset_server.load("models/VolumetricFogExample/VolumetricFogExample.glb#Scene0"), + ..default() + }); + + // Spawn the camera. Add the volumetric fog. + commands + .spawn(Camera3dBundle { + transform: Transform::from_xyz(-1.7, 1.5, 4.5) + .looking_at(vec3(-1.5, 1.7, 3.5), Vec3::Y), + camera: Camera { + hdr: true, + ..default() + }, + ..default() + }) + .insert(Tonemapping::TonyMcMapface) + .insert(BloomSettings::default()) + .insert(Skybox { + image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + brightness: 1000.0, + }) + .insert(VolumetricFogSettings::default()); + + // Add the help text. + commands.spawn( + TextBundle { + text: Text::from_section( + "Press WASD or the arrow keys to change the light direction", + TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 24.0, + ..default() + }, + ), + ..default() + } + .with_style(Style { + position_type: PositionType::Absolute, + bottom: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }), + ); +} + +/// A system that makes directional lights in the glTF scene into volumetric +/// lights with shadows. +fn tweak_scene( + mut commands: Commands, + mut lights: Query<(Entity, &mut DirectionalLight), Changed>, +) { + for (light, mut directional_light) in lights.iter_mut() { + // Shadows are needed for volumetric lights to work. + directional_light.shadows_enabled = true; + commands.entity(light).insert(VolumetricLight); + } +} + +/// Processes user requests to move the directional light. +fn move_directional_light( + input: Res>, + mut directional_lights: Query<&mut Transform, With>, +) { + let mut delta_theta = Vec2::ZERO; + if input.pressed(KeyCode::KeyW) || input.pressed(KeyCode::ArrowUp) { + delta_theta.y += DIRECTIONAL_LIGHT_MOVEMENT_SPEED; + } + if input.pressed(KeyCode::KeyS) || input.pressed(KeyCode::ArrowDown) { + delta_theta.y -= DIRECTIONAL_LIGHT_MOVEMENT_SPEED; + } + if input.pressed(KeyCode::KeyA) || input.pressed(KeyCode::ArrowLeft) { + delta_theta.x += DIRECTIONAL_LIGHT_MOVEMENT_SPEED; + } + if input.pressed(KeyCode::KeyD) || input.pressed(KeyCode::ArrowRight) { + delta_theta.x -= DIRECTIONAL_LIGHT_MOVEMENT_SPEED; + } + + if delta_theta == Vec2::ZERO { + return; + } + + let delta_quat = Quat::from_euler(EulerRot::XZY, delta_theta.y, 0.0, delta_theta.x); + for mut transform in directional_lights.iter_mut() { + transform.rotate(delta_quat); + } +} diff --git a/examples/README.md b/examples/README.md index 0125037e08..4a0abc3e44 100644 --- a/examples/README.md +++ b/examples/README.md @@ -165,6 +165,7 @@ Example | Description [Update glTF Scene](../examples/3d/update_gltf_scene.rs) | Update a scene from a glTF file, either by spawning the scene as a child of another entity, or by accessing the entities of the scene [Vertex Colors](../examples/3d/vertex_colors.rs) | Shows the use of vertex colors [Visibility range](../examples/3d/visibility_range.rs) | Demonstrates visibility ranges +[Volumetric fog](../examples/3d/volumetric_fog.rs) | Demonstrates volumetric fog and lighting [Wireframe](../examples/3d/wireframe.rs) | Showcases wireframe rendering ## Animation