diff --git a/Cargo.toml b/Cargo.toml index bcae2fefde..9f4c62f88c 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 0000000000..d7cde9983e Binary files /dev/null and b/assets/models/cube/cube.bin differ diff --git a/assets/models/cube/cube.gltf b/assets/models/cube/cube.gltf new file mode 100644 index 0000000000..6a5a6ea75f --- /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 0000000000..b705998af1 Binary files /dev/null and b/assets/models/sphere/sphere.bin differ diff --git a/assets/models/sphere/sphere.gltf b/assets/models/sphere/sphere.gltf new file mode 100644 index 0000000000..a2a3034d31 --- /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 d323dd3a1a..9762c9ef91 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 e4e30dd20f..0000000000 --- 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 ad34b31750..9ed95f8e8d 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_root_path(&self) -> Result { - if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { - Ok(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) - }), - Err(err) => Err(AssetServerError::Io(err)), - } - } + 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)) } - 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)?; + #[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(()) } - pub fn load(&self, path: &str) -> Result, AssetServerError> { + #[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; + } + } + } + + fn get_root_path(&self) -> Result { + if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { + Ok(PathBuf::from(manifest_dir)) + } else { + match env::current_exe() { + Ok(exe_path) => exe_path + .parent() + .ok_or(AssetServerError::InvalidRootPath) + .map(|exe_parent_path| exe_parent_path.to_owned()), + Err(err) => Err(AssetServerError::Io(err)), + } + } + } + + 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 f2ace9b4bc..9433de2f01 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 0000000000..d89bc03ea9 --- /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 f49e8c8dee..53d99046c3 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 81bf1d7b98..d3eb361a67 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 fb856672af..2e3142212f 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 bf6a6397dd..bf03d6a4ec 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 9bd9971a1c..68c114d2c3 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 662f37f2bc..02d4dbf858 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 e63f60e44c..79542c63ab 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 0000000000..18a765d7a4 --- /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 0000000000..922bb4ec52 --- /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 ba06a1a967..0000000000 --- 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"); -}