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 image::ImageLoaderSettings,
11 light::ClusteredDecal,
12 prelude::*,
13};
14use chacha20::ChaCha8Rng;
15use rand::{RngExt, SeedableRng};
16
17use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender};
18
19#[path = "../helpers/widgets.rs"]
20mod widgets;
21
22#[derive(Resource)]
26struct AppTextures {
27 decal_base_color_texture: Handle<Image>,
29
30 decal_normal_map_texture: Handle<Image>,
34
35 decal_metallic_roughness_map_texture: Handle<Image>,
40
41 decal_emissive_texture: Handle<Image>,
45}
46
47impl FromWorld for AppTextures {
48 fn from_world(world: &mut World) -> Self {
49 let asset_server = world.resource::<AssetServer>();
51 AppTextures {
52 decal_base_color_texture: asset_server.load("branding/bevy_bird_dark.png"),
53 decal_normal_map_texture: asset_server
54 .load_builder()
55 .with_settings(|settings: &mut ImageLoaderSettings| settings.is_srgb = false)
56 .load(get_web_asset_url("BevyLogo-Normal.png")),
57 decal_metallic_roughness_map_texture: asset_server
58 .load_builder()
59 .with_settings(|settings: &mut ImageLoaderSettings| settings.is_srgb = false)
60 .load(get_web_asset_url("BevyLogo-MetallicRoughness.png")),
61 decal_emissive_texture: asset_server.load(get_web_asset_url("BevyLogo-Emissive.png")),
62 }
63 }
64}
65
66#[derive(Component)]
69struct ExampleDecal {
70 size: f32,
72 state: ExampleDecalState,
74}
75
76enum ExampleDecalState {
80 AnimatingIn(Timer),
82 Idling(Timer),
84 AnimatingOut(Timer),
88}
89
90#[derive(Clone, Copy, PartialEq)]
94enum AppSetting {
95 EmissiveDecals(bool),
98}
99
100#[derive(Default, Resource)]
104struct AppStatus {
105 emissive_decals: bool,
108}
109
110const PLANE_HALF_SIZE: f32 = 2.0;
113const DECAL_MIN_SIZE: f32 = 0.5;
117const DECAL_MAX_SIZE: f32 = 1.5;
121
122const DECAL_ANIMATE_IN_DURATION: Duration = Duration::from_millis(300);
124const DECAL_IDLE_DURATION: Duration = Duration::from_secs(10);
126const DECAL_ANIMATE_OUT_DURATION: Duration = Duration::from_millis(300);
128
129fn main() {
131 App::new()
132 .add_plugins(
133 DefaultPlugins
134 .set(WebAssetPlugin {
135 silence_startup_warning: true,
136 })
137 .set(WindowPlugin {
138 primary_window: Some(Window {
139 title: "Bevy Clustered Decal Maps Example".into(),
140 ..default()
141 }),
142 ..default()
143 }),
144 )
145 .add_message::<WidgetClickEvent<AppSetting>>()
146 .init_resource::<AppStatus>()
147 .init_resource::<AppTextures>()
148 .add_systems(Startup, setup)
149 .add_systems(Update, draw_gizmos)
150 .add_systems(Update, spawn_decal)
151 .add_systems(Update, animate_decals)
152 .add_systems(
153 Update,
154 (
155 widgets::handle_ui_interactions::<AppSetting>,
156 update_radio_buttons,
157 ),
158 )
159 .add_systems(
160 Update,
161 handle_emission_type_change.after(widgets::handle_ui_interactions::<AppSetting>),
162 )
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((
251 widgets::main_ui_node(),
252 children![widgets::option_buttons(
253 "Emissive Decals",
254 &[
255 (AppSetting::EmissiveDecals(true), "On"),
256 (AppSetting::EmissiveDecals(false), "Off"),
257 ],
258 ),],
259 ));
260}
261
262fn draw_gizmos(mut gizmos: Gizmos, decals: Query<&GlobalTransform, With<ClusteredDecal>>) {
264 for global_transform in &decals {
265 gizmos.primitive_3d(
266 &Cuboid {
267 half_size: global_transform.scale() * 0.5,
270 },
271 Isometry3d {
272 rotation: global_transform.rotation(),
273 translation: global_transform.translation_vec3a(),
274 },
275 GOLD,
276 );
277 }
278}
279
280fn spawn_decal(
282 mut commands: Commands,
283 app_status: Res<AppStatus>,
284 app_textures: Res<AppTextures>,
285 time: Res<Time>,
286 mut decal_spawn_timer: Local<Option<Timer>>,
287 mut seeded_rng: ResMut<SeededRng>,
288) {
289 let decal_spawn_timer = decal_spawn_timer
292 .get_or_insert_with(|| Timer::new(Duration::from_millis(1000), TimerMode::Repeating));
293 decal_spawn_timer.tick(time.delta());
294 if !decal_spawn_timer.just_finished() {
295 return;
296 }
297
298 let decal_position = vec3(
300 seeded_rng.0.random_range(-PLANE_HALF_SIZE..PLANE_HALF_SIZE),
301 seeded_rng.0.random_range(-PLANE_HALF_SIZE..PLANE_HALF_SIZE),
302 0.0,
303 );
304
305 let decal_size = seeded_rng.0.random_range(DECAL_MIN_SIZE..DECAL_MAX_SIZE);
307
308 let theta = seeded_rng.0.random_range(0.0f32..PI);
310
311 commands.spawn((
313 ClusteredDecal {
315 base_color_texture: Some(app_textures.decal_base_color_texture.clone()),
316 normal_map_texture: Some(app_textures.decal_normal_map_texture.clone()),
317 metallic_roughness_texture: Some(
318 app_textures.decal_metallic_roughness_map_texture.clone(),
319 ),
320 emissive_texture: if app_status.emissive_decals {
321 Some(app_textures.decal_emissive_texture.clone())
322 } else {
323 None
324 },
325 ..ClusteredDecal::default()
326 },
327 Transform::from_translation(decal_position)
330 .with_scale(Vec3::ZERO)
331 .looking_to(Vec3::Z, Vec3::ZERO.with_xy(Vec2::from_angle(theta))),
332 ExampleDecal {
334 size: decal_size,
335 state: ExampleDecalState::AnimatingIn(Timer::new(
336 DECAL_ANIMATE_IN_DURATION,
337 TimerMode::Once,
338 )),
339 },
340 ));
341}
342
343fn animate_decals(
346 mut commands: Commands,
347 mut decals_query: Query<(Entity, &mut ExampleDecal, &mut Transform)>,
348 time: Res<Time>,
349) {
350 for (decal_entity, mut example_decal, mut decal_transform) in decals_query.iter_mut() {
351 match example_decal.state {
354 ExampleDecalState::AnimatingIn(ref mut timer) => {
355 timer.tick(time.delta());
356 if timer.just_finished() {
357 example_decal.state =
358 ExampleDecalState::Idling(Timer::new(DECAL_IDLE_DURATION, TimerMode::Once));
359 }
360 }
361 ExampleDecalState::Idling(ref mut timer) => {
362 timer.tick(time.delta());
363 if timer.just_finished() {
364 example_decal.state = ExampleDecalState::AnimatingOut(Timer::new(
365 DECAL_ANIMATE_OUT_DURATION,
366 TimerMode::Once,
367 ));
368 }
369 }
370 ExampleDecalState::AnimatingOut(ref mut timer) => {
371 timer.tick(time.delta());
372 if timer.just_finished() {
373 commands.entity(decal_entity).despawn();
374 continue;
375 }
376 }
377 }
378
379 let new_decal_scale_factor = match example_decal.state {
383 ExampleDecalState::AnimatingIn(ref timer) => timer.fraction(),
384 ExampleDecalState::Idling(_) => 1.0,
385 ExampleDecalState::AnimatingOut(ref timer) => timer.fraction_remaining(),
386 };
387 decal_transform.scale =
388 Vec3::splat(example_decal.size * new_decal_scale_factor).with_z(1.0);
389 }
390}
391
392fn update_radio_buttons(
395 mut widgets: Query<
396 (
397 Entity,
398 Option<&mut BackgroundColor>,
399 Has<Text>,
400 &WidgetClickSender<AppSetting>,
401 ),
402 Or<(With<RadioButton>, With<RadioButtonText>)>,
403 >,
404 app_status: Res<AppStatus>,
405 mut writer: TextUiWriter,
406) {
407 for (entity, image, has_text, sender) in widgets.iter_mut() {
408 let selected = match **sender {
410 AppSetting::EmissiveDecals(emissive_decals) => {
411 emissive_decals == app_status.emissive_decals
412 }
413 };
414
415 if let Some(mut bg_color) = image {
416 widgets::update_ui_radio_button(&mut bg_color, selected);
418 }
419 if has_text {
420 widgets::update_ui_radio_button_text(entity, &mut writer, selected);
422 }
423 }
424}
425
426fn handle_emission_type_change(
429 mut app_status: ResMut<AppStatus>,
430 mut events: MessageReader<WidgetClickEvent<AppSetting>>,
431) {
432 for event in events.read() {
433 let AppSetting::EmissiveDecals(on) = **event;
434 app_status.emissive_decals = on;
435 }
436}
437
438fn get_web_asset_url(name: &str) -> String {
445 format!(
446 "https://raw.githubusercontent.com/bevyengine/bevy_asset_files/refs/heads/main/\
447clustered_decal_maps/{}",
448 name
449 )
450}