diff --git a/.gitignore b/.gitignore index 65a88e51..78748cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ Cargo.lock # mdbook generated files design-book/book + +crates/bevy_editor_launcher/assets/Bevy Assets diff --git a/Cargo.toml b/Cargo.toml index 7e57d616..0db54322 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ unused_qualifications = "warn" [workspace.dependencies] bevy = { git = "https://github.com/bevyengine/bevy.git", rev = "015f2c69ca2a2c009fd04eadada282482deaf469", features = ["wayland"] } bevy_derive = { git = "https://github.com/bevyengine/bevy.git", rev = "015f2c69ca2a2c009fd04eadada282482deaf469" } +crossbeam-channel = "0.5" thiserror = "1" serde = { version = "1", features = ["derive"] } atomicow = "1.0.0" diff --git a/bevy_editor_panes/bevy_asset_browser/Cargo.toml b/bevy_editor_panes/bevy_asset_browser/Cargo.toml index f6d0c58f..e1a22eb9 100644 --- a/bevy_editor_panes/bevy_asset_browser/Cargo.toml +++ b/bevy_editor_panes/bevy_asset_browser/Cargo.toml @@ -12,6 +12,7 @@ bevy_pane_layout.workspace = true bevy_scroll_box.workspace = true bevy_context_menu.workspace = true atomicow.workspace = true +bevy_asset_preview.workspace = true [lints] workspace = true diff --git a/bevy_editor_panes/bevy_asset_browser/src/lib.rs b/bevy_editor_panes/bevy_asset_browser/src/lib.rs index ea0e417b..86fc2534 100644 --- a/bevy_editor_panes/bevy_asset_browser/src/lib.rs +++ b/bevy_editor_panes/bevy_asset_browser/src/lib.rs @@ -11,6 +11,7 @@ use bevy::{ }, prelude::*, }; +use bevy_asset_preview::AssetPreviewPlugin; use bevy_pane_layout::PaneRegistry; use bevy_scroll_box::ScrollBoxPlugin; use ui::{top_bar::location_as_changed, AssetBrowserNode}; @@ -19,7 +20,10 @@ mod io; mod ui; /// The bevy asset browser plugin -pub struct AssetBrowserPanePlugin; +pub struct AssetBrowserPanePlugin { + /// Enable asset preview or not. + pub preview: bool, +} impl Plugin for AssetBrowserPanePlugin { fn build(&self, app: &mut App) { @@ -27,6 +31,10 @@ impl Plugin for AssetBrowserPanePlugin { embedded_asset!(app, "assets/source_icon.png"); embedded_asset!(app, "assets/file_icon.png"); + if self.preview { + app.add_plugins(AssetPreviewPlugin); + } + app.world_mut() .get_resource_or_init::() .register("Asset Browser", |mut commands, pane_root| { diff --git a/bevy_editor_panes/bevy_asset_browser/src/ui/directory_content.rs b/bevy_editor_panes/bevy_asset_browser/src/ui/directory_content.rs index d51f4c66..3174290d 100644 --- a/bevy_editor_panes/bevy_asset_browser/src/ui/directory_content.rs +++ b/bevy_editor_panes/bevy_asset_browser/src/ui/directory_content.rs @@ -127,8 +127,7 @@ fn populate_directory_content( .set_parent(parent_entity); } Entry::File(name) => { - spawn_file_node(commands, name.clone(), asset_server, theme) - .set_parent(parent_entity); + spawn_file_node(commands, name.clone(), location, theme).set_parent(parent_entity); } } } diff --git a/bevy_editor_panes/bevy_asset_browser/src/ui/nodes.rs b/bevy_editor_panes/bevy_asset_browser/src/ui/nodes.rs index e5d16c1e..0779a60b 100644 --- a/bevy_editor_panes/bevy_asset_browser/src/ui/nodes.rs +++ b/bevy_editor_panes/bevy_asset_browser/src/ui/nodes.rs @@ -7,6 +7,7 @@ use bevy::{ window::SystemCursorIcon, winit::cursor::CursorIcon, }; +use bevy_asset_preview::PreviewAsset; use bevy_context_menu::{ContextMenu, ContextMenuOption}; use bevy_editor_styles::Theme; @@ -152,7 +153,7 @@ pub(crate) fn spawn_folder_node<'a>( pub(crate) fn spawn_file_node<'a>( commands: &'a mut Commands, file_name: String, - asset_server: &Res, + location: &Res, theme: &Res, ) -> EntityCommands<'a> { let base_node = spawn_base_node(commands, theme).id(); @@ -160,7 +161,7 @@ pub(crate) fn spawn_file_node<'a>( // Icon commands .spawn(( - UiImage::new(asset_server.load("embedded://bevy_asset_browser/assets/file_icon.png")), + PreviewAsset(location.path.join(&file_name)), Node { height: Val::Px(50.0), ..default() diff --git a/crates/bevy_asset_preview/Cargo.toml b/crates/bevy_asset_preview/Cargo.toml index c2b3ef6f..aeb17027 100644 --- a/crates/bevy_asset_preview/Cargo.toml +++ b/crates/bevy_asset_preview/Cargo.toml @@ -4,4 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -bevy.workspace = true \ No newline at end of file +bevy.workspace = true +crossbeam-channel.workspace = true +image = "0.25" diff --git a/crates/bevy_asset_preview/src/io/mod.rs b/crates/bevy_asset_preview/src/io/mod.rs new file mode 100644 index 00000000..165834c0 --- /dev/null +++ b/crates/bevy_asset_preview/src/io/mod.rs @@ -0,0 +1,93 @@ +use std::{ + env, + io::{BufWriter, Cursor}, + path::{Path, PathBuf}, +}; + +use bevy::{ + asset::{ + io::{file::FileAssetWriter, AssetWriter}, + AssetServer, Assets, + }, + prelude::{Image, Res, ResMut}, + render::{renderer::RenderDevice, texture::TextureFormatPixelInfo}, + tasks::IoTaskPool, +}; +use image::ImageEncoder; + +use crate::render::{ + receive::{MainWorldPreviewImageReceiver, PreviewImageCopies}, + RenderedScenePreviews, +}; + +pub(crate) fn get_base_path() -> PathBuf { + if let Ok(manifest_dir) = env::var("BEVY_ASSET_ROOT") { + PathBuf::from(manifest_dir) + } else if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { + PathBuf::from(manifest_dir) + } else { + env::current_exe() + .map(|path| path.parent().map(ToOwned::to_owned).unwrap()) + .unwrap() + } +} + +pub fn receive_preview( + mut previews: ResMut, + asset_server: Res, + mut images: ResMut>, + receiver: Res, + mut image_copies: ResMut, +) { + let thread_pool = IoTaskPool::get(); + + while let Ok((id, data)) = receiver.try_recv() { + let image = images.get_mut(id).unwrap(); + let row_bytes = image.width() as usize * image.texture_descriptor.format.pixel_size(); + let aligned_row_bytes = RenderDevice::align_copy_bytes_per_row(row_bytes); + if row_bytes == aligned_row_bytes { + image.data = data; + } else { + image.data = data + .chunks(aligned_row_bytes) + .take(image.height() as usize) + .flat_map(|row| &row[..row_bytes.min(row.len())]) + .cloned() + .collect() + } + + image_copies.remove(&id); + + let Some(scene_handle) = previews.changed.remove(&id) else { + continue; + }; + let Some(path) = asset_server.get_path(scene_handle) else { + continue; + }; + + let image_buffer = image.clone().try_into_dynamic().unwrap().into_rgba8(); + let image_path = + Path::new("assets/cache/asset_preview").join(path.path().with_extension("png")); + + thread_pool + .spawn(async move { + let image_path_full = get_base_path().join(&image_path); + FileAssetWriter::new("", true) + .write(&image_path) + .await + .unwrap(); + image_buffer.save(image_path_full).unwrap(); + + // TODO use the following code once know why it fails sometimes. + // let mut writer = BufWriter::new(Cursor::new(Vec::new())); + // image_buffer + // .write_to(&mut writer, image::ImageFormat::Png) + // .unwrap(); + // FileAssetWriter::new("", true) + // .write_bytes(&image_path, writer.buffer()) + // .await + // .unwrap(); + }) + .detach(); + } +} diff --git a/crates/bevy_asset_preview/src/lib.rs b/crates/bevy_asset_preview/src/lib.rs index 45574a5b..7b06dd7f 100644 --- a/crates/bevy_asset_preview/src/lib.rs +++ b/crates/bevy_asset_preview/src/lib.rs @@ -1,4 +1,28 @@ -use bevy::prelude::*; +use std::path::PathBuf; + +use bevy::{ + app::{App, Last, Plugin, Update}, + asset::{AssetPath, AssetServer, Handle}, + gltf::GltfAssetLabel, + prelude::{Component, Deref, Image, IntoSystemConfigs}, + render::{ + extract_resource::ExtractResourcePlugin, graph::CameraDriverLabel, + render_graph::RenderGraph, Render, RenderApp, RenderSet, + }, + scene::Scene, +}; + +use crate::render::{ + receive::{ + MainWorldPreviewImageReceiver, PreviewImageCopies, PreviewTextureToBufferLabel, + PreviewTextureToBufferNode, RenderWorldPreviewImageSender, + }, + PreviewRendered, PreviewSceneState, PreviewSettings, RenderedScenePreviews, +}; + +mod io; +mod render; +mod ui; /// This crate is a work in progress and is not yet ready for use. /// The intention is to provide a way to load/render/unload assets in the background and provide previews of them in the Bevy Editor. @@ -7,8 +31,43 @@ use bevy::prelude::*; /// So long as the assets are unchanged, the previews will be cached and will not need to be re-rendered. /// In theory this can be done passively in the background, and the previews will be ready when the user needs them. +#[derive(Component, Deref)] +pub struct PreviewAsset(pub PathBuf); + pub struct AssetPreviewPlugin; impl Plugin for AssetPreviewPlugin { - fn build(&self, _app: &mut App) {} + fn build(&self, app: &mut App) { + let (s, r) = crossbeam_channel::unbounded(); + + app.add_plugins(ExtractResourcePlugin::::default()) + .add_event::() + .add_systems( + Update, + ( + render::update_queue, + render::update_preview_frames_counter, + ui::preview_handler, + io::receive_preview, + ), + ) + .add_systems(Last, render::change_render_layers) + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .insert_resource(MainWorldPreviewImageReceiver(r)); + + let render_app = app.sub_app_mut(RenderApp); + render_app + .add_systems( + Render, + render::receive::receive_image_from_buffer.after(RenderSet::Render), + ) + .insert_resource(RenderWorldPreviewImageSender(s)); + + let mut graph = render_app.world_mut().resource_mut::(); + graph.add_node(PreviewTextureToBufferLabel, PreviewTextureToBufferNode); + graph.add_node_edge(CameraDriverLabel, PreviewTextureToBufferLabel); + } } diff --git a/crates/bevy_asset_preview/src/render/mod.rs b/crates/bevy_asset_preview/src/render/mod.rs new file mode 100644 index 00000000..b2069e66 --- /dev/null +++ b/crates/bevy_asset_preview/src/render/mod.rs @@ -0,0 +1,299 @@ +use std::path::{Path, PathBuf}; + +use bevy::{ + asset::{ + io::{file::FileAssetReader, AssetReader}, + AssetId, AssetServer, Assets, Handle, + }, + gltf::GltfAssetLabel, + math::{UVec2, Vec3}, + pbr::DirectionalLight, + prelude::{ + Camera, Camera3d, Commands, Component, Entity, Event, EventReader, EventWriter, Image, + Query, Res, ResMut, Resource, Transform, + }, + render::{ + camera::RenderTarget, + render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, + renderer::RenderDevice, + texture::BevyDefault, + view::RenderLayers, + }, + scene::{InstanceId, Scene, SceneSpawner}, + tasks::block_on, + utils::{Entry, HashMap, HashSet}, +}; + +use crate::render::receive::{PreviewImageCopies, PreviewImageCopy}; + +pub const BASE_PREVIEW_LAYER: usize = 128; +pub const PREVIEW_LAYERS_COUNT: usize = 8; +pub const PREVIEW_RENDER_FRAMES: u32 = 32; + +pub mod receive; + +#[derive(Resource)] +pub struct PreviewSettings { + pub resolution: UVec2, +} + +impl Default for PreviewSettings { + fn default() -> Self { + Self { + resolution: UVec2::splat(256), + } + } +} + +fn create_prerender_target(settings: &PreviewSettings) -> Image { + let mut image = Image::new_fill( + Extent3d { + width: settings.resolution.x, + height: settings.resolution.y, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[0, 0, 0, 0], + TextureFormat::bevy_default(), + Default::default(), + ); + + image.texture_descriptor.usage |= TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::COPY_SRC + | TextureUsages::RENDER_ATTACHMENT; + + image +} + +#[derive(Component)] +pub struct PreviewRenderView { + pub layer: usize, +} + +#[derive(Component, Default)] +pub struct PreviewRenderedFrames { + pub cur_frame: u32, +} + +#[derive(Event)] +pub struct PreviewRendered { + pub layer: usize, +} + +#[derive(Resource)] +pub struct PreviewSceneState { + available_layers: u8, + cameras: [Entity; PREVIEW_LAYERS_COUNT], + lights: [Entity; PREVIEW_LAYERS_COUNT], + scene_handles: [Handle; PREVIEW_LAYERS_COUNT], + scene_instances: [Option; PREVIEW_LAYERS_COUNT], + applied_layer: [bool; PREVIEW_LAYERS_COUNT], + render_targets: [Handle; PREVIEW_LAYERS_COUNT], +} + +impl Default for PreviewSceneState { + fn default() -> Self { + Self { + available_layers: !0, + cameras: [Entity::PLACEHOLDER; PREVIEW_LAYERS_COUNT], + lights: [Entity::PLACEHOLDER; PREVIEW_LAYERS_COUNT], + scene_handles: Default::default(), + scene_instances: Default::default(), + applied_layer: Default::default(), + render_targets: Default::default(), + } + } +} + +impl PreviewSceneState { + pub fn occupy( + &mut self, + handle: Handle, + instance: InstanceId, + render_target: Handle, + commands: &mut Commands, + ) { + if self.is_full() { + return; + } + + let layer = self.available_layers.trailing_zeros() as usize; + self.available_layers &= !(1 << layer); + + self.lights[layer] = commands + .spawn(( + DirectionalLight::default(), + Transform::IDENTITY.looking_to(Vec3::new(1.0, -1.0, 1.0), Vec3::Y), + RenderLayers::from_layers(&[layer + BASE_PREVIEW_LAYER]), + )) + .id(); + self.cameras[layer] = commands + .spawn(( + Camera3d::default(), + Camera { + target: RenderTarget::Image(render_target.clone()), + ..Default::default() + }, + Transform::from_translation(Vec3::new(-5.0, 2.0, -5.0)) + .looking_at(Vec3::ZERO, Vec3::Y), + RenderLayers::from_layers(&[layer + BASE_PREVIEW_LAYER]), + PreviewRenderView { layer }, + PreviewRenderedFrames::default(), + )) + .id(); + self.render_targets[layer] = render_target; + self.scene_handles[layer] = handle; + self.scene_instances[layer] = Some(instance); + } + + pub fn free(&mut self, layer: usize, commands: &mut Commands) { + self.available_layers |= 1 << layer; + commands.entity(self.lights[layer]).despawn(); + commands.entity(self.cameras[layer]).despawn(); + self.applied_layer[layer] = false; + self.scene_instances[layer] = None; + } + + pub fn is_full(&self) -> bool { + self.available_layers.trailing_zeros() == PREVIEW_LAYERS_COUNT as u32 + } +} + +/// Scenes that are rendered for preview purpose. This should be inserted into +/// main world. +#[derive(Resource, Default)] +pub struct RenderedScenePreviews { + pub(crate) cached: HashMap>, + pub(crate) path_to_handle: HashMap>, + pub(crate) changed: HashMap, AssetId>, + pub(crate) available: HashMap, Handle>, + pub(crate) rendering: HashSet>, + pub(crate) queue: HashSet>, +} + +impl RenderedScenePreviews { + pub fn get_or_schedule( + &mut self, + path: PathBuf, + asset_server: &AssetServer, + ) -> Option> { + if let Some(cached) = self.cached.get(&path) { + return Some(cached.clone()); + } + + match self.path_to_handle.entry(path.clone()) { + Entry::Occupied(e) => { + let handle = e.get(); + let id = handle.id(); + match self.available.entry(id) { + Entry::Occupied(e) => Some(e.get().clone()), + Entry::Vacant(_) => { + if !self.rendering.contains(&id) { + self.queue.insert(handle.clone()); + self.rendering.insert(id); + } else { + } + None + } + } + } + Entry::Vacant(e) => { + let cached_path = Path::new("cache/asset_preview").join(path.with_extension("png")); + let reader = FileAssetReader::new("assets"); + + if block_on(reader.read(&cached_path)).is_ok() { + let cached = asset_server.load(cached_path); + self.cached.insert(path, cached.clone()); + Some(cached) + } else { + e.insert(asset_server.load(GltfAssetLabel::Scene(0).from_asset(path))); + None + } + } + } + } +} + +pub(crate) fn update_queue( + mut commands: Commands, + mut previews: ResMut, + mut scene_spawner: ResMut, + mut scene_state: ResMut, + settings: Res, + mut images: ResMut>, + mut preview_rendered: EventReader, + mut image_copies: ResMut, + render_device: Res, +) { + while !scene_state.is_full() { + let Some(handle) = previews.queue.iter().nth(0).cloned() else { + break; + }; + previews.queue.remove(&handle); + + let instance = scene_spawner.spawn(handle.clone()); + let render_target = images.add(create_prerender_target(&settings)); + scene_state.occupy(handle, instance, render_target, &mut commands); + } + + for finished in preview_rendered.read() { + let scene_handle = scene_state.scene_handles[finished.layer].clone(); + previews.rendering.remove(&scene_handle.id()); + let render_target = scene_state.render_targets[finished.layer].clone(); + image_copies.insert( + render_target.id(), + PreviewImageCopy::new(settings.resolution.x, settings.resolution.y, &render_device), + ); + previews + .changed + .insert(render_target.id(), scene_handle.id()); + previews.available.insert(scene_handle.id(), render_target); + + let instance = scene_state.scene_instances[finished.layer].unwrap(); + scene_spawner.despawn_instance(instance); + scene_state.free(finished.layer, &mut commands); + } +} + +pub(crate) fn update_preview_frames_counter( + mut commands: Commands, + mut counters_query: Query<(Entity, &mut PreviewRenderedFrames, &PreviewRenderView)>, + mut preview_rendered: EventWriter, + scene_state: Res, + scene_spawner: Res, +) { + for (entity, mut cnt, view) in &mut counters_query { + if scene_state.scene_instances[view.layer] + .is_some_and(|inst| scene_spawner.instance_is_ready(inst)) + { + cnt.cur_frame += 1; + + if cnt.cur_frame >= PREVIEW_RENDER_FRAMES { + commands.entity(entity).remove::(); + preview_rendered.send(PreviewRendered { layer: view.layer }); + } + } + } +} + +pub(crate) fn change_render_layers( + mut commands: Commands, + mut scene_state: ResMut, + scene_spawner: Res, +) { + for layer in 0..PREVIEW_LAYERS_COUNT { + if let Some(instance) = scene_state.scene_instances[layer] { + if !scene_state.applied_layer[layer] && scene_spawner.instance_is_ready(instance) { + scene_state.applied_layer[layer] = true; + + commands.insert_batch( + scene_spawner + .iter_instance_entities(instance) + .map(|e| (e, RenderLayers::from_layers(&[layer + BASE_PREVIEW_LAYER]))) + .collect::>(), + ); + } + } + } +} diff --git a/crates/bevy_asset_preview/src/render/receive.rs b/crates/bevy_asset_preview/src/render/receive.rs new file mode 100644 index 00000000..1208d2aa --- /dev/null +++ b/crates/bevy_asset_preview/src/render/receive.rs @@ -0,0 +1,120 @@ +use bevy::{ + asset::AssetId, + prelude::{Deref, DerefMut, Image, Res, Resource, World}, + render::{ + extract_resource::ExtractResource, + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderLabel}, + render_resource::{ + Buffer, BufferDescriptor, BufferUsages, Extent3d, ImageCopyBuffer, ImageDataLayout, + Maintain, MapMode, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::GpuImage, + }, + scene::Scene, + utils::HashMap, +}; +use crossbeam_channel::{Receiver, Sender}; + +#[derive(Clone, Deref)] +pub struct PreviewImageCopy(Buffer); + +impl PreviewImageCopy { + pub fn new(width: u32, height: u32, render_device: &RenderDevice) -> Self { + let padding_bytes_per_row = RenderDevice::align_copy_bytes_per_row(width as usize * 4); + let buffer = render_device.create_buffer(&BufferDescriptor { + label: None, + size: padding_bytes_per_row as u64 * height as u64, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + Self(buffer) + } +} + +#[derive(ExtractResource, Resource, Default, Clone, Deref, DerefMut)] +pub struct PreviewImageCopies(HashMap, PreviewImageCopy>); + +#[derive(Resource, Default, Clone, Deref, DerefMut)] +pub struct TransferredPreviewImages(Vec<(AssetId, AssetId)>); + +#[derive(Resource, Deref)] +pub struct MainWorldPreviewImageReceiver(pub Receiver<(AssetId, Vec)>); + +#[derive(Resource, Deref)] +pub struct RenderWorldPreviewImageSender(pub Sender<(AssetId, Vec)>); + +#[derive(RenderLabel, Debug, Clone, PartialEq, Eq, Hash)] +pub struct PreviewTextureToBufferLabel; + +pub struct PreviewTextureToBufferNode; + +impl Node for PreviewTextureToBufferNode { + fn run<'w>( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let image_copies = world.resource::(); + let images = world.resource::>(); + + for (id, buffer) in &**image_copies { + let src = images.get(*id).unwrap(); + let mut encoder = render_context + .render_device() + .create_command_encoder(&Default::default()); + + let block_dimension = src.texture_format.block_dimensions(); + let block_size = src.texture_format.block_copy_size(None).unwrap(); + + let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row( + (src.size.x as usize / block_dimension.0 as usize) * block_size as usize, + ); + + encoder.copy_texture_to_buffer( + src.texture.as_image_copy(), + ImageCopyBuffer { + buffer: &buffer, + layout: ImageDataLayout { + offset: 0, + bytes_per_row: Some((padded_bytes_per_row as u32).into()), + rows_per_image: None, + }, + }, + Extent3d { + width: src.size.x, + height: src.size.y, + depth_or_array_layers: 1, + }, + ); + + world.resource::().submit([encoder.finish()]); + } + + Ok(()) + } +} + +pub fn receive_image_from_buffer( + image_copies: Res, + render_device: Res, + sender: Res, +) { + for (id, copy) in &**image_copies { + let buffer_slice = copy.slice(..); + + let (s, r) = crossbeam_channel::bounded(1); + buffer_slice.map_async(MapMode::Read, move |r| match r { + Ok(r) => s.send(r).expect("Failed to send map update."), + Err(err) => panic!("Failed to map preview image buffer {:?}", err), + }); + + render_device.poll(Maintain::wait()).panic_on_timeout(); + r.recv().expect("Failed to receive the map_async message"); + + let _ = sender.send((*id, buffer_slice.get_mapped_range().to_vec())); + copy.unmap(); + } +} diff --git a/crates/bevy_asset_preview/src/ui.rs b/crates/bevy_asset_preview/src/ui.rs new file mode 100644 index 00000000..1f0c52ec --- /dev/null +++ b/crates/bevy_asset_preview/src/ui.rs @@ -0,0 +1,73 @@ +use std::path::Path; + +use bevy::{ + asset::{ + io::{file::FileAssetReader, AssetReader}, + AssetServer, + }, + prelude::{Commands, Entity, Query, Res, ResMut}, + tasks::block_on, + ui::UiImage, +}; + +use crate::{render::RenderedScenePreviews, PreviewAsset}; + +const FILE_PLACEHOLDER: &'static str = "embedded://bevy_asset_browser/assets/file_icon.png"; + +enum PreviewType { + Image, + Scene, + Other, +} + +// TODO: handle assets modification +pub fn preview_handler( + mut commands: Commands, + mut requests_query: Query<(Entity, &PreviewAsset, Option<&mut UiImage>)>, + asset_server: Res, + mut prerendered: ResMut, +) { + for (entity, preview, reuseable_image) in &mut requests_query { + let ty = match preview.extension() { + Some(ext) => match ext.to_str().unwrap() { + "jpeg" | "png" | "bmp" | "gif" | "ico" | "pnm" | "pam" | "pbm" | "pgm" | "ppm" + | "tga" | "webp" => PreviewType::Image, + "glb" | "gltf" => PreviewType::Scene, + _ => PreviewType::Other, + }, + None => PreviewType::Other, + }; + + let preview = match ty { + PreviewType::Image => { + commands.entity(entity).remove::(); + asset_server.load(preview.as_path()) + } + PreviewType::Scene => { + if let Some(handle) = + prerendered.get_or_schedule((**preview).clone(), &asset_server) + { + commands.entity(entity).remove::(); + handle + } else { + // Not rendered yet, fall back to default. + asset_server.load(FILE_PLACEHOLDER) + } + } + PreviewType::Other => { + commands.entity(entity).remove::(); + asset_server.load(FILE_PLACEHOLDER) + } + }; + + if let Some(mut reuseable) = reuseable_image { + reuseable.texture = preview; + } else { + // TODO: sprite atlas. + commands.entity(entity).insert(UiImage { + texture: preview, + ..Default::default() + }); + } + } +} diff --git a/crates/bevy_editor/src/lib.rs b/crates/bevy_editor/src/lib.rs index 8d8685a6..21cd138c 100644 --- a/crates/bevy_editor/src/lib.rs +++ b/crates/bevy_editor/src/lib.rs @@ -54,7 +54,7 @@ impl Plugin for EditorPlugin { Viewport2dPanePlugin, Viewport3dPanePlugin, ui::EditorUIPlugin, - AssetBrowserPanePlugin, + AssetBrowserPanePlugin { preview: true }, LoadGltfPlugin, )) .add_systems(Startup, dummy_setup); diff --git a/crates/bevy_infinite_grid/src/render/mod.rs b/crates/bevy_infinite_grid/src/render/mod.rs old mode 100755 new mode 100644