//! Demonstrates use of the mipmap generation plugin to generate mipmaps for a //! texture. //! //! This example demonstrates use of the [`MipGenerationJobs`] resource to //! generate mipmap levels for a texture at runtime. It generates the first //! mipmap level of a texture on CPU, which consists of two ellipses with //! randomly chosen colors. Then it invokes Bevy's mipmap generation pass to //! generate the remaining mipmap levels for the texture on the GPU. You can use //! the UI to regenerate the texture and adjust its size to prove that the //! texture, and its mipmaps, are truly being generated at runtime and aren't //! being built ahead of time. use std::array; use bevy::{ asset::RenderAssetUsages, core_pipeline::{ mip_generation::{ generate_mips_for_phase, MipGenerationJobs, MipGenerationPhaseId, MipGenerationPipelines, }, schedule::Core2d, }, feathers::{ controls::FeathersButton, dark_theme::create_dark_theme, display::caption, theme::UiTheme, FeathersPlugins, }, prelude::*, reflect::TypePath, render::{ render_asset::RenderAssets, render_resource::{ AsBindGroup, Extent3d, PipelineCache, TextureDimension, TextureFormat, TextureUsages, }, renderer::RenderContext, texture::GpuImage, Extract, RenderApp, }, shader::ShaderRef, sprite::Text2dShadow, sprite_render::{AlphaMode2d, Material2d, Material2dPlugin}, ui_widgets::{radio_self_update, Activate, ValueChange}, window::{PrimaryWindow, WindowResized}, }; use chacha20::ChaCha8Rng; use rand::{RngExt, SeedableRng}; use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue}; #[path = "../helpers/radio.rs"] mod radio; /// The time in seconds that it takes the animation of the image shrinking and /// growing to play. const ANIMATION_PERIOD: f32 = 2.0; /// The path to the single mip level 2D material shader inside the `assets` /// directory. const SINGLE_MIP_LEVEL_SHADER_ASSET_PATH: &str = "shaders/single_mip_level.wesl"; /// The distance from the left side of the column of mipmap slices to the right /// side of the area used for the animation. const MIP_SLICES_MARGIN_LEFT: f32 = 64.0; /// The distance from the right side of the window to the right side of the /// column of mipmap slices. const MIP_SLICES_MARGIN_RIGHT: f32 = 12.0; /// The width of the column of mipmap slices, not counting the labels, as a /// fraction of the width of the window. const MIP_SLICES_WIDTH: f32 = 1.0 / 6.0; /// The size of the mipmap level label font. const FONT_SIZE: FontSize = FontSize::Px(16.0); /// All settings that the user can change via the UI. #[derive(Resource)] struct AppStatus { /// Whether mipmaps are to be generated for the image. enable_mip_generation: EnableMipGeneration, /// The width of the image. image_width: ImageSize, /// The height of the image. image_height: ImageSize, /// Seeded random generator. rng: ChaCha8Rng, } impl Default for AppStatus { fn default() -> Self { AppStatus { enable_mip_generation: EnableMipGeneration::On, image_width: ImageSize::Size640, image_height: ImageSize::Size480, rng: ChaCha8Rng::seed_from_u64(19878367467713), } } } /// Whether mipmap levels will be generated. /// /// Turning off the generation of mipmap levels, and then regenerating the /// image, will cause all mipmap levels other than the first to be blank. This /// will in turn cause the image to fade out as it shrinks, as the GPU switches /// to rendering mipmap levels that don't have associated images. #[derive(Component, Clone, Copy, Default, PartialEq)] enum EnableMipGeneration { /// Mipmap levels are generated for the image. #[default] On, /// Mipmap levels aren't generated for the image. Off, } /// The setting that updates the width or height of the image. #[derive(Component, Clone, Copy, PartialEq)] enum ImageSizeSetting { /// The width of the image. ImageWidth(ImageSize), /// The height of the image. ImageHeight(ImageSize), } impl Default for ImageSizeSetting { fn default() -> Self { Self::ImageWidth(default()) } } /// Possible lengths for an image side from which the user can choose. #[derive(Clone, Copy, Default, PartialEq)] #[repr(u32)] enum ImageSize { /// 240px. Size240 = 240, /// 480px (the default height). Size480 = 480, /// 640px (the default width). #[default] Size640 = 640, /// 1080px. Size1080 = 1080, /// 1920px. Size1920 = 1920, } /// A 2D material that displays only one mipmap level of a texture. /// /// This is the material used for the column of mip levels on the right side of /// the window. #[derive(Clone, Asset, TypePath, AsBindGroup, Debug)] struct SingleMipLevelMaterial { /// The mip level that this material will show, starting from 0. #[uniform(0)] mip_level: u32, /// The image that is to be shown. #[texture(1)] #[sampler(2)] texture: Handle, } impl Material2d for SingleMipLevelMaterial { fn fragment_shader() -> ShaderRef { SINGLE_MIP_LEVEL_SHADER_ASSET_PATH.into() } fn alpha_mode(&self) -> AlphaMode2d { AlphaMode2d::Blend } } /// A marker component for the image on the left side of the window. /// /// This is the image that grows and shrinks to demonstrate the effect of mip /// levels' presence and absence. #[derive(Component)] struct AnimatedImage; /// A resource that stores the main image for which mipmaps are to be generated /// (or not generated, depending on the application settings). #[derive(Resource, Deref, DerefMut)] struct MipmapSourceImage(Handle); /// An iterator that yields the size of each mipmap level for an image, one /// after another. struct MipmapSizeIterator { /// The size of the previous mipmap level, or `None` if this iterator is /// finished. size: Option, } const MIP_GENERATION_PHASE_ID: MipGenerationPhaseId = MipGenerationPhaseId(0); /// A marker component for every mesh that displays the image. /// /// When the image is regenerated, we despawn and respawn all entities with this /// component. #[derive(Component)] struct ImageView; /// A marker component for the Regenerate Top Mip Level button. #[derive(Clone, Copy, Component, Default)] struct RegenerateTopMipLevelButton; /// A message that's sent whenever the image and the corresponding views need to /// be regenerated. #[derive(Clone, Copy, Debug, Message)] struct RegenerateImage; /// The application entry point. fn main() { let mut app = App::new(); app.add_plugins(( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Bevy Dynamic Mipmap Generation Example".into(), ..default() }), ..default() }), Material2dPlugin::::default(), FeathersPlugins, )) .init_resource::() .init_resource::() .insert_resource(UiTheme(create_dark_theme())) .add_message::() .add_systems(Startup, setup) .add_systems(Update, animate_image_scale) .add_systems( Update, (handle_window_resize_events, regenerate_image_when_requested).chain(), ) .add_observer(radio_self_update) .add_observer(handle_regenerate_top_mip_level_activate) .add_observer(handle_enable_mip_generation_change) .add_observer(handle_image_size_setting_change); // Because `MipGenerationJobs` is part of the render app, we need to add the // associated systems to that app, not the main one. let render_app = app.get_sub_app_mut(RenderApp).expect("Need a render app"); render_app.add_systems(Core2d, generate_mips_for_example); // Add the system that adds the image into the `MipGenerationJobs` list. // Note that this must run as part of the extract schedule, because it needs // access to resources from both the main world and the render world. render_app.add_systems(ExtractSchedule, extract_mipmap_source_image); app.run(); } fn generate_mips_for_example( mip_generation_jobs: Res, pipeline_cache: Res, mip_generation_pipelines: Option>, gpu_images: Res>, mut ctx: RenderContext, ) { let Some(mip_generation_pipelines) = mip_generation_pipelines else { return; }; generate_mips_for_phase( MIP_GENERATION_PHASE_ID, &mip_generation_jobs, &pipeline_cache, &mip_generation_pipelines, &gpu_images, &mut ctx, ); } /// Global assets used for this example. #[derive(Resource)] struct AppAssets { /// A 2D rectangle mesh, used to display the individual images. rectangle: Handle, /// The font used to display the mipmap level labels on the right side of /// the window. text_font: TextFont, } impl FromWorld for AppAssets { fn from_world(world: &mut World) -> Self { let mut meshes = world.resource_mut::>(); let rectangle = meshes.add(Rectangle::default()); let asset_server = world.resource::(); let font = asset_server.load("fonts/FiraSans-Bold.ttf"); let text_font = TextFont { font: font.into(), font_size: FONT_SIZE, ..default() }; AppAssets { rectangle, text_font, } } } /// Spawns all the objects in the scene and creates the initial image and /// associated resources. fn setup( mut commands: Commands, mut regenerate_image_message_writer: MessageWriter, ) { // Spawn the camera. commands.spawn(Camera2d); // Spawn the UI widgets at the bottom of the window. spawn_ui(&mut commands); // Schedule the image to be generated. regenerate_image_message_writer.write(RegenerateImage); } /// Spawns the UI widgets at the bottom of the window. fn spawn_ui(commands: &mut Commands) { commands.spawn_scene(bsn! { @main_ui_node_scene() Children [ // Spawn the "Regenerate Top Mip Level" button. @FeathersButton { @caption: bsn! { @caption("Regenerate Top Mip Level") } } RegenerateTopMipLevelButton Node { justify_content: JustifyContent::Center, align_items: AlignItems::Center, } BackgroundColor(Color::BLACK) -- // Spawn the "Mip Generation" switch that allows the user to toggle // mip generation on and off. @feathers_option_buttons( "Mip Generation", &[ ( EnableMipGeneration::On, "On" ), ( EnableMipGeneration::Off, "Off" ), ], 0 ) -- // Spawn the "Image Width" control that allows the user to set the // width of the image. @feathers_option_buttons( "Image Width", &[ (ImageSizeSetting::ImageWidth(ImageSize::Size240), "240"), (ImageSizeSetting::ImageWidth(ImageSize::Size480), "480"), (ImageSizeSetting::ImageWidth(ImageSize::Size640), "640"), (ImageSizeSetting::ImageWidth(ImageSize::Size1080), "1080"), (ImageSizeSetting::ImageWidth(ImageSize::Size1920), "1920"), ], 2 ) -- // Spawn the "Image Height" control that allows the user to set the // height of the image. @feathers_option_buttons( "Image Height", &[ (ImageSizeSetting::ImageHeight(ImageSize::Size240), "240"), (ImageSizeSetting::ImageHeight(ImageSize::Size480), "480"), (ImageSizeSetting::ImageHeight(ImageSize::Size640), "640"), (ImageSizeSetting::ImageHeight(ImageSize::Size1080), "1080"), (ImageSizeSetting::ImageHeight(ImageSize::Size1920), "1920"), ], 1 ) ] }); } impl MipmapSizeIterator { /// Creates a [`MipmapSizeIterator`] corresponding to the size of the image /// currently being displayed. fn new(app_status: &AppStatus) -> MipmapSizeIterator { MipmapSizeIterator { size: Some(app_status.image_size_u32()), } } } impl Iterator for MipmapSizeIterator { type Item = UVec2; fn next(&mut self) -> Option { // The size of mipmap level N + 1 is equal to half the size of mipmap // level N, rounding down, except that the size can never go below 1 // pixel on either axis. let result = self.size; if let Some(size) = self.size { self.size = if size == UVec2::splat(1) { None } else { Some((size / 2).max(UVec2::splat(1))) }; } result } } /// Updates the size of the image on the left side of the window each frame. /// /// Resizing the image every frame effectively cycles through all the image's /// mipmap levels, demonstrating the difference between the presence of mipmap /// levels and their absence. fn animate_image_scale( mut animated_images_query: Query<&mut Transform, With>, windows_query: Query<&Window, With>, app_status: Res, time: Res