1use 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
51const ANIMATION_PERIOD: f32 = 2.0;
54
55const SINGLE_MIP_LEVEL_SHADER_ASSET_PATH: &str = "shaders/single_mip_level.wgsl";
58
59const MIP_SLICES_MARGIN_LEFT: f32 = 64.0;
62const MIP_SLICES_MARGIN_RIGHT: f32 = 12.0;
65const MIP_SLICES_WIDTH: f32 = 1.0 / 6.0;
68
69const FONT_SIZE: FontSize = FontSize::Px(16.0);
71
72#[derive(Resource)]
74struct AppStatus {
75 enable_mip_generation: EnableMipGeneration,
77 image_width: ImageSize,
79 image_height: ImageSize,
81 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#[derive(Clone)]
98enum AppSetting {
99 RegenerateTopMipLevel,
105
106 EnableMipGeneration(EnableMipGeneration),
108
109 ImageWidth(ImageSize),
111
112 ImageHeight(ImageSize),
114}
115
116#[derive(Clone, Copy, Default, PartialEq)]
123enum EnableMipGeneration {
124 #[default]
126 On,
127 Off,
129}
130
131#[derive(Clone, Copy, Default, PartialEq)]
133#[repr(u32)]
134enum ImageSize {
135 Size240 = 240,
137 Size480 = 480,
139 #[default]
141 Size640 = 640,
142 Size1080 = 1080,
144 Size1920 = 1920,
146}
147
148#[derive(Clone, Asset, TypePath, AsBindGroup, Debug)]
153struct SingleMipLevelMaterial {
154 #[uniform(0)]
156 mip_level: u32,
157 #[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#[derive(Component)]
178struct AnimatedImage;
179
180#[derive(Resource, Deref, DerefMut)]
183struct MipmapSourceImage(Handle<Image>);
184
185struct MipmapSizeIterator {
188 size: Option<UVec2>,
191}
192
193const MIP_GENERATION_PHASE_ID: MipGenerationPhaseId = MipGenerationPhaseId(0);
194
195#[derive(Component)]
200struct ImageView;
201
202#[derive(Clone, Copy, Debug, Message)]
205struct RegenerateImage;
206
207fn 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 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 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#[derive(Resource)]
282struct AppAssets {
283 rectangle: Handle<Mesh>,
285 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
310fn setup(
313 mut commands: Commands,
314 mut regenerate_image_message_writer: MessageWriter<RegenerateImage>,
315) {
316 commands.spawn(Camera2d);
318
319 spawn_ui(&mut commands);
321
322 regenerate_image_message_writer.write(RegenerateImage);
324}
325
326fn spawn_ui(commands: &mut Commands) {
328 commands.spawn((
329 widgets::main_ui_node(),
330 children![
331 (
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 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 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 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 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 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
422fn 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
442fn triangle_wave(time: f32, wavelength: f32) -> f32 {
449 2.0 * ops::abs(time / wavelength - ops::floor(time / wavelength + 0.5))
450}
451
452fn 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
467fn 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
501fn 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 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 regenerate_image_message_writer.write(RegenerateImage);
525 }
526}
527
528fn 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
543fn 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 if message_reader.read().count() == 0 {
563 return;
564 }
565
566 for entity in image_views_query.iter() {
568 commands.entity(entity).despawn();
569 }
570
571 let image_handle = app_status.regenerate_mipmap_source_image(&mut commands, &mut images);
573
574 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 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
595fn 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
627fn 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 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 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 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 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
681fn point_in_ellipse(point: Vec2, center: Vec2, radii: Vec2) -> bool {
684 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 fn vertical_mip_slice_spacing(&self, window_size: Vec2) -> f32 {
698 window_size.y / self.image_mip_level_count() as f32
699 }
700
701 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 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 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 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 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 fn image_size_u32(&self) -> UVec2 {
754 uvec2(self.image_width as u32, self.image_height as u32)
755 }
756
757 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 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 fn generate_image_data(&mut self) -> Vec<u8> {
797 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 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 fn outer_ellipse_radii(&self) -> Vec2 {
844 self.image_size_f32() * 0.5
845 }
846
847 fn inner_ellipse_radii(&self) -> Vec2 {
850 self.image_size_f32() * 0.25
851 }
852}