mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
Hot asset reloading
This commit is contained in:
parent
623c8a8d9a
commit
870f715df3
20 changed files with 590 additions and 149 deletions
|
@ -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"
|
||||
|
|
BIN
assets/models/cube/cube.bin
Normal file
BIN
assets/models/cube/cube.bin
Normal file
Binary file not shown.
122
assets/models/cube/cube.gltf
Normal file
122
assets/models/cube/cube.gltf
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
BIN
assets/models/sphere/sphere.bin
Normal file
BIN
assets/models/sphere/sphere.bin
Normal file
Binary file not shown.
100
assets/models/sphere/sphere.gltf
Normal file
100
assets/models/sphere/sphere.gltf
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -4,6 +4,10 @@ version = "0.1.0"
|
|||
authors = ["Carter Anderson <mcanders1@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["filesystem_watcher"]
|
||||
filesystem_watcher = ["notify"]
|
||||
|
||||
[dependencies]
|
||||
bevy_app = { path = "../bevy_app" }
|
||||
bevy_core = { path = "../bevy_core" }
|
||||
|
@ -13,3 +17,4 @@ uuid = { version = "0.8", features = ["v4", "serde"] }
|
|||
crossbeam-channel = "0.4.2"
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
notify = { version = "5.0.0-pre.2", optional = true }
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<String>,
|
||||
asset_folders: Vec<PathBuf>,
|
||||
loader_threads: RwLock<Vec<LoaderThread>>,
|
||||
max_loader_threads: usize,
|
||||
asset_handlers: Arc<RwLock<Vec<Box<dyn AssetLoadRequestHandler>>>>,
|
||||
|
@ -44,6 +47,9 @@ pub struct AssetServer {
|
|||
loaders: Vec<Resources>,
|
||||
extension_to_handler_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 {
|
||||
|
@ -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<P: AsRef<Path>>(&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<String, AssetServerError> {
|
||||
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<T, P: AsRef<Path>>(&self, path: P) -> Option<Handle<T>> {
|
||||
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<P: AsRef<Path>>(
|
||||
filesystem_watcher: &mut Option<FilesystemWatcher>,
|
||||
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<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)
|
||||
.map(|handle_id| Handle::from(handle_id))
|
||||
}
|
||||
|
||||
pub fn load_sync<T>(
|
||||
pub fn load_sync<T, P: AsRef<Path>>(
|
||||
&self,
|
||||
assets: &mut Assets<T>,
|
||||
path: &str,
|
||||
path: P,
|
||||
) -> Result<Handle<T>, 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::<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);
|
||||
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<HandleId, AssetServerError> {
|
||||
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()) {
|
||||
pub fn load_untyped<P: AsRef<Path>>(&self, path: P) -> Result<HandleId, AssetServerError> {
|
||||
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)?;
|
||||
}
|
||||
|
|
|
@ -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<T> {
|
||||
Created { handle: Handle<T> },
|
||||
|
@ -14,7 +14,7 @@ pub enum AssetEvent<T> {
|
|||
|
||||
pub struct Assets<T> {
|
||||
assets: HashMap<HandleId, T>,
|
||||
paths: HashMap<String, Handle<T>>,
|
||||
paths: HashMap<PathBuf, Handle<T>>,
|
||||
events: Events<AssetEvent<T>>,
|
||||
}
|
||||
|
||||
|
@ -29,8 +29,8 @@ impl<T> Default for Assets<T> {
|
|||
}
|
||||
|
||||
impl<T> Assets<T> {
|
||||
pub fn get_with_path(&mut self, path: &str) -> Option<Handle<T>> {
|
||||
self.paths.get(path).map(|handle| *handle)
|
||||
pub fn get_with_path<P: AsRef<Path>>(&mut self, path: P) -> Option<Handle<T>> {
|
||||
self.paths.get(path.as_ref()).map(|handle| *handle)
|
||||
}
|
||||
|
||||
pub fn add(&mut self, asset: T) -> Handle<T> {
|
||||
|
@ -64,8 +64,8 @@ impl<T> Assets<T> {
|
|||
handle
|
||||
}
|
||||
|
||||
pub fn set_path(&mut self, handle: Handle<T>, path: &str) {
|
||||
self.paths.insert(path.to_string(), handle);
|
||||
pub fn set_path<P: AsRef<Path>>(&mut self, handle: Handle<T>, path: P) {
|
||||
self.paths.insert(path.as_ref().to_owned(), handle);
|
||||
}
|
||||
|
||||
pub fn get_id(&self, id: HandleId) -> Option<&T> {
|
||||
|
|
26
crates/bevy_asset/src/filesystem_watcher.rs
Normal file
26
crates/bevy_asset/src/filesystem_watcher.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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::<AssetServer>();
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
app.add_system_to_stage(stage::LOAD_ASSETS, AssetServer::filesystem_watcher_system.system());
|
||||
}
|
||||
}
|
|
@ -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<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();
|
||||
file.read_to_end(&mut bytes)?;
|
||||
let asset = self.loader.from_bytes(&load_request.path, bytes)?;
|
||||
|
|
|
@ -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<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 load_from_file(&self, asset_path: &AssetPath) -> Result<T, AssetLoadError> {
|
||||
let mut file = File::open(Path::new(asset_path.path.as_ref()))?;
|
||||
fn load_from_file(&self, asset_path: &Path) -> Result<T, AssetLoadError> {
|
||||
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<T>: Send + Sync + 'static {
|
|||
pub struct AssetResult<T> {
|
||||
pub result: Result<T, AssetLoadError>,
|
||||
pub handle: Handle<T>,
|
||||
pub path: AssetPath,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
pub struct AssetChannel<T> {
|
||||
|
@ -54,7 +57,7 @@ pub fn update_asset_storage_system<T>(
|
|||
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;
|
||||
|
|
|
@ -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<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)?;
|
||||
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 buffer_data = load_buffers(gltf.buffers(), asset_path)?;
|
||||
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")
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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<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 (info, mut reader) = decoder.read_info()?;
|
||||
let mut data = vec![0; info.buffer_size()];
|
||||
|
|
|
@ -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<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)?)
|
||||
}
|
||||
fn extensions(&self) -> &[&str] {
|
||||
|
|
80
examples/asset/asset_loading.rs
Normal file
80
examples/asset/asset_loading.rs
Normal 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()
|
||||
});
|
||||
}
|
60
examples/asset/hot_asset_reload.rs
Normal file
60
examples/asset/hot_asset_reload.rs
Normal 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()
|
||||
});
|
||||
}
|
|
@ -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");
|
||||
}
|
Loading…
Reference in a new issue