Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/2d/dynamic_mip_generation.rs
9328 views
1
//! Demonstrates use of the mipmap generation plugin to generate mipmaps for a
2
//! texture.
3
//!
4
//! This example demonstrates use of the [`MipGenerationJobs`] resource to
5
//! generate mipmap levels for a texture at runtime. It generates the first
6
//! mipmap level of a texture on CPU, which consists of two ellipses with
7
//! randomly chosen colors. Then it invokes Bevy's mipmap generation pass to
8
//! generate the remaining mipmap levels for the texture on the GPU. You can use
9
//! the UI to regenerate the texture and adjust its size to prove that the
10
//! texture, and its mipmaps, are truly being generated at runtime and aren't
11
//! being built ahead of time.
12
13
use std::array;
14
15
use bevy::{
16
asset::RenderAssetUsages,
17
core_pipeline::{
18
mip_generation::{
19
generate_mips_for_phase, MipGenerationJobs, MipGenerationPhaseId,
20
MipGenerationPipelines,
21
},
22
schedule::Core2d,
23
},
24
prelude::*,
25
reflect::TypePath,
26
render::{
27
render_asset::RenderAssets,
28
render_resource::{
29
AsBindGroup, Extent3d, PipelineCache, TextureDimension, TextureFormat, TextureUsages,
30
},
31
renderer::RenderContext,
32
texture::GpuImage,
33
Extract, RenderApp,
34
},
35
shader::ShaderRef,
36
sprite::Text2dShadow,
37
sprite_render::{AlphaMode2d, Material2d, Material2dPlugin},
38
window::{PrimaryWindow, WindowResized},
39
};
40
use rand::{Rng, SeedableRng};
41
use rand_chacha::ChaCha8Rng;
42
43
use crate::widgets::{
44
RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender, BUTTON_BORDER,
45
BUTTON_BORDER_COLOR, BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING,
46
};
47
48
#[path = "../helpers/widgets.rs"]
49
mod widgets;
50
51
/// The time in seconds that it takes the animation of the image shrinking and
52
/// growing to play.
53
const ANIMATION_PERIOD: f32 = 2.0;
54
55
/// The path to the single mip level 2D material shader inside the `assets`
56
/// directory.
57
const SINGLE_MIP_LEVEL_SHADER_ASSET_PATH: &str = "shaders/single_mip_level.wgsl";
58
59
/// The distance from the left side of the column of mipmap slices to the right
60
/// side of the area used for the animation.
61
const MIP_SLICES_MARGIN_LEFT: f32 = 64.0;
62
/// The distance from the right side of the window to the right side of the
63
/// column of mipmap slices.
64
const MIP_SLICES_MARGIN_RIGHT: f32 = 12.0;
65
/// The width of the column of mipmap slices, not counting the labels, as a
66
/// fraction of the width of the window.
67
const MIP_SLICES_WIDTH: f32 = 1.0 / 6.0;
68
69
/// The size of the mipmap level label font.
70
const FONT_SIZE: FontSize = FontSize::Px(16.0);
71
72
/// All settings that the user can change via the UI.
73
#[derive(Resource)]
74
struct AppStatus {
75
/// Whether mipmaps are to be generated for the image.
76
enable_mip_generation: EnableMipGeneration,
77
/// The width of the image.
78
image_width: ImageSize,
79
/// The height of the image.
80
image_height: ImageSize,
81
/// Seeded random generator.
82
rng: ChaCha8Rng,
83
}
84
85
impl Default for AppStatus {
86
fn default() -> Self {
87
AppStatus {
88
enable_mip_generation: EnableMipGeneration::On,
89
image_width: ImageSize::Size640,
90
image_height: ImageSize::Size480,
91
rng: ChaCha8Rng::seed_from_u64(19878367467713),
92
}
93
}
94
}
95
96
/// Identifies one of the settings that can be changed by the user.
97
#[derive(Clone)]
98
enum AppSetting {
99
/// Regenerates the top mipmap level.
100
///
101
/// This is more of an *operation* than a *setting* per se, but it was
102
/// convenient to use the `AppSetting` infrastructure for the "Regenerate
103
/// Top Mip Level" button.
104
RegenerateTopMipLevel,
105
106
/// Whether mipmaps should be generated.
107
EnableMipGeneration(EnableMipGeneration),
108
109
/// The width of the image.
110
ImageWidth(ImageSize),
111
112
/// The height of the image.
113
ImageHeight(ImageSize),
114
}
115
116
/// Whether mipmap levels will be generated.
117
///
118
/// Turning off the generation of mipmap levels, and then regenerating the
119
/// image, will cause all mipmap levels other than the first to be blank. This
120
/// will in turn cause the image to fade out as it shrinks, as the GPU switches
121
/// to rendering mipmap levels that don't have associated images.
122
#[derive(Clone, Copy, Default, PartialEq)]
123
enum EnableMipGeneration {
124
/// Mipmap levels are generated for the image.
125
#[default]
126
On,
127
/// Mipmap levels aren't generated for the image.
128
Off,
129
}
130
131
/// Possible lengths for an image side from which the user can choose.
132
#[derive(Clone, Copy, Default, PartialEq)]
133
#[repr(u32)]
134
enum ImageSize {
135
/// 240px.
136
Size240 = 240,
137
/// 480px (the default height).
138
Size480 = 480,
139
/// 640px (the default width).
140
#[default]
141
Size640 = 640,
142
/// 1080px.
143
Size1080 = 1080,
144
/// 1920px.
145
Size1920 = 1920,
146
}
147
148
/// A 2D material that displays only one mipmap level of a texture.
149
///
150
/// This is the material used for the column of mip levels on the right side of
151
/// the window.
152
#[derive(Clone, Asset, TypePath, AsBindGroup, Debug)]
153
struct SingleMipLevelMaterial {
154
/// The mip level that this material will show, starting from 0.
155
#[uniform(0)]
156
mip_level: u32,
157
/// The image that is to be shown.
158
#[texture(1)]
159
#[sampler(2)]
160
texture: Handle<Image>,
161
}
162
163
impl Material2d for SingleMipLevelMaterial {
164
fn fragment_shader() -> ShaderRef {
165
SINGLE_MIP_LEVEL_SHADER_ASSET_PATH.into()
166
}
167
168
fn alpha_mode(&self) -> AlphaMode2d {
169
AlphaMode2d::Blend
170
}
171
}
172
173
/// A marker component for the image on the left side of the window.
174
///
175
/// This is the image that grows and shrinks to demonstrate the effect of mip
176
/// levels' presence and absence.
177
#[derive(Component)]
178
struct AnimatedImage;
179
180
/// A resource that stores the main image for which mipmaps are to be generated
181
/// (or not generated, depending on the application settings).
182
#[derive(Resource, Deref, DerefMut)]
183
struct MipmapSourceImage(Handle<Image>);
184
185
/// An iterator that yields the size of each mipmap level for an image, one
186
/// after another.
187
struct MipmapSizeIterator {
188
/// The size of the previous mipmap level, or `None` if this iterator is
189
/// finished.
190
size: Option<UVec2>,
191
}
192
193
const MIP_GENERATION_PHASE_ID: MipGenerationPhaseId = MipGenerationPhaseId(0);
194
195
/// A marker component for every mesh that displays the image.
196
///
197
/// When the image is regenerated, we despawn and respawn all entities with this
198
/// component.
199
#[derive(Component)]
200
struct ImageView;
201
202
/// A message that's sent whenever the image and the corresponding views need to
203
/// be regenerated.
204
#[derive(Clone, Copy, Debug, Message)]
205
struct RegenerateImage;
206
207
/// The application entry point.
208
fn main() {
209
let mut app = App::new();
210
app.add_plugins((
211
DefaultPlugins.set(WindowPlugin {
212
primary_window: Some(Window {
213
title: "Bevy Dynamic Mipmap Generation Example".into(),
214
..default()
215
}),
216
..default()
217
}),
218
Material2dPlugin::<SingleMipLevelMaterial>::default(),
219
))
220
.init_resource::<AppStatus>()
221
.init_resource::<AppAssets>()
222
.add_message::<RegenerateImage>()
223
.add_message::<WidgetClickEvent<AppSetting>>()
224
.add_systems(Startup, setup)
225
.add_systems(Update, animate_image_scale)
226
.add_systems(
227
Update,
228
(
229
widgets::handle_ui_interactions::<AppSetting>,
230
update_radio_buttons,
231
)
232
.chain(),
233
)
234
.add_systems(
235
Update,
236
(handle_window_resize_events, regenerate_image_when_requested).chain(),
237
)
238
.add_systems(
239
Update,
240
handle_app_setting_change
241
.after(widgets::handle_ui_interactions::<AppSetting>)
242
.before(regenerate_image_when_requested),
243
);
244
245
// Because `MipGenerationJobs` is part of the render app, we need to add the
246
// associated systems to that app, not the main one.
247
248
let render_app = app.get_sub_app_mut(RenderApp).expect("Need a render app");
249
250
render_app.add_systems(Core2d, generate_mips_for_example);
251
252
// Add the system that adds the image into the `MipGenerationJobs` list.
253
// Note that this must run as part of the extract schedule, because it needs
254
// access to resources from both the main world and the render world.
255
render_app.add_systems(ExtractSchedule, extract_mipmap_source_image);
256
257
app.run();
258
}
259
260
fn generate_mips_for_example(
261
mip_generation_jobs: Res<MipGenerationJobs>,
262
pipeline_cache: Res<PipelineCache>,
263
mip_generation_pipelines: Option<Res<MipGenerationPipelines>>,
264
gpu_images: Res<RenderAssets<GpuImage>>,
265
mut ctx: RenderContext,
266
) {
267
let Some(mip_generation_pipelines) = mip_generation_pipelines else {
268
return;
269
};
270
generate_mips_for_phase(
271
MIP_GENERATION_PHASE_ID,
272
&mip_generation_jobs,
273
&pipeline_cache,
274
&mip_generation_pipelines,
275
&gpu_images,
276
&mut ctx,
277
);
278
}
279
280
/// Global assets used for this example.
281
#[derive(Resource)]
282
struct AppAssets {
283
/// A 2D rectangle mesh, used to display the individual images.
284
rectangle: Handle<Mesh>,
285
/// The font used to display the mipmap level labels on the right side of
286
/// the window.
287
text_font: TextFont,
288
}
289
290
impl FromWorld for AppAssets {
291
fn from_world(world: &mut World) -> Self {
292
let mut meshes = world.resource_mut::<Assets<Mesh>>();
293
let rectangle = meshes.add(Rectangle::default());
294
295
let asset_server = world.resource::<AssetServer>();
296
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
297
let text_font = TextFont {
298
font: font.into(),
299
font_size: FONT_SIZE,
300
..default()
301
};
302
303
AppAssets {
304
rectangle,
305
text_font,
306
}
307
}
308
}
309
310
/// Spawns all the objects in the scene and creates the initial image and
311
/// associated resources.
312
fn setup(
313
mut commands: Commands,
314
mut regenerate_image_message_writer: MessageWriter<RegenerateImage>,
315
) {
316
// Spawn the camera.
317
commands.spawn(Camera2d);
318
319
// Spawn the UI widgets at the bottom of the window.
320
spawn_ui(&mut commands);
321
322
// Schedule the image to be generated.
323
regenerate_image_message_writer.write(RegenerateImage);
324
}
325
326
/// Spawns the UI widgets at the bottom of the window.
327
fn spawn_ui(commands: &mut Commands) {
328
commands.spawn((
329
widgets::main_ui_node(),
330
children![
331
// Spawn the "Regenerate Top Mip Level" button.
332
(
333
Button,
334
Node {
335
border: BUTTON_BORDER,
336
justify_content: JustifyContent::Center,
337
align_items: AlignItems::Center,
338
padding: BUTTON_PADDING,
339
border_radius: BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE),
340
..default()
341
},
342
BUTTON_BORDER_COLOR,
343
BackgroundColor(Color::BLACK),
344
WidgetClickSender(AppSetting::RegenerateTopMipLevel),
345
children![(
346
widgets::ui_text("Regenerate Top Mip Level", Color::WHITE),
347
WidgetClickSender(AppSetting::RegenerateTopMipLevel),
348
)],
349
),
350
// Spawn the "Mip Generation" switch that allows the user to toggle
351
// mip generation on and off.
352
widgets::option_buttons(
353
"Mip Generation",
354
&[
355
(
356
AppSetting::EnableMipGeneration(EnableMipGeneration::On),
357
"On"
358
),
359
(
360
AppSetting::EnableMipGeneration(EnableMipGeneration::Off),
361
"Off"
362
),
363
]
364
),
365
// Spawn the "Image Width" control that allows the user to set the
366
// width of the image.
367
widgets::option_buttons(
368
"Image Width",
369
&[
370
(AppSetting::ImageWidth(ImageSize::Size240), "240"),
371
(AppSetting::ImageWidth(ImageSize::Size480), "480"),
372
(AppSetting::ImageWidth(ImageSize::Size640), "640"),
373
(AppSetting::ImageWidth(ImageSize::Size1080), "1080"),
374
(AppSetting::ImageWidth(ImageSize::Size1920), "1920"),
375
]
376
),
377
// Spawn the "Image Height" control that allows the user to set the
378
// height of the image.
379
widgets::option_buttons(
380
"Image Height",
381
&[
382
(AppSetting::ImageHeight(ImageSize::Size240), "240"),
383
(AppSetting::ImageHeight(ImageSize::Size480), "480"),
384
(AppSetting::ImageHeight(ImageSize::Size640), "640"),
385
(AppSetting::ImageHeight(ImageSize::Size1080), "1080"),
386
(AppSetting::ImageHeight(ImageSize::Size1920), "1920"),
387
]
388
),
389
],
390
));
391
}
392
393
impl MipmapSizeIterator {
394
/// Creates a [`MipmapSizeIterator`] corresponding to the size of the image
395
/// currently being displayed.
396
fn new(app_status: &AppStatus) -> MipmapSizeIterator {
397
MipmapSizeIterator {
398
size: Some(app_status.image_size_u32()),
399
}
400
}
401
}
402
403
impl Iterator for MipmapSizeIterator {
404
type Item = UVec2;
405
406
fn next(&mut self) -> Option<Self::Item> {
407
// The size of mipmap level N + 1 is equal to half the size of mipmap
408
// level N, rounding down, except that the size can never go below 1
409
// pixel on either axis.
410
let result = self.size;
411
if let Some(size) = self.size {
412
self.size = if size == UVec2::splat(1) {
413
None
414
} else {
415
Some((size / 2).max(UVec2::splat(1)))
416
};
417
}
418
result
419
}
420
}
421
422
/// Updates the size of the image on the left side of the window each frame.
423
///
424
/// Resizing the image every frame effectively cycles through all the image's
425
/// mipmap levels, demonstrating the difference between the presence of mipmap
426
/// levels and their absence.
427
fn animate_image_scale(
428
mut animated_images_query: Query<&mut Transform, With<AnimatedImage>>,
429
windows_query: Query<&Window, With<PrimaryWindow>>,
430
app_status: Res<AppStatus>,
431
time: Res<Time>,
432
) {
433
let window_size = windows_query.iter().next().unwrap().size();
434
let animated_mesh_size = app_status.animated_mesh_size(window_size);
435
436
for mut animated_image_transform in &mut animated_images_query {
437
animated_image_transform.scale =
438
animated_mesh_size.extend(1.0) * triangle_wave(time.elapsed_secs(), ANIMATION_PERIOD);
439
}
440
}
441
442
/// Evaluates a [triangle wave] with the given wavelength.
443
///
444
/// This is used as part of [`animate_image_scale`], to derive the scale from
445
/// the current elapsed time.
446
///
447
/// [triangle wave]: https://en.wikipedia.org/wiki/Triangle_wave#Definition
448
fn triangle_wave(time: f32, wavelength: f32) -> f32 {
449
2.0 * ops::abs(time / wavelength - ops::floor(time / wavelength + 0.5))
450
}
451
452
/// Adds the top mipmap level of the image to [`MipGenerationJobs`].
453
///
454
/// Note that this must run in the render world, not the main world, as
455
/// [`MipGenerationJobs`] is a resource that exists in the former. Consequently,
456
/// it must use [`Extract`] to access main world resources.
457
fn extract_mipmap_source_image(
458
mipmap_source_image: Extract<Res<MipmapSourceImage>>,
459
app_status: Extract<Res<AppStatus>>,
460
mut mip_generation_jobs: ResMut<MipGenerationJobs>,
461
) {
462
if app_status.enable_mip_generation == EnableMipGeneration::On {
463
mip_generation_jobs.add(MIP_GENERATION_PHASE_ID, mipmap_source_image.id());
464
}
465
}
466
467
/// Updates the widgets at the bottom of the screen to reflect the settings that
468
/// the user has chosen.
469
fn update_radio_buttons(
470
mut widgets: Query<
471
(
472
Entity,
473
Option<&mut BackgroundColor>,
474
Has<Text>,
475
&WidgetClickSender<AppSetting>,
476
),
477
Or<(With<RadioButton>, With<RadioButtonText>)>,
478
>,
479
app_status: Res<AppStatus>,
480
mut writer: TextUiWriter,
481
) {
482
for (entity, image, has_text, sender) in widgets.iter_mut() {
483
let selected = match **sender {
484
AppSetting::RegenerateTopMipLevel => continue,
485
AppSetting::EnableMipGeneration(enable_mip_generation) => {
486
enable_mip_generation == app_status.enable_mip_generation
487
}
488
AppSetting::ImageWidth(image_width) => image_width == app_status.image_width,
489
AppSetting::ImageHeight(image_height) => image_height == app_status.image_height,
490
};
491
492
if let Some(mut bg_color) = image {
493
widgets::update_ui_radio_button(&mut bg_color, selected);
494
}
495
if has_text {
496
widgets::update_ui_radio_button_text(entity, &mut writer, selected);
497
}
498
}
499
}
500
501
/// Handles a request from the user to change application settings via the UI.
502
///
503
/// This also handles clicks on the "Regenerate Top Mip Level" button.
504
fn handle_app_setting_change(
505
mut events: MessageReader<WidgetClickEvent<AppSetting>>,
506
mut app_status: ResMut<AppStatus>,
507
mut regenerate_image_message_writer: MessageWriter<RegenerateImage>,
508
) {
509
for event in events.read() {
510
// If this is a setting, update the setting. Fall through if, in
511
// addition to updating the setting, we need to regenerate the image.
512
match **event {
513
AppSetting::EnableMipGeneration(enable_mip_generation) => {
514
app_status.enable_mip_generation = enable_mip_generation;
515
continue;
516
}
517
518
AppSetting::RegenerateTopMipLevel => {}
519
AppSetting::ImageWidth(image_size) => app_status.image_width = image_size,
520
AppSetting::ImageHeight(image_size) => app_status.image_height = image_size,
521
}
522
523
// Schedule the image to be regenerated.
524
regenerate_image_message_writer.write(RegenerateImage);
525
}
526
}
527
528
/// Handles resize events for the window.
529
///
530
/// Resizing the window invalidates the image and repositions all image views.
531
/// (Regenerating the image isn't strictly necessary, but it's simplest to have
532
/// a single function that both regenerates the image and recreates the image
533
/// views.)
534
fn handle_window_resize_events(
535
mut events: MessageReader<WindowResized>,
536
mut regenerate_image_message_writer: MessageWriter<RegenerateImage>,
537
) {
538
for _ in events.read() {
539
regenerate_image_message_writer.write(RegenerateImage);
540
}
541
}
542
543
/// Recreates the image, as well as all views that show the image, when a
544
/// [`RegenerateImage`] message is received.
545
///
546
/// The views that show the image consist of the animated mesh on the left side
547
/// of the window and the column of mipmap level views on the right side of the
548
/// window.
549
fn regenerate_image_when_requested(
550
mut commands: Commands,
551
image_views_query: Query<Entity, With<ImageView>>,
552
windows_query: Query<&Window, With<PrimaryWindow>>,
553
app_assets: Res<AppAssets>,
554
mut app_status: ResMut<AppStatus>,
555
mut images: ResMut<Assets<Image>>,
556
mut single_mip_level_materials: ResMut<Assets<SingleMipLevelMaterial>>,
557
mut color_materials: ResMut<Assets<ColorMaterial>>,
558
mut message_reader: MessageReader<RegenerateImage>,
559
) {
560
// Only do this at most once per frame, or else the despawn logic below will
561
// get confused.
562
if message_reader.read().count() == 0 {
563
return;
564
}
565
566
// Despawn all entities that show the image.
567
for entity in image_views_query.iter() {
568
commands.entity(entity).despawn();
569
}
570
571
// Regenerate the image.
572
let image_handle = app_status.regenerate_mipmap_source_image(&mut commands, &mut images);
573
574
// Respawn the animated image view on the left side of the window.
575
spawn_animated_mesh(
576
&mut commands,
577
&app_status,
578
&app_assets,
579
&windows_query,
580
&mut color_materials,
581
&image_handle,
582
);
583
584
// Respawn the column of mip level views on the right side of the window.
585
spawn_mip_level_views(
586
&mut commands,
587
&app_status,
588
&app_assets,
589
&windows_query,
590
&mut single_mip_level_materials,
591
&image_handle,
592
);
593
}
594
595
/// Spawns the image on the left that continually changes scale.
596
///
597
/// Continually changing scale effectively cycles though each mip level,
598
/// demonstrating the difference between mip level images being present and mip
599
/// level image being absent.
600
fn spawn_animated_mesh(
601
commands: &mut Commands,
602
app_status: &AppStatus,
603
app_assets: &AppAssets,
604
windows_query: &Query<&Window, With<PrimaryWindow>>,
605
color_materials: &mut Assets<ColorMaterial>,
606
image_handle: &Handle<Image>,
607
) {
608
let window_size = windows_query.iter().next().unwrap().size();
609
let animated_mesh_area_size = app_status.animated_mesh_area_size(window_size);
610
let animated_mesh_size = app_status.animated_mesh_size(window_size);
611
612
commands.spawn((
613
Mesh2d(app_assets.rectangle.clone()),
614
MeshMaterial2d(color_materials.add(ColorMaterial {
615
texture: Some(image_handle.clone()),
616
..default()
617
})),
618
Transform::from_translation(
619
(animated_mesh_area_size * 0.5 - window_size * 0.5).extend(0.0),
620
)
621
.with_scale(animated_mesh_size.extend(1.0)),
622
AnimatedImage,
623
ImageView,
624
));
625
}
626
627
/// Creates the column on the right side of the window that displays each mip
628
/// level by itself.
629
fn spawn_mip_level_views(
630
commands: &mut Commands,
631
app_status: &AppStatus,
632
app_assets: &AppAssets,
633
windows_query: &Query<&Window, With<PrimaryWindow>>,
634
single_mip_level_materials: &mut Assets<SingleMipLevelMaterial>,
635
image_handle: &Handle<Image>,
636
) {
637
let window_size = windows_query.iter().next().unwrap().size();
638
639
// Calculate the placement of the column of mipmap levels.
640
let max_slice_size = app_status.max_mip_slice_size(window_size);
641
let y_origin = app_status.vertical_mip_slice_origin(window_size);
642
let y_spacing = app_status.vertical_mip_slice_spacing(window_size);
643
let x_origin = app_status.horizontal_mip_slice_origin(window_size);
644
645
for (mip_level, mip_size) in MipmapSizeIterator::new(app_status).enumerate() {
646
let y_center = y_origin - y_spacing * mip_level as f32;
647
648
// Size each image to fit its container, preserving aspect ratio.
649
let mut slice_size = mip_size.as_vec2();
650
let ratios = max_slice_size / slice_size;
651
let slice_scale = ratios.x.min(ratios.y).min(1.0);
652
slice_size *= slice_scale;
653
654
// Spawn the image. Use the `SingleMipLevelMaterial` with its custom
655
// shader so that only the mip level in question is displayed.
656
commands.spawn((
657
Mesh2d(app_assets.rectangle.clone()),
658
MeshMaterial2d(single_mip_level_materials.add(SingleMipLevelMaterial {
659
mip_level: mip_level as u32,
660
texture: image_handle.clone(),
661
})),
662
Transform::from_xyz(x_origin, y_center, 0.0).with_scale(slice_size.extend(1.0)),
663
ImageView,
664
));
665
666
// Display a label to the side.
667
commands.spawn((
668
Text2d::new(format!(
669
"Level {}\n{}×{}",
670
mip_level, mip_size.x, mip_size.y
671
)),
672
app_assets.text_font.clone(),
673
TextLayout::new_with_justify(Justify::Center),
674
Text2dShadow::default(),
675
Transform::from_xyz(x_origin - max_slice_size.x * 0.5 - 64.0, y_center, 0.0),
676
ImageView,
677
));
678
}
679
}
680
681
/// Returns true if the given point is inside a 2D ellipse with the given center
682
/// and given radii or false otherwise.
683
fn point_in_ellipse(point: Vec2, center: Vec2, radii: Vec2) -> bool {
684
// This can be derived from the standard equation of an ellipse:
685
//
686
// x² y²
687
// ⎯⎯ + ⎯⎯ = 1
688
// a² b²
689
let (nums, denoms) = (point - center, radii);
690
let terms = (nums * nums) / (denoms * denoms);
691
terms.x + terms.y < 1.0
692
}
693
694
impl AppStatus {
695
/// Returns the vertical distance between each mip slice image in the column
696
/// on the right side of the window.
697
fn vertical_mip_slice_spacing(&self, window_size: Vec2) -> f32 {
698
window_size.y / self.image_mip_level_count() as f32
699
}
700
701
/// Returns the Y position of the center of the image that represents the
702
/// first mipmap level in the column on the right side of the window.
703
fn vertical_mip_slice_origin(&self, window_size: Vec2) -> f32 {
704
let spacing = self.vertical_mip_slice_spacing(window_size);
705
window_size.y * 0.5 - spacing * 0.5
706
}
707
708
/// Returns the maximum area that a single mipmap slice can occupy in the
709
/// column at the right side of the window.
710
///
711
/// Because the slices may be smaller than this area, and because the size
712
/// of each slice preserves the aspect ratio of the image, the actual
713
/// displayed size of each slice may be smaller than this.
714
fn max_mip_slice_size(&self, window_size: Vec2) -> Vec2 {
715
let spacing = self.vertical_mip_slice_spacing(window_size);
716
vec2(window_size.x * MIP_SLICES_WIDTH, spacing)
717
}
718
719
/// Returns the horizontal center point of each mip slice image in the
720
/// column at the right side of the window.
721
fn horizontal_mip_slice_origin(&self, window_size: Vec2) -> f32 {
722
let max_slice_size = self.max_mip_slice_size(window_size);
723
window_size.x * 0.5 - max_slice_size.x * 0.5 - MIP_SLICES_MARGIN_RIGHT
724
}
725
726
/// Calculates and returns the area reserved for the animated image on the
727
/// left side of the window.
728
///
729
/// Note that this isn't necessarily equal to the final size of the animated
730
/// image, because that size preserves the image's aspect ratio.
731
fn animated_mesh_area_size(&self, window_size: Vec2) -> Vec2 {
732
vec2(
733
self.horizontal_mip_slice_origin(window_size) * 2.0 - MIP_SLICES_MARGIN_LEFT * 2.0,
734
window_size.y,
735
)
736
}
737
738
/// Calculates and returns the actual maximum size of the animated image on
739
/// the left side of the window.
740
///
741
/// This is equal to the maximum portion of the
742
/// [`Self::animated_mesh_area_size`] that the image can occupy while
743
/// preserving its aspect ratio.
744
fn animated_mesh_size(&self, window_size: Vec2) -> Vec2 {
745
let max_image_size = self.animated_mesh_area_size(window_size);
746
let image_size = self.image_size_f32();
747
let ratios = max_image_size / image_size;
748
let image_scale = ratios.x.min(ratios.y);
749
image_size * image_scale
750
}
751
752
/// Returns the size of the image as a [`UVec2`].
753
fn image_size_u32(&self) -> UVec2 {
754
uvec2(self.image_width as u32, self.image_height as u32)
755
}
756
757
/// Returns the size of the image as a [`Vec2`].
758
fn image_size_f32(&self) -> Vec2 {
759
vec2(
760
self.image_width as u32 as f32,
761
self.image_height as u32 as f32,
762
)
763
}
764
765
/// Regenerates the main image based on the image size selected by the user.
766
fn regenerate_mipmap_source_image(
767
&mut self,
768
commands: &mut Commands,
769
images: &mut Assets<Image>,
770
) -> Handle<Image> {
771
let image_data = self.generate_image_data();
772
773
let mut image = Image::new_uninit(
774
Extent3d {
775
width: self.image_width as u32,
776
height: self.image_height as u32,
777
depth_or_array_layers: 1,
778
},
779
TextureDimension::D2,
780
TextureFormat::Rgba8Unorm,
781
RenderAssetUsages::all(),
782
);
783
image.texture_descriptor.mip_level_count = self.image_mip_level_count();
784
image.texture_descriptor.usage |= TextureUsages::STORAGE_BINDING;
785
image.data = Some(image_data);
786
787
let image_handle = images.add(image);
788
commands.insert_resource(MipmapSourceImage(image_handle.clone()));
789
790
image_handle
791
}
792
793
/// Draws the concentric ellipses that make up the image.
794
///
795
/// Returns the RGBA8 image data.
796
fn generate_image_data(&mut self) -> Vec<u8> {
797
// Select random colors for the inner and outer ellipses.
798
let outer_color: [u8; 3] = array::from_fn(|_| self.rng.random());
799
let inner_color: [u8; 3] = array::from_fn(|_| self.rng.random());
800
801
let image_byte_size = 4usize
802
* MipmapSizeIterator::new(self)
803
.map(|size| size.x as usize * size.y as usize)
804
.sum::<usize>();
805
let mut image_data = vec![0u8; image_byte_size];
806
807
let center = self.image_size_f32() * 0.5;
808
809
let inner_ellipse_radii = self.inner_ellipse_radii();
810
let outer_ellipse_radii = self.outer_ellipse_radii();
811
812
for y in 0..(self.image_height as u32) {
813
for x in 0..(self.image_width as u32) {
814
let p = vec2(x as f32, y as f32);
815
let (color, alpha) = if point_in_ellipse(p, center, inner_ellipse_radii) {
816
(inner_color, 255)
817
} else if point_in_ellipse(p, center, outer_ellipse_radii) {
818
(outer_color, 255)
819
} else {
820
([0; 3], 0)
821
};
822
let start = (4 * (x + y * (self.image_width as u32))) as usize;
823
image_data[start..(start + 3)].copy_from_slice(&color);
824
image_data[start + 3] = alpha;
825
}
826
}
827
828
image_data
829
}
830
831
/// Returns the number of mipmap levels that the image should possess.
832
///
833
/// This will be equal to the maximum number of mipmap levels that an image
834
/// of the appropriate size can have.
835
fn image_mip_level_count(&self) -> u32 {
836
32 - (self.image_width as u32)
837
.max(self.image_height as u32)
838
.leading_zeros()
839
}
840
841
/// Returns the X and Y radii of the outer ellipse drawn in the texture,
842
/// respectively.
843
fn outer_ellipse_radii(&self) -> Vec2 {
844
self.image_size_f32() * 0.5
845
}
846
847
/// Returns the X and Y radii of the inner ellipse drawn in the texture,
848
/// respectively.
849
fn inner_ellipse_radii(&self) -> Vec2 {
850
self.image_size_f32() * 0.25
851
}
852
}
853
854