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    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};
40use chacha20::ChaCha8Rng;
41use rand::{RngExt, SeedableRng};
42
43use 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"]
49mod widgets;
50
51/// The time in seconds that it takes the animation of the image shrinking and
52/// growing to play.
53const ANIMATION_PERIOD: f32 = 2.0;
54
55/// The path to the single mip level 2D material shader inside the `assets`
56/// directory.
57const 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.
61const 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.
64const 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.
67const MIP_SLICES_WIDTH: f32 = 1.0 / 6.0;
68
69/// The size of the mipmap level label font.
70const FONT_SIZE: FontSize = FontSize::Px(16.0);
71
72/// All settings that the user can change via the UI.
73#[derive(Resource)]
74struct 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
85impl 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)]
98enum 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)]
123enum 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)]
134enum 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)]
153struct 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
163impl 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)]
178struct 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)]
183struct MipmapSourceImage(Handle<Image>);
184
185/// An iterator that yields the size of each mipmap level for an image, one
186/// after another.
187struct MipmapSizeIterator {
188    /// The size of the previous mipmap level, or `None` if this iterator is
189    /// finished.
190    size: Option<UVec2>,
191}
192
193const 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)]
200struct 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)]
205struct RegenerateImage;
206
207/// The application entry point.
208fn 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
260fn 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)]
282struct 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
290impl 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.
312fn 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.
327fn 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
393impl 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
403impl 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.
427fn 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
448fn 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.
457fn 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.
469fn 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.
504fn 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.)
534fn 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.
549fn 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.
600fn 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.
629fn 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::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.
683fn 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
694impl 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}