bloom_3d/
bloom_3d.rs

1//! Illustrates bloom post-processing using HDR and emissive materials.
2
3use bevy::{
4    core_pipeline::tonemapping::Tonemapping,
5    math::ops,
6    post_process::bloom::{Bloom, BloomCompositeMode},
7    prelude::*,
8};
9use std::{
10    collections::hash_map::DefaultHasher,
11    hash::{Hash, Hasher},
12};
13
14fn main() {
15    App::new()
16        .add_plugins(DefaultPlugins)
17        .add_systems(Startup, setup_scene)
18        .add_systems(Update, (update_bloom_settings, bounce_spheres))
19        .run();
20}
21
22fn setup_scene(
23    mut commands: Commands,
24    mut meshes: ResMut<Assets<Mesh>>,
25    mut materials: ResMut<Assets<StandardMaterial>>,
26) {
27    commands.spawn((
28        Camera3d::default(),
29        Camera {
30            clear_color: ClearColorConfig::Custom(Color::BLACK),
31            ..default()
32        },
33        Tonemapping::TonyMcMapface, // 1. Using a tonemapper that desaturates to white is recommended
34        Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
35        Bloom::NATURAL, // 2. Enable bloom for the camera
36    ));
37
38    let material_emissive1 = materials.add(StandardMaterial {
39        emissive: LinearRgba::rgb(0.0, 0.0, 150.0), // 3. Put something bright in a dark environment to see the effect
40        ..default()
41    });
42    let material_emissive2 = materials.add(StandardMaterial {
43        emissive: LinearRgba::rgb(1000.0, 1000.0, 1000.0),
44        ..default()
45    });
46    let material_emissive3 = materials.add(StandardMaterial {
47        emissive: LinearRgba::rgb(50.0, 0.0, 0.0),
48        ..default()
49    });
50    let material_non_emissive = materials.add(StandardMaterial {
51        base_color: Color::BLACK,
52        ..default()
53    });
54
55    let mesh = meshes.add(Sphere::new(0.4).mesh().ico(5).unwrap());
56
57    for x in -5..5 {
58        for z in -5..5 {
59            // This generates a pseudo-random integer between `[0, 6)`, but deterministically so
60            // the same spheres are always the same colors.
61            let mut hasher = DefaultHasher::new();
62            (x, z).hash(&mut hasher);
63            let rand = (hasher.finish() + 3) % 6;
64
65            let (material, scale) = match rand {
66                0 => (material_emissive1.clone(), 0.5),
67                1 => (material_emissive2.clone(), 0.1),
68                2 => (material_emissive3.clone(), 1.0),
69                3..=5 => (material_non_emissive.clone(), 1.5),
70                _ => unreachable!(),
71            };
72
73            commands.spawn((
74                Mesh3d(mesh.clone()),
75                MeshMaterial3d(material),
76                Transform::from_xyz(x as f32 * 2.0, 0.0, z as f32 * 2.0)
77                    .with_scale(Vec3::splat(scale)),
78                Bouncing,
79            ));
80        }
81    }
82
83    // example instructions
84    commands.spawn((
85        Text::default(),
86        Node {
87            position_type: PositionType::Absolute,
88            bottom: px(12),
89            left: px(12),
90            ..default()
91        },
92    ));
93}
94
95// ------------------------------------------------------------------------------------------------
96
97fn update_bloom_settings(
98    camera: Single<(Entity, Option<&mut Bloom>), With<Camera>>,
99    mut text: Single<&mut Text>,
100    mut commands: Commands,
101    keycode: Res<ButtonInput<KeyCode>>,
102    time: Res<Time>,
103) {
104    let bloom = camera.into_inner();
105
106    match bloom {
107        (entity, Some(mut bloom)) => {
108            text.0 = "Bloom (Toggle: Space)\n".to_string();
109            text.push_str(&format!("(Q/A) Intensity: {:.2}\n", bloom.intensity));
110            text.push_str(&format!(
111                "(W/S) Low-frequency boost: {:.2}\n",
112                bloom.low_frequency_boost
113            ));
114            text.push_str(&format!(
115                "(E/D) Low-frequency boost curvature: {:.2}\n",
116                bloom.low_frequency_boost_curvature
117            ));
118            text.push_str(&format!(
119                "(R/F) High-pass frequency: {:.2}\n",
120                bloom.high_pass_frequency
121            ));
122            text.push_str(&format!(
123                "(T/G) Mode: {}\n",
124                match bloom.composite_mode {
125                    BloomCompositeMode::EnergyConserving => "Energy-conserving",
126                    BloomCompositeMode::Additive => "Additive",
127                }
128            ));
129            text.push_str(&format!(
130                "(Y/H) Threshold: {:.2}\n",
131                bloom.prefilter.threshold
132            ));
133            text.push_str(&format!(
134                "(U/J) Threshold softness: {:.2}\n",
135                bloom.prefilter.threshold_softness
136            ));
137            text.push_str(&format!("(I/K) Horizontal Scale: {:.2}\n", bloom.scale.x));
138
139            if keycode.just_pressed(KeyCode::Space) {
140                commands.entity(entity).remove::<Bloom>();
141            }
142
143            let dt = time.delta_secs();
144
145            if keycode.pressed(KeyCode::KeyA) {
146                bloom.intensity -= dt / 10.0;
147            }
148            if keycode.pressed(KeyCode::KeyQ) {
149                bloom.intensity += dt / 10.0;
150            }
151            bloom.intensity = bloom.intensity.clamp(0.0, 1.0);
152
153            if keycode.pressed(KeyCode::KeyS) {
154                bloom.low_frequency_boost -= dt / 10.0;
155            }
156            if keycode.pressed(KeyCode::KeyW) {
157                bloom.low_frequency_boost += dt / 10.0;
158            }
159            bloom.low_frequency_boost = bloom.low_frequency_boost.clamp(0.0, 1.0);
160
161            if keycode.pressed(KeyCode::KeyD) {
162                bloom.low_frequency_boost_curvature -= dt / 10.0;
163            }
164            if keycode.pressed(KeyCode::KeyE) {
165                bloom.low_frequency_boost_curvature += dt / 10.0;
166            }
167            bloom.low_frequency_boost_curvature =
168                bloom.low_frequency_boost_curvature.clamp(0.0, 1.0);
169
170            if keycode.pressed(KeyCode::KeyF) {
171                bloom.high_pass_frequency -= dt / 10.0;
172            }
173            if keycode.pressed(KeyCode::KeyR) {
174                bloom.high_pass_frequency += dt / 10.0;
175            }
176            bloom.high_pass_frequency = bloom.high_pass_frequency.clamp(0.0, 1.0);
177
178            if keycode.pressed(KeyCode::KeyG) {
179                bloom.composite_mode = BloomCompositeMode::Additive;
180            }
181            if keycode.pressed(KeyCode::KeyT) {
182                bloom.composite_mode = BloomCompositeMode::EnergyConserving;
183            }
184
185            if keycode.pressed(KeyCode::KeyH) {
186                bloom.prefilter.threshold -= dt;
187            }
188            if keycode.pressed(KeyCode::KeyY) {
189                bloom.prefilter.threshold += dt;
190            }
191            bloom.prefilter.threshold = bloom.prefilter.threshold.max(0.0);
192
193            if keycode.pressed(KeyCode::KeyJ) {
194                bloom.prefilter.threshold_softness -= dt / 10.0;
195            }
196            if keycode.pressed(KeyCode::KeyU) {
197                bloom.prefilter.threshold_softness += dt / 10.0;
198            }
199            bloom.prefilter.threshold_softness = bloom.prefilter.threshold_softness.clamp(0.0, 1.0);
200
201            if keycode.pressed(KeyCode::KeyK) {
202                bloom.scale.x -= dt * 2.0;
203            }
204            if keycode.pressed(KeyCode::KeyI) {
205                bloom.scale.x += dt * 2.0;
206            }
207            bloom.scale.x = bloom.scale.x.clamp(0.0, 8.0);
208        }
209
210        (entity, None) => {
211            text.0 = "Bloom: Off (Toggle: Space)".to_string();
212
213            if keycode.just_pressed(KeyCode::Space) {
214                commands.entity(entity).insert(Bloom::NATURAL);
215            }
216        }
217    }
218}
219
220#[derive(Component)]
221struct Bouncing;
222
223fn bounce_spheres(time: Res<Time>, mut query: Query<&mut Transform, With<Bouncing>>) {
224    for mut transform in query.iter_mut() {
225        transform.translation.y =
226            ops::sin(transform.translation.x + transform.translation.z + time.elapsed_secs());
227    }
228}