Hot asset reloading

This commit is contained in:
Carter Anderson 2020-05-16 20:18:30 -07:00
parent 623c8a8d9a
commit 870f715df3
20 changed files with 590 additions and 149 deletions

View file

@ -111,6 +111,14 @@ path = "examples/app/headless.rs"
name = "plugin" name = "plugin"
path = "examples/app/plugin.rs" 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]] [[example]]
name = "custom_diagnostic" name = "custom_diagnostic"
path = "examples/diagnostics/custom_diagnostic.rs" path = "examples/diagnostics/custom_diagnostic.rs"

BIN
assets/models/cube/cube.bin Normal file

Binary file not shown.

View file

@ -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"
}
]
}

Binary file not shown.

View file

@ -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"
}
]
}

View file

@ -4,6 +4,10 @@ version = "0.1.0"
authors = ["Carter Anderson <mcanders1@gmail.com>"] authors = ["Carter Anderson <mcanders1@gmail.com>"]
edition = "2018" edition = "2018"
[features]
default = ["filesystem_watcher"]
filesystem_watcher = ["notify"]
[dependencies] [dependencies]
bevy_app = { path = "../bevy_app" } bevy_app = { path = "../bevy_app" }
bevy_core = { path = "../bevy_core" } bevy_core = { path = "../bevy_core" }
@ -13,3 +17,4 @@ uuid = { version = "0.8", features = ["v4", "serde"] }
crossbeam-channel = "0.4.2" crossbeam-channel = "0.4.2"
anyhow = "1.0" anyhow = "1.0"
thiserror = "1.0" thiserror = "1.0"
notify = { version = "5.0.0-pre.2", optional = true }

View file

@ -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<Cow<'static, str>>,
}
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)
}
}

View file

@ -1,13 +1,14 @@
use crate::{ use crate::{
AssetLoadError, AssetLoadRequestHandler, AssetLoader, AssetPath, Assets, Handle, HandleId, filesystem_watcher::FilesystemWatcher, AssetLoadError, AssetLoadRequestHandler, AssetLoader,
LoadRequest, Assets, Handle, HandleId, LoadRequest,
}; };
use anyhow::Result; use anyhow::Result;
use legion::prelude::Resources; use crossbeam_channel::TryRecvError;
use legion::prelude::{Res, Resources};
use std::{ use std::{
collections::HashMap, collections::{HashMap, HashSet},
env, fs, io, env, fs, io,
path::Path, path::{Path, PathBuf},
sync::{Arc, RwLock}, sync::{Arc, RwLock},
thread, thread,
}; };
@ -27,6 +28,8 @@ pub enum AssetServerError {
AssetLoadError(#[from] AssetLoadError), AssetLoadError(#[from] AssetLoadError),
#[error("Encountered an io error.")] #[error("Encountered an io error.")]
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("Failed to watch asset folder.")]
AssetFolderWatchError { path: PathBuf },
} }
struct LoaderThread { struct LoaderThread {
@ -36,7 +39,7 @@ struct LoaderThread {
} }
pub struct AssetServer { pub struct AssetServer {
asset_folders: Vec<String>, asset_folders: Vec<PathBuf>,
loader_threads: RwLock<Vec<LoaderThread>>, loader_threads: RwLock<Vec<LoaderThread>>,
max_loader_threads: usize, max_loader_threads: usize,
asset_handlers: Arc<RwLock<Vec<Box<dyn AssetLoadRequestHandler>>>>, asset_handlers: Arc<RwLock<Vec<Box<dyn AssetLoadRequestHandler>>>>,
@ -44,6 +47,9 @@ pub struct AssetServer {
loaders: Vec<Resources>, loaders: Vec<Resources>,
extension_to_handler_index: HashMap<String, usize>, extension_to_handler_index: HashMap<String, usize>,
extension_to_loader_index: HashMap<String, usize>, extension_to_loader_index: HashMap<String, usize>,
path_to_handle: RwLock<HashMap<PathBuf, HandleId>>,
#[cfg(feature = "filesystem_watcher")]
filesystem_watcher: Option<FilesystemWatcher>,
} }
impl Default for AssetServer { impl Default for AssetServer {
@ -56,6 +62,9 @@ impl Default for AssetServer {
loaders: Vec::new(), loaders: Vec::new(),
extension_to_handler_index: HashMap::new(), extension_to_handler_index: HashMap::new(),
extension_to_loader_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); self.loaders.push(resources);
} }
pub fn add_asset_folder(&mut self, path: &str) { pub fn load_asset_folder<P: AsRef<Path>>(&mut self, path: P) -> Result<(), AssetServerError> {
self.asset_folders.push(path.to_string()); 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<String, AssetServerError> { pub fn get_handle<T, P: AsRef<Path>>(&self, path: P) -> Option<Handle<T>> {
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { self.path_to_handle
Ok(manifest_dir) .read()
} else { .expect("RwLock poisoned")
match env::current_exe() { .get(path.as_ref())
Ok(exe_path) => exe_path .map(|h| Handle::from(*h))
.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 load_assets(&self) -> Result<(), AssetServerError> { #[cfg(feature = "filesystem_watcher")]
let root_path_str = self.get_root_path()?; fn watch_folder_for_changes<P: AsRef<Path>>(
let root_path = Path::new(&root_path_str); filesystem_watcher: &mut Option<FilesystemWatcher>,
for folder in self.asset_folders.iter() { path: P,
let asset_folder_path = root_path.join(folder); ) -> Result<(), AssetServerError> {
self.load_assets_in_folder_recursive(&asset_folder_path)?; if let Some(watcher) = filesystem_watcher {
watcher.watch_folder(&path).map_err(|_error| {
AssetServerError::AssetFolderWatchError {
path: path.as_ref().to_owned(),
}
})?;
} }
Ok(()) Ok(())
} }
pub fn load<T>(&self, path: &str) -> Result<Handle<T>, 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<AssetServer>) {
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<PathBuf, AssetServerError> {
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<T, P: AsRef<Path>>(&self, path: P) -> Result<Handle<T>, AssetServerError> {
self.load_untyped(path) self.load_untyped(path)
.map(|handle_id| Handle::from(handle_id)) .map(|handle_id| Handle::from(handle_id))
} }
pub fn load_sync<T>( pub fn load_sync<T, P: AsRef<Path>>(
&self, &self,
assets: &mut Assets<T>, assets: &mut Assets<T>,
path: &str, path: P,
) -> Result<Handle<T>, AssetServerError> ) -> Result<Handle<T>, AssetServerError>
where where
T: 'static, T: 'static,
{ {
let asset_path = AssetPath::from(path); let path = path.as_ref();
if let Some(ref extension) = asset_path.extension { if let Some(ref extension) = path.extension() {
if let Some(index) = self.extension_to_loader_index.get(extension.as_ref()) { 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 handle_id = HandleId::new();
let resources = &self.loaders[*index]; let resources = &self.loaders[*index];
let loader = resources.get::<Box<dyn AssetLoader<T>>>().unwrap(); let loader = resources.get::<Box<dyn AssetLoader<T>>>().unwrap();
let asset = loader.load_from_file(&asset_path)?; let asset = loader.load_from_file(path)?;
let handle = Handle::from(handle_id); let handle = Handle::from(handle_id);
assets.set(handle, asset); assets.set(handle, asset);
assets.set_path(handle, &asset_path.path); assets.set_path(handle, path);
Ok(handle) Ok(handle)
} else { } else {
Err(AssetServerError::MissingAssetHandler) Err(AssetServerError::MissingAssetHandler)
@ -157,14 +242,28 @@ impl AssetServer {
} }
} }
pub fn load_untyped(&self, path: &str) -> Result<HandleId, AssetServerError> { pub fn load_untyped<P: AsRef<Path>>(&self, path: P) -> Result<HandleId, AssetServerError> {
let asset_path = AssetPath::from(path); let path = path.as_ref();
if let Some(ref extension) = asset_path.extension { if let Some(ref extension) = path.extension() {
if let Some(index) = self.extension_to_handler_index.get(extension.as_ref()) { 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(); let handle_id = HandleId::new();
path_to_handle.insert(path.to_owned(), handle_id.clone());
handle_id
}
};
self.send_request_to_loader_thread(LoadRequest { self.send_request_to_loader_thread(LoadRequest {
handle_id, handle_id,
path: asset_path, path: path.to_owned(),
handler_index: *index, handler_index: *index,
}); });
Ok(handle_id) 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 entry = entry?;
let child_path = entry.path(); let child_path = entry.path();
if !child_path.is_dir() { if !child_path.is_dir() {
let relative_child_path = child_path.strip_prefix(&root_path).unwrap();
let _ = 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 { } else {
self.load_assets_in_folder_recursive(&child_path)?; self.load_assets_in_folder_recursive(&child_path)?;
} }

View file

@ -5,7 +5,7 @@ use crate::{
use bevy_app::{stage, AppBuilder, Events}; use bevy_app::{stage, AppBuilder, Events};
use bevy_core::bytes::GetBytes; use bevy_core::bytes::GetBytes;
use legion::prelude::*; use legion::prelude::*;
use std::collections::HashMap; use std::{path::{Path, PathBuf}, collections::HashMap};
pub enum AssetEvent<T> { pub enum AssetEvent<T> {
Created { handle: Handle<T> }, Created { handle: Handle<T> },
@ -14,7 +14,7 @@ pub enum AssetEvent<T> {
pub struct Assets<T> { pub struct Assets<T> {
assets: HashMap<HandleId, T>, assets: HashMap<HandleId, T>,
paths: HashMap<String, Handle<T>>, paths: HashMap<PathBuf, Handle<T>>,
events: Events<AssetEvent<T>>, events: Events<AssetEvent<T>>,
} }
@ -29,8 +29,8 @@ impl<T> Default for Assets<T> {
} }
impl<T> Assets<T> { impl<T> Assets<T> {
pub fn get_with_path(&mut self, path: &str) -> Option<Handle<T>> { pub fn get_with_path<P: AsRef<Path>>(&mut self, path: P) -> Option<Handle<T>> {
self.paths.get(path).map(|handle| *handle) self.paths.get(path.as_ref()).map(|handle| *handle)
} }
pub fn add(&mut self, asset: T) -> Handle<T> { pub fn add(&mut self, asset: T) -> Handle<T> {
@ -64,8 +64,8 @@ impl<T> Assets<T> {
handle handle
} }
pub fn set_path(&mut self, handle: Handle<T>, path: &str) { pub fn set_path<P: AsRef<Path>>(&mut self, handle: Handle<T>, path: P) {
self.paths.insert(path.to_string(), handle); self.paths.insert(path.as_ref().to_owned(), handle);
} }
pub fn get_id(&self, id: HandleId) -> Option<&T> { pub fn get_id(&self, id: HandleId) -> Option<&T> {

View file

@ -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<Result<Event>>,
}
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<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.watcher.watch(path, RecursiveMode::Recursive)
}
}

View file

@ -1,11 +1,11 @@
mod asset_path;
mod asset_server; mod asset_server;
mod assets; mod assets;
mod handle; mod handle;
mod load_request; mod load_request;
mod loader; mod loader;
#[cfg(feature = "filesystem_watcher")]
pub mod filesystem_watcher;
pub use asset_path::*;
pub use asset_server::*; pub use asset_server::*;
pub use assets::*; pub use assets::*;
pub use handle::*; pub use handle::*;
@ -13,6 +13,7 @@ pub use load_request::*;
pub use loader::*; pub use loader::*;
use bevy_app::{AppBuilder, AppPlugin}; use bevy_app::{AppBuilder, AppPlugin};
use legion::prelude::IntoSystem;
pub mod stage { pub mod stage {
pub const LOAD_ASSETS: &str = "load_assets"; pub const LOAD_ASSETS: &str = "load_assets";
@ -23,7 +24,9 @@ pub struct AssetPlugin;
impl AppPlugin for AssetPlugin { impl AppPlugin for AssetPlugin {
fn build(&self, app: &mut AppBuilder) { 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::<AssetServer>(); .init_resource::<AssetServer>();
#[cfg(feature = "filesystem_watcher")]
app.add_system_to_stage(stage::LOAD_ASSETS, AssetServer::filesystem_watcher_system.system());
} }
} }

View file

@ -1,13 +1,13 @@
use crate::{AssetLoadError, AssetLoader, AssetPath, AssetResult, Handle, HandleId}; use crate::{AssetLoadError, AssetLoader, AssetResult, Handle, HandleId};
use anyhow::Result; use anyhow::Result;
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use fs::File; use fs::File;
use io::Read; use io::Read;
use std::{fs, io, path::Path}; use std::{fs, io, path::PathBuf};
#[derive(Debug)] #[derive(Debug)]
pub struct LoadRequest { pub struct LoadRequest {
pub path: AssetPath, pub path: PathBuf,
pub handle_id: HandleId, pub handle_id: HandleId,
pub handler_index: usize, pub handler_index: usize,
} }
@ -34,7 +34,7 @@ where
} }
fn load_asset(&self, load_request: &LoadRequest) -> Result<TAsset, AssetLoadError> { fn load_asset(&self, load_request: &LoadRequest) -> Result<TAsset, AssetLoadError> {
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(); let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?; file.read_to_end(&mut bytes)?;
let asset = self.loader.from_bytes(&load_request.path, bytes)?; let asset = self.loader.from_bytes(&load_request.path, bytes)?;

View file

@ -1,10 +1,13 @@
use crate::{AssetPath, Assets, Handle}; use crate::{Assets, Handle};
use anyhow::Result; use anyhow::Result;
use crossbeam_channel::{Receiver, Sender, TryRecvError}; use crossbeam_channel::{Receiver, Sender, TryRecvError};
use fs::File; use fs::File;
use io::Read; use io::Read;
use legion::prelude::{Res, ResMut}; use legion::prelude::{Res, ResMut};
use std::{fs, io, path::Path}; use std::{
fs, io,
path::{Path, PathBuf},
};
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -16,10 +19,10 @@ pub enum AssetLoadError {
} }
pub trait AssetLoader<T>: Send + Sync + 'static { pub trait AssetLoader<T>: Send + Sync + 'static {
fn from_bytes(&self, asset_path: &AssetPath, bytes: Vec<u8>) -> Result<T, anyhow::Error>; fn from_bytes(&self, asset_path: &Path, bytes: Vec<u8>) -> Result<T, anyhow::Error>;
fn extensions(&self) -> &[&str]; fn extensions(&self) -> &[&str];
fn load_from_file(&self, asset_path: &AssetPath) -> Result<T, AssetLoadError> { fn load_from_file(&self, asset_path: &Path) -> Result<T, AssetLoadError> {
let mut file = File::open(Path::new(asset_path.path.as_ref()))?; let mut file = File::open(asset_path)?;
let mut bytes = Vec::new(); let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?; file.read_to_end(&mut bytes)?;
let asset = self.from_bytes(asset_path, bytes)?; let asset = self.from_bytes(asset_path, bytes)?;
@ -30,7 +33,7 @@ pub trait AssetLoader<T>: Send + Sync + 'static {
pub struct AssetResult<T> { pub struct AssetResult<T> {
pub result: Result<T, AssetLoadError>, pub result: Result<T, AssetLoadError>,
pub handle: Handle<T>, pub handle: Handle<T>,
pub path: AssetPath, pub path: PathBuf,
} }
pub struct AssetChannel<T> { pub struct AssetChannel<T> {
@ -54,7 +57,7 @@ pub fn update_asset_storage_system<T>(
Ok(result) => { Ok(result) => {
let asset = result.result.unwrap(); let asset = result.result.unwrap();
assets.set(result.handle, asset); assets.set(result.handle, asset);
assets.set_path(result.handle, &result.path.path); assets.set_path(result.handle, &result.path);
} }
Err(TryRecvError::Empty) => { Err(TryRecvError::Empty) => {
break; break;

View file

@ -4,7 +4,7 @@ use bevy_render::{
}; };
use anyhow::Result; use anyhow::Result;
use bevy_asset::{AssetLoader, AssetPath}; use bevy_asset::AssetLoader;
use gltf::{buffer::Source, iter, mesh::Mode}; use gltf::{buffer::Source, iter, mesh::Mode};
use std::{fs, io, path::Path}; use std::{fs, io, path::Path};
use thiserror::Error; use thiserror::Error;
@ -13,7 +13,7 @@ use thiserror::Error;
pub struct GltfLoader; pub struct GltfLoader;
impl AssetLoader<Mesh> for GltfLoader { impl AssetLoader<Mesh> for GltfLoader {
fn from_bytes(&self, asset_path: &AssetPath, bytes: Vec<u8>) -> Result<Mesh> { fn from_bytes(&self, asset_path: &Path, bytes: Vec<u8>) -> Result<Mesh> {
let mesh = load_gltf(asset_path, bytes)?; let mesh = load_gltf(asset_path, bytes)?;
Ok(mesh) Ok(mesh)
} }
@ -46,7 +46,7 @@ fn get_primitive_topology(mode: Mode) -> Result<PrimitiveTopology, GltfError> {
} }
} }
pub fn load_gltf(asset_path: &AssetPath, bytes: Vec<u8>) -> Result<Mesh, GltfError> { pub fn load_gltf(asset_path: &Path, bytes: Vec<u8>) -> Result<Mesh, GltfError> {
let gltf = gltf::Gltf::from_slice(&bytes)?; let gltf = gltf::Gltf::from_slice(&bytes)?;
let buffer_data = load_buffers(gltf.buffers(), asset_path)?; let buffer_data = load_buffers(gltf.buffers(), asset_path)?;
for scene in gltf.scenes() { for scene in gltf.scenes() {
@ -104,15 +104,14 @@ fn load_node(buffer_data: &[Vec<u8>], node: &gltf::Node, depth: i32) -> Result<M
panic!("failed to find mesh") panic!("failed to find mesh")
} }
fn load_buffers(buffers: iter::Buffers, asset_path: &AssetPath) -> Result<Vec<Vec<u8>>, GltfError> { fn load_buffers(buffers: iter::Buffers, asset_path: &Path) -> Result<Vec<Vec<u8>>, GltfError> {
let mut buffer_data = Vec::new(); let mut buffer_data = Vec::new();
for buffer in buffers { for buffer in buffers {
match buffer.source() { match buffer.source() {
Source::Uri(uri) => { Source::Uri(uri) => {
if uri.starts_with("data:") { if uri.starts_with("data:") {
} else { } else {
let path = Path::new(asset_path.path.as_ref()); let buffer_path = asset_path.parent().unwrap().join(uri);
let buffer_path = path.parent().unwrap().join(uri);
let buffer_bytes = fs::read(buffer_path)?; let buffer_bytes = fs::read(buffer_path)?;
buffer_data.push(buffer_bytes); buffer_data.push(buffer_bytes);
} }

View file

@ -3,8 +3,8 @@ use crate::{
state_descriptors::{IndexFormat, PrimitiveTopology}, state_descriptors::{IndexFormat, PrimitiveTopology},
VertexBufferDescriptor, VertexBufferDescriptors, VertexFormat, VertexBufferDescriptor, VertexBufferDescriptors, VertexFormat,
}, },
render_resource::{BufferInfo, BufferUsage, EntitiesWaitingForAssets}, render_resource::{BufferInfo, BufferUsage},
renderer::{RenderResourceContext, RenderResources}, renderer::RenderResources,
shader::AsUniforms, shader::AsUniforms,
Renderable, Vertex, Renderable, Vertex,
}; };

View file

@ -1,12 +1,13 @@
use super::Texture; use super::Texture;
use anyhow::Result; use anyhow::Result;
use bevy_asset::{AssetLoader, AssetPath}; use bevy_asset::AssetLoader;
use std::path::Path;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct PngTextureLoader; pub struct PngTextureLoader;
impl AssetLoader<Texture> for PngTextureLoader { impl AssetLoader<Texture> for PngTextureLoader {
fn from_bytes(&self, _asset_path: &AssetPath, bytes: Vec<u8>) -> Result<Texture> { fn from_bytes(&self, _asset_path: &Path, bytes: Vec<u8>) -> Result<Texture> {
let decoder = png::Decoder::new(bytes.as_slice()); let decoder = png::Decoder::new(bytes.as_slice());
let (info, mut reader) = decoder.read_info()?; let (info, mut reader) = decoder.read_info()?;
let mut data = vec![0; info.buffer_size()]; let mut data = vec![0; info.buffer_size()];

View file

@ -1,12 +1,13 @@
use crate::Font; use crate::Font;
use anyhow::Result; use anyhow::Result;
use bevy_asset::{AssetLoader, AssetPath}; use bevy_asset::AssetLoader;
use std::path::Path;
#[derive(Clone)] #[derive(Clone)]
pub struct FontLoader; pub struct FontLoader;
impl AssetLoader<Font> for FontLoader { impl AssetLoader<Font> for FontLoader {
fn from_bytes(&self, _asset_path: &AssetPath, bytes: Vec<u8>) -> Result<Font> { fn from_bytes(&self, _asset_path: &Path, bytes: Vec<u8>) -> Result<Font> {
Ok(Font::try_from_bytes(bytes)?) Ok(Font::try_from_bytes(bytes)?)
} }
fn extensions(&self) -> &[&str] { fn extensions(&self) -> &[&str] {

View file

@ -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<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// 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<T> 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()
});
}

View file

@ -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<AssetServer>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// 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()
});
}

View file

@ -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<AssetServer>) {
asset_server.add_asset_folder("assets");
asset_server.load_assets().expect("Assets should exist");
}