Skip to content

Commit

Permalink
Support dynamic image assets and remove requirement to wrap optional …
Browse files Browse the repository at this point in the history
…values in Some
  • Loading branch information
NiklasEi committed Nov 1, 2023
1 parent a245fb9 commit 6209917
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 37 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
- Make `loading_state::LoadingStateSet` public for explicit system ordering
- Support configuring an image sampler through a derive attribute ([#156](https://github.com/NiklasEi/bevy_asset_loader/pull/156))
- See [the new example](bevy_asset_loader/examples/image_asset.rs)
- This can is also supported in dynamic assets through the new standard dynamic asset `Image`
- Optional fields of dynamic assets no longer require wrapping their values in `Some`
- E.g. configuring `padding_x` for a texture atlas is now just `padding_x: 1.5` instead of `padding_x: Some(1.5)`

## v0.17.0
- update to Bevy 0.11
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,16 @@ struct ImageAssets {
}
```

The corresponding dynamic asset would be
```ron
({
"pixel_tree": Image (
path: "images/tree.png",
sampler: Nearest
),
})
```

### Standard materials

You can directly load standard materials if you enable the feature `3d`. For a complete example please take a look at [standard_material.rs](bevy_asset_loader/examples/standard_material.rs).
Expand Down
6 changes: 5 additions & 1 deletion bevy_asset_loader/assets/full_dynamic_collection.assets.ron
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
tile_size_x: 96.,
tile_size_y: 99.,
columns: 8,
rows: 1
rows: 1,
),
"pixel_tree": Image (
path: "images/tree.png",
sampler: Nearest
),
"folder_untyped": Folder (
path: "images",
Expand Down
21 changes: 17 additions & 4 deletions bevy_asset_loader/examples/full_dynamic_collection.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bevy::app::AppExit;
use bevy::asset::LoadState;
use bevy::prelude::*;
use bevy::render::texture::ImageSampler;
use bevy::utils::HashMap;
use bevy_asset_loader::prelude::*;
use path_slash::PathExt;
Expand Down Expand Up @@ -49,6 +50,9 @@ struct MyAssets {
// Type in `assets/full_dynamic_collection.assets.ron`: `File`, `StandardMaterial`, or `TextureAtlas`
#[asset(key = "optional_file", optional)]
optional_file: Option<Handle<AudioSource>>,
// Image asset with sampler nearest (good for crisp pixel art)
#[asset(key = "pixel_tree")]
image_tree_nearest: Handle<Image>,

// Collections of files

Expand Down Expand Up @@ -93,6 +97,7 @@ fn expectations(
asset_server: Res<AssetServer>,
standard_materials: Res<Assets<StandardMaterial>>,
texture_atlases: Res<Assets<TextureAtlas>>,
images: Res<Assets<Image>>,
mut quit: EventWriter<AppExit>,
) {
info!("Done loading the collection. Checking expectations...");
Expand Down Expand Up @@ -121,14 +126,22 @@ fn expectations(
LoadState::Loaded
);
assert_eq!(assets.optional_file, None);
assert_eq!(assets.folder_untyped.len(), 6);
let image = images
.get(&assets.image_tree_nearest)
.expect("Image should be added to its asset resource");
let ImageSampler::Descriptor(descriptor) = &image.sampler_descriptor else {
panic!("Descriptor was not set to non default value nearest");
};
assert_eq!(descriptor, &ImageSampler::nearest_descriptor());

assert_eq!(assets.folder_untyped.len(), 7);
for handle in assets.folder_untyped.iter() {
assert_eq!(
asset_server.get_load_state(handle.clone()),
LoadState::Loaded
);
}
assert_eq!(assets.folder_untyped_mapped.len(), 6);
assert_eq!(assets.folder_untyped_mapped.len(), 7);
for (name, handle) in assets.folder_untyped_mapped.iter() {
assert_eq!(
asset_server.get_load_state(handle.clone()),
Expand All @@ -145,14 +158,14 @@ fn expectations(
name
);
}
assert_eq!(assets.folder_typed.len(), 6);
assert_eq!(assets.folder_typed.len(), 7);
for handle in assets.folder_typed.iter() {
assert_eq!(
asset_server.get_load_state(handle.clone()),
LoadState::Loaded
);
}
assert_eq!(assets.folder_typed_mapped.len(), 6);
assert_eq!(assets.folder_typed_mapped.len(), 7);
for (name, handle) in assets.folder_typed_mapped.iter() {
assert_eq!(
asset_server.get_load_state(handle.clone()),
Expand Down
92 changes: 91 additions & 1 deletion bevy_asset_loader/src/standard_dynamic_asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ use bevy::math::Vec2;

use crate::dynamic_asset::{DynamicAssetCollection, DynamicAssets};
use bevy::reflect::{TypePath, TypeUuid};
#[cfg(any(feature = "3d", feature = "2d"))]
use bevy::render::render_resource::SamplerDescriptor;
#[cfg(any(feature = "3d", feature = "2d"))]
use bevy::render::texture::ImageSampler;
use bevy::utils::HashMap;
use serde::{Deserialize, Deserializer};

/// These asset variants can be loaded from configuration files. They will then replace
/// a dynamic asset based on their keys.
#[derive(Debug, Clone, serde::Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub enum StandardDynamicAsset {
/// A dynamic asset directly loaded from a single file
File {
Expand All @@ -32,6 +37,15 @@ pub enum StandardDynamicAsset {
/// Asset file paths
paths: Vec<String>,
},
/// An image asset
#[cfg(any(feature = "3d", feature = "2d"))]
Image {
/// Image file path
path: String,
/// Sampler
#[serde(deserialize_with = "deserialize_some", default)]
sampler: Option<ImageSamplerType>,
},
/// A dynamic standard material asset directly loaded from an image file
#[cfg(feature = "3d")]
StandardMaterial {
Expand All @@ -52,16 +66,61 @@ pub enum StandardDynamicAsset {
/// Rows on the sprite sheet
rows: usize,
/// Padding between columns in pixels
#[serde(deserialize_with = "deserialize_some", default)]
padding_x: Option<f32>,
/// Padding between rows in pixels
#[serde(deserialize_with = "deserialize_some", default)]
padding_y: Option<f32>,
/// Number of pixels offset of the first tile
#[serde(deserialize_with = "deserialize_some", default)]
offset_x: Option<f32>,
/// Number of pixels offset of the first tile
#[serde(deserialize_with = "deserialize_some", default)]
offset_y: Option<f32>,
},
}

#[cfg(any(feature = "3d", feature = "2d"))]
fn deserialize_some<'de, D, G>(deserializer: D) -> Result<Option<G>, D::Error>
where
D: Deserializer<'de>,
G: Deserialize<'de>,
{
let opt: G = G::deserialize(deserializer)?;
Ok(Some(opt))
}

/// Define the image sampler to configure for an image asset
#[cfg(any(feature = "3d", feature = "2d"))]
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ImageSamplerType {
/// See [`ImageSampler::nearest`]
Nearest,
/// See [`ImageSampler::linear`]
Linear,
}

#[cfg(any(feature = "3d", feature = "2d"))]
impl From<ImageSamplerType> for SamplerDescriptor<'_> {
fn from(value: ImageSamplerType) -> Self {
match value {
ImageSamplerType::Nearest => ImageSampler::nearest_descriptor(),
ImageSamplerType::Linear => ImageSampler::linear_descriptor(),
}
}
}

#[cfg(any(feature = "3d", feature = "2d"))]
impl From<ImageSamplerType> for ImageSampler {
fn from(value: ImageSamplerType) -> Self {
match value {
ImageSamplerType::Nearest => ImageSampler::nearest(),
ImageSamplerType::Linear => ImageSampler::linear(),
}
}
}

impl DynamicAsset for StandardDynamicAsset {
fn load(&self, asset_server: &AssetServer) -> Vec<HandleUntyped> {
match self {
Expand All @@ -73,6 +132,10 @@ impl DynamicAsset for StandardDynamicAsset {
.iter()
.map(|path| asset_server.load_untyped(path))
.collect(),
#[cfg(any(feature = "3d", feature = "2d"))]
StandardDynamicAsset::Image { path, .. } => {
vec![asset_server.load_untyped(path)]
}
#[cfg(feature = "3d")]
StandardDynamicAsset::StandardMaterial { path } => {
vec![asset_server.load_untyped(path)]
Expand All @@ -93,6 +156,33 @@ impl DynamicAsset for StandardDynamicAsset {
StandardDynamicAsset::File { path } => Ok(DynamicAssetType::Single(
asset_server.get_handle_untyped(path),
)),
#[cfg(any(feature = "3d", feature = "2d"))]
StandardDynamicAsset::Image { path, sampler } => {
let mut handle = asset_server.load(path);
if let Some(sampler) = sampler {
let mut images = cell
.get_resource_mut::<::bevy::asset::Assets<::bevy::render::texture::Image>>()
.expect("Cannot get resource Assets<Image>");
let image = images.get_mut(&handle).unwrap();

let is_different_sampler =
if let ImageSampler::Descriptor(descriptor) = &image.sampler_descriptor {
!descriptor.eq(&sampler.clone().into())
} else {
false
};

if is_different_sampler {
let mut cloned_image = image.clone();
cloned_image.sampler_descriptor = sampler.clone().into();
handle = images.add(cloned_image);
} else {
image.sampler_descriptor = sampler.clone().into();
}
}

Ok(DynamicAssetType::Single(handle.clone_untyped()))
}
#[cfg(feature = "3d")]
StandardDynamicAsset::StandardMaterial { path } => {
let mut materials = cell
Expand Down
73 changes: 42 additions & 31 deletions bevy_asset_loader_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ fn impl_asset_collection(
"This attribute requires the '3d' feature",
));
}
ParseFieldError::Missing2dOr3dFeature(token_stream) => {
compile_errors.push(syn::Error::new_spanned(
token_stream,
"This attribute requires the '3d' or '2d' feature",
));
}
ParseFieldError::PathAndPathsAreExclusive => {
compile_errors.push(syn::Error::new_spanned(
field.into_token_stream(),
Expand Down Expand Up @@ -232,6 +238,8 @@ enum ParseFieldError {
Missing2dFeature(proc_macro2::TokenStream),
#[allow(dead_code)]
Missing3dFeature(proc_macro2::TokenStream),
#[allow(dead_code)]
Missing2dOr3dFeature(proc_macro2::TokenStream),
}

fn parse_field(field: &Field) -> Result<AssetField, Vec<ParseFieldError>> {
Expand Down Expand Up @@ -419,47 +427,50 @@ fn parse_field(field: &Field) -> Result<AssetField, Vec<ParseFieldError>> {
let mut paths = vec![];
for path in paths_meta_list.unwrap() {
paths.push(path.value());
// } else {
// errors.push(ParseFieldError::UnknownAttributeType(
// attribute.into_token_stream(),
// ));
// }
}
builder.asset_paths = Some(paths);
}
Meta::List(meta_list) if meta_list.path.is_ident(IMAGE_ATTRIBUTE) => {
let image_meta_list =
meta_list.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated);

for attribute in image_meta_list.unwrap() {
match attribute {
Meta::NameValue(named_value) => {
let path = named_value.path.get_ident().unwrap().clone();
if path == ImageAttribute::SAMPLER {
if let Expr::Path(ExprPath { path, .. }) = &named_value.value {
let sampler_result = SamplerType::try_from(
path.get_ident().unwrap().to_string(),
);
#[cfg(all(not(feature = "2d"), not(feature = "3d")))]
errors.push(ParseFieldError::Missing2dOr3dFeature(
meta_list.into_token_stream(),
));
#[cfg(any(feature = "2d", feature = "3d"))]
{
let image_meta_list = meta_list
.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated);
for attribute in image_meta_list.unwrap() {
match attribute {
Meta::NameValue(named_value) => {
let path = named_value.path.get_ident().unwrap().clone();
if path == ImageAttribute::SAMPLER {
if let Expr::Path(ExprPath { path, .. }) =
&named_value.value
{
let sampler_result = SamplerType::try_from(
path.get_ident().unwrap().to_string(),
);

if sampler_result.is_ok() {
builder.sampler = Some(sampler_result.unwrap());
if let Ok(sampler) = sampler_result {
builder.sampler = Some(sampler);
} else {
errors.push(ParseFieldError::UnknownAttribute(
named_value.value.into_token_stream(),
));
}
} else {
errors.push(ParseFieldError::UnknownAttribute(
named_value.value.into_token_stream(),
errors.push(ParseFieldError::WrongAttributeType(
named_value.into_token_stream(),
"path",
));
}
} else {
errors.push(ParseFieldError::WrongAttributeType(
named_value.into_token_stream(),
"path",
));
}
}
}
_ => {
errors.push(ParseFieldError::UnknownAttributeType(
attribute.into_token_stream(),
));
_ => {
errors.push(ParseFieldError::UnknownAttributeType(
attribute.into_token_stream(),
));
}
}
}
}
Expand Down

0 comments on commit 6209917

Please sign in to comment.