Implement volumetric fog and volumetric lighting, also known as light shafts or god rays. (#13057)

This commit implements a more physically-accurate, but slower, form of
fog than the `bevy_pbr::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` has 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.

Volumetric rendering is a huge subject, and I deliberately kept the
scope of this commit small. Possible follow-ups include:

1. Raymarching at a lower resolution.

2. A post-processing blur (especially useful when combined with (1)).

3. Supporting point lights and spot lights.

4. Supporting lights with no shadow maps.

5. Supporting irradiance volumes and reflection probes.

6. Voxel components that reuse the volumetric fog code to create voxel
shapes.

7. *Horizon: Zero Dawn*-style clouds.

These are all useful, but out of scope of this patch for now, to keep
things tidy and easy to review.

A new example, `volumetric_fog`, has been added to demonstrate the
effect.

## Changelog

### Added

* A new component, `VolumetricFog`, is available, to allow for a more
physically-accurate, but more resource-intensive, form of fog.

* A new component, `VolumetricLight`, can be placed on directional
lights to make them interact with `VolumetricFog`. Notably, this allows
such lights to emit light shafts/god rays.

![Screenshot 2024-04-21
162808](https://github.com/bevyengine/bevy/assets/157897/7a1fc81d-eed5-4735-9419-286c496391a9)

![Screenshot 2024-04-21
132005](https://github.com/bevyengine/bevy/assets/157897/e6d3b5ca-8f59-488d-a3de-15e95aaf4995)

[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
This commit is contained in:
Patrick Walton 2024-05-16 12:13:18 -05:00 committed by GitHub
parent 4c3b7679ec
commit 19bfa41768
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1085 additions and 27 deletions

View file

@ -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"

View file

@ -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<TextureUsages> for Camera3dDepthTextureUsage {
fn from(value: TextureUsages) -> Self {

View file

@ -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();

View file

@ -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 {

View file

@ -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,

View file

@ -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
}

View file

@ -165,7 +165,10 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass<M: Material>(
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(),
],

View file

@ -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<SpotLight>,
>,
@ -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;
}

View file

@ -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());

View file

@ -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),
);

View file

@ -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

View file

@ -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<f32>, surface_normal: vec3<f32>) -> 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<f32>
) -> vec4<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<f32>(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<f32>(-1.0)) || offset_position_ndc.z < 0.0
|| any(offset_position_ndc > vec3<f32>(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<f32>, surface_normal: vec3<f32>) -> 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<f32>(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<f32>, surface_normal: vec3<f32>, view_z: f32) -> f32 {

View file

@ -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<Shader> = 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<VolumetricFogUniform>);
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::<SpecializedRenderPipelines<VolumetricFogPipeline>>()
.init_resource::<VolumetricFogUniformBuffer>()
.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::<VolumetricFogPipeline>()
.add_render_graph_node::<ViewNodeRunner<VolumetricFogNode>>(
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::<RenderDevice>();
let mesh_view_layouts = world.resource::<MeshPipelineViewLayouts>();
// 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::<VolumetricFogUniform>(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<Query<(Entity, &VolumetricFogSettings)>>,
volumetric_lights: Extract<Query<(Entity, &VolumetricLight)>>,
) {
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<ViewTarget>,
Read<ViewDepthTexture>,
Read<ViewVolumetricFogPipeline>,
Read<ViewUniformOffset>,
Read<ViewLightsUniformOffset>,
Read<ViewFogUniformOffset>,
Read<ViewLightProbesUniformOffset>,
Read<ViewVolumetricFogUniformOffset>,
Read<MeshViewBindGroup>,
);
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::<PipelineCache>();
let volumetric_lighting_pipeline = world.resource::<VolumetricFogPipeline>();
let volumetric_lighting_uniform_buffer = world.resource::<VolumetricFogUniformBuffer>();
let msaa = world.resource::<Msaa>();
// 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<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<VolumetricFogPipeline>>,
volumetric_lighting_pipeline: Res<VolumetricFogPipeline>,
view_targets: Query<
(
Entity,
&ExtractedView,
Has<NormalPrepass>,
Has<DepthPrepass>,
Has<MotionVectorPrepass>,
Has<DeferredPrepass>,
),
With<VolumetricFogSettings>,
>,
msaa: Res<Msaa>,
) {
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<VolumetricFogUniformBuffer>,
view_targets: Query<(Entity, &VolumetricFogSettings)>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
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<VolumetricFogSettings>>,
) {
for mut camera in view_targets.iter_mut() {
camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
}
}

View file

@ -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<f32>,
light_tint: vec3<f32>,
ambient_color: vec3<f32>,
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<uniform> volumetric_fog: VolumetricFog;
@group(1) @binding(1) var color_texture: texture_2d<f32>;
@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<f32> {
// 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<i32>(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);
}

View file

@ -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<AssetServer>) {
// 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<DirectionalLight>>,
) {
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<ButtonInput<KeyCode>>,
mut directional_lights: Query<&mut Transform, With<DirectionalLight>>,
) {
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);
}
}

View file

@ -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