Skip to main content

dynamic_mip_generation/
dynamic_mip_generation.rs

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