Skip to content

Commit f6b34bd

Browse files
authored
Fix GLTF files being broken when loaded from custom asset sources. (#21643)
# Objective - Fixes #10903 ## Solution - Stop using `Path::join` for joining asset paths and instead use `AssetPath::resolve_embed`. ## Testing - Added 2 tests!
1 parent 7d29266 commit f6b34bd

File tree

2 files changed

+181
-18
lines changed

2 files changed

+181
-18
lines changed

crates/bevy_gltf/src/loader/gltf_ext/texture.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ pub(crate) fn texture_handle(
3030
if let Ok(_data_uri) = DataUri::parse(uri) {
3131
load_context.get_label_handle(texture_label(texture).to_string())
3232
} else {
33-
let parent = load_context.path().parent().unwrap();
34-
let image_path = parent.join(uri);
33+
let image_path = load_context
34+
.asset_path()
35+
.resolve_embed(uri)
36+
.expect("all URIs were already validated when we initially loaded textures");
3537
load_context.load(image_path)
3638
}
3739
}

crates/bevy_gltf/src/loader/mod.rs

Lines changed: 177 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@ mod extensions;
22
mod gltf_ext;
33

44
use alloc::sync::Arc;
5-
use std::{
6-
io::Error,
7-
path::{Path, PathBuf},
8-
sync::Mutex,
9-
};
5+
use std::{io::Error, sync::Mutex};
106

117
#[cfg(feature = "bevy_animation")]
128
use bevy_animation::{prelude::*, AnimatedBy, AnimationTargetId};
139
use bevy_asset::{
14-
io::Reader, AssetLoadError, AssetLoader, Handle, LoadContext, ReadAssetBytesError,
15-
RenderAssetUsages,
10+
io::Reader, AssetLoadError, AssetLoader, AssetPath, Handle, LoadContext, ParseAssetPathError,
11+
ReadAssetBytesError, RenderAssetUsages,
1612
};
1713
use bevy_camera::{
1814
primitives::Aabb, visibility::Visibility, Camera, Camera3d, OrthographicProjection,
@@ -103,13 +99,19 @@ pub enum GltfError {
10399
/// Unsupported buffer format.
104100
#[error("unsupported buffer format")]
105101
BufferFormatUnsupported,
102+
/// The buffer URI was unable to be resolved with respect to the asset path.
103+
#[error("invalid buffer uri: {0}. asset path error={1}")]
104+
InvalidBufferUri(String, ParseAssetPathError),
106105
/// Invalid image mime type.
107106
#[error("invalid image mime type: {0}")]
108107
#[from(ignore)]
109108
InvalidImageMimeType(String),
110109
/// Error when loading a texture. Might be due to a disabled image file format feature.
111110
#[error("You may need to add the feature for the file format: {0}")]
112111
ImageError(#[from] TextureError),
112+
/// The image URI was unable to be resolved with respect to the asset path.
113+
#[error("invalid image uri: {0}. asset path error={1}")]
114+
InvalidImageUri(String, ParseAssetPathError),
113115
/// Failed to read bytes from an asset path.
114116
#[error("failed to read bytes from an asset path: {0}")]
115117
ReadAssetBytesError(#[from] ReadAssetBytesError),
@@ -595,12 +597,11 @@ impl GltfLoader {
595597
let mut _texture_handles = Vec::new();
596598
if gltf.textures().len() == 1 || cfg!(target_arch = "wasm32") {
597599
for texture in gltf.textures() {
598-
let parent_path = load_context.path().parent().unwrap();
599600
let image = load_image(
600601
texture,
601602
&buffer_data,
602603
&linear_textures,
603-
parent_path,
604+
load_context.asset_path(),
604605
loader.supported_compressed_formats,
605606
default_sampler,
606607
settings,
@@ -613,15 +614,15 @@ impl GltfLoader {
613614
IoTaskPool::get()
614615
.scope(|scope| {
615616
gltf.textures().for_each(|gltf_texture| {
616-
let parent_path = load_context.path().parent().unwrap();
617+
let asset_path = load_context.asset_path().clone();
617618
let linear_textures = &linear_textures;
618619
let buffer_data = &buffer_data;
619620
scope.spawn(async move {
620621
load_image(
621622
gltf_texture,
622623
buffer_data,
623624
linear_textures,
624-
parent_path,
625+
&asset_path,
625626
loader.supported_compressed_formats,
626627
default_sampler,
627628
settings,
@@ -1079,7 +1080,7 @@ async fn load_image<'a, 'b>(
10791080
gltf_texture: gltf::Texture<'a>,
10801081
buffer_data: &[Vec<u8>],
10811082
linear_textures: &HashSet<usize>,
1082-
parent_path: &'b Path,
1083+
gltf_path: &'b AssetPath<'b>,
10831084
supported_compressed_formats: CompressedImageFormats,
10841085
default_sampler: &ImageSamplerDescriptor,
10851086
settings: &GltfLoaderSettings,
@@ -1129,7 +1130,9 @@ async fn load_image<'a, 'b>(
11291130
label: GltfAssetLabel::Texture(gltf_texture.index()),
11301131
})
11311132
} else {
1132-
let image_path = parent_path.join(uri);
1133+
let image_path = gltf_path
1134+
.resolve_embed(uri)
1135+
.map_err(|err| GltfError::InvalidImageUri(uri.to_owned(), err))?;
11331136
Ok(ImageOrPath::Path {
11341137
path: image_path,
11351138
is_srgb,
@@ -1740,7 +1743,10 @@ async fn load_buffers(
17401743
Ok(_) => return Err(GltfError::BufferFormatUnsupported),
17411744
Err(()) => {
17421745
// TODO: Remove this and add dep
1743-
let buffer_path = load_context.path().parent().unwrap().join(uri);
1746+
let buffer_path = load_context
1747+
.asset_path()
1748+
.resolve_embed(uri)
1749+
.map_err(|err| GltfError::InvalidBufferUri(uri.to_owned(), err))?;
17441750
load_context.read_asset_bytes(buffer_path).await?
17451751
}
17461752
};
@@ -1802,7 +1808,7 @@ enum ImageOrPath {
18021808
label: GltfAssetLabel,
18031809
},
18041810
Path {
1805-
path: PathBuf,
1811+
path: AssetPath<'static>,
18061812
is_srgb: bool,
18071813
sampler_descriptor: ImageSamplerDescriptor,
18081814
},
@@ -1904,12 +1910,14 @@ mod test {
19041910
memory::{Dir, MemoryAssetReader},
19051911
AssetSource, AssetSourceId,
19061912
},
1907-
AssetApp, AssetPlugin, AssetServer, Assets, Handle, LoadState,
1913+
AssetApp, AssetLoader, AssetPlugin, AssetServer, Assets, Handle, LoadState,
19081914
};
19091915
use bevy_ecs::{resource::Resource, world::World};
1916+
use bevy_image::{Image, ImageLoaderSettings};
19101917
use bevy_log::LogPlugin;
19111918
use bevy_mesh::skinning::SkinnedMeshInverseBindposes;
19121919
use bevy_mesh::MeshPlugin;
1920+
use bevy_pbr::StandardMaterial;
19131921
use bevy_scene::ScenePlugin;
19141922

19151923
fn test_app(dir: Dir) -> App {
@@ -2327,4 +2335,157 @@ mod test {
23272335
assert_eq!(skinned_node.children.len(), 2);
23282336
assert_eq!(skinned_node.skin.as_ref(), Some(&gltf_root.skins[0]));
23292337
}
2338+
2339+
fn test_app_custom_asset_source() -> (App, Dir) {
2340+
let dir = Dir::default();
2341+
2342+
let mut app = App::new();
2343+
let custom_reader = MemoryAssetReader { root: dir.clone() };
2344+
// Create a default asset source so we definitely don't try to read from disk.
2345+
app.register_asset_source(
2346+
AssetSourceId::Default,
2347+
AssetSource::build().with_reader(move || {
2348+
Box::new(MemoryAssetReader {
2349+
root: Dir::default(),
2350+
})
2351+
}),
2352+
)
2353+
.register_asset_source(
2354+
"custom",
2355+
AssetSource::build().with_reader(move || Box::new(custom_reader.clone())),
2356+
)
2357+
.add_plugins((
2358+
LogPlugin::default(),
2359+
TaskPoolPlugin::default(),
2360+
AssetPlugin::default(),
2361+
ScenePlugin,
2362+
MeshPlugin,
2363+
crate::GltfPlugin::default(),
2364+
));
2365+
2366+
app.finish();
2367+
app.cleanup();
2368+
2369+
(app, dir)
2370+
}
2371+
2372+
#[test]
2373+
fn reads_buffer_in_custom_asset_source() {
2374+
let (mut app, dir) = test_app_custom_asset_source();
2375+
2376+
dir.insert_asset_text(
2377+
Path::new("abc.gltf"),
2378+
r#"
2379+
{
2380+
"asset": {
2381+
"version": "2.0"
2382+
},
2383+
"buffers": [
2384+
{
2385+
"uri": "abc.bin",
2386+
"byteLength": 3
2387+
}
2388+
]
2389+
}
2390+
"#,
2391+
);
2392+
// We don't care that the buffer contains reasonable info since we won't actually use it.
2393+
dir.insert_asset_text(Path::new("abc.bin"), "Sup");
2394+
2395+
let asset_server = app.world().resource::<AssetServer>().clone();
2396+
let handle: Handle<Gltf> = asset_server.load("custom://abc.gltf");
2397+
run_app_until(&mut app, |_world| {
2398+
let load_state = asset_server.get_load_state(handle.id()).unwrap();
2399+
match load_state {
2400+
LoadState::Loaded => Some(()),
2401+
LoadState::Failed(err) => panic!("{err}"),
2402+
_ => None,
2403+
}
2404+
});
2405+
}
2406+
2407+
#[test]
2408+
fn reads_images_in_custom_asset_source() {
2409+
let (mut app, dir) = test_app_custom_asset_source();
2410+
2411+
app.init_asset::<StandardMaterial>();
2412+
2413+
// Note: We need the material here since otherwise we don't store the texture handle, which
2414+
// can result in the image getting dropped leading to the gltf never being loaded with
2415+
// dependencies.
2416+
dir.insert_asset_text(
2417+
Path::new("abc.gltf"),
2418+
r#"
2419+
{
2420+
"asset": {
2421+
"version": "2.0"
2422+
},
2423+
"textures": [
2424+
{
2425+
"source": 0,
2426+
"sampler": 0
2427+
}
2428+
],
2429+
"images": [
2430+
{
2431+
"uri": "abc.png"
2432+
}
2433+
],
2434+
"samplers": [
2435+
{
2436+
"magFilter": 9729,
2437+
"minFilter": 9729
2438+
}
2439+
],
2440+
"materials": [
2441+
{
2442+
"pbrMetallicRoughness": {
2443+
"baseColorTexture": {
2444+
"index": 0,
2445+
"texCoord": 0
2446+
}
2447+
}
2448+
}
2449+
]
2450+
}
2451+
"#,
2452+
);
2453+
// We don't care that the image contains reasonable info since we won't actually use it.
2454+
dir.insert_asset_text(Path::new("abc.png"), "Sup");
2455+
2456+
/// A fake loader to avoid actually loading any image data and just return an image.
2457+
struct FakePngLoader;
2458+
2459+
impl AssetLoader for FakePngLoader {
2460+
type Asset = Image;
2461+
type Error = std::io::Error;
2462+
type Settings = ImageLoaderSettings;
2463+
2464+
async fn load(
2465+
&self,
2466+
_reader: &mut dyn bevy_asset::io::Reader,
2467+
_settings: &Self::Settings,
2468+
_load_context: &mut bevy_asset::LoadContext<'_>,
2469+
) -> Result<Self::Asset, Self::Error> {
2470+
Ok(Image::default())
2471+
}
2472+
2473+
fn extensions(&self) -> &[&str] {
2474+
&["png"]
2475+
}
2476+
}
2477+
2478+
app.init_asset::<Image>()
2479+
.register_asset_loader(FakePngLoader);
2480+
2481+
let asset_server = app.world().resource::<AssetServer>().clone();
2482+
let handle: Handle<Gltf> = asset_server.load("custom://abc.gltf");
2483+
run_app_until(&mut app, |_world| {
2484+
// Note: we can't assert for failure since it's the nested load that fails, not the GLTF
2485+
// load.
2486+
asset_server
2487+
.is_loaded_with_dependencies(&handle)
2488+
.then_some(())
2489+
});
2490+
}
23302491
}

0 commit comments

Comments
 (0)