Skip to content
80 changes: 80 additions & 0 deletions crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2830,4 +2830,84 @@ mod tests {
META_TEXT
);
}

#[test]
fn asset_dependency_is_tracked_when_not_loaded() {
let (mut app, dir) = create_app();

#[derive(Asset, TypePath)]
struct AssetWithDep {
#[dependency]
dep: Handle<TestAsset>,
}

#[derive(TypePath)]
struct AssetWithDepLoader;

impl AssetLoader for AssetWithDepLoader {
type Asset = TestAsset;
type Settings = ();
type Error = std::io::Error;

async fn load(
&self,
_reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
// Load the asset in the root context, but then put the handle in the subasset. So
// the subasset's (internal) load context never loaded `dep`.
let dep = load_context.load::<TestAsset>("abc.ron");
load_context.add_labeled_asset("subasset".into(), AssetWithDep { dep });
Ok(TestAsset)
}

fn extensions(&self) -> &[&str] {
&["with_deps"]
}
}

// Write some data so the loaders have something to load (even though they don't use the
// data).
dir.insert_asset_text(Path::new("abc.ron"), "");
dir.insert_asset_text(Path::new("blah.with_deps"), "");

let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1);
let (gate_sender, gate_receiver) = async_channel::bounded(1);
app.init_asset::<TestAsset>()
.init_asset::<AssetWithDep>()
.register_asset_loader(GatedLoader {
in_loader_sender,
gate_receiver,
})
.register_asset_loader(AssetWithDepLoader);

let asset_server = app.world().resource::<AssetServer>().clone();
let subasset_handle: Handle<AssetWithDep> = asset_server.load("blah.with_deps#subasset");

run_app_until(&mut app, |_| {
asset_server.is_loaded(&subasset_handle).then_some(())
});
// Even though the subasset is loaded, and its load context never loaded its dependency, it
// still depends on its dependency, so that is tracked correctly here.
assert!(!asset_server.is_loaded_with_dependencies(&subasset_handle));

let dep_handle: Handle<TestAsset> = app
.world()
.resource::<Assets<AssetWithDep>>()
.get(&subasset_handle)
.unwrap()
.dep
.clone();

// Pass the gate in the dependency loader.
in_loader_receiver.recv_blocking().unwrap();
gate_sender.send_blocking(()).unwrap();

run_app_until(&mut app, |_| {
asset_server.is_loaded(&dep_handle).then_some(())
});
// Now that the dependency is loaded, the subasset is counted as loaded with dependencies!
assert!(asset_server.is_loaded_with_dependencies(&subasset_handle));
}
}
19 changes: 17 additions & 2 deletions crates/bevy_asset/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
meta::{AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfo, ProcessedInfoMinimal, Settings},
path::AssetPath,
Asset, AssetIndex, AssetLoadError, AssetServer, AssetServerMode, Assets, ErasedAssetIndex,
Handle, UntypedHandle,
Handle, UntypedAssetId, UntypedHandle,
};
use alloc::{
boxed::Box,
Expand Down Expand Up @@ -477,7 +477,22 @@ impl<'a> LoadContext<'a> {
}

/// "Finishes" this context by populating the final [`Asset`] value.
pub fn finish<A: Asset>(self, value: A) -> LoadedAsset<A> {
pub fn finish<A: Asset>(mut self, value: A) -> LoadedAsset<A> {
// At this point, we assume the asset/subasset is "locked in" and won't be changed, so we
// can ensure all the dependencies are included (in case a handle was used without loading
// it through this `LoadContext`). If in the future we provide an API for mutating assets in
// `LoadedAsset`, `ErasedLoadedAsset`, or `LoadContext` (for mutating existing subassets),
// we should move this to some point after those mutations are not possible. This spot is
// convenient because we still have access to the static type of `A`.
value.visit_dependencies(&mut |asset_id| {
let (type_id, index) = match asset_id {
UntypedAssetId::Index { type_id, index } => (type_id, index),
// UUID assets can't be loaded anyway, so just ignore this ID.
UntypedAssetId::Uuid { .. } => return,
};
self.dependencies
.insert(ErasedAssetIndex { index, type_id });
});
LoadedAsset {
value,
dependencies: self.dependencies,
Expand Down
38 changes: 15 additions & 23 deletions crates/bevy_gltf/src/loader/extensions/khr_materials_anisotropy.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
use bevy_asset::LoadContext;
use bevy_asset::{AssetPath, Handle};
use bevy_image::Image;

use gltf::{Document, Material};
use gltf::Material;

use serde_json::Value;

#[cfg(feature = "pbr_anisotropy_texture")]
use {
crate::loader::gltf_ext::{material::uv_channel, texture::texture_handle_from_info},
bevy_asset::Handle,
bevy_image::Image,
bevy_pbr::UvChannel,
gltf::json::texture::Info,
serde_json::value,
};
use {crate::loader::gltf_ext::material::parse_material_extension_texture, bevy_pbr::UvChannel};

/// Parsed data from the `KHR_materials_anisotropy` extension.
///
Expand All @@ -38,32 +32,30 @@ impl AnisotropyExtension {
reason = "Depending on what features are used to compile this crate, certain parameters may end up unused."
)]
pub(crate) fn parse(
load_context: &mut LoadContext,
document: &Document,
material: &Material,
textures: &[Handle<Image>],
asset_path: AssetPath<'_>,
) -> Option<AnisotropyExtension> {
let extension = material
.extensions()?
.get("KHR_materials_anisotropy")?
.as_object()?;

#[cfg(feature = "pbr_anisotropy_texture")]
let (anisotropy_channel, anisotropy_texture) = extension
.get("anisotropyTexture")
.and_then(|value| value::from_value::<Info>(value.clone()).ok())
.map(|json_info| {
(
uv_channel(material, "anisotropy", json_info.tex_coord),
texture_handle_from_info(&json_info, document, load_context),
)
})
.unzip();
let (anisotropy_channel, anisotropy_texture) = parse_material_extension_texture(
material,
extension,
"anisotropyTexture",
"anisotropy",
textures,
asset_path,
);

Some(AnisotropyExtension {
anisotropy_strength: extension.get("anisotropyStrength").and_then(Value::as_f64),
anisotropy_rotation: extension.get("anisotropyRotation").and_then(Value::as_f64),
#[cfg(feature = "pbr_anisotropy_texture")]
anisotropy_channel: anisotropy_channel.unwrap_or_default(),
anisotropy_channel,
#[cfg(feature = "pbr_anisotropy_texture")]
anisotropy_texture,
})
Expand Down
26 changes: 12 additions & 14 deletions crates/bevy_gltf/src/loader/extensions/khr_materials_clearcoat.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use bevy_asset::LoadContext;
use bevy_asset::{AssetPath, Handle};
use bevy_image::Image;

use gltf::{Document, Material};
use gltf::Material;

use serde_json::Value;

#[cfg(feature = "pbr_multi_layer_material_textures")]
use {
crate::loader::gltf_ext::material::parse_material_extension_texture, bevy_asset::Handle,
bevy_image::Image, bevy_pbr::UvChannel,
};
use {crate::loader::gltf_ext::material::parse_material_extension_texture, bevy_pbr::UvChannel};

/// Parsed data from the `KHR_materials_clearcoat` extension.
///
Expand Down Expand Up @@ -42,9 +40,9 @@ impl ClearcoatExtension {
reason = "Depending on what features are used to compile this crate, certain parameters may end up unused."
)]
pub(crate) fn parse(
load_context: &mut LoadContext,
document: &Document,
material: &Material,
textures: &[Handle<Image>],
asset_path: AssetPath<'_>,
) -> Option<ClearcoatExtension> {
let extension = material
.extensions()?
Expand All @@ -54,32 +52,32 @@ impl ClearcoatExtension {
#[cfg(feature = "pbr_multi_layer_material_textures")]
let (clearcoat_channel, clearcoat_texture) = parse_material_extension_texture(
material,
load_context,
document,
extension,
"clearcoatTexture",
"clearcoat",
textures,
asset_path.clone(),
);

#[cfg(feature = "pbr_multi_layer_material_textures")]
let (clearcoat_roughness_channel, clearcoat_roughness_texture) =
parse_material_extension_texture(
material,
load_context,
document,
extension,
"clearcoatRoughnessTexture",
"clearcoat roughness",
textures,
asset_path.clone(),
);

#[cfg(feature = "pbr_multi_layer_material_textures")]
let (clearcoat_normal_channel, clearcoat_normal_texture) = parse_material_extension_texture(
material,
load_context,
document,
extension,
"clearcoatNormalTexture",
"clearcoat normal",
textures,
asset_path,
);

Some(ClearcoatExtension {
Expand Down
30 changes: 18 additions & 12 deletions crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use bevy_asset::LoadContext;
use bevy_asset::{AssetPath, Handle};
use bevy_image::Image;

use gltf::{Document, Material};
use gltf::Material;

use serde_json::Value;

#[cfg(feature = "pbr_specular_textures")]
use {
crate::loader::gltf_ext::material::parse_material_extension_texture, bevy_asset::Handle,
bevy_image::Image, bevy_pbr::UvChannel,
};
use {crate::loader::gltf_ext::material::parse_material_extension_texture, bevy_pbr::UvChannel};

/// Parsed data from the `KHR_materials_specular` extension.
///
Expand Down Expand Up @@ -41,10 +39,18 @@ pub(crate) struct SpecularExtension {
}

impl SpecularExtension {
#[expect(
clippy::allow_attributes,
reason = "`unused_variables` is not always linted"
)]
#[allow(
unused_variables,
reason = "Depending on what features are used to compile this crate, certain parameters may end up unused."
)]
pub(crate) fn parse(
_load_context: &mut LoadContext,
_document: &Document,
material: &Material,
textures: &[Handle<Image>],
asset_path: AssetPath<'_>,
) -> Option<Self> {
let extension = material
.extensions()?
Expand All @@ -54,21 +60,21 @@ impl SpecularExtension {
#[cfg(feature = "pbr_specular_textures")]
let (_specular_channel, _specular_texture) = parse_material_extension_texture(
material,
_load_context,
_document,
extension,
"specularTexture",
"specular",
textures,
asset_path.clone(),
);

#[cfg(feature = "pbr_specular_textures")]
let (_specular_color_channel, _specular_color_texture) = parse_material_extension_texture(
material,
_load_context,
_document,
extension,
"specularColorTexture",
"specular color",
textures,
asset_path,
);

Some(SpecularExtension {
Expand Down
20 changes: 14 additions & 6 deletions crates/bevy_gltf/src/loader/gltf_ext/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,46 @@ use crate::GltfAssetLabel;
use super::texture::texture_transform_to_affine2;

#[cfg(any(
feature = "pbr_anisotropy_texture",
feature = "pbr_specular_textures",
feature = "pbr_multi_layer_material_textures"
))]
use {
super::texture::texture_handle_from_info,
bevy_asset::{Handle, LoadContext},
bevy_asset::{AssetPath, Handle},
bevy_image::Image,
gltf::Document,
serde_json::{Map, Value},
};

/// Parses a texture that's part of a material extension block and returns its
/// UV channel and image reference.
#[cfg(any(
feature = "pbr_anisotropy_texture",
feature = "pbr_specular_textures",
feature = "pbr_multi_layer_material_textures"
))]
pub(crate) fn parse_material_extension_texture(
material: &Material,
load_context: &mut LoadContext,
document: &Document,
extension: &Map<String, Value>,
texture_name: &str,
texture_kind: &str,
textures: &[Handle<Image>],
asset_path: AssetPath<'_>,
) -> (UvChannel, Option<Handle<Image>>) {
match extension
.get(texture_name)
.and_then(|value| value::from_value::<Info>(value.clone()).ok())
{
Some(json_info) => (
uv_channel(material, texture_kind, json_info.tex_coord),
Some(texture_handle_from_info(&json_info, document, load_context)),
Some({
match textures.get(json_info.index.value()).cloned() {
None => {
tracing::warn!("Gltf at path \"{asset_path}\" contains invalid texture index <{}> for texture {texture_name}. Using default image.", json_info.index.value());
Handle::default()
}
Some(handle) => handle,
}
}),
),
None => (UvChannel::default(), None),
}
Expand Down
Loading