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 rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::widgets::{
RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender, BUTTON_BORDER,
BUTTON_BORDER_COLOR, BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING,
};
#[path = "../helpers/widgets.rs"]
mod widgets;
const ANIMATION_PERIOD: f32 = 2.0;
const SINGLE_MIP_LEVEL_SHADER_ASSET_PATH: &str = "shaders/single_mip_level.wgsl";
const MIP_SLICES_MARGIN_LEFT: f32 = 64.0;
const MIP_SLICES_MARGIN_RIGHT: f32 = 12.0;
const MIP_SLICES_WIDTH: f32 = 1.0 / 6.0;
const FONT_SIZE: FontSize = FontSize::Px(16.0);
#[derive(Resource)]
struct AppStatus {
enable_mip_generation: EnableMipGeneration,
image_width: ImageSize,
image_height: ImageSize,
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),
}
}
}
#[derive(Clone)]
enum AppSetting {
RegenerateTopMipLevel,
EnableMipGeneration(EnableMipGeneration),
ImageWidth(ImageSize),
ImageHeight(ImageSize),
}
#[derive(Clone, Copy, Default, PartialEq)]
enum EnableMipGeneration {
#[default]
On,
Off,
}
#[derive(Clone, Copy, Default, PartialEq)]
#[repr(u32)]
enum ImageSize {
Size240 = 240,
Size480 = 480,
#[default]
Size640 = 640,
Size1080 = 1080,
Size1920 = 1920,
}
#[derive(Clone, Asset, TypePath, AsBindGroup, Debug)]
struct SingleMipLevelMaterial {
#[uniform(0)]
mip_level: u32,
#[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
}
}
#[derive(Component)]
struct AnimatedImage;
#[derive(Resource, Deref, DerefMut)]
struct MipmapSourceImage(Handle<Image>);
struct MipmapSizeIterator {
size: Option<UVec2>,
}
const MIP_GENERATION_PHASE_ID: MipGenerationPhaseId = MipGenerationPhaseId(0);
#[derive(Component)]
struct ImageView;
#[derive(Clone, Copy, Debug, Message)]
struct RegenerateImage;
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),
);
let render_app = app.get_sub_app_mut(RenderApp).expect("Need a render app");
render_app.add_systems(Core2d, generate_mips_for_example);
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,
);
}
#[derive(Resource)]
struct AppAssets {
rectangle: Handle<Mesh>,
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,
}
}
}
fn setup(
mut commands: Commands,
mut regenerate_image_message_writer: MessageWriter<RegenerateImage>,
) {
commands.spawn(Camera2d);
spawn_ui(&mut commands);
regenerate_image_message_writer.write(RegenerateImage);
}
fn spawn_ui(commands: &mut Commands) {
commands.spawn((
widgets::main_ui_node(),
children![
(
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),
)],
),
widgets::option_buttons(
"Mip Generation",
&[
(
AppSetting::EnableMipGeneration(EnableMipGeneration::On),
"On"
),
(
AppSetting::EnableMipGeneration(EnableMipGeneration::Off),
"Off"
),
]
),
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"),
]
),
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 {
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> {
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
}
}
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);
}
}
fn triangle_wave(time: f32, wavelength: f32) -> f32 {
2.0 * ops::abs(time / wavelength - ops::floor(time / wavelength + 0.5))
}
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());
}
}
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);
}
}
}
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() {
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,
}
regenerate_image_message_writer.write(RegenerateImage);
}
}
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);
}
}
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>,
) {
if message_reader.read().count() == 0 {
return;
}
for entity in image_views_query.iter() {
commands.entity(entity).despawn();
}
let image_handle = app_status.regenerate_mipmap_source_image(&mut commands, &mut images);
spawn_animated_mesh(
&mut commands,
&app_status,
&app_assets,
&windows_query,
&mut color_materials,
&image_handle,
);
spawn_mip_level_views(
&mut commands,
&app_status,
&app_assets,
&windows_query,
&mut single_mip_level_materials,
&image_handle,
);
}
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,
));
}
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();
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;
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;
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,
));
commands.spawn((
Text2d::new(format!(
"Level {}\n{}×{}",
mip_level, mip_size.x, mip_size.y
)),
app_assets.text_font.clone(),
TextLayout::new_with_justify(Justify::Center),
Text2dShadow::default(),
Transform::from_xyz(x_origin - max_slice_size.x * 0.5 - 64.0, y_center, 0.0),
ImageView,
));
}
}
fn point_in_ellipse(point: Vec2, center: Vec2, radii: Vec2) -> bool {
let (nums, denoms) = (point - center, radii);
let terms = (nums * nums) / (denoms * denoms);
terms.x + terms.y < 1.0
}
impl AppStatus {
fn vertical_mip_slice_spacing(&self, window_size: Vec2) -> f32 {
window_size.y / self.image_mip_level_count() as f32
}
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
}
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)
}
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
}
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,
)
}
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
}
fn image_size_u32(&self) -> UVec2 {
uvec2(self.image_width as u32, self.image_height as u32)
}
fn image_size_f32(&self) -> Vec2 {
vec2(
self.image_width as u32 as f32,
self.image_height as u32 as f32,
)
}
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
}
fn generate_image_data(&mut self) -> Vec<u8> {
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
}
fn image_mip_level_count(&self) -> u32 {
32 - (self.image_width as u32)
.max(self.image_height as u32)
.leading_zeros()
}
fn outer_ellipse_radii(&self) -> Vec2 {
self.image_size_f32() * 0.5
}
fn inner_ellipse_radii(&self) -> Vec2 {
self.image_size_f32() * 0.25
}
}