Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/3d/clustered_decal_maps.rs
9334 views
1
//! Demonstrates the normal map, metallic-roughness map, and emissive features
2
//! of clustered decals.
3
4
use std::{f32::consts::PI, time::Duration};
5
6
use bevy::{
7
asset::io::web::WebAssetPlugin,
8
camera::Hdr,
9
color::palettes::css::{CRIMSON, GOLD},
10
image::ImageLoaderSettings,
11
light::ClusteredDecal,
12
prelude::*,
13
};
14
use rand::{Rng, SeedableRng};
15
use rand_chacha::ChaCha8Rng;
16
17
use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender};
18
19
#[path = "../helpers/widgets.rs"]
20
mod widgets;
21
22
/// The demonstration textures that we use.
23
///
24
/// We cache these for efficiency.
25
#[derive(Resource)]
26
struct AppTextures {
27
/// The base color that all our decals have (the Bevy logo).
28
decal_base_color_texture: Handle<Image>,
29
30
/// A normal map that all our decals have.
31
///
32
/// This provides a nice raised embossed look.
33
decal_normal_map_texture: Handle<Image>,
34
35
/// The metallic-roughness map that all our decals have.
36
///
37
/// Metallic is in the blue channel and roughness is in the green channel,
38
/// like glTF requires.
39
decal_metallic_roughness_map_texture: Handle<Image>,
40
41
/// The emissive texture that can optionally be enabled.
42
///
43
/// This causes the white bird to glow.
44
decal_emissive_texture: Handle<Image>,
45
}
46
47
impl FromWorld for AppTextures {
48
fn from_world(world: &mut World) -> Self {
49
// Load all the decal textures.
50
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.load_with_settings(
54
get_web_asset_url("BevyLogo-Normal.png"),
55
|settings: &mut ImageLoaderSettings| settings.is_srgb = false,
56
),
57
decal_metallic_roughness_map_texture: asset_server.load_with_settings(
58
get_web_asset_url("BevyLogo-MetallicRoughness.png"),
59
|settings: &mut ImageLoaderSettings| settings.is_srgb = false,
60
),
61
decal_emissive_texture: asset_server.load(get_web_asset_url("BevyLogo-Emissive.png")),
62
}
63
}
64
}
65
66
/// A component that we place on our decals to track them for animation
67
/// purposes.
68
#[derive(Component)]
69
struct ExampleDecal {
70
/// The width and height of the square decal in meters.
71
size: f32,
72
/// What state the decal is in (animating in, idling, or animating out).
73
state: ExampleDecalState,
74
}
75
76
/// The animation state of a decal.
77
///
78
/// When each [`Timer`] goes off, the decal advances to the next state.
79
enum ExampleDecalState {
80
/// The decal has just been spawned and is animating in.
81
AnimatingIn(Timer),
82
/// The decal has animated in and is waiting to animate out.
83
Idling(Timer),
84
/// The decal is animating out.
85
///
86
/// When this timer expires, the decal is despawned.
87
AnimatingOut(Timer),
88
}
89
90
/// All settings that the user can change.
91
///
92
/// This app only has one: whether newly-spawned decals are emissive.
93
#[derive(Clone, Copy, PartialEq)]
94
enum AppSetting {
95
/// True if newly-spawned decals have an emissive channel (i.e. they glow),
96
/// or false otherwise.
97
EmissiveDecals(bool),
98
}
99
100
/// The current values of the settings that the user can change.
101
///
102
/// This app only has one: whether newly-spawned decals are emissive.
103
#[derive(Default, Resource)]
104
struct AppStatus {
105
/// True if newly-spawned decals have an emissive channel (i.e. they glow),
106
/// or false otherwise.
107
emissive_decals: bool,
108
}
109
110
/// Half of the width and height of the plane onto which the decals are
111
/// projected.
112
const PLANE_HALF_SIZE: f32 = 2.0;
113
/// The minimum width and height that a decal may have.
114
///
115
/// The actual size is determined randomly, using this value as a lower bound.
116
const DECAL_MIN_SIZE: f32 = 0.5;
117
/// The maximum width and height that a decal may have.
118
///
119
/// The actual size is determined randomly, using this value as an upper bound.
120
const DECAL_MAX_SIZE: f32 = 1.5;
121
122
/// How long it takes the decal to grow to its full size when animating in.
123
const DECAL_ANIMATE_IN_DURATION: Duration = Duration::from_millis(300);
124
/// How long a decal stays in the idle state before starting to animate out.
125
const DECAL_IDLE_DURATION: Duration = Duration::from_secs(10);
126
/// How long it takes the decal to shrink down to nothing when animating out.
127
const DECAL_ANIMATE_OUT_DURATION: Duration = Duration::from_millis(300);
128
129
/// The demo entry point.
130
fn 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)]
168
struct SeededRng(ChaCha8Rng);
169
170
/// Spawns all the objects in the scene.
171
fn 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
183
/// Spawns the plane onto which the decals are projected.
184
fn spawn_plane_mesh(
185
commands: &mut Commands,
186
asset_server: &AssetServer,
187
meshes: &mut Assets<Mesh>,
188
materials: &mut Assets<StandardMaterial>,
189
) {
190
// Create a plane onto which we project decals.
191
//
192
// As the plane has a normal map, we must generate tangents for the
193
// vertices.
194
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
// Give the plane some texture.
208
//
209
// Note that, as this is a normal map, we must disable sRGB when loading.
210
let normal_map_texture = asset_server.load_with_settings(
211
"textures/ScratchedGold-Normal.png",
212
|settings: &mut ImageLoaderSettings| settings.is_srgb = false,
213
);
214
215
// Actually spawn the plane.
216
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
227
/// Spawns a light to illuminate the scene.
228
fn 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
239
/// Spawns a camera.
240
fn 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
248
/// Spawns all the buttons at the bottom of the screen.
249
fn 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
262
/// Draws the outlines that show the bounds of the clustered decals.
263
fn draw_gizmos(mut gizmos: Gizmos, decals: Query<&GlobalTransform, With<ClusteredDecal>>) {
264
for global_transform in &decals {
265
gizmos.primitive_3d(
266
&Cuboid {
267
// Since the clustered decal is a 1×1×1 cube in model space, its
268
// half-size is half of the scaling part of its transform.
269
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
280
/// A system that spawns new decals at fixed intervals.
281
fn 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
// Tick the decal spawn timer. Check to see if we should spawn a new decal,
290
// and bail out if it's not yet time to.
291
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
// Generate a random position along the plane.
299
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
// Generate a random size for the decal.
306
let decal_size = seeded_rng.0.random_range(DECAL_MIN_SIZE..DECAL_MAX_SIZE);
307
308
// Generate a random rotation for the decal.
309
let theta = seeded_rng.0.random_range(0.0f32..PI);
310
311
// Now spawn the decal.
312
commands.spawn((
313
// Apply the textures.
314
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
// Spawn the decal at the right place. Note that the scale is initially
328
// zero; we'll animate it later.
329
Transform::from_translation(decal_position)
330
.with_scale(Vec3::ZERO)
331
.looking_to(Vec3::Z, Vec3::ZERO.with_xy(Vec2::from_angle(theta))),
332
// Create the component that tracks the animation state.
333
ExampleDecal {
334
size: decal_size,
335
state: ExampleDecalState::AnimatingIn(Timer::new(
336
DECAL_ANIMATE_IN_DURATION,
337
TimerMode::Once,
338
)),
339
},
340
));
341
}
342
343
/// A system that animates the decals growing as they enter and shrinking as
344
/// they leave.
345
fn 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
// Update the animation timers, and advance the animation state if the
352
// timer has expired.
353
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
// Actually animate the decal by adjusting its transform.
380
// All we have to do here is to compute the decal's scale as a fraction
381
// of its full size.
382
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
392
/// Updates the appearance of the radio buttons to reflect the current
393
/// application status.
394
fn 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
// We only have one setting in this particular application.
409
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
// Update the colors of the button itself.
417
widgets::update_ui_radio_button(&mut bg_color, selected);
418
}
419
if has_text {
420
// Update the colors of the button text.
421
widgets::update_ui_radio_button_text(entity, &mut writer, selected);
422
}
423
}
424
}
425
426
/// Handles the user's clicks on the radio button that determines whether the
427
/// newly-spawned decals have an emissive map.
428
fn 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
438
/// Returns the GitHub download URL for the given asset.
439
///
440
/// The files are expected to be in the `clustered_decal_maps` directory in the
441
/// [repository].
442
///
443
/// [repository]: https://github.com/bevyengine/bevy_asset_files
444
fn get_web_asset_url(name: &str) -> String {
445
format!(
446
"https://raw.githubusercontent.com/bevyengine/bevy_asset_files/refs/heads/main/\
447
clustered_decal_maps/{}",
448
name
449
)
450
}
451
452