From 870f715df359a60908ce3a398876796129db2f78 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 16 May 2020 20:18:30 -0700 Subject: [PATCH] Hot asset reloading --- Cargo.toml | 8 + assets/models/cube/cube.bin | Bin 0 -> 840 bytes assets/models/cube/cube.gltf | 122 ++++++++++++ assets/models/sphere/sphere.bin | Bin 0 -> 92384 bytes assets/models/sphere/sphere.gltf | 100 ++++++++++ crates/bevy_asset/Cargo.toml | 7 +- crates/bevy_asset/src/asset_path.rs | 55 ------ crates/bevy_asset/src/asset_server.rs | 187 ++++++++++++++---- crates/bevy_asset/src/assets.rs | 12 +- crates/bevy_asset/src/filesystem_watcher.rs | 26 +++ crates/bevy_asset/src/lib.rs | 11 +- crates/bevy_asset/src/load_request.rs | 8 +- crates/bevy_asset/src/loader.rs | 17 +- crates/bevy_gltf/src/loader.rs | 11 +- crates/bevy_render/src/mesh.rs | 4 +- .../src/texture/png_texture_loader.rs | 5 +- crates/bevy_text/src/font_loader.rs | 5 +- examples/asset/asset_loading.rs | 80 ++++++++ examples/asset/hot_asset_reload.rs | 60 ++++++ examples/asset/load_asset_folder.rs | 13 -- 20 files changed, 586 insertions(+), 145 deletions(-) create mode 100644 assets/models/cube/cube.bin create mode 100644 assets/models/cube/cube.gltf create mode 100644 assets/models/sphere/sphere.bin create mode 100644 assets/models/sphere/sphere.gltf delete mode 100644 crates/bevy_asset/src/asset_path.rs create mode 100644 crates/bevy_asset/src/filesystem_watcher.rs create mode 100644 examples/asset/asset_loading.rs create mode 100644 examples/asset/hot_asset_reload.rs delete mode 100644 examples/asset/load_asset_folder.rs diff --git a/Cargo.toml b/Cargo.toml index bcae2fefdedad..9f4c62f88cd39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,14 @@ path = "examples/app/headless.rs" name = "plugin" path = "examples/app/plugin.rs" +[[example]] +name = "hot_asset_reload" +path = "examples/asset/hot_asset_reload.rs" + +[[example]] +name = "asset_loading" +path = "examples/asset/asset_loading.rs" + [[example]] name = "custom_diagnostic" path = "examples/diagnostics/custom_diagnostic.rs" diff --git a/assets/models/cube/cube.bin b/assets/models/cube/cube.bin new file mode 100644 index 0000000000000000000000000000000000000000..d7cde9983e09f1affd21ccf89a9e65c67e0a3777 GIT binary patch literal 840 zcma)&Sq{QL3_~AV*~<>UJqd6Wj?$aK(THITqXMFl5<9kMIx5sBKKmM)T3*h*3(e){ zySdT6@LMbAjJbEA`gak zf4uEGp0~Z{ukSbYO8@ByLZUAjzVxtekx>b)8_#h@y0MPEiWZXcLQ>A?^TJ!o3FTKh xo1t~#IqAX<-3WH+(PuyrvtY=GF_V}Rrp%aA#w=N|WW_q>nhjfa>|^dZa0i21efIzW literal 0 HcmV?d00001 diff --git a/assets/models/cube/cube.gltf b/assets/models/cube/cube.gltf new file mode 100644 index 0000000000000..6a5a6ea75f982 --- /dev/null +++ b/assets/models/cube/cube.gltf @@ -0,0 +1,122 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v1.1.46", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Cube" + } + ], + "materials" : [ + { + "doubleSided" : true, + "emissiveFactor" : [ + 0, + 0, + 0 + ], + "name" : "Material", + "pbrMetallicRoughness" : { + "baseColorFactor" : [ + 0.800000011920929, + 0.800000011920929, + 0.800000011920929, + 1 + ], + "metallicFactor" : 0, + "roughnessFactor" : 0.4000000059604645 + } + } + ], + "meshes" : [ + { + "name" : "Cube", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3, + "material" : 0 + } + ] + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 24, + "max" : [ + 1, + 1, + 1 + ], + "min" : [ + -1, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 24, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 24, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 36, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 288 + }, + { + "buffer" : 0, + "byteLength" : 192, + "byteOffset" : 576 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 768 + } + ], + "buffers" : [ + { + "byteLength" : 840, + "uri" : "cube.bin" + } + ] +} diff --git a/assets/models/sphere/sphere.bin b/assets/models/sphere/sphere.bin new file mode 100644 index 0000000000000000000000000000000000000000..b705998af1c1c56e04fb8f47349103905f9ed73a GIT binary patch literal 92384 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 M9?}+KGMm)tKQJD0dH?_b literal 0 HcmV?d00001 diff --git a/assets/models/sphere/sphere.gltf b/assets/models/sphere/sphere.gltf new file mode 100644 index 0000000000000..a2a3034d31398 --- /dev/null +++ b/assets/models/sphere/sphere.gltf @@ -0,0 +1,100 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v1.1.46", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Sphere" + } + ], + "meshes" : [ + { + "name" : "Sphere", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3 + } + ] + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 2143, + "max" : [ + 1.0000005960464478, + 1, + 1.000001072883606 + ], + "min" : [ + -1.000000238418579, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 2143, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 2143, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 11904, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 25716, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 25716, + "byteOffset" : 25716 + }, + { + "buffer" : 0, + "byteLength" : 17144, + "byteOffset" : 51432 + }, + { + "buffer" : 0, + "byteLength" : 23808, + "byteOffset" : 68576 + } + ], + "buffers" : [ + { + "byteLength" : 92384, + "uri" : "sphere.bin" + } + ] +} diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index d323dd3a1a222..9762c9ef918b5 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -4,6 +4,10 @@ version = "0.1.0" authors = ["Carter Anderson "] edition = "2018" +[features] +default = ["filesystem_watcher"] +filesystem_watcher = ["notify"] + [dependencies] bevy_app = { path = "../bevy_app" } bevy_core = { path = "../bevy_core" } @@ -12,4 +16,5 @@ legion = { path = "../bevy_legion" } uuid = { version = "0.8", features = ["v4", "serde"] } crossbeam-channel = "0.4.2" anyhow = "1.0" -thiserror = "1.0" \ No newline at end of file +thiserror = "1.0" +notify = { version = "5.0.0-pre.2", optional = true } \ No newline at end of file diff --git a/crates/bevy_asset/src/asset_path.rs b/crates/bevy_asset/src/asset_path.rs deleted file mode 100644 index e4e30dd20f900..0000000000000 --- a/crates/bevy_asset/src/asset_path.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{ - borrow::Cow, - path::{Path, PathBuf}, -}; - -#[derive(Clone, Debug)] -pub struct AssetPath { - pub path: Cow<'static, str>, - pub extension: Option>, -} - -impl From<&Path> for AssetPath { - fn from(path: &Path) -> Self { - AssetPath { - path: Cow::Owned( - path.to_str() - .expect("Path should be a valid string.") - .to_string(), - ), - extension: path.extension().map(|e| { - Cow::Owned( - e.to_str() - .expect("Extension should be a valid string.") - .to_string(), - ) - }), - } - } -} - -impl From<&PathBuf> for AssetPath { - fn from(path: &PathBuf) -> Self { - AssetPath { - path: Cow::Owned( - path.to_str() - .expect("Path should be a valid string.") - .to_string(), - ), - extension: path.extension().map(|e| { - Cow::Owned( - e.to_str() - .expect("Extension should be a valid string.") - .to_string(), - ) - }), - } - } -} - -impl From<&str> for AssetPath { - fn from(path: &str) -> Self { - let path = Path::new(path); - AssetPath::from(path) - } -} diff --git a/crates/bevy_asset/src/asset_server.rs b/crates/bevy_asset/src/asset_server.rs index ad34b31750fd8..9ed95f8e8df41 100644 --- a/crates/bevy_asset/src/asset_server.rs +++ b/crates/bevy_asset/src/asset_server.rs @@ -1,13 +1,14 @@ use crate::{ - AssetLoadError, AssetLoadRequestHandler, AssetLoader, AssetPath, Assets, Handle, HandleId, - LoadRequest, + filesystem_watcher::FilesystemWatcher, AssetLoadError, AssetLoadRequestHandler, AssetLoader, + Assets, Handle, HandleId, LoadRequest, }; use anyhow::Result; -use legion::prelude::Resources; +use crossbeam_channel::TryRecvError; +use legion::prelude::{Res, Resources}; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, env, fs, io, - path::Path, + path::{Path, PathBuf}, sync::{Arc, RwLock}, thread, }; @@ -27,6 +28,8 @@ pub enum AssetServerError { AssetLoadError(#[from] AssetLoadError), #[error("Encountered an io error.")] Io(#[from] io::Error), + #[error("Failed to watch asset folder.")] + AssetFolderWatchError { path: PathBuf }, } struct LoaderThread { @@ -36,7 +39,7 @@ struct LoaderThread { } pub struct AssetServer { - asset_folders: Vec, + asset_folders: Vec, loader_threads: RwLock>, max_loader_threads: usize, asset_handlers: Arc>>>, @@ -44,6 +47,9 @@ pub struct AssetServer { loaders: Vec, extension_to_handler_index: HashMap, extension_to_loader_index: HashMap, + path_to_handle: RwLock>, + #[cfg(feature = "filesystem_watcher")] + filesystem_watcher: Option, } impl Default for AssetServer { @@ -56,6 +62,9 @@ impl Default for AssetServer { loaders: Vec::new(), extension_to_handler_index: HashMap::new(), extension_to_loader_index: HashMap::new(), + path_to_handle: RwLock::new(HashMap::default()), + #[cfg(feature = "filesystem_watcher")] + filesystem_watcher: None, } } } @@ -91,63 +100,139 @@ impl AssetServer { self.loaders.push(resources); } - pub fn add_asset_folder(&mut self, path: &str) { - self.asset_folders.push(path.to_string()); + pub fn load_asset_folder>(&mut self, path: P) -> Result<(), AssetServerError> { + let root_path = self.get_root_path()?; + let asset_folder = root_path.join(path); + + #[cfg(feature = "filesystem_watcher")] + Self::watch_folder_for_changes(&mut self.filesystem_watcher, &asset_folder)?; + + self.load_assets_in_folder_recursive(&asset_folder)?; + self.asset_folders.push(asset_folder); + Ok(()) + } + + pub fn get_handle>(&self, path: P) -> Option> { + self.path_to_handle + .read() + .expect("RwLock poisoned") + .get(path.as_ref()) + .map(|h| Handle::from(*h)) + } + + #[cfg(feature = "filesystem_watcher")] + fn watch_folder_for_changes>( + filesystem_watcher: &mut Option, + path: P, + ) -> Result<(), AssetServerError> { + if let Some(watcher) = filesystem_watcher { + watcher.watch_folder(&path).map_err(|_error| { + AssetServerError::AssetFolderWatchError { + path: path.as_ref().to_owned(), + } + })?; + } + + Ok(()) + } + + #[cfg(feature = "filesystem_watcher")] + pub fn watch_for_changes(&mut self) -> Result<(), AssetServerError> { + let _ = self + .filesystem_watcher + .get_or_insert_with(|| FilesystemWatcher::default()); + for asset_folder in self.asset_folders.iter() { + Self::watch_folder_for_changes(&mut self.filesystem_watcher, asset_folder)?; + } + + Ok(()) + } + + #[cfg(feature = "filesystem_watcher")] + pub fn filesystem_watcher_system(asset_server: Res) { + use notify::event::{Event, EventKind, ModifyKind}; + let mut changed = HashSet::new(); + loop { + if let Some(ref filesystem_watcher) = asset_server.filesystem_watcher { + match filesystem_watcher.receiver.try_recv() { + Ok(result) => { + let event = result.unwrap(); + match event { + Event { + kind: EventKind::Modify(ModifyKind::Data(_)), + paths, + .. + } => { + for path in paths.iter() { + if !changed.contains(path) { + let root_path = asset_server.get_root_path().unwrap(); + let relative_path = path.strip_prefix(root_path).unwrap(); + match asset_server.load_untyped(relative_path) { + Ok(_) => {} + Err(AssetServerError::AssetLoadError(error)) => { + panic!("{:?}", error) + } + Err(_) => {} + } + } + } + changed.extend(paths); + } + _ => {} + } + } + Err(TryRecvError::Empty) => { + break; + } + Err(TryRecvError::Disconnected) => panic!("FilesystemWatcher disconnected"), + } + } else { + break; + } + } } - pub fn get_root_path(&self) -> Result { + fn get_root_path(&self) -> Result { if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { - Ok(manifest_dir) + Ok(PathBuf::from(manifest_dir)) } else { match env::current_exe() { Ok(exe_path) => exe_path .parent() .ok_or(AssetServerError::InvalidRootPath) - .and_then(|exe_parent_path| { - exe_parent_path - .to_str() - .map(|path| path.to_string()) - .ok_or(AssetServerError::InvalidRootPath) - }), + .map(|exe_parent_path| exe_parent_path.to_owned()), Err(err) => Err(AssetServerError::Io(err)), } } } - pub fn load_assets(&self) -> Result<(), AssetServerError> { - let root_path_str = self.get_root_path()?; - let root_path = Path::new(&root_path_str); - for folder in self.asset_folders.iter() { - let asset_folder_path = root_path.join(folder); - self.load_assets_in_folder_recursive(&asset_folder_path)?; - } - - Ok(()) - } - - pub fn load(&self, path: &str) -> Result, AssetServerError> { + pub fn load>(&self, path: P) -> Result, AssetServerError> { self.load_untyped(path) .map(|handle_id| Handle::from(handle_id)) } - pub fn load_sync( + pub fn load_sync>( &self, assets: &mut Assets, - path: &str, + path: P, ) -> Result, AssetServerError> where T: 'static, { - let asset_path = AssetPath::from(path); - if let Some(ref extension) = asset_path.extension { - if let Some(index) = self.extension_to_loader_index.get(extension.as_ref()) { + let path = path.as_ref(); + if let Some(ref extension) = path.extension() { + if let Some(index) = self.extension_to_loader_index.get( + extension + .to_str() + .expect("extension should be a valid string"), + ) { let handle_id = HandleId::new(); let resources = &self.loaders[*index]; let loader = resources.get::>>().unwrap(); - let asset = loader.load_from_file(&asset_path)?; + let asset = loader.load_from_file(path)?; let handle = Handle::from(handle_id); assets.set(handle, asset); - assets.set_path(handle, &asset_path.path); + assets.set_path(handle, path); Ok(handle) } else { Err(AssetServerError::MissingAssetHandler) @@ -157,14 +242,28 @@ impl AssetServer { } } - pub fn load_untyped(&self, path: &str) -> Result { - let asset_path = AssetPath::from(path); - if let Some(ref extension) = asset_path.extension { - if let Some(index) = self.extension_to_handler_index.get(extension.as_ref()) { - let handle_id = HandleId::new(); + pub fn load_untyped>(&self, path: P) -> Result { + let path = path.as_ref(); + if let Some(ref extension) = path.extension() { + if let Some(index) = self.extension_to_handler_index.get( + extension + .to_str() + .expect("Extension should be a valid string."), + ) { + let handle_id = { + let mut path_to_handle = self.path_to_handle.write().expect("RwLock poisoned"); + if let Some(handle_id) = path_to_handle.get(path) { + *handle_id + } else { + let handle_id = HandleId::new(); + path_to_handle.insert(path.to_owned(), handle_id.clone()); + handle_id + } + }; + self.send_request_to_loader_thread(LoadRequest { handle_id, - path: asset_path, + path: path.to_owned(), handler_index: *index, }); Ok(handle_id) @@ -232,12 +331,14 @@ impl AssetServer { )); } - for entry in fs::read_dir(&path)? { + let root_path = self.get_root_path()?; + for entry in fs::read_dir(path)? { let entry = entry?; let child_path = entry.path(); if !child_path.is_dir() { + let relative_child_path = child_path.strip_prefix(&root_path).unwrap(); let _ = - self.load_untyped(child_path.to_str().expect("Path should be a valid string")); + self.load_untyped(relative_child_path.to_str().expect("Path should be a valid string")); } else { self.load_assets_in_folder_recursive(&child_path)?; } diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index f2ace9b4bcc38..9433de2f01e73 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -5,7 +5,7 @@ use crate::{ use bevy_app::{stage, AppBuilder, Events}; use bevy_core::bytes::GetBytes; use legion::prelude::*; -use std::collections::HashMap; +use std::{path::{Path, PathBuf}, collections::HashMap}; pub enum AssetEvent { Created { handle: Handle }, @@ -14,7 +14,7 @@ pub enum AssetEvent { pub struct Assets { assets: HashMap, - paths: HashMap>, + paths: HashMap>, events: Events>, } @@ -29,8 +29,8 @@ impl Default for Assets { } impl Assets { - pub fn get_with_path(&mut self, path: &str) -> Option> { - self.paths.get(path).map(|handle| *handle) + pub fn get_with_path>(&mut self, path: P) -> Option> { + self.paths.get(path.as_ref()).map(|handle| *handle) } pub fn add(&mut self, asset: T) -> Handle { @@ -64,8 +64,8 @@ impl Assets { handle } - pub fn set_path(&mut self, handle: Handle, path: &str) { - self.paths.insert(path.to_string(), handle); + pub fn set_path>(&mut self, handle: Handle, path: P) { + self.paths.insert(path.as_ref().to_owned(), handle); } pub fn get_id(&self, id: HandleId) -> Option<&T> { diff --git a/crates/bevy_asset/src/filesystem_watcher.rs b/crates/bevy_asset/src/filesystem_watcher.rs new file mode 100644 index 0000000000000..d89bc03ea9c48 --- /dev/null +++ b/crates/bevy_asset/src/filesystem_watcher.rs @@ -0,0 +1,26 @@ +use crossbeam_channel::Receiver; +use notify::{Event, RecommendedWatcher, RecursiveMode, Result, Watcher}; +use std::path::Path; + +/// Watches for changes to assets on the filesystem and informs the `AssetServer` to reload them +pub struct FilesystemWatcher { + pub watcher: RecommendedWatcher, + pub receiver: Receiver>, +} + +impl Default for FilesystemWatcher { + fn default() -> Self { + let (sender, receiver) = crossbeam_channel::unbounded(); + let watcher: RecommendedWatcher = Watcher::new_immediate(move |res| { + sender.send(res).expect("Watch event send failure"); + }) + .expect("Failed to create filesystem watcher"); + FilesystemWatcher { watcher, receiver } + } +} + +impl FilesystemWatcher { + pub fn watch_folder>(&mut self, path: P) -> Result<()> { + self.watcher.watch(path, RecursiveMode::Recursive) + } +} diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index f49e8c8deeff6..53d99046c35bb 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -1,11 +1,11 @@ -mod asset_path; mod asset_server; mod assets; mod handle; mod load_request; mod loader; +#[cfg(feature = "filesystem_watcher")] +pub mod filesystem_watcher; -pub use asset_path::*; pub use asset_server::*; pub use assets::*; pub use handle::*; @@ -13,6 +13,7 @@ pub use load_request::*; pub use loader::*; use bevy_app::{AppBuilder, AppPlugin}; +use legion::prelude::IntoSystem; pub mod stage { pub const LOAD_ASSETS: &str = "load_assets"; @@ -23,7 +24,9 @@ pub struct AssetPlugin; impl AppPlugin for AssetPlugin { fn build(&self, app: &mut AppBuilder) { - app.add_stage(stage::LOAD_ASSETS) + app.add_stage_before(bevy_app::stage::PRE_UPDATE, stage::LOAD_ASSETS) .init_resource::(); + #[cfg(feature = "filesystem_watcher")] + app.add_system_to_stage(stage::LOAD_ASSETS, AssetServer::filesystem_watcher_system.system()); } -} +} \ No newline at end of file diff --git a/crates/bevy_asset/src/load_request.rs b/crates/bevy_asset/src/load_request.rs index 81bf1d7b98690..d3eb361a67953 100644 --- a/crates/bevy_asset/src/load_request.rs +++ b/crates/bevy_asset/src/load_request.rs @@ -1,13 +1,13 @@ -use crate::{AssetLoadError, AssetLoader, AssetPath, AssetResult, Handle, HandleId}; +use crate::{AssetLoadError, AssetLoader, AssetResult, Handle, HandleId}; use anyhow::Result; use crossbeam_channel::Sender; use fs::File; use io::Read; -use std::{fs, io, path::Path}; +use std::{fs, io, path::PathBuf}; #[derive(Debug)] pub struct LoadRequest { - pub path: AssetPath, + pub path: PathBuf, pub handle_id: HandleId, pub handler_index: usize, } @@ -34,7 +34,7 @@ where } fn load_asset(&self, load_request: &LoadRequest) -> Result { - let mut file = File::open(Path::new(load_request.path.path.as_ref()))?; + let mut file = File::open(&load_request.path)?; let mut bytes = Vec::new(); file.read_to_end(&mut bytes)?; let asset = self.loader.from_bytes(&load_request.path, bytes)?; diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index fb856672af4bb..2e3142212fa78 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -1,10 +1,13 @@ -use crate::{AssetPath, Assets, Handle}; +use crate::{Assets, Handle}; use anyhow::Result; use crossbeam_channel::{Receiver, Sender, TryRecvError}; use fs::File; use io::Read; use legion::prelude::{Res, ResMut}; -use std::{fs, io, path::Path}; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; use thiserror::Error; #[derive(Error, Debug)] @@ -16,10 +19,10 @@ pub enum AssetLoadError { } pub trait AssetLoader: Send + Sync + 'static { - fn from_bytes(&self, asset_path: &AssetPath, bytes: Vec) -> Result; + fn from_bytes(&self, asset_path: &Path, bytes: Vec) -> Result; fn extensions(&self) -> &[&str]; - fn load_from_file(&self, asset_path: &AssetPath) -> Result { - let mut file = File::open(Path::new(asset_path.path.as_ref()))?; + fn load_from_file(&self, asset_path: &Path) -> Result { + let mut file = File::open(asset_path)?; let mut bytes = Vec::new(); file.read_to_end(&mut bytes)?; let asset = self.from_bytes(asset_path, bytes)?; @@ -30,7 +33,7 @@ pub trait AssetLoader: Send + Sync + 'static { pub struct AssetResult { pub result: Result, pub handle: Handle, - pub path: AssetPath, + pub path: PathBuf, } pub struct AssetChannel { @@ -54,7 +57,7 @@ pub fn update_asset_storage_system( Ok(result) => { let asset = result.result.unwrap(); assets.set(result.handle, asset); - assets.set_path(result.handle, &result.path.path); + assets.set_path(result.handle, &result.path); } Err(TryRecvError::Empty) => { break; diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index bf6a6397dd735..bf03d6a4ec3ca 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -4,7 +4,7 @@ use bevy_render::{ }; use anyhow::Result; -use bevy_asset::{AssetLoader, AssetPath}; +use bevy_asset::AssetLoader; use gltf::{buffer::Source, iter, mesh::Mode}; use std::{fs, io, path::Path}; use thiserror::Error; @@ -13,7 +13,7 @@ use thiserror::Error; pub struct GltfLoader; impl AssetLoader for GltfLoader { - fn from_bytes(&self, asset_path: &AssetPath, bytes: Vec) -> Result { + fn from_bytes(&self, asset_path: &Path, bytes: Vec) -> Result { let mesh = load_gltf(asset_path, bytes)?; Ok(mesh) } @@ -46,7 +46,7 @@ fn get_primitive_topology(mode: Mode) -> Result { } } -pub fn load_gltf(asset_path: &AssetPath, bytes: Vec) -> Result { +pub fn load_gltf(asset_path: &Path, bytes: Vec) -> Result { let gltf = gltf::Gltf::from_slice(&bytes)?; let buffer_data = load_buffers(gltf.buffers(), asset_path)?; for scene in gltf.scenes() { @@ -104,15 +104,14 @@ fn load_node(buffer_data: &[Vec], node: &gltf::Node, depth: i32) -> Result Result>, GltfError> { +fn load_buffers(buffers: iter::Buffers, asset_path: &Path) -> Result>, GltfError> { let mut buffer_data = Vec::new(); for buffer in buffers { match buffer.source() { Source::Uri(uri) => { if uri.starts_with("data:") { } else { - let path = Path::new(asset_path.path.as_ref()); - let buffer_path = path.parent().unwrap().join(uri); + let buffer_path = asset_path.parent().unwrap().join(uri); let buffer_bytes = fs::read(buffer_path)?; buffer_data.push(buffer_bytes); } diff --git a/crates/bevy_render/src/mesh.rs b/crates/bevy_render/src/mesh.rs index 9bd9971a1cc4a..68c114d2c39a9 100644 --- a/crates/bevy_render/src/mesh.rs +++ b/crates/bevy_render/src/mesh.rs @@ -3,8 +3,8 @@ use crate::{ state_descriptors::{IndexFormat, PrimitiveTopology}, VertexBufferDescriptor, VertexBufferDescriptors, VertexFormat, }, - render_resource::{BufferInfo, BufferUsage, EntitiesWaitingForAssets}, - renderer::{RenderResourceContext, RenderResources}, + render_resource::{BufferInfo, BufferUsage}, + renderer::RenderResources, shader::AsUniforms, Renderable, Vertex, }; diff --git a/crates/bevy_render/src/texture/png_texture_loader.rs b/crates/bevy_render/src/texture/png_texture_loader.rs index 662f37f2bc3f7..02d4dbf858343 100644 --- a/crates/bevy_render/src/texture/png_texture_loader.rs +++ b/crates/bevy_render/src/texture/png_texture_loader.rs @@ -1,12 +1,13 @@ use super::Texture; use anyhow::Result; -use bevy_asset::{AssetLoader, AssetPath}; +use bevy_asset::AssetLoader; +use std::path::Path; #[derive(Clone, Default)] pub struct PngTextureLoader; impl AssetLoader for PngTextureLoader { - fn from_bytes(&self, _asset_path: &AssetPath, bytes: Vec) -> Result { + fn from_bytes(&self, _asset_path: &Path, bytes: Vec) -> Result { let decoder = png::Decoder::new(bytes.as_slice()); let (info, mut reader) = decoder.read_info()?; let mut data = vec![0; info.buffer_size()]; diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index e63f60e44c354..79542c63ab4a2 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -1,12 +1,13 @@ use crate::Font; use anyhow::Result; -use bevy_asset::{AssetLoader, AssetPath}; +use bevy_asset::AssetLoader; +use std::path::Path; #[derive(Clone)] pub struct FontLoader; impl AssetLoader for FontLoader { - fn from_bytes(&self, _asset_path: &AssetPath, bytes: Vec) -> Result { + fn from_bytes(&self, _asset_path: &Path, bytes: Vec) -> Result { Ok(Font::try_from_bytes(bytes)?) } fn extensions(&self) -> &[&str] { diff --git a/examples/asset/asset_loading.rs b/examples/asset/asset_loading.rs new file mode 100644 index 0000000000000..18a765d7a46dc --- /dev/null +++ b/examples/asset/asset_loading.rs @@ -0,0 +1,80 @@ +use bevy::prelude::*; +use bevy_asset::AssetServer; + +fn main() { + App::build() + .add_default_plugins() + .add_startup_system(setup.system()) + .run(); +} + +fn setup( + command_buffer: &mut CommandBuffer, + mut asset_server: ResMut, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // You can load all assets in a folder like this. they will be loaded in parallel without blocking + asset_server.load_asset_folder("assets/models/monkey").unwrap(); + + // Then any asset in the folder can be accessed like this: + let monkey_handle = asset_server + .get_handle("assets/models/monkey/Monkey.gltf") + .unwrap(); + + // You can load individual assets like this: + let cube_handle = asset_server + .load("assets/models/cube/cube.gltf") + .unwrap(); + + // Assets are loaded in the background by default, which means they might not be available immediately after loading. + // If you need immediate access you can load assets synchronously like this: + let sphere_handle = asset_server.load_sync(&mut meshes, "assets/models/sphere/sphere.gltf").unwrap(); + let sphere = meshes.get(&sphere_handle).unwrap(); + println!("{:?}", sphere.primitive_topology); + + // You can also add assets directly to their Assets storage + let material_handle = materials.add(StandardMaterial { + albedo: Color::rgb(0.5, 0.4, 0.3), + ..Default::default() + }); + + // add entities to the world + command_buffer + .build() + // monkey + .add_entity(MeshEntity { + mesh: monkey_handle, + material: material_handle, + translation: Translation::new(-3.0, 0.0, 0.0), + ..Default::default() + }) + // cube + .add_entity(MeshEntity { + mesh: cube_handle, + material: material_handle, + translation: Translation::new(0.0, 0.0, 0.0), + ..Default::default() + }) + // sphere + .add_entity(MeshEntity { + mesh: sphere_handle, + material: material_handle, + translation: Translation::new(3.0, 0.0, 0.0), + ..Default::default() + }) + // light + .add_entity(LightEntity { + translation: Translation::new(4.0, -4.0, 5.0), + ..Default::default() + }) + // camera + .add_entity(CameraEntity { + local_to_world: LocalToWorld(Mat4::look_at_rh( + Vec3::new(0.0, -10.0, 3.0), + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + )), + ..Default::default() + }); +} diff --git a/examples/asset/hot_asset_reload.rs b/examples/asset/hot_asset_reload.rs new file mode 100644 index 0000000000000..922bb4ec527c6 --- /dev/null +++ b/examples/asset/hot_asset_reload.rs @@ -0,0 +1,60 @@ +use bevy::prelude::*; +use bevy_asset::AssetServer; + +/// Hot reloading allows you to modify assets on disk and they will be "live reloaded" while your game is running. +/// This lets you immediately see the results of your changes without restarting the game. +fn main() { + App::build() + .add_default_plugins() + .add_startup_system(setup.system()) + .run(); +} + +fn setup( + command_buffer: &mut CommandBuffer, + mut asset_server: ResMut, + mut materials: ResMut>, +) { + // load an asset folder + asset_server.load_asset_folder("assets").unwrap(); + + // tell the asset server to watch for changes + asset_server.watch_for_changes().unwrap(); + + // load the mesh + let mesh_handle = asset_server + .get_handle("assets/models/monkey/Monkey.gltf") + .unwrap(); + + // now any changes to the mesh will be reloaded automatically! + + // create a material for the mesh + let material_handle = materials.add(StandardMaterial { + albedo: Color::rgb(0.5, 0.4, 0.3), + ..Default::default() + }); + + // add entities to the world + command_buffer + .build() + // mesh + .add_entity(MeshEntity { + mesh: mesh_handle, + material: material_handle, + ..Default::default() + }) + // light + .add_entity(LightEntity { + translation: Translation::new(4.0, -4.0, 5.0), + ..Default::default() + }) + // camera + .add_entity(CameraEntity { + local_to_world: LocalToWorld(Mat4::look_at_rh( + Vec3::new(2.0, -6.0, 2.0), + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + )), + ..Default::default() + }); +} diff --git a/examples/asset/load_asset_folder.rs b/examples/asset/load_asset_folder.rs deleted file mode 100644 index ba06a1a967a3a..0000000000000 --- a/examples/asset/load_asset_folder.rs +++ /dev/null @@ -1,13 +0,0 @@ -use bevy::{asset::AssetServer, prelude::*}; - -fn main() { - App::build() - .add_default_plugins() - .add_startup_system(setup.system()) - .run(); -} - -fn setup(mut asset_server: ResMut) { - asset_server.add_asset_folder("assets"); - asset_server.load_assets().expect("Assets should exist"); -}