docs/bevy/examples/2d/dynamic_mip_generation.rs 28.7 K · 826 lines · raw · history

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
13 use std::array;
14
15 use 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 };
45 use chacha20::ChaCha8Rng;
46 use rand::{RngExt, SeedableRng};
47
48 use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
49
50 #[path = "../helpers/radio.rs"]
51 mod radio;
52
53 /// The time in seconds that it takes the animation of the image shrinking and
54 /// growing to play.
55 const ANIMATION_PERIOD: f32 = 2.0;
56
57 /// The path to the single mip level 2D material shader inside the `assets`
58 /// directory.
59 const 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.
63 const 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.
66 const 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.
69 const MIP_SLICES_WIDTH: f32 = 1.0 / 6.0;
70
71 /// The size of the mipmap level label font.
72 const FONT_SIZE: FontSize = FontSize::Px(16.0);
73
74 /// All settings that the user can change via the UI.
75 #[derive(Resource)]
76 struct 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
87 impl 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)]
105 enum 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)]
115 enum ImageSizeSetting {
116 /// The width of the image.
117 ImageWidth(ImageSize),
118
119 /// The height of the image.
120 ImageHeight(ImageSize),
121 }
122
123 impl 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)]
132 enum 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)]
151 struct 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
161 impl 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)]
176 struct 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)]
181 struct MipmapSourceImage(Handle<Image>);
182
183 /// An iterator that yields the size of each mipmap level for an image, one
184 /// after another.
185 struct MipmapSizeIterator {
186 /// The size of the previous mipmap level, or `None` if this iterator is
187 /// finished.
188 size: Option<UVec2>,
189 }
190
191 const 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)]
198 struct ImageView;
199
200 /// A marker component for the Regenerate Top Mip Level button.
201 #[derive(Clone, Copy, Component, Default)]
202 struct 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)]
207 struct RegenerateImage;
208
209 /// The application entry point.
210 fn 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
253 fn 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)]
275 struct 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
283 impl 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.
305 fn 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.
320 fn 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
382 impl 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
392 impl 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.
416 fn 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
437 fn 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.
446 fn 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`.
457 fn 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.
469 fn 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.
483 fn 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.)
508 fn 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.
523 fn 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.
574 fn 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.
603 fn 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.
657 fn 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
668 impl 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 }