Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/bevy_gltf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ gltf = { version = "1.4.0", default-features = false, features = [
"KHR_materials_unlit",
"KHR_materials_emissive_strength",
"KHR_texture_transform",
"allow_empty_texture",
"extras",
"extensions",
"names",
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_gltf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@
//! | `KHR_materials_variants` | ❌ | |
//! | `KHR_materials_volume` | ✅ | |
//! | `KHR_mesh_quantization` | ❌ | |
//! | `KHR_texture_basisu` | ❌\* | |
//! | `KHR_texture_basisu` | | `ktx2` |
//! | `KHR_texture_transform` | ✅\** | |
//! | `KHR_xmp_json_ld` | ❌ | |
//! | `EXT_mesh_gpu_instancing` | ❌ | |
//! | `EXT_meshopt_compression` | ❌ | |
//! | `EXT_texture_webp` | ❌\* | |
//!
//! \*Bevy supports ktx2 and webp formats but doesn't support the extension's syntax, see [#19104](https://github.com/bevyengine/bevy/issues/19104).
//! \*Bevy supports webp formats but doesn't support the extension's syntax, see [#19104](https://github.com/bevyengine/bevy/issues/19104).
//!
//! \**`KHR_texture_transform` is only supported on `base_color_texture`, see [#15310](https://github.com/bevyengine/bevy/issues/15310).
//!
Expand Down
27 changes: 26 additions & 1 deletion crates/bevy_gltf/src/loader/gltf_ext/texture.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use bevy_image::{ImageAddressMode, ImageFilterMode, ImageSamplerDescriptor};
use bevy_math::Affine2;

use gltf::texture::{MagFilter, MinFilter, Texture, TextureTransform, WrappingMode};
use gltf::{
image::Image,
texture::{MagFilter, MinFilter, Texture, TextureTransform, WrappingMode},
Document,
};

/// Extracts the texture sampler data from the glTF [`Texture`].
pub(crate) fn texture_sampler(
Expand Down Expand Up @@ -48,6 +52,27 @@ pub(crate) fn texture_sampler(
sampler
}

pub(crate) fn texture_source<'a>(
texture: &Texture<'a>,
document: &'a Document,
) -> Result<Option<Image<'a>>, String> {
if let Some(extension) = texture.extension_value("KHR_texture_basisu") {
let source = extension
.get("source")
.and_then(|source| source.as_u64())
.and_then(|source| usize::try_from(source).ok())
.ok_or_else(|| extension.to_string())?;

return document
.images()
.nth(source)
.ok_or_else(|| source.to_string())
.map(Some);
}

Ok(texture.source())
}

pub(crate) fn address_mode(wrapping_mode: &WrappingMode) -> ImageAddressMode {
match wrapping_mode {
WrappingMode::ClampToEdge => ImageAddressMode::ClampToEdge,
Expand Down
237 changes: 234 additions & 3 deletions crates/bevy_gltf/src/loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use gltf::{
accessor::Iter,
image::Source,
mesh::{util::ReadIndices, Mode},
Material, Node, Semantic,
Document, Material, Node, Semantic,
};
use serde::{Deserialize, Serialize};
#[cfg(feature = "bevy_animation")]
Expand Down Expand Up @@ -72,7 +72,7 @@ use self::{
},
mesh::{primitive_name, primitive_topology},
scene::{node_name, node_transform},
texture::{texture_sampler, texture_transform_to_affine2},
texture::{texture_sampler, texture_source, texture_transform_to_affine2},
},
};
use crate::convert_coordinates::GltfConvertCoordinates;
Expand All @@ -92,6 +92,9 @@ pub enum GltfError {
/// Invalid glTF file.
#[error("invalid glTF file: {0}")]
Gltf(#[from] gltf::Error),
/// Unsupported required glTF extension.
#[error("unsupported required glTF extension: {0}")]
UnsupportedRequiredExtension(String),
/// Binary blob is missing.
#[error("binary blob is missing")]
MissingBlob,
Expand All @@ -114,6 +117,17 @@ pub enum GltfError {
/// The image URI was unable to be resolved with respect to the asset path.
#[error("invalid image uri: {0}. asset path error={1}")]
InvalidImageUri(String, ParseAssetPathError),
/// A texture did not have an image source.
#[error("texture {0} is missing an image source")]
MissingImageSource(usize),
/// A texture extension contained an invalid image source.
#[error("texture {texture} contains an invalid KHR_texture_basisu source: {value}")]
InvalidTextureBasisuSource {
/// The texture index.
texture: usize,
/// The invalid source value.
value: String,
},
/// Failed to read bytes from an asset path.
#[error("failed to read bytes from an asset path: {0}")]
ReadAssetBytesError(#[from] ReadAssetBytesError),
Expand Down Expand Up @@ -248,6 +262,9 @@ impl GltfLoader {
} else {
gltf::Gltf::from_slice_without_validation(bytes)?
};
if settings.validate {
reject_unsupported_empty_texture_extensions(&gltf.document)?;
}

// clone extensions to start with a fresh processing state
let mut extensions = loader.extensions.read().await.clone();
Expand Down Expand Up @@ -619,6 +636,7 @@ impl GltfLoader {
for texture in gltf.textures() {
let image = load_image(
texture.clone(),
&gltf.document,
&buffer_data,
&linear_textures,
load_context.path(),
Expand All @@ -641,11 +659,13 @@ impl GltfLoader {
let textures = IoTaskPool::get().scope(|scope| {
gltf.textures().for_each(|gltf_texture| {
let asset_path = load_context.path().clone();
let gltf_document = &gltf.document;
let linear_textures = &linear_textures;
let buffer_data = &buffer_data;
scope.spawn(async move {
load_image(
gltf_texture,
gltf_document,
buffer_data,
linear_textures,
&asset_path,
Expand Down Expand Up @@ -1180,9 +1200,22 @@ impl AssetLoader for GltfLoader {
}
}

fn reject_unsupported_empty_texture_extensions(document: &Document) -> Result<(), GltfError> {
for extension in document.extensions_required() {
if matches!(extension, "EXT_texture_webp" | "MSFT_texture_dds") {
return Err(GltfError::UnsupportedRequiredExtension(
extension.to_string(),
));
}
}

Ok(())
}

/// Loads a glTF texture as a bevy [`Image`] and returns it together with its label.
async fn load_image<'a, 'b>(
gltf_texture: gltf::Texture<'a>,
gltf_document: &'a Document,
buffer_data: &[Vec<u8>],
linear_textures: &HashSet<usize>,
gltf_path: &'b AssetPath<'b>,
Expand All @@ -1197,7 +1230,16 @@ async fn load_image<'a, 'b>(
texture_sampler(&gltf_texture, default_sampler)
};

match gltf_texture.source().source() {
let gltf_image = texture_source(&gltf_texture, gltf_document).map_err(|source| {
GltfError::InvalidTextureBasisuSource {
texture: gltf_texture.index(),
value: source,
}
})?;
let gltf_image =
gltf_image.ok_or_else(|| GltfError::MissingImageSource(gltf_texture.index()))?;

match gltf_image.source() {
Source::View { view, mime_type } => {
let start = view.offset();
let end = view.offset() + view.length();
Expand Down Expand Up @@ -2121,6 +2163,28 @@ mod test {
use bevy_reflect::TypePath;
use bevy_world_serialization::WorldSerializationPlugin;

#[derive(TypePath)]
struct FakeKtx2Loader;

impl AssetLoader for FakeKtx2Loader {
type Asset = Image;
type Error = std::io::Error;
type Settings = ImageLoaderSettings;

async fn load(
&self,
_reader: &mut dyn bevy_asset::io::Reader,
_settings: &Self::Settings,
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
Ok(Image::default())
}

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

fn test_app(dir: Dir) -> App {
let mut app = App::new();
let reader = MemoryAssetReader { root: dir };
Expand Down Expand Up @@ -2691,6 +2755,173 @@ mod test {
});
}

#[test]
fn reads_khr_texture_basisu_source() {
let (mut app, dir) = test_app_custom_asset_source();

app.init_asset::<GltfMaterial>();

dir.insert_asset_text(
Path::new("abc.gltf"),
r#"
{
"asset": {
"version": "2.0"
},
"extensionsUsed": [
"KHR_texture_basisu"
],
"textures": [
{
"source": 0,
"extensions": {
"KHR_texture_basisu": {
"source": 1
}
}
}
],
"images": [
{
"uri": "abc.png"
},
{
"uri": "abc.ktx2"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0,
"texCoord": 0
}
}
}
]
}
"#,
);
dir.insert_asset_text(Path::new("abc.png"), "png");
dir.insert_asset_text(Path::new("abc.ktx2"), "ktx2");

app.init_asset::<Image>()
.register_asset_loader(FakeKtx2Loader);

let asset_server = app.world().resource::<AssetServer>().clone();
let handle: Handle<Gltf> = asset_server.load("custom://abc.gltf");
run_app_until(&mut app, |_world| {
asset_server
.is_loaded_with_dependencies(&handle)
.then_some(())
});
}

#[test]
fn reads_required_khr_texture_basisu_source() {
let (mut app, dir) = test_app_custom_asset_source();

app.init_asset::<GltfMaterial>();

dir.insert_asset_text(
Path::new("abc.gltf"),
r#"
{
"asset": {
"version": "2.0"
},
"extensionsUsed": [
"KHR_texture_basisu"
],
"extensionsRequired": [
"KHR_texture_basisu"
],
"textures": [
{
"extensions": {
"KHR_texture_basisu": {
"source": 0
}
}
}
],
"images": [
{
"uri": "abc.ktx2"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0,
"texCoord": 0
}
}
}
]
}
"#,
);
dir.insert_asset_text(Path::new("abc.ktx2"), "ktx2");

app.init_asset::<Image>()
.register_asset_loader(FakeKtx2Loader);

let asset_server = app.world().resource::<AssetServer>().clone();
let handle: Handle<Gltf> = asset_server.load("custom://abc.gltf");
run_app_until(&mut app, |_world| {
asset_server
.is_loaded_with_dependencies(&handle)
.then_some(())
});
}

#[test]
fn invalid_khr_texture_basisu_source_is_an_error() {
let (mut app, dir) = test_app_custom_asset_source();

dir.insert_asset_text(
Path::new("abc.gltf"),
r#"
{
"asset": {
"version": "2.0"
},
"extensionsUsed": [
"KHR_texture_basisu"
],
"textures": [
{
"extensions": {
"KHR_texture_basisu": {
"source": 0
}
}
}
]
}
"#,
);

app.init_asset::<Image>();

let asset_server = app.world().resource::<AssetServer>().clone();
let handle: Handle<Gltf> = asset_server.load("custom://abc.gltf");
run_app_until(&mut app, |_| match asset_server.load_state(&handle) {
LoadState::Failed(err) => {
let err = err.to_string();
assert!(
err.contains("invalid KHR_texture_basisu source: 0"),
"incorrect error message: {err}"
);
Some(())
}
LoadState::Loading => None,
state => panic!("Unexpected load state: {state:?}"),
});
}

#[test]
fn image_error_is_an_error() {
let (mut app, dir) = test_app_custom_asset_source();
Expand Down
Loading