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 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
53const ANIMATION_PERIOD: f32 = 2.0;
56
57const SINGLE_MIP_LEVEL_SHADER_ASSET_PATH: &str = "shaders/single_mip_level.wesl";
60
61const MIP_SLICES_MARGIN_LEFT: f32 = 64.0;
64const MIP_SLICES_MARGIN_RIGHT: f32 = 12.0;
67const MIP_SLICES_WIDTH: f32 = 1.0 / 6.0;
70
71const FONT_SIZE: FontSize = FontSize::Px(16.0);
73
74#[derive(Resource)]
76struct AppStatus {
77 enable_mip_generation: EnableMipGeneration,
79 image_width: ImageSize,
81 image_height: ImageSize,
83 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#[derive(Component, Clone, Copy, Default, PartialEq)]
105enum EnableMipGeneration {
106 #[default]
108 On,
109 Off,
111}
112
113#[derive(Component, Clone, Copy, PartialEq)]
115enum ImageSizeSetting {
116 ImageWidth(ImageSize),
118
119 ImageHeight(ImageSize),
121}
122
123impl Default for ImageSizeSetting {
124 fn default() -> Self {
125 Self::ImageWidth(default())
126 }
127}
128
129#[derive(Clone, Copy, Default, PartialEq)]
131#[repr(u32)]
132enum ImageSize {
133 Size240 = 240,
135 Size480 = 480,
137 #[default]
139 Size640 = 640,
140 Size1080 = 1080,
142 Size1920 = 1920,
144}
145
146#[derive(Clone, Asset, TypePath, AsBindGroup, Debug)]
151struct SingleMipLevelMaterial {
152 #[uniform(0)]
154 mip_level: u32,
155 #[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#[derive(Component)]
176struct AnimatedImage;
177
178#[derive(Resource, Deref, DerefMut)]
181struct MipmapSourceImage(Handle<Image>);
182
183struct MipmapSizeIterator {
186 size: Option<UVec2>,
189}
190
191const MIP_GENERATION_PHASE_ID: MipGenerationPhaseId = MipGenerationPhaseId(0);
192
193#[derive(Component)]
198struct ImageView;
199
200#[derive(Clone, Copy, Component, Default)]
202struct RegenerateTopMipLevelButton;
203
204#[derive(Clone, Copy, Debug, Message)]
207struct RegenerateImage;
208
209fn 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 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 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#[derive(Resource)]
275struct AppAssets {
276 rectangle: Handle<Mesh>,
278 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
303fn setup(
306 mut commands: Commands,
307 mut regenerate_image_message_writer: MessageWriter<RegenerateImage>,
308) {
309 commands.spawn(Camera2d);
311
312 spawn_ui(&mut commands);
314
315 regenerate_image_message_writer.write(RegenerateImage);
317}
318
319fn spawn_ui(commands: &mut Commands) {
321 commands.spawn_scene(bsn! {
322 @main_ui_node_scene()
323 Children [
324 @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 @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 @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 @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 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 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
411fn 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
431fn triangle_wave(time: f32, wavelength: f32) -> f32 {
438 2.0 * ops::abs(time / wavelength - ops::floor(time / wavelength + 0.5))
439}
440
441fn 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
456fn 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 regenerate_image_message_writer.write(RegenerateImage);
465 }
466}
467
468fn 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 }
481
482fn 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 regenerate_image_message_writer.write(RegenerateImage);
500}
501
502fn 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
517fn 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 if message_reader.read().count() == 0 {
537 return;
538 }
539
540 for entity in image_views_query.iter() {
542 commands.entity(entity).despawn();
543 }
544
545 let image_handle = app_status.regenerate_mipmap_source_image(&mut commands, &mut images);
547
548 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 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
569fn 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
601fn 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 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 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 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 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
655fn point_in_ellipse(point: Vec2, center: Vec2, radii: Vec2) -> bool {
658 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 fn vertical_mip_slice_spacing(&self, window_size: Vec2) -> f32 {
672 window_size.y / self.image_mip_level_count() as f32
673 }
674
675 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 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 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 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 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 fn image_size_u32(&self) -> UVec2 {
728 uvec2(self.image_width as u32, self.image_height as u32)
729 }
730
731 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 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 fn generate_image_data(&mut self) -> Vec<u8> {
771 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 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 fn outer_ellipse_radii(&self) -> Vec2 {
818 self.image_size_f32() * 0.5
819 }
820
821 fn inner_ellipse_radii(&self) -> Vec2 {
824 self.image_size_f32() * 0.25
825 }
826}