1use std::{f32::consts::PI, time::Duration};
5
6use bevy::{
7 asset::io::web::WebAssetPlugin,
8 camera::Hdr,
9 color::palettes::css::{CRIMSON, GOLD},
10 feathers::{dark_theme::create_dark_theme, theme::UiTheme, FeathersPlugins},
11 image::ImageLoaderSettings,
12 light::ClusteredDecal,
13 prelude::*,
14 ui_widgets::{radio_self_update, ValueChange},
15};
16use chacha20::ChaCha8Rng;
17use rand::{RngExt, SeedableRng};
18
19use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
20
21#[path = "../helpers/radio.rs"]
22mod radio;
23
24#[derive(Resource)]
28struct AppTextures {
29 decal_base_color_texture: Handle<Image>,
31
32 decal_normal_map_texture: Handle<Image>,
36
37 decal_metallic_roughness_map_texture: Handle<Image>,
42
43 decal_emissive_texture: Handle<Image>,
47}
48
49impl FromWorld for AppTextures {
50 fn from_world(world: &mut World) -> Self {
51 let asset_server = world.resource::<AssetServer>();
53 AppTextures {
54 decal_base_color_texture: asset_server.load("branding/bevy_bird_dark.png"),
55 decal_normal_map_texture: asset_server
56 .load_builder()
57 .with_settings(|settings: &mut ImageLoaderSettings| settings.is_srgb = false)
58 .load(get_web_asset_url("BevyLogo-Normal.png")),
59 decal_metallic_roughness_map_texture: asset_server
60 .load_builder()
61 .with_settings(|settings: &mut ImageLoaderSettings| settings.is_srgb = false)
62 .load(get_web_asset_url("BevyLogo-MetallicRoughness.png")),
63 decal_emissive_texture: asset_server.load(get_web_asset_url("BevyLogo-Emissive.png")),
64 }
65 }
66}
67
68#[derive(Component)]
71struct ExampleDecal {
72 size: f32,
74 state: ExampleDecalState,
76}
77
78enum ExampleDecalState {
82 AnimatingIn(Timer),
84 Idling(Timer),
86 AnimatingOut(Timer),
90}
91
92#[derive(Clone, Copy, Component, PartialEq)]
96enum AppSetting {
97 EmissiveDecals(bool),
100}
101
102impl Default for AppSetting {
103 fn default() -> Self {
104 Self::EmissiveDecals(false)
105 }
106}
107
108#[derive(Default, Resource)]
112struct AppStatus {
113 emissive_decals: bool,
116}
117
118const PLANE_HALF_SIZE: f32 = 2.0;
121const DECAL_MIN_SIZE: f32 = 0.5;
125const DECAL_MAX_SIZE: f32 = 1.5;
129
130const DECAL_ANIMATE_IN_DURATION: Duration = Duration::from_millis(300);
132const DECAL_IDLE_DURATION: Duration = Duration::from_secs(10);
134const DECAL_ANIMATE_OUT_DURATION: Duration = Duration::from_millis(300);
136
137fn main() {
139 App::new()
140 .add_plugins((
141 DefaultPlugins
142 .set(WebAssetPlugin {
143 silence_startup_warning: true,
144 })
145 .set(WindowPlugin {
146 primary_window: Some(Window {
147 title: "Bevy Clustered Decal Maps Example".into(),
148 ..default()
149 }),
150 ..default()
151 }),
152 FeathersPlugins,
153 ))
154 .init_resource::<AppStatus>()
155 .init_resource::<AppTextures>()
156 .insert_resource(UiTheme(create_dark_theme()))
157 .add_systems(Startup, setup)
158 .add_systems(Update, draw_gizmos)
159 .add_systems(Update, spawn_decal)
160 .add_systems(Update, animate_decals)
161 .add_observer(handle_emission_type_change)
162 .add_observer(radio_self_update)
163 .insert_resource(SeededRng(ChaCha8Rng::seed_from_u64(19878367467712)))
164 .run();
165}
166
167#[derive(Resource)]
168struct SeededRng(ChaCha8Rng);
169
170fn setup(
172 mut commands: Commands,
173 asset_server: Res<AssetServer>,
174 mut meshes: ResMut<Assets<Mesh>>,
175 mut materials: ResMut<Assets<StandardMaterial>>,
176) {
177 spawn_plane_mesh(&mut commands, &asset_server, &mut meshes, &mut materials);
178 spawn_light(&mut commands);
179 spawn_camera(&mut commands);
180 spawn_buttons(&mut commands);
181}
182
183fn spawn_plane_mesh(
185 commands: &mut Commands,
186 asset_server: &AssetServer,
187 meshes: &mut Assets<Mesh>,
188 materials: &mut Assets<StandardMaterial>,
189) {
190 let plane_mesh = meshes.add(
195 Plane3d {
196 normal: Dir3::NEG_Z,
197 half_size: Vec2::splat(PLANE_HALF_SIZE),
198 }
199 .mesh()
200 .build()
201 .with_duplicated_vertices()
202 .with_computed_flat_normals()
203 .with_generated_tangents()
204 .unwrap(),
205 );
206
207 let normal_map_texture = asset_server
211 .load_builder()
212 .with_settings(|settings: &mut ImageLoaderSettings| settings.is_srgb = false)
213 .load("textures/ScratchedGold-Normal.png");
214
215 commands.spawn((
217 Mesh3d(plane_mesh),
218 MeshMaterial3d(materials.add(StandardMaterial {
219 base_color: Color::from(CRIMSON),
220 normal_map_texture: Some(normal_map_texture),
221 ..StandardMaterial::default()
222 })),
223 Transform::IDENTITY,
224 ));
225}
226
227fn spawn_light(commands: &mut Commands) {
229 commands.spawn((
230 PointLight {
231 intensity: 10_000_000.,
232 range: 100.0,
233 ..default()
234 },
235 Transform::from_xyz(8.0, 16.0, -8.0),
236 ));
237}
238
239fn spawn_camera(commands: &mut Commands) {
241 commands.spawn((
242 Camera3d::default(),
243 Transform::from_xyz(2.0, 0.0, -7.0).looking_at(Vec3::ZERO, Vec3::Y),
244 Hdr,
245 ));
246}
247
248fn spawn_buttons(commands: &mut Commands) {
250 commands.spawn_scene(bsn! {
251 @main_ui_node_scene()
252 Children [
253 @feathers_option_buttons(
254 "Emissive Decals",
255 &[
256 (AppSetting::EmissiveDecals(false), "Off"),
257 (AppSetting::EmissiveDecals(true), "On"),
258 ],
259 0,
260 )
261 ]
262 });
263}
264
265fn draw_gizmos(mut gizmos: Gizmos, decals: Query<&GlobalTransform, With<ClusteredDecal>>) {
267 for global_transform in &decals {
268 gizmos.primitive_3d(
269 &Cuboid {
270 half_size: global_transform.scale() * 0.5,
273 },
274 Isometry3d {
275 rotation: global_transform.rotation(),
276 translation: global_transform.translation_vec3a(),
277 },
278 GOLD,
279 );
280 }
281}
282
283fn spawn_decal(
285 mut commands: Commands,
286 app_status: Res<AppStatus>,
287 app_textures: Res<AppTextures>,
288 time: Res<Time>,
289 mut decal_spawn_timer: Local<Option<Timer>>,
290 mut seeded_rng: ResMut<SeededRng>,
291) {
292 let decal_spawn_timer = decal_spawn_timer
295 .get_or_insert_with(|| Timer::new(Duration::from_millis(1000), TimerMode::Repeating));
296 decal_spawn_timer.tick(time.delta());
297 if !decal_spawn_timer.just_finished() {
298 return;
299 }
300
301 let decal_position = vec3(
303 seeded_rng.0.random_range(-PLANE_HALF_SIZE..PLANE_HALF_SIZE),
304 seeded_rng.0.random_range(-PLANE_HALF_SIZE..PLANE_HALF_SIZE),
305 0.0,
306 );
307
308 let decal_size = seeded_rng.0.random_range(DECAL_MIN_SIZE..DECAL_MAX_SIZE);
310
311 let theta = seeded_rng.0.random_range(0.0f32..PI);
313
314 commands.spawn((
316 ClusteredDecal {
318 base_color_texture: Some(app_textures.decal_base_color_texture.clone()),
319 normal_map_texture: Some(app_textures.decal_normal_map_texture.clone()),
320 metallic_roughness_texture: Some(
321 app_textures.decal_metallic_roughness_map_texture.clone(),
322 ),
323 emissive_texture: if app_status.emissive_decals {
324 Some(app_textures.decal_emissive_texture.clone())
325 } else {
326 None
327 },
328 ..ClusteredDecal::default()
329 },
330 Transform::from_translation(decal_position)
333 .with_scale(Vec3::ZERO)
334 .looking_to(Vec3::Z, Vec3::ZERO.with_xy(Vec2::from_angle(theta))),
335 ExampleDecal {
337 size: decal_size,
338 state: ExampleDecalState::AnimatingIn(Timer::new(
339 DECAL_ANIMATE_IN_DURATION,
340 TimerMode::Once,
341 )),
342 },
343 ));
344}
345
346fn animate_decals(
349 mut commands: Commands,
350 mut decals_query: Query<(Entity, &mut ExampleDecal, &mut Transform)>,
351 time: Res<Time>,
352) {
353 for (decal_entity, mut example_decal, mut decal_transform) in decals_query.iter_mut() {
354 match example_decal.state {
357 ExampleDecalState::AnimatingIn(ref mut timer) => {
358 timer.tick(time.delta());
359 if timer.just_finished() {
360 example_decal.state =
361 ExampleDecalState::Idling(Timer::new(DECAL_IDLE_DURATION, TimerMode::Once));
362 }
363 }
364 ExampleDecalState::Idling(ref mut timer) => {
365 timer.tick(time.delta());
366 if timer.just_finished() {
367 example_decal.state = ExampleDecalState::AnimatingOut(Timer::new(
368 DECAL_ANIMATE_OUT_DURATION,
369 TimerMode::Once,
370 ));
371 }
372 }
373 ExampleDecalState::AnimatingOut(ref mut timer) => {
374 timer.tick(time.delta());
375 if timer.just_finished() {
376 commands.entity(decal_entity).despawn();
377 continue;
378 }
379 }
380 }
381
382 let new_decal_scale_factor = match example_decal.state {
386 ExampleDecalState::AnimatingIn(ref timer) => timer.fraction(),
387 ExampleDecalState::Idling(_) => 1.0,
388 ExampleDecalState::AnimatingOut(ref timer) => timer.fraction_remaining(),
389 };
390 decal_transform.scale =
391 Vec3::splat(example_decal.size * new_decal_scale_factor).with_z(1.0);
392 }
393}
394
395fn handle_emission_type_change(
398 event: On<ValueChange<Entity>>,
399 new_value_q: Query<&RadioButtonOptionValue<AppSetting>>,
400 mut app_status: ResMut<AppStatus>,
401) {
402 let Ok(RadioButtonOptionValue(setting)) = new_value_q.get(event.value) else {
403 return;
404 };
405
406 let AppSetting::EmissiveDecals(on) = *setting;
407 app_status.emissive_decals = on;
408}
409
410fn get_web_asset_url(name: &str) -> String {
417 format!(
418 "https://raw.githubusercontent.com/bevyengine/bevy_asset_files/refs/heads/main/\
419clustered_decal_maps/{}",
420 name
421 )
422}