mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
b00ff93c87
# Objective Remove the `new_with_` prefixes from the `TextLayout` constuctor functions. Generally, the "new" part is redundant and "with" is used by fluent APIs. ## Solution Just delete the prefixes, shorten the names (all on `TextLayout`). * `new_with_justify` -> `justify` * `new_with_linebreak` -> `linebreak` * `new_with_no_wrap` -> `no_wrap`
853 lines
30 KiB
Rust
853 lines
30 KiB
Rust
//! 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,
|
||
},
|
||
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},
|
||
window::{PrimaryWindow, WindowResized},
|
||
};
|
||
use chacha20::ChaCha8Rng;
|
||
use rand::{RngExt, SeedableRng};
|
||
|
||
use crate::widgets::{
|
||
RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender, BUTTON_BORDER,
|
||
BUTTON_BORDER_COLOR, BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING,
|
||
};
|
||
|
||
#[path = "../helpers/widgets.rs"]
|
||
mod widgets;
|
||
|
||
/// 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.wgsl";
|
||
|
||
/// 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),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Identifies one of the settings that can be changed by the user.
|
||
#[derive(Clone)]
|
||
enum AppSetting {
|
||
/// Regenerates the top mipmap level.
|
||
///
|
||
/// This is more of an *operation* than a *setting* per se, but it was
|
||
/// convenient to use the `AppSetting` infrastructure for the "Regenerate
|
||
/// Top Mip Level" button.
|
||
RegenerateTopMipLevel,
|
||
|
||
/// Whether mipmaps should be generated.
|
||
EnableMipGeneration(EnableMipGeneration),
|
||
|
||
/// The width of the image.
|
||
ImageWidth(ImageSize),
|
||
|
||
/// The height of the image.
|
||
ImageHeight(ImageSize),
|
||
}
|
||
|
||
/// 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(Clone, Copy, Default, PartialEq)]
|
||
enum EnableMipGeneration {
|
||
/// Mipmap levels are generated for the image.
|
||
#[default]
|
||
On,
|
||
/// Mipmap levels aren't generated for the image.
|
||
Off,
|
||
}
|
||
|
||
/// 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<Image>,
|
||
}
|
||
|
||
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<Image>);
|
||
|
||
/// 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<UVec2>,
|
||
}
|
||
|
||
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 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::<SingleMipLevelMaterial>::default(),
|
||
))
|
||
.init_resource::<AppStatus>()
|
||
.init_resource::<AppAssets>()
|
||
.add_message::<RegenerateImage>()
|
||
.add_message::<WidgetClickEvent<AppSetting>>()
|
||
.add_systems(Startup, setup)
|
||
.add_systems(Update, animate_image_scale)
|
||
.add_systems(
|
||
Update,
|
||
(
|
||
widgets::handle_ui_interactions::<AppSetting>,
|
||
update_radio_buttons,
|
||
)
|
||
.chain(),
|
||
)
|
||
.add_systems(
|
||
Update,
|
||
(handle_window_resize_events, regenerate_image_when_requested).chain(),
|
||
)
|
||
.add_systems(
|
||
Update,
|
||
handle_app_setting_change
|
||
.after(widgets::handle_ui_interactions::<AppSetting>)
|
||
.before(regenerate_image_when_requested),
|
||
);
|
||
|
||
// 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<MipGenerationJobs>,
|
||
pipeline_cache: Res<PipelineCache>,
|
||
mip_generation_pipelines: Option<Res<MipGenerationPipelines>>,
|
||
gpu_images: Res<RenderAssets<GpuImage>>,
|
||
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<Mesh>,
|
||
/// 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::<Assets<Mesh>>();
|
||
let rectangle = meshes.add(Rectangle::default());
|
||
|
||
let asset_server = world.resource::<AssetServer>();
|
||
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<RegenerateImage>,
|
||
) {
|
||
// 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((
|
||
widgets::main_ui_node(),
|
||
children![
|
||
// Spawn the "Regenerate Top Mip Level" button.
|
||
(
|
||
Button,
|
||
Node {
|
||
border: BUTTON_BORDER,
|
||
justify_content: JustifyContent::Center,
|
||
align_items: AlignItems::Center,
|
||
padding: BUTTON_PADDING,
|
||
border_radius: BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE),
|
||
..default()
|
||
},
|
||
BUTTON_BORDER_COLOR,
|
||
BackgroundColor(Color::BLACK),
|
||
WidgetClickSender(AppSetting::RegenerateTopMipLevel),
|
||
children![(
|
||
widgets::ui_text("Regenerate Top Mip Level", Color::WHITE),
|
||
WidgetClickSender(AppSetting::RegenerateTopMipLevel),
|
||
)],
|
||
),
|
||
// Spawn the "Mip Generation" switch that allows the user to toggle
|
||
// mip generation on and off.
|
||
widgets::option_buttons(
|
||
"Mip Generation",
|
||
&[
|
||
(
|
||
AppSetting::EnableMipGeneration(EnableMipGeneration::On),
|
||
"On"
|
||
),
|
||
(
|
||
AppSetting::EnableMipGeneration(EnableMipGeneration::Off),
|
||
"Off"
|
||
),
|
||
]
|
||
),
|
||
// Spawn the "Image Width" control that allows the user to set the
|
||
// width of the image.
|
||
widgets::option_buttons(
|
||
"Image Width",
|
||
&[
|
||
(AppSetting::ImageWidth(ImageSize::Size240), "240"),
|
||
(AppSetting::ImageWidth(ImageSize::Size480), "480"),
|
||
(AppSetting::ImageWidth(ImageSize::Size640), "640"),
|
||
(AppSetting::ImageWidth(ImageSize::Size1080), "1080"),
|
||
(AppSetting::ImageWidth(ImageSize::Size1920), "1920"),
|
||
]
|
||
),
|
||
// Spawn the "Image Height" control that allows the user to set the
|
||
// height of the image.
|
||
widgets::option_buttons(
|
||
"Image Height",
|
||
&[
|
||
(AppSetting::ImageHeight(ImageSize::Size240), "240"),
|
||
(AppSetting::ImageHeight(ImageSize::Size480), "480"),
|
||
(AppSetting::ImageHeight(ImageSize::Size640), "640"),
|
||
(AppSetting::ImageHeight(ImageSize::Size1080), "1080"),
|
||
(AppSetting::ImageHeight(ImageSize::Size1920), "1920"),
|
||
]
|
||
),
|
||
],
|
||
));
|
||
}
|
||
|
||
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<Self::Item> {
|
||
// 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<AnimatedImage>>,
|
||
windows_query: Query<&Window, With<PrimaryWindow>>,
|
||
app_status: Res<AppStatus>,
|
||
time: Res<Time>,
|
||
) {
|
||
let window_size = windows_query.iter().next().unwrap().size();
|
||
let animated_mesh_size = app_status.animated_mesh_size(window_size);
|
||
|
||
for mut animated_image_transform in &mut animated_images_query {
|
||
animated_image_transform.scale =
|
||
animated_mesh_size.extend(1.0) * triangle_wave(time.elapsed_secs(), ANIMATION_PERIOD);
|
||
}
|
||
}
|
||
|
||
/// Evaluates a [triangle wave] with the given wavelength.
|
||
///
|
||
/// This is used as part of [`animate_image_scale`], to derive the scale from
|
||
/// the current elapsed time.
|
||
///
|
||
/// [triangle wave]: https://en.wikipedia.org/wiki/Triangle_wave#Definition
|
||
fn triangle_wave(time: f32, wavelength: f32) -> f32 {
|
||
2.0 * ops::abs(time / wavelength - ops::floor(time / wavelength + 0.5))
|
||
}
|
||
|
||
/// Adds the top mipmap level of the image to [`MipGenerationJobs`].
|
||
///
|
||
/// Note that this must run in the render world, not the main world, as
|
||
/// [`MipGenerationJobs`] is a resource that exists in the former. Consequently,
|
||
/// it must use [`Extract`] to access main world resources.
|
||
fn extract_mipmap_source_image(
|
||
mipmap_source_image: Extract<Res<MipmapSourceImage>>,
|
||
app_status: Extract<Res<AppStatus>>,
|
||
mut mip_generation_jobs: ResMut<MipGenerationJobs>,
|
||
) {
|
||
if app_status.enable_mip_generation == EnableMipGeneration::On {
|
||
mip_generation_jobs.add(MIP_GENERATION_PHASE_ID, mipmap_source_image.id());
|
||
}
|
||
}
|
||
|
||
/// Updates the widgets at the bottom of the screen to reflect the settings that
|
||
/// the user has chosen.
|
||
fn update_radio_buttons(
|
||
mut widgets: Query<
|
||
(
|
||
Entity,
|
||
Option<&mut BackgroundColor>,
|
||
Has<Text>,
|
||
&WidgetClickSender<AppSetting>,
|
||
),
|
||
Or<(With<RadioButton>, With<RadioButtonText>)>,
|
||
>,
|
||
app_status: Res<AppStatus>,
|
||
mut writer: TextUiWriter,
|
||
) {
|
||
for (entity, image, has_text, sender) in widgets.iter_mut() {
|
||
let selected = match **sender {
|
||
AppSetting::RegenerateTopMipLevel => continue,
|
||
AppSetting::EnableMipGeneration(enable_mip_generation) => {
|
||
enable_mip_generation == app_status.enable_mip_generation
|
||
}
|
||
AppSetting::ImageWidth(image_width) => image_width == app_status.image_width,
|
||
AppSetting::ImageHeight(image_height) => image_height == app_status.image_height,
|
||
};
|
||
|
||
if let Some(mut bg_color) = image {
|
||
widgets::update_ui_radio_button(&mut bg_color, selected);
|
||
}
|
||
if has_text {
|
||
widgets::update_ui_radio_button_text(entity, &mut writer, selected);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Handles a request from the user to change application settings via the UI.
|
||
///
|
||
/// This also handles clicks on the "Regenerate Top Mip Level" button.
|
||
fn handle_app_setting_change(
|
||
mut events: MessageReader<WidgetClickEvent<AppSetting>>,
|
||
mut app_status: ResMut<AppStatus>,
|
||
mut regenerate_image_message_writer: MessageWriter<RegenerateImage>,
|
||
) {
|
||
for event in events.read() {
|
||
// If this is a setting, update the setting. Fall through if, in
|
||
// addition to updating the setting, we need to regenerate the image.
|
||
match **event {
|
||
AppSetting::EnableMipGeneration(enable_mip_generation) => {
|
||
app_status.enable_mip_generation = enable_mip_generation;
|
||
continue;
|
||
}
|
||
|
||
AppSetting::RegenerateTopMipLevel => {}
|
||
AppSetting::ImageWidth(image_size) => app_status.image_width = image_size,
|
||
AppSetting::ImageHeight(image_size) => app_status.image_height = image_size,
|
||
}
|
||
|
||
// Schedule the image to be regenerated.
|
||
regenerate_image_message_writer.write(RegenerateImage);
|
||
}
|
||
}
|
||
|
||
/// Handles resize events for the window.
|
||
///
|
||
/// Resizing the window invalidates the image and repositions all image views.
|
||
/// (Regenerating the image isn't strictly necessary, but it's simplest to have
|
||
/// a single function that both regenerates the image and recreates the image
|
||
/// views.)
|
||
fn handle_window_resize_events(
|
||
mut events: MessageReader<WindowResized>,
|
||
mut regenerate_image_message_writer: MessageWriter<RegenerateImage>,
|
||
) {
|
||
for _ in events.read() {
|
||
regenerate_image_message_writer.write(RegenerateImage);
|
||
}
|
||
}
|
||
|
||
/// Recreates the image, as well as all views that show the image, when a
|
||
/// [`RegenerateImage`] message is received.
|
||
///
|
||
/// The views that show the image consist of the animated mesh on the left side
|
||
/// of the window and the column of mipmap level views on the right side of the
|
||
/// window.
|
||
fn regenerate_image_when_requested(
|
||
mut commands: Commands,
|
||
image_views_query: Query<Entity, With<ImageView>>,
|
||
windows_query: Query<&Window, With<PrimaryWindow>>,
|
||
app_assets: Res<AppAssets>,
|
||
mut app_status: ResMut<AppStatus>,
|
||
mut images: ResMut<Assets<Image>>,
|
||
mut single_mip_level_materials: ResMut<Assets<SingleMipLevelMaterial>>,
|
||
mut color_materials: ResMut<Assets<ColorMaterial>>,
|
||
mut message_reader: MessageReader<RegenerateImage>,
|
||
) {
|
||
// Only do this at most once per frame, or else the despawn logic below will
|
||
// get confused.
|
||
if message_reader.read().count() == 0 {
|
||
return;
|
||
}
|
||
|
||
// Despawn all entities that show the image.
|
||
for entity in image_views_query.iter() {
|
||
commands.entity(entity).despawn();
|
||
}
|
||
|
||
// Regenerate the image.
|
||
let image_handle = app_status.regenerate_mipmap_source_image(&mut commands, &mut images);
|
||
|
||
// Respawn the animated image view on the left side of the window.
|
||
spawn_animated_mesh(
|
||
&mut commands,
|
||
&app_status,
|
||
&app_assets,
|
||
&windows_query,
|
||
&mut color_materials,
|
||
&image_handle,
|
||
);
|
||
|
||
// Respawn the column of mip level views on the right side of the window.
|
||
spawn_mip_level_views(
|
||
&mut commands,
|
||
&app_status,
|
||
&app_assets,
|
||
&windows_query,
|
||
&mut single_mip_level_materials,
|
||
&image_handle,
|
||
);
|
||
}
|
||
|
||
/// Spawns the image on the left that continually changes scale.
|
||
///
|
||
/// Continually changing scale effectively cycles though each mip level,
|
||
/// demonstrating the difference between mip level images being present and mip
|
||
/// level image being absent.
|
||
fn spawn_animated_mesh(
|
||
commands: &mut Commands,
|
||
app_status: &AppStatus,
|
||
app_assets: &AppAssets,
|
||
windows_query: &Query<&Window, With<PrimaryWindow>>,
|
||
color_materials: &mut Assets<ColorMaterial>,
|
||
image_handle: &Handle<Image>,
|
||
) {
|
||
let window_size = windows_query.iter().next().unwrap().size();
|
||
let animated_mesh_area_size = app_status.animated_mesh_area_size(window_size);
|
||
let animated_mesh_size = app_status.animated_mesh_size(window_size);
|
||
|
||
commands.spawn((
|
||
Mesh2d(app_assets.rectangle.clone()),
|
||
MeshMaterial2d(color_materials.add(ColorMaterial {
|
||
texture: Some(image_handle.clone()),
|
||
..default()
|
||
})),
|
||
Transform::from_translation(
|
||
(animated_mesh_area_size * 0.5 - window_size * 0.5).extend(0.0),
|
||
)
|
||
.with_scale(animated_mesh_size.extend(1.0)),
|
||
AnimatedImage,
|
||
ImageView,
|
||
));
|
||
}
|
||
|
||
/// Creates the column on the right side of the window that displays each mip
|
||
/// level by itself.
|
||
fn spawn_mip_level_views(
|
||
commands: &mut Commands,
|
||
app_status: &AppStatus,
|
||
app_assets: &AppAssets,
|
||
windows_query: &Query<&Window, With<PrimaryWindow>>,
|
||
single_mip_level_materials: &mut Assets<SingleMipLevelMaterial>,
|
||
image_handle: &Handle<Image>,
|
||
) {
|
||
let window_size = windows_query.iter().next().unwrap().size();
|
||
|
||
// Calculate the placement of the column of mipmap levels.
|
||
let max_slice_size = app_status.max_mip_slice_size(window_size);
|
||
let y_origin = app_status.vertical_mip_slice_origin(window_size);
|
||
let y_spacing = app_status.vertical_mip_slice_spacing(window_size);
|
||
let x_origin = app_status.horizontal_mip_slice_origin(window_size);
|
||
|
||
for (mip_level, mip_size) in MipmapSizeIterator::new(app_status).enumerate() {
|
||
let y_center = y_origin - y_spacing * mip_level as f32;
|
||
|
||
// Size each image to fit its container, preserving aspect ratio.
|
||
let mut slice_size = mip_size.as_vec2();
|
||
let ratios = max_slice_size / slice_size;
|
||
let slice_scale = ratios.x.min(ratios.y).min(1.0);
|
||
slice_size *= slice_scale;
|
||
|
||
// Spawn the image. Use the `SingleMipLevelMaterial` with its custom
|
||
// shader so that only the mip level in question is displayed.
|
||
commands.spawn((
|
||
Mesh2d(app_assets.rectangle.clone()),
|
||
MeshMaterial2d(single_mip_level_materials.add(SingleMipLevelMaterial {
|
||
mip_level: mip_level as u32,
|
||
texture: image_handle.clone(),
|
||
})),
|
||
Transform::from_xyz(x_origin, y_center, 0.0).with_scale(slice_size.extend(1.0)),
|
||
ImageView,
|
||
));
|
||
|
||
// Display a label to the side.
|
||
commands.spawn((
|
||
Text2d::new(format!(
|
||
"Level {}\n{}×{}",
|
||
mip_level, mip_size.x, mip_size.y
|
||
)),
|
||
app_assets.text_font.clone(),
|
||
TextLayout::justify(Justify::Center),
|
||
Text2dShadow::default(),
|
||
Transform::from_xyz(x_origin - max_slice_size.x * 0.5 - 64.0, y_center, 0.0),
|
||
ImageView,
|
||
));
|
||
}
|
||
}
|
||
|
||
/// Returns true if the given point is inside a 2D ellipse with the given center
|
||
/// and given radii or false otherwise.
|
||
fn point_in_ellipse(point: Vec2, center: Vec2, radii: Vec2) -> bool {
|
||
// This can be derived from the standard equation of an ellipse:
|
||
//
|
||
// x² y²
|
||
// ⎯⎯ + ⎯⎯ = 1
|
||
// a² b²
|
||
let (nums, denoms) = (point - center, radii);
|
||
let terms = (nums * nums) / (denoms * denoms);
|
||
terms.x + terms.y < 1.0
|
||
}
|
||
|
||
impl AppStatus {
|
||
/// Returns the vertical distance between each mip slice image in the column
|
||
/// on the right side of the window.
|
||
fn vertical_mip_slice_spacing(&self, window_size: Vec2) -> f32 {
|
||
window_size.y / self.image_mip_level_count() as f32
|
||
}
|
||
|
||
/// Returns the Y position of the center of the image that represents the
|
||
/// first mipmap level in the column on the right side of the window.
|
||
fn vertical_mip_slice_origin(&self, window_size: Vec2) -> f32 {
|
||
let spacing = self.vertical_mip_slice_spacing(window_size);
|
||
window_size.y * 0.5 - spacing * 0.5
|
||
}
|
||
|
||
/// Returns the maximum area that a single mipmap slice can occupy in the
|
||
/// column at the right side of the window.
|
||
///
|
||
/// Because the slices may be smaller than this area, and because the size
|
||
/// of each slice preserves the aspect ratio of the image, the actual
|
||
/// displayed size of each slice may be smaller than this.
|
||
fn max_mip_slice_size(&self, window_size: Vec2) -> Vec2 {
|
||
let spacing = self.vertical_mip_slice_spacing(window_size);
|
||
vec2(window_size.x * MIP_SLICES_WIDTH, spacing)
|
||
}
|
||
|
||
/// Returns the horizontal center point of each mip slice image in the
|
||
/// column at the right side of the window.
|
||
fn horizontal_mip_slice_origin(&self, window_size: Vec2) -> f32 {
|
||
let max_slice_size = self.max_mip_slice_size(window_size);
|
||
window_size.x * 0.5 - max_slice_size.x * 0.5 - MIP_SLICES_MARGIN_RIGHT
|
||
}
|
||
|
||
/// Calculates and returns the area reserved for the animated image on the
|
||
/// left side of the window.
|
||
///
|
||
/// Note that this isn't necessarily equal to the final size of the animated
|
||
/// image, because that size preserves the image's aspect ratio.
|
||
fn animated_mesh_area_size(&self, window_size: Vec2) -> Vec2 {
|
||
vec2(
|
||
self.horizontal_mip_slice_origin(window_size) * 2.0 - MIP_SLICES_MARGIN_LEFT * 2.0,
|
||
window_size.y,
|
||
)
|
||
}
|
||
|
||
/// Calculates and returns the actual maximum size of the animated image on
|
||
/// the left side of the window.
|
||
///
|
||
/// This is equal to the maximum portion of the
|
||
/// [`Self::animated_mesh_area_size`] that the image can occupy while
|
||
/// preserving its aspect ratio.
|
||
fn animated_mesh_size(&self, window_size: Vec2) -> Vec2 {
|
||
let max_image_size = self.animated_mesh_area_size(window_size);
|
||
let image_size = self.image_size_f32();
|
||
let ratios = max_image_size / image_size;
|
||
let image_scale = ratios.x.min(ratios.y);
|
||
image_size * image_scale
|
||
}
|
||
|
||
/// Returns the size of the image as a [`UVec2`].
|
||
fn image_size_u32(&self) -> UVec2 {
|
||
uvec2(self.image_width as u32, self.image_height as u32)
|
||
}
|
||
|
||
/// Returns the size of the image as a [`Vec2`].
|
||
fn image_size_f32(&self) -> Vec2 {
|
||
vec2(
|
||
self.image_width as u32 as f32,
|
||
self.image_height as u32 as f32,
|
||
)
|
||
}
|
||
|
||
/// Regenerates the main image based on the image size selected by the user.
|
||
fn regenerate_mipmap_source_image(
|
||
&mut self,
|
||
commands: &mut Commands,
|
||
images: &mut Assets<Image>,
|
||
) -> Handle<Image> {
|
||
let image_data = self.generate_image_data();
|
||
|
||
let mut image = Image::new_uninit(
|
||
Extent3d {
|
||
width: self.image_width as u32,
|
||
height: self.image_height as u32,
|
||
depth_or_array_layers: 1,
|
||
},
|
||
TextureDimension::D2,
|
||
TextureFormat::Rgba8Unorm,
|
||
RenderAssetUsages::all(),
|
||
);
|
||
image.texture_descriptor.mip_level_count = self.image_mip_level_count();
|
||
image.texture_descriptor.usage |= TextureUsages::STORAGE_BINDING;
|
||
image.data = Some(image_data);
|
||
|
||
let image_handle = images.add(image);
|
||
commands.insert_resource(MipmapSourceImage(image_handle.clone()));
|
||
|
||
image_handle
|
||
}
|
||
|
||
/// Draws the concentric ellipses that make up the image.
|
||
///
|
||
/// Returns the RGBA8 image data.
|
||
fn generate_image_data(&mut self) -> Vec<u8> {
|
||
// Select random colors for the inner and outer ellipses.
|
||
let outer_color: [u8; 3] = array::from_fn(|_| self.rng.random());
|
||
let inner_color: [u8; 3] = array::from_fn(|_| self.rng.random());
|
||
|
||
let image_byte_size = 4usize
|
||
* MipmapSizeIterator::new(self)
|
||
.map(|size| size.x as usize * size.y as usize)
|
||
.sum::<usize>();
|
||
let mut image_data = vec![0u8; image_byte_size];
|
||
|
||
let center = self.image_size_f32() * 0.5;
|
||
|
||
let inner_ellipse_radii = self.inner_ellipse_radii();
|
||
let outer_ellipse_radii = self.outer_ellipse_radii();
|
||
|
||
for y in 0..(self.image_height as u32) {
|
||
for x in 0..(self.image_width as u32) {
|
||
let p = vec2(x as f32, y as f32);
|
||
let (color, alpha) = if point_in_ellipse(p, center, inner_ellipse_radii) {
|
||
(inner_color, 255)
|
||
} else if point_in_ellipse(p, center, outer_ellipse_radii) {
|
||
(outer_color, 255)
|
||
} else {
|
||
([0; 3], 0)
|
||
};
|
||
let start = (4 * (x + y * (self.image_width as u32))) as usize;
|
||
image_data[start..(start + 3)].copy_from_slice(&color);
|
||
image_data[start + 3] = alpha;
|
||
}
|
||
}
|
||
|
||
image_data
|
||
}
|
||
|
||
/// Returns the number of mipmap levels that the image should possess.
|
||
///
|
||
/// This will be equal to the maximum number of mipmap levels that an image
|
||
/// of the appropriate size can have.
|
||
fn image_mip_level_count(&self) -> u32 {
|
||
32 - (self.image_width as u32)
|
||
.max(self.image_height as u32)
|
||
.leading_zeros()
|
||
}
|
||
|
||
/// Returns the X and Y radii of the outer ellipse drawn in the texture,
|
||
/// respectively.
|
||
fn outer_ellipse_radii(&self) -> Vec2 {
|
||
self.image_size_f32() * 0.5
|
||
}
|
||
|
||
/// Returns the X and Y radii of the inner ellipse drawn in the texture,
|
||
/// respectively.
|
||
fn inner_ellipse_radii(&self) -> Vec2 {
|
||
self.image_size_f32() * 0.25
|
||
}
|
||
}
|