Skip to content

Commit 0d41569

Browse files
committed
Support for KHR_texture_basisu in bevy-gltf
1 parent 654bcde commit 0d41569

4 files changed

Lines changed: 260 additions & 5 deletions

File tree

crates/bevy_gltf/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ gltf = { version = "1.4.0", default-features = false, features = [
5050
"KHR_materials_unlit",
5151
"KHR_materials_emissive_strength",
5252
"KHR_texture_transform",
53+
"allow_empty_texture",
5354
"extras",
5455
"extensions",
5556
"names",

crates/bevy_gltf/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
//! | `KHR_materials_variants` | ❌ | |
115115
//! | `KHR_materials_volume` | ✅ | |
116116
//! | `KHR_mesh_quantization` | ❌ | |
117-
//! | `KHR_texture_basisu` | ❌\* | |
117+
//! | `KHR_texture_basisu` | | `ktx2` |
118118
//! | `KHR_texture_transform` | ✅\** | |
119119
//! | `KHR_xmp_json_ld` | ❌ | |
120120
//! | `EXT_mesh_gpu_instancing` | ❌ | |

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use bevy_image::{ImageAddressMode, ImageFilterMode, ImageSamplerDescriptor};
22
use bevy_math::Affine2;
33

4-
use gltf::texture::{MagFilter, MinFilter, Texture, TextureTransform, WrappingMode};
4+
use gltf::{
5+
image::Image,
6+
texture::{MagFilter, MinFilter, Texture, TextureTransform, WrappingMode},
7+
Document,
8+
};
59

610
/// Extracts the texture sampler data from the glTF [`Texture`].
711
pub(crate) fn texture_sampler(
@@ -48,6 +52,27 @@ pub(crate) fn texture_sampler(
4852
sampler
4953
}
5054

55+
pub(crate) fn texture_source<'a>(
56+
texture: &Texture<'a>,
57+
document: &'a Document,
58+
) -> Result<Option<Image<'a>>, String> {
59+
if let Some(extension) = texture.extension_value("KHR_texture_basisu") {
60+
let source = extension
61+
.get("source")
62+
.and_then(|source| source.as_u64())
63+
.and_then(|source| usize::try_from(source).ok())
64+
.ok_or_else(|| extension.to_string())?;
65+
66+
return document
67+
.images()
68+
.nth(source)
69+
.ok_or_else(|| source.to_string())
70+
.map(Some);
71+
}
72+
73+
Ok(texture.source())
74+
}
75+
5176
pub(crate) fn address_mode(wrapping_mode: &WrappingMode) -> ImageAddressMode {
5277
match wrapping_mode {
5378
WrappingMode::ClampToEdge => ImageAddressMode::ClampToEdge,

crates/bevy_gltf/src/loader/mod.rs

Lines changed: 232 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use gltf::{
4444
accessor::Iter,
4545
image::Source,
4646
mesh::{util::ReadIndices, Mode},
47-
Material, Node, Semantic,
47+
Document, Material, Node, Semantic,
4848
};
4949
use serde::{Deserialize, Serialize};
5050
#[cfg(feature = "bevy_animation")]
@@ -72,7 +72,7 @@ use self::{
7272
},
7373
mesh::{primitive_name, primitive_topology},
7474
scene::{node_name, node_transform},
75-
texture::{texture_sampler, texture_transform_to_affine2},
75+
texture::{texture_sampler, texture_source, texture_transform_to_affine2},
7676
},
7777
};
7878
use crate::convert_coordinates::GltfConvertCoordinates;
@@ -92,6 +92,9 @@ pub enum GltfError {
9292
/// Invalid glTF file.
9393
#[error("invalid glTF file: {0}")]
9494
Gltf(#[from] gltf::Error),
95+
/// Unsupported required glTF extension.
96+
#[error("unsupported required glTF extension: {0}")]
97+
UnsupportedRequiredExtension(String),
9598
/// Binary blob is missing.
9699
#[error("binary blob is missing")]
97100
MissingBlob,
@@ -114,6 +117,17 @@ pub enum GltfError {
114117
/// The image URI was unable to be resolved with respect to the asset path.
115118
#[error("invalid image uri: {0}. asset path error={1}")]
116119
InvalidImageUri(String, ParseAssetPathError),
120+
/// A texture did not have an image source.
121+
#[error("texture {0} is missing an image source")]
122+
MissingImageSource(usize),
123+
/// A texture extension contained an invalid image source.
124+
#[error("texture {texture} contains an invalid KHR_texture_basisu source: {value}")]
125+
InvalidTextureBasisuSource {
126+
/// The texture index.
127+
texture: usize,
128+
/// The invalid source value.
129+
value: String,
130+
},
117131
/// Failed to read bytes from an asset path.
118132
#[error("failed to read bytes from an asset path: {0}")]
119133
ReadAssetBytesError(#[from] ReadAssetBytesError),
@@ -248,6 +262,7 @@ impl GltfLoader {
248262
} else {
249263
gltf::Gltf::from_slice_without_validation(bytes)?
250264
};
265+
reject_unsupported_empty_texture_extensions(&gltf.document)?;
251266

252267
// clone extensions to start with a fresh processing state
253268
let mut extensions = loader.extensions.read().await.clone();
@@ -619,6 +634,7 @@ impl GltfLoader {
619634
for texture in gltf.textures() {
620635
let image = load_image(
621636
texture.clone(),
637+
&gltf.document,
622638
&buffer_data,
623639
&linear_textures,
624640
load_context.path(),
@@ -641,11 +657,13 @@ impl GltfLoader {
641657
let textures = IoTaskPool::get().scope(|scope| {
642658
gltf.textures().for_each(|gltf_texture| {
643659
let asset_path = load_context.path().clone();
660+
let gltf_document = &gltf.document;
644661
let linear_textures = &linear_textures;
645662
let buffer_data = &buffer_data;
646663
scope.spawn(async move {
647664
load_image(
648665
gltf_texture,
666+
gltf_document,
649667
buffer_data,
650668
linear_textures,
651669
&asset_path,
@@ -1180,9 +1198,22 @@ impl AssetLoader for GltfLoader {
11801198
}
11811199
}
11821200

1201+
fn reject_unsupported_empty_texture_extensions(document: &Document) -> Result<(), GltfError> {
1202+
for extension in document.extensions_required() {
1203+
if matches!(extension, "EXT_texture_webp" | "MSFT_texture_dds") {
1204+
return Err(GltfError::UnsupportedRequiredExtension(
1205+
extension.to_string(),
1206+
));
1207+
}
1208+
}
1209+
1210+
Ok(())
1211+
}
1212+
11831213
/// Loads a glTF texture as a bevy [`Image`] and returns it together with its label.
11841214
async fn load_image<'a, 'b>(
11851215
gltf_texture: gltf::Texture<'a>,
1216+
gltf_document: &'a Document,
11861217
buffer_data: &[Vec<u8>],
11871218
linear_textures: &HashSet<usize>,
11881219
gltf_path: &'b AssetPath<'b>,
@@ -1197,7 +1228,16 @@ async fn load_image<'a, 'b>(
11971228
texture_sampler(&gltf_texture, default_sampler)
11981229
};
11991230

1200-
match gltf_texture.source().source() {
1231+
let gltf_image = texture_source(&gltf_texture, gltf_document).map_err(|source| {
1232+
GltfError::InvalidTextureBasisuSource {
1233+
texture: gltf_texture.index(),
1234+
value: source,
1235+
}
1236+
})?;
1237+
let gltf_image =
1238+
gltf_image.ok_or_else(|| GltfError::MissingImageSource(gltf_texture.index()))?;
1239+
1240+
match gltf_image.source() {
12011241
Source::View { view, mime_type } => {
12021242
let start = view.offset();
12031243
let end = view.offset() + view.length();
@@ -2121,6 +2161,28 @@ mod test {
21212161
use bevy_reflect::TypePath;
21222162
use bevy_world_serialization::WorldSerializationPlugin;
21232163

2164+
#[derive(TypePath)]
2165+
struct FakeKtx2Loader;
2166+
2167+
impl AssetLoader for FakeKtx2Loader {
2168+
type Asset = Image;
2169+
type Error = std::io::Error;
2170+
type Settings = ImageLoaderSettings;
2171+
2172+
async fn load(
2173+
&self,
2174+
_reader: &mut dyn bevy_asset::io::Reader,
2175+
_settings: &Self::Settings,
2176+
_load_context: &mut LoadContext<'_>,
2177+
) -> Result<Self::Asset, Self::Error> {
2178+
Ok(Image::default())
2179+
}
2180+
2181+
fn extensions(&self) -> &[&str] {
2182+
&["ktx2"]
2183+
}
2184+
}
2185+
21242186
fn test_app(dir: Dir) -> App {
21252187
let mut app = App::new();
21262188
let reader = MemoryAssetReader { root: dir };
@@ -2691,6 +2753,173 @@ mod test {
26912753
});
26922754
}
26932755

2756+
#[test]
2757+
fn reads_khr_texture_basisu_source() {
2758+
let (mut app, dir) = test_app_custom_asset_source();
2759+
2760+
app.init_asset::<GltfMaterial>();
2761+
2762+
dir.insert_asset_text(
2763+
Path::new("abc.gltf"),
2764+
r#"
2765+
{
2766+
"asset": {
2767+
"version": "2.0"
2768+
},
2769+
"extensionsUsed": [
2770+
"KHR_texture_basisu"
2771+
],
2772+
"textures": [
2773+
{
2774+
"source": 0,
2775+
"extensions": {
2776+
"KHR_texture_basisu": {
2777+
"source": 1
2778+
}
2779+
}
2780+
}
2781+
],
2782+
"images": [
2783+
{
2784+
"uri": "abc.png"
2785+
},
2786+
{
2787+
"uri": "abc.ktx2"
2788+
}
2789+
],
2790+
"materials": [
2791+
{
2792+
"pbrMetallicRoughness": {
2793+
"baseColorTexture": {
2794+
"index": 0,
2795+
"texCoord": 0
2796+
}
2797+
}
2798+
}
2799+
]
2800+
}
2801+
"#,
2802+
);
2803+
dir.insert_asset_text(Path::new("abc.png"), "png");
2804+
dir.insert_asset_text(Path::new("abc.ktx2"), "ktx2");
2805+
2806+
app.init_asset::<Image>()
2807+
.register_asset_loader(FakeKtx2Loader);
2808+
2809+
let asset_server = app.world().resource::<AssetServer>().clone();
2810+
let handle: Handle<Gltf> = asset_server.load("custom://abc.gltf");
2811+
run_app_until(&mut app, |_world| {
2812+
asset_server
2813+
.is_loaded_with_dependencies(&handle)
2814+
.then_some(())
2815+
});
2816+
}
2817+
2818+
#[test]
2819+
fn reads_required_khr_texture_basisu_source() {
2820+
let (mut app, dir) = test_app_custom_asset_source();
2821+
2822+
app.init_asset::<GltfMaterial>();
2823+
2824+
dir.insert_asset_text(
2825+
Path::new("abc.gltf"),
2826+
r#"
2827+
{
2828+
"asset": {
2829+
"version": "2.0"
2830+
},
2831+
"extensionsUsed": [
2832+
"KHR_texture_basisu"
2833+
],
2834+
"extensionsRequired": [
2835+
"KHR_texture_basisu"
2836+
],
2837+
"textures": [
2838+
{
2839+
"extensions": {
2840+
"KHR_texture_basisu": {
2841+
"source": 0
2842+
}
2843+
}
2844+
}
2845+
],
2846+
"images": [
2847+
{
2848+
"uri": "abc.ktx2"
2849+
}
2850+
],
2851+
"materials": [
2852+
{
2853+
"pbrMetallicRoughness": {
2854+
"baseColorTexture": {
2855+
"index": 0,
2856+
"texCoord": 0
2857+
}
2858+
}
2859+
}
2860+
]
2861+
}
2862+
"#,
2863+
);
2864+
dir.insert_asset_text(Path::new("abc.ktx2"), "ktx2");
2865+
2866+
app.init_asset::<Image>()
2867+
.register_asset_loader(FakeKtx2Loader);
2868+
2869+
let asset_server = app.world().resource::<AssetServer>().clone();
2870+
let handle: Handle<Gltf> = asset_server.load("custom://abc.gltf");
2871+
run_app_until(&mut app, |_world| {
2872+
asset_server
2873+
.is_loaded_with_dependencies(&handle)
2874+
.then_some(())
2875+
});
2876+
}
2877+
2878+
#[test]
2879+
fn invalid_khr_texture_basisu_source_is_an_error() {
2880+
let (mut app, dir) = test_app_custom_asset_source();
2881+
2882+
dir.insert_asset_text(
2883+
Path::new("abc.gltf"),
2884+
r#"
2885+
{
2886+
"asset": {
2887+
"version": "2.0"
2888+
},
2889+
"extensionsUsed": [
2890+
"KHR_texture_basisu"
2891+
],
2892+
"textures": [
2893+
{
2894+
"extensions": {
2895+
"KHR_texture_basisu": {
2896+
"source": 0
2897+
}
2898+
}
2899+
}
2900+
]
2901+
}
2902+
"#,
2903+
);
2904+
2905+
app.init_asset::<Image>();
2906+
2907+
let asset_server = app.world().resource::<AssetServer>().clone();
2908+
let handle: Handle<Gltf> = asset_server.load("custom://abc.gltf");
2909+
run_app_until(&mut app, |_| match asset_server.load_state(&handle) {
2910+
LoadState::Failed(err) => {
2911+
let err = err.to_string();
2912+
assert!(
2913+
err.contains("invalid KHR_texture_basisu source: 0"),
2914+
"incorrect error message: {err}"
2915+
);
2916+
Some(())
2917+
}
2918+
LoadState::Loading => None,
2919+
state => panic!("Unexpected load state: {state:?}"),
2920+
});
2921+
}
2922+
26942923
#[test]
26952924
fn image_error_is_an_error() {
26962925
let (mut app, dir) = test_app_custom_asset_source();

0 commit comments

Comments
 (0)