diff --git a/Cargo.toml b/Cargo.toml index 9f938756170ddf..88d62e2400206b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,10 +64,15 @@ wgpu_trace = ["bevy_internal/wgpu_trace"] # Image format support for texture loading (PNG and HDR are enabled by default) hdr = ["bevy_internal/hdr"] png = ["bevy_internal/png"] -dds = ["bevy_internal/dds"] tga = ["bevy_internal/tga"] jpeg = ["bevy_internal/jpeg"] bmp = ["bevy_internal/bmp"] +basis-universal = ["bevy_internal/basis-universal"] +dds = ["bevy_internal/dds"] +ktx2 = ["bevy_internal/ktx2"] +# For ktx2 supercompression +zlib = ["bevy_internal/zlib"] +zstd = ["bevy_internal/zstd"] # Audio format support (vorbis is enabled by default) flac = ["bevy_internal/flac"] diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 464f89c4a9be69..db42a5731dc5f6 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -3,7 +3,7 @@ use bevy_asset::{ AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset, }; use bevy_core::Name; -use bevy_ecs::world::World; +use bevy_ecs::{prelude::FromWorld, world::World}; use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder}; use bevy_log::warn; use bevy_math::{Mat4, Vec3}; @@ -18,10 +18,9 @@ use bevy_render::{ color::Color, mesh::{Indices, Mesh, VertexAttributeValues}, primitives::{Aabb, Frustum}, - render_resource::{ - AddressMode, FilterMode, PrimitiveTopology, SamplerDescriptor, TextureFormat, - }, - texture::{Image, ImageType, TextureError}, + render_resource::{AddressMode, FilterMode, PrimitiveTopology, SamplerDescriptor}, + renderer::RenderDevice, + texture::{CompressedImageFormats, Image, ImageType, TextureError}, view::VisibleEntities, }; use bevy_scene::Scene; @@ -60,8 +59,9 @@ pub enum GltfError { } /// Loads glTF files with all of their data as their corresponding bevy representations. -#[derive(Default)] -pub struct GltfLoader; +pub struct GltfLoader { + supported_compressed_formats: CompressedImageFormats, +} impl AssetLoader for GltfLoader { fn load<'a>( @@ -69,7 +69,9 @@ impl AssetLoader for GltfLoader { bytes: &'a [u8], load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result<()>> { - Box::pin(async move { Ok(load_gltf(bytes, load_context).await?) }) + Box::pin(async move { + Ok(load_gltf(bytes, load_context, self.supported_compressed_formats).await?) + }) } fn extensions(&self) -> &[&str] { @@ -77,10 +79,21 @@ impl AssetLoader for GltfLoader { } } +impl FromWorld for GltfLoader { + fn from_world(world: &mut World) -> Self { + Self { + supported_compressed_formats: CompressedImageFormats::from_features( + world.resource::().features(), + ), + } + } +} + /// Loads an entire glTF file. async fn load_gltf<'a, 'b>( bytes: &'a [u8], load_context: &'a mut LoadContext<'b>, + supported_compressed_formats: CompressedImageFormats, ) -> Result<(), GltfError> { let gltf = gltf::Gltf::from_slice(bytes)?; let buffer_data = load_buffers(&gltf, load_context, load_context.path()).await?; @@ -251,8 +264,14 @@ async fn load_gltf<'a, 'b>( // to avoid https://github.com/bevyengine/bevy/pull/2725 if gltf.textures().len() == 1 || cfg!(target_arch = "wasm32") { for gltf_texture in gltf.textures() { - let (texture, label) = - load_texture(gltf_texture, &buffer_data, &linear_textures, load_context).await?; + let (texture, label) = load_texture( + gltf_texture, + &buffer_data, + &linear_textures, + load_context, + supported_compressed_formats, + ) + .await?; load_context.set_labeled_asset(&label, LoadedAsset::new(texture)); } } else { @@ -265,7 +284,14 @@ async fn load_gltf<'a, 'b>( let load_context: &LoadContext = load_context; let buffer_data = &buffer_data; scope.spawn(async move { - load_texture(gltf_texture, buffer_data, linear_textures, load_context).await + load_texture( + gltf_texture, + buffer_data, + linear_textures, + load_context, + supported_compressed_formats, + ) + .await }); }); }) @@ -334,13 +360,20 @@ async fn load_texture<'a>( buffer_data: &[Vec], linear_textures: &HashSet, load_context: &LoadContext<'a>, + supported_compressed_formats: CompressedImageFormats, ) -> Result<(Image, String), GltfError> { + let is_srgb = !linear_textures.contains(&gltf_texture.index()); let mut texture = match gltf_texture.source().source() { gltf::image::Source::View { view, mime_type } => { let start = view.offset() as usize; let end = (view.offset() + view.length()) as usize; let buffer = &buffer_data[view.buffer().index()][start..end]; - Image::from_buffer(buffer, ImageType::MimeType(mime_type))? + Image::from_buffer( + buffer, + ImageType::MimeType(mime_type), + supported_compressed_formats, + is_srgb, + )? } gltf::image::Source::Uri { uri, mime_type } => { let uri = percent_encoding::percent_decode_str(uri) @@ -363,13 +396,12 @@ async fn load_texture<'a>( Image::from_buffer( &bytes, mime_type.map(ImageType::MimeType).unwrap_or(image_type), + supported_compressed_formats, + is_srgb, )? } }; texture.sampler_descriptor = texture_sampler(&gltf_texture); - if (linear_textures).contains(&gltf_texture.index()) { - texture.texture_descriptor.format = TextureFormat::Rgba8Unorm; - } Ok((texture, texture_label(&gltf_texture))) } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 4b9891f3ff0658..900d71b4c55007 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -19,10 +19,15 @@ debug_asset_server = ["bevy_asset/debug_asset_server"] # Image format support for texture loading (PNG and HDR are enabled by default) hdr = ["bevy_render/hdr"] png = ["bevy_render/png"] -dds = ["bevy_render/dds"] tga = ["bevy_render/tga"] jpeg = ["bevy_render/jpeg"] bmp = ["bevy_render/bmp"] +basis-universal = ["bevy_render/basis-universal"] +dds = ["bevy_render/dds"] +ktx2 = ["bevy_render/ktx2"] +# For ktx2 supercompression +zlib = ["bevy_render/zlib"] +zstd = ["bevy_render/zstd"] # Audio format support (vorbis is enabled by default) flac = ["bevy_audio/flac"] diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index d4badf887d49c2..9b26f79c3d8288 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -58,6 +58,8 @@ impl PluginGroup for DefaultPlugins { #[cfg(feature = "bevy_pbr")] group.add(bevy_pbr::PbrPlugin::default()); + // NOTE: Load this after renderer initialization so that it knows about the supported + // compressed texture formats #[cfg(feature = "bevy_gltf")] group.add(bevy_gltf::GltfPlugin::default()); diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 803960a6478442..a9dccbdc3b9609 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -123,6 +123,7 @@ bitflags::bitflags! { const ALPHA_MODE_OPAQUE = (1 << 6); const ALPHA_MODE_MASK = (1 << 7); const ALPHA_MODE_BLEND = (1 << 8); + const TWO_COMPONENT_NORMAL_MAP = (1 << 9); const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -246,6 +247,22 @@ impl RenderAsset for StandardMaterial { flags |= StandardMaterialFlags::UNLIT; } let has_normal_map = material.normal_map_texture.is_some(); + if has_normal_map { + match gpu_images + .get(material.normal_map_texture.as_ref().unwrap()) + .unwrap() + .texture_format + { + // All 2-component unorm formats + TextureFormat::Rg8Unorm + | TextureFormat::Rg16Unorm + | TextureFormat::Bc5RgUnorm + | TextureFormat::EacRg11Unorm => { + flags |= StandardMaterialFlags::TWO_COMPONENT_NORMAL_MAP + } + _ => {} + } + } // NOTE: 0.5 is from the glTF default - do we want this? let mut alpha_cutoff = 0.5; match material.alpha_mode { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index ac0ec3acd43607..8d00086881215c 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -327,6 +327,7 @@ impl FromWorld for MeshPipeline { GpuImage { texture, texture_view, + texture_format: image.texture_descriptor.format, sampler, size: Size::new( image.texture_descriptor.size.width as f32, diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 073a77fb1520a7..1e1e69f02a2351 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -58,6 +58,7 @@ let STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u; let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 64u; let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 128u; let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 256u; +let STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 512u; [[group(1), binding(0)]] var material: StandardMaterial; @@ -515,7 +516,16 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { #ifdef VERTEX_TANGENTS #ifdef STANDARDMATERIAL_NORMAL_MAP let TBN = mat3x3(T, B, N); - N = TBN * normalize(textureSample(normal_map_texture, normal_map_sampler, in.uv).rgb * 2.0 - 1.0); + // Nt is the tangent-space normal. + var Nt: vec3; + if ((material.flags & STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u) { + // Only use the xy components and derive z for 2-component normal maps. + Nt = vec3(textureSample(normal_map_texture, normal_map_sampler, in.uv).rg * 2.0 - 1.0, 0.0); + Nt.z = sqrt(1.0 - Nt.x * Nt.x - Nt.y * Nt.y); + } else { + Nt = textureSample(normal_map_texture, normal_map_sampler, in.uv).rgb * 2.0 - 1.0; + } + N = normalize(TBN * Nt); #endif #endif diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 49b3db948ad201..8bce6efe1caab1 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -11,10 +11,15 @@ keywords = ["bevy"] [features] png = ["image/png"] hdr = ["image/hdr"] -dds = ["image/dds"] tga = ["image/tga"] jpeg = ["image/jpeg"] bmp = ["image/bmp"] +dds = ["ddsfile"] + +# For ktx2 supercompression +zlib = ["flate2"] +zstd = ["ruzstd"] + trace = [] wgpu_trace = ["wgpu/trace"] ci_limits = [] @@ -54,3 +59,10 @@ hexasphere = "7.0.0" parking_lot = "0.11.0" regex = "1.5" copyless = "0.1.5" +ddsfile = { version = "0.5.0", optional = true } +ktx2 = { version = "0.3.0", optional = true } +# For ktx2 supercompression +flate2 = { version = "1.0.22", optional = true } +ruzstd = { version = "0.2.4", optional = true } +# For transcoding of UASTC/ETC1S universal formats, and for .basis file support +basis-universal = { version = "0.2.0", optional = true } diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 36f4cbc2925000..5133fdc9a198c5 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -289,6 +289,8 @@ impl Plugin for RenderPlugin { .add_plugin(CameraPlugin) .add_plugin(ViewPlugin) .add_plugin(MeshPlugin) + // NOTE: Load this after renderer initialization so that it knows about the supported + // compressed texture formats .add_plugin(ImagePlugin); } } diff --git a/crates/bevy_render/src/renderer/render_device.rs b/crates/bevy_render/src/renderer/render_device.rs index e5659cacd8cea9..7ff9e88ef6dd0d 100644 --- a/crates/bevy_render/src/renderer/render_device.rs +++ b/crates/bevy_render/src/renderer/render_device.rs @@ -6,6 +6,8 @@ use futures_lite::future; use std::sync::Arc; use wgpu::util::DeviceExt; +use super::RenderQueue; + /// This GPU device is responsible for the creation of most rendering and compute resources. #[derive(Clone)] pub struct RenderDevice { @@ -121,6 +123,22 @@ impl RenderDevice { Buffer::from(wgpu_buffer) } + /// Creates a new [`Texture`] and initializes it with the specified data. + /// + /// `desc` specifies the general format of the texture. + /// `data` is the raw data. + pub fn create_texture_with_data( + &self, + render_queue: &RenderQueue, + desc: &wgpu::TextureDescriptor, + data: &[u8], + ) -> Texture { + let wgpu_texture = self + .device + .create_texture_with_data(render_queue.as_ref(), desc, data); + Texture::from(wgpu_texture) + } + /// Creates a new [`Texture`]. /// /// `desc` specifies the general format of the texture. diff --git a/crates/bevy_render/src/texture/basis.rs b/crates/bevy_render/src/texture/basis.rs new file mode 100644 index 00000000000000..34e817c95ed0b3 --- /dev/null +++ b/crates/bevy_render/src/texture/basis.rs @@ -0,0 +1,176 @@ +use basis_universal::{ + BasisTextureType, DecodeFlags, TranscodeParameters, Transcoder, TranscoderTextureFormat, +}; +use wgpu::{Extent3d, TextureDimension, TextureFormat}; + +use super::{CompressedImageFormats, Image, TextureError}; + +pub fn basis_buffer_to_image( + buffer: &[u8], + supported_compressed_formats: CompressedImageFormats, + is_srgb: bool, +) -> Result { + let mut transcoder = Transcoder::new(); + + #[cfg(debug_assertions)] + if !transcoder.validate_file_checksums(buffer, true) { + return Err(TextureError::InvalidData("Invalid checksum".to_string())); + } + if !transcoder.validate_header(buffer) { + return Err(TextureError::InvalidData("Invalid header".to_string())); + } + + let image0_info = if let Some(image_info) = transcoder.image_info(buffer, 0) { + image_info + } else { + return Err(TextureError::InvalidData( + "Failed to get image info".to_string(), + )); + }; + + // First deal with transcoding to the desired format + // FIXME: Use external metadata to transcode to more appropriate formats for 1- or 2-component sources + let (transcode_format, texture_format) = + get_transcoded_formats(supported_compressed_formats, is_srgb); + let basis_texture_format = transcoder.basis_texture_format(buffer); + if !basis_texture_format.can_transcode_to_format(transcode_format) { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?} cannot be transcoded to {:?}", + basis_texture_format, transcode_format + ))); + } + transcoder.prepare_transcoding(buffer).map_err(|_| { + TextureError::TranscodeError(format!( + "Failed to prepare for transcoding from {:?}", + basis_texture_format + )) + })?; + let mut transcoded = Vec::new(); + + let image_count = transcoder.image_count(buffer); + let texture_type = transcoder.basis_texture_type(buffer); + if texture_type == BasisTextureType::TextureTypeCubemapArray && image_count % 6 != 0 { + return Err(TextureError::InvalidData(format!( + "Basis file with cube map array texture with non-modulo 6 number of images: {}", + image_count, + ))); + } + + let image0_mip_level_count = transcoder.image_level_count(buffer, 0); + for image_index in 0..image_count { + if let Some(image_info) = transcoder.image_info(buffer, image_index) { + if texture_type == BasisTextureType::TextureType2D + && (image_info.m_orig_width != image0_info.m_orig_width + || image_info.m_orig_height != image0_info.m_orig_height) + { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Basis file with multiple 2D textures with different sizes not supported. Image {} {}x{}, image 0 {}x{}", + image_index, + image_info.m_orig_width, + image_info.m_orig_height, + image0_info.m_orig_width, + image0_info.m_orig_height, + ))); + } + } + let mip_level_count = transcoder.image_level_count(buffer, image_index); + if mip_level_count != image0_mip_level_count { + return Err(TextureError::InvalidData(format!( + "Array or volume texture has inconsistent number of mip levels. Image {} has {} but image 0 has {}", + image_index, + mip_level_count, + image0_mip_level_count, + ))); + } + for level_index in 0..mip_level_count { + let mut data = transcoder + .transcode_image_level( + buffer, + transcode_format, + TranscodeParameters { + image_index, + level_index, + decode_flags: Some(DecodeFlags::HIGH_QUALITY), + ..Default::default() + }, + ) + .map_err(|error| { + TextureError::TranscodeError(format!( + "Failed to transcode mip level {} from {:?} to {:?}: {:?}", + level_index, basis_texture_format, transcode_format, error + )) + })?; + transcoded.append(&mut data); + } + } + + // Then prepare the Image + let mut image = Image::default(); + image.texture_descriptor.size = Extent3d { + width: image0_info.m_orig_width, + height: image0_info.m_orig_height, + depth_or_array_layers: image_count, + }; + image.texture_descriptor.mip_level_count = image0_mip_level_count; + image.texture_descriptor.format = texture_format; + image.texture_descriptor.dimension = match texture_type { + BasisTextureType::TextureType2D => TextureDimension::D2, + BasisTextureType::TextureType2DArray => TextureDimension::D2, + BasisTextureType::TextureTypeCubemapArray => TextureDimension::D2, + BasisTextureType::TextureTypeVolume => TextureDimension::D3, + basis_texture_type => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + basis_texture_type + ))) + } + }; + image.data = transcoded; + Ok(image) +} + +pub fn get_transcoded_formats( + supported_compressed_formats: CompressedImageFormats, + is_srgb: bool, +) -> (TranscoderTextureFormat, TextureFormat) { + // NOTE: UASTC can be losslessly transcoded to ASTC4x4 and ASTC uses the same + // space as BC7 (128-bits per 4x4 texel block) so prefer ASTC over BC for + // transcoding speed and quality. + if supported_compressed_formats.contains(CompressedImageFormats::ASTC_LDR) { + ( + TranscoderTextureFormat::ASTC_4x4_RGBA, + if is_srgb { + TextureFormat::Astc4x4RgbaUnormSrgb + } else { + TextureFormat::Astc4x4RgbaUnorm + }, + ) + } else if supported_compressed_formats.contains(CompressedImageFormats::BC) { + ( + TranscoderTextureFormat::BC7_RGBA, + if is_srgb { + TextureFormat::Bc7RgbaUnormSrgb + } else { + TextureFormat::Bc7RgbaUnorm + }, + ) + } else if supported_compressed_formats.contains(CompressedImageFormats::ETC2) { + ( + TranscoderTextureFormat::ETC2_RGBA, + if is_srgb { + TextureFormat::Etc2Rgba8UnormSrgb + } else { + TextureFormat::Etc2Rgba8Unorm + }, + ) + } else { + ( + TranscoderTextureFormat::RGBA32, + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }, + ) + } +} diff --git a/crates/bevy_render/src/texture/dds.rs b/crates/bevy_render/src/texture/dds.rs new file mode 100644 index 00000000000000..49db9d090c07e2 --- /dev/null +++ b/crates/bevy_render/src/texture/dds.rs @@ -0,0 +1,338 @@ +use ddsfile::{D3DFormat, Dds, DxgiFormat}; +use std::io::Cursor; +use wgpu::{Extent3d, TextureDimension, TextureFormat}; + +use super::{CompressedImageFormats, Image, TextureError}; + +pub fn dds_buffer_to_image( + buffer: &[u8], + supported_compressed_formats: CompressedImageFormats, + is_srgb: bool, +) -> Result { + let mut cursor = Cursor::new(buffer); + let dds = Dds::read(&mut cursor).expect("Failed to parse DDS file"); + let texture_format = dds_format_to_texture_format(&dds, is_srgb)?; + if !supported_compressed_formats.supports(texture_format) { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Format not supported by this GPU: {:?}", + texture_format + ))); + } + let mut image = Image::default(); + image.texture_descriptor.size = Extent3d { + width: dds.get_width(), + height: dds.get_height(), + depth_or_array_layers: if dds.get_num_array_layers() > 1 { + dds.get_num_array_layers() + } else { + dds.get_depth() + }, + }; + image.texture_descriptor.mip_level_count = dds.get_num_mipmap_levels(); + image.texture_descriptor.format = texture_format; + image.texture_descriptor.dimension = if dds.get_depth() > 1 { + TextureDimension::D3 + } else if image.is_compressed() || dds.get_height() > 1 { + TextureDimension::D2 + } else { + TextureDimension::D1 + }; + image.data = dds.data; + Ok(image) +} + +pub fn dds_format_to_texture_format( + dds: &Dds, + is_srgb: bool, +) -> Result { + Ok(if let Some(d3d_format) = dds.get_d3d_format() { + match d3d_format { + D3DFormat::A8B8G8R8 => { + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + } + } + D3DFormat::A8 => TextureFormat::R8Unorm, + D3DFormat::A8R8G8B8 => { + if is_srgb { + TextureFormat::Bgra8UnormSrgb + } else { + TextureFormat::Bgra8Unorm + } + } + D3DFormat::G16R16 => TextureFormat::Rg16Uint, + D3DFormat::A2B10G10R10 => TextureFormat::Rgb10a2Unorm, + D3DFormat::A8L8 => TextureFormat::Rg8Uint, + D3DFormat::L16 => TextureFormat::R16Uint, + D3DFormat::L8 => TextureFormat::R8Uint, + D3DFormat::DXT1 => { + if is_srgb { + TextureFormat::Bc1RgbaUnormSrgb + } else { + TextureFormat::Bc1RgbaUnorm + } + } + D3DFormat::DXT3 => { + if is_srgb { + TextureFormat::Bc2RgbaUnormSrgb + } else { + TextureFormat::Bc2RgbaUnorm + } + } + D3DFormat::DXT5 => { + if is_srgb { + TextureFormat::Bc3RgbaUnormSrgb + } else { + TextureFormat::Bc3RgbaUnorm + } + } + D3DFormat::A16B16G16R16 => TextureFormat::Rgba16Uint, + D3DFormat::Q16W16V16U16 => TextureFormat::Rgba16Sint, + D3DFormat::R16F => TextureFormat::R16Float, + D3DFormat::G16R16F => TextureFormat::Rg16Float, + D3DFormat::A16B16G16R16F => TextureFormat::Rgba16Float, + D3DFormat::R32F => TextureFormat::R32Float, + D3DFormat::G32R32F => TextureFormat::Rg32Float, + D3DFormat::A32B32G32R32F => TextureFormat::Rgba32Float, + D3DFormat::DXT2 => { + if is_srgb { + TextureFormat::Bc2RgbaUnormSrgb + } else { + TextureFormat::Bc2RgbaUnorm + } + } + D3DFormat::DXT4 => { + if is_srgb { + TextureFormat::Bc3RgbaUnormSrgb + } else { + TextureFormat::Bc3RgbaUnorm + } + } + D3DFormat::A1R5G5B5 + | D3DFormat::R5G6B5 + // FIXME: Map to argb format and user has to know to ignore the alpha channel? + | D3DFormat::X8R8G8B8 + // FIXME: Map to argb format and user has to know to ignore the alpha channel? + | D3DFormat::X8B8G8R8 + | D3DFormat::A2R10G10B10 + | D3DFormat::R8G8B8 + | D3DFormat::X1R5G5B5 + | D3DFormat::A4R4G4B4 + | D3DFormat::X4R4G4B4 + | D3DFormat::A8R3G3B2 + | D3DFormat::A4L4 + | D3DFormat::R8G8_B8G8 + | D3DFormat::G8R8_G8B8 + | D3DFormat::UYVY + | D3DFormat::YUY2 + | D3DFormat::CXV8U8 => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + d3d_format + ))) + } + } + } else if let Some(dxgi_format) = dds.get_dxgi_format() { + match dxgi_format { + DxgiFormat::R32G32B32A32_Typeless => TextureFormat::Rgba32Float, + DxgiFormat::R32G32B32A32_Float => TextureFormat::Rgba32Float, + DxgiFormat::R32G32B32A32_UInt => TextureFormat::Rgba32Uint, + DxgiFormat::R32G32B32A32_SInt => TextureFormat::Rgba32Sint, + DxgiFormat::R16G16B16A16_Typeless => TextureFormat::Rgba16Float, + DxgiFormat::R16G16B16A16_Float => TextureFormat::Rgba16Float, + DxgiFormat::R16G16B16A16_UNorm => TextureFormat::Rgba16Unorm, + DxgiFormat::R16G16B16A16_UInt => TextureFormat::Rgba16Uint, + DxgiFormat::R16G16B16A16_SNorm => TextureFormat::Rgba16Snorm, + DxgiFormat::R16G16B16A16_SInt => TextureFormat::Rgba16Sint, + DxgiFormat::R32G32_Typeless => TextureFormat::Rg32Float, + DxgiFormat::R32G32_Float => TextureFormat::Rg32Float, + DxgiFormat::R32G32_UInt => TextureFormat::Rg32Uint, + DxgiFormat::R32G32_SInt => TextureFormat::Rg32Sint, + DxgiFormat::R10G10B10A2_Typeless => TextureFormat::Rgb10a2Unorm, + DxgiFormat::R10G10B10A2_UNorm => TextureFormat::Rgb10a2Unorm, + DxgiFormat::R11G11B10_Float => TextureFormat::Rg11b10Float, + DxgiFormat::R8G8B8A8_Typeless => { + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + } + } + DxgiFormat::R8G8B8A8_UNorm => { + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + } + } + DxgiFormat::R8G8B8A8_UNorm_sRGB => { + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + } + } + DxgiFormat::R8G8B8A8_UInt => TextureFormat::Rgba8Uint, + DxgiFormat::R8G8B8A8_SNorm => TextureFormat::Rgba8Snorm, + DxgiFormat::R8G8B8A8_SInt => TextureFormat::Rgba8Sint, + DxgiFormat::R16G16_Typeless => TextureFormat::Rg16Float, + DxgiFormat::R16G16_Float => TextureFormat::Rg16Float, + DxgiFormat::R16G16_UNorm => TextureFormat::Rg16Unorm, + DxgiFormat::R16G16_UInt => TextureFormat::Rg16Uint, + DxgiFormat::R16G16_SNorm => TextureFormat::Rg16Snorm, + DxgiFormat::R16G16_SInt => TextureFormat::Rg16Sint, + DxgiFormat::R32_Typeless => TextureFormat::R32Float, + DxgiFormat::D32_Float => TextureFormat::Depth32Float, + DxgiFormat::R32_Float => TextureFormat::R32Float, + DxgiFormat::R32_UInt => TextureFormat::R32Uint, + DxgiFormat::R32_SInt => TextureFormat::R32Sint, + DxgiFormat::R24G8_Typeless => TextureFormat::Depth24PlusStencil8, + DxgiFormat::D24_UNorm_S8_UInt => TextureFormat::Depth24PlusStencil8, + DxgiFormat::R24_UNorm_X8_Typeless => TextureFormat::Depth24Plus, + DxgiFormat::R8G8_Typeless => TextureFormat::Rg8Unorm, + DxgiFormat::R8G8_UNorm => TextureFormat::Rg8Unorm, + DxgiFormat::R8G8_UInt => TextureFormat::Rg8Uint, + DxgiFormat::R8G8_SNorm => TextureFormat::Rg8Snorm, + DxgiFormat::R8G8_SInt => TextureFormat::Rg8Sint, + DxgiFormat::R16_Typeless => TextureFormat::R16Float, + DxgiFormat::R16_Float => TextureFormat::R16Float, + DxgiFormat::R16_UNorm => TextureFormat::R16Unorm, + DxgiFormat::R16_UInt => TextureFormat::R16Uint, + DxgiFormat::R16_SNorm => TextureFormat::R16Snorm, + DxgiFormat::R16_SInt => TextureFormat::R16Sint, + DxgiFormat::R8_Typeless => TextureFormat::R8Unorm, + DxgiFormat::R8_UNorm => TextureFormat::R8Unorm, + DxgiFormat::R8_UInt => TextureFormat::R8Uint, + DxgiFormat::R8_SNorm => TextureFormat::R8Snorm, + DxgiFormat::R8_SInt => TextureFormat::R8Sint, + DxgiFormat::R9G9B9E5_SharedExp => TextureFormat::Rgb9e5Ufloat, + DxgiFormat::BC1_Typeless => { + if is_srgb { + TextureFormat::Bc1RgbaUnormSrgb + } else { + TextureFormat::Bc1RgbaUnorm + } + } + DxgiFormat::BC1_UNorm => { + if is_srgb { + TextureFormat::Bc1RgbaUnormSrgb + } else { + TextureFormat::Bc1RgbaUnorm + } + } + DxgiFormat::BC1_UNorm_sRGB => { + if is_srgb { + TextureFormat::Bc1RgbaUnormSrgb + } else { + TextureFormat::Bc1RgbaUnorm + } + } + DxgiFormat::BC2_Typeless => { + if is_srgb { + TextureFormat::Bc2RgbaUnormSrgb + } else { + TextureFormat::Bc2RgbaUnorm + } + } + DxgiFormat::BC2_UNorm => { + if is_srgb { + TextureFormat::Bc2RgbaUnormSrgb + } else { + TextureFormat::Bc2RgbaUnorm + } + } + DxgiFormat::BC2_UNorm_sRGB => { + if is_srgb { + TextureFormat::Bc2RgbaUnormSrgb + } else { + TextureFormat::Bc2RgbaUnorm + } + } + DxgiFormat::BC3_Typeless => { + if is_srgb { + TextureFormat::Bc3RgbaUnormSrgb + } else { + TextureFormat::Bc3RgbaUnorm + } + } + DxgiFormat::BC3_UNorm => { + if is_srgb { + TextureFormat::Bc3RgbaUnormSrgb + } else { + TextureFormat::Bc3RgbaUnorm + } + } + DxgiFormat::BC3_UNorm_sRGB => { + if is_srgb { + TextureFormat::Bc3RgbaUnormSrgb + } else { + TextureFormat::Bc3RgbaUnorm + } + } + DxgiFormat::BC4_Typeless => TextureFormat::Bc4RUnorm, + DxgiFormat::BC4_UNorm => TextureFormat::Bc4RUnorm, + DxgiFormat::BC4_SNorm => TextureFormat::Bc4RSnorm, + DxgiFormat::BC5_Typeless => TextureFormat::Bc5RgUnorm, + DxgiFormat::BC5_UNorm => TextureFormat::Bc5RgUnorm, + DxgiFormat::BC5_SNorm => TextureFormat::Bc5RgSnorm, + DxgiFormat::B8G8R8A8_UNorm => { + if is_srgb { + TextureFormat::Bgra8UnormSrgb + } else { + TextureFormat::Bgra8Unorm + } + } + DxgiFormat::B8G8R8A8_Typeless => { + if is_srgb { + TextureFormat::Bgra8UnormSrgb + } else { + TextureFormat::Bgra8Unorm + } + } + DxgiFormat::B8G8R8A8_UNorm_sRGB => { + if is_srgb { + TextureFormat::Bgra8UnormSrgb + } else { + TextureFormat::Bgra8Unorm + } + } + + DxgiFormat::BC6H_Typeless => TextureFormat::Bc6hRgbUfloat, + DxgiFormat::BC6H_UF16 => TextureFormat::Bc6hRgbUfloat, + DxgiFormat::BC6H_SF16 => TextureFormat::Bc6hRgbSfloat, + DxgiFormat::BC7_Typeless => { + if is_srgb { + TextureFormat::Bc7RgbaUnormSrgb + } else { + TextureFormat::Bc7RgbaUnorm + } + } + DxgiFormat::BC7_UNorm => { + if is_srgb { + TextureFormat::Bc7RgbaUnormSrgb + } else { + TextureFormat::Bc7RgbaUnorm + } + } + DxgiFormat::BC7_UNorm_sRGB => { + if is_srgb { + TextureFormat::Bc7RgbaUnormSrgb + } else { + TextureFormat::Bc7RgbaUnorm + } + } + _ => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + dxgi_format + ))) + } + } + } else { + return Err(TextureError::UnsupportedTextureFormat( + "unspecified".to_string(), + )); + }) +} diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index 2fed3f23a7fbc7..fa4afe04924efd 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -1,3 +1,10 @@ +#[cfg(feature = "basis-universal")] +use super::basis::*; +#[cfg(feature = "dds")] +use super::dds::*; +#[cfg(feature = "ktx2")] +use super::ktx2::*; + use super::image_texture_conversion::image_to_texture; use crate::{ render_asset::{PrepareAssetError, RenderAsset}, @@ -20,6 +27,82 @@ pub const SAMPLER_ASSET_INDEX: u64 = 1; pub const DEFAULT_IMAGE_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Image::TYPE_UUID, 13148262314052771789); +#[derive(Debug)] +pub enum ImageFormat { + Avif, + Basis, + Bmp, + Dds, + Farbfeld, + Gif, + Hdr, + Ico, + Jpeg, + Ktx2, + Png, + Pnm, + Tga, + Tiff, + WebP, +} + +impl ImageFormat { + pub fn from_mime_type(mime_type: &str) -> Option { + Some(match mime_type.to_ascii_lowercase().as_str() { + "image/bmp" => ImageFormat::Bmp, + "image/x-bmp" => ImageFormat::Bmp, + "image/vnd-ms.dds" => ImageFormat::Dds, + "image/jpeg" => ImageFormat::Jpeg, + "image/ktx2" => ImageFormat::Ktx2, + "image/png" => ImageFormat::Png, + "image/x-targa" => ImageFormat::Tga, + "image/x-tga" => ImageFormat::Tga, + _ => return None, + }) + } + + pub fn from_extension(extension: &str) -> Option { + Some(match extension.to_ascii_lowercase().as_str() { + "avif" => ImageFormat::Avif, + "basis" => ImageFormat::Basis, + "bmp" => ImageFormat::Bmp, + "dds" => ImageFormat::Dds, + "ff" | "farbfeld" => ImageFormat::Farbfeld, + "gif" => ImageFormat::Gif, + "hdr" => ImageFormat::Hdr, + "ico" => ImageFormat::Ico, + "jpg" | "jpeg" => ImageFormat::Jpeg, + "ktx2" => ImageFormat::Ktx2, + "pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm, + "png" => ImageFormat::Png, + "tga" => ImageFormat::Tga, + "tif" | "tiff" => ImageFormat::Tiff, + "webp" => ImageFormat::WebP, + _ => return None, + }) + } + + pub fn as_image_crate_format(&self) -> Option { + Some(match self { + ImageFormat::Avif => image::ImageFormat::Avif, + ImageFormat::Basis => return None, + ImageFormat::Bmp => image::ImageFormat::Bmp, + ImageFormat::Dds => image::ImageFormat::Dds, + ImageFormat::Farbfeld => image::ImageFormat::Farbfeld, + ImageFormat::Gif => image::ImageFormat::Gif, + ImageFormat::Hdr => image::ImageFormat::Hdr, + ImageFormat::Ico => image::ImageFormat::Ico, + ImageFormat::Jpeg => image::ImageFormat::Jpeg, + ImageFormat::Ktx2 => return None, + ImageFormat::Png => image::ImageFormat::Png, + ImageFormat::Pnm => image::ImageFormat::Pnm, + ImageFormat::Tga => image::ImageFormat::Tga, + ImageFormat::Tiff => image::ImageFormat::Tiff, + ImageFormat::WebP => image::ImageFormat::WebP, + }) + } +} + #[derive(Debug, Clone, TypeUuid)] #[uuid = "6ea26da6-6cf8-4ea2-9986-1d7bf6c17d6f"] pub struct Image { @@ -181,38 +264,35 @@ impl Image { pub fn convert(&self, new_format: TextureFormat) -> Option { super::image_texture_conversion::texture_to_image(self) .and_then(|img| match new_format { - TextureFormat::R8Unorm => Some(image::DynamicImage::ImageLuma8(img.into_luma8())), - TextureFormat::Rg8Unorm => { - Some(image::DynamicImage::ImageLumaA8(img.into_luma_alpha8())) + TextureFormat::R8Unorm => { + Some((image::DynamicImage::ImageLuma8(img.into_luma8()), false)) } + TextureFormat::Rg8Unorm => Some(( + image::DynamicImage::ImageLumaA8(img.into_luma_alpha8()), + false, + )), TextureFormat::Rgba8UnormSrgb => { - Some(image::DynamicImage::ImageRgba8(img.into_rgba8())) + Some((image::DynamicImage::ImageRgba8(img.into_rgba8()), true)) } TextureFormat::Bgra8UnormSrgb => { - Some(image::DynamicImage::ImageBgra8(img.into_bgra8())) + Some((image::DynamicImage::ImageBgra8(img.into_bgra8()), true)) } _ => None, }) - .map(super::image_texture_conversion::image_to_texture) + .map(|(dyn_img, is_srgb)| { + super::image_texture_conversion::image_to_texture(dyn_img, is_srgb) + }) } /// Load a bytes buffer in a [`Image`], according to type `image_type`, using the `image` /// crate - pub fn from_buffer(buffer: &[u8], image_type: ImageType) -> Result { - let format = match image_type { - ImageType::MimeType(mime_type) => match mime_type { - "image/png" => Ok(image::ImageFormat::Png), - "image/vnd-ms.dds" => Ok(image::ImageFormat::Dds), - "image/x-targa" => Ok(image::ImageFormat::Tga), - "image/x-tga" => Ok(image::ImageFormat::Tga), - "image/jpeg" => Ok(image::ImageFormat::Jpeg), - "image/bmp" => Ok(image::ImageFormat::Bmp), - "image/x-bmp" => Ok(image::ImageFormat::Bmp), - _ => Err(TextureError::InvalidImageMimeType(mime_type.to_string())), - }, - ImageType::Extension(extension) => image::ImageFormat::from_extension(extension) - .ok_or_else(|| TextureError::InvalidImageMimeType(extension.to_string())), - }?; + pub fn from_buffer( + buffer: &[u8], + image_type: ImageType, + #[allow(unused_variables)] supported_compressed_formats: CompressedImageFormats, + is_srgb: bool, + ) -> Result { + let format = image_type.to_image_format()?; // Load the image in the expected format. // Some formats like PNG allow for R or RG textures too, so the texture @@ -220,20 +300,80 @@ impl Image { // needs to be added, so the image data needs to be converted in those // cases. - let dyn_img = image::load_from_memory_with_format(buffer, format)?; - Ok(image_to_texture(dyn_img)) + match format { + #[cfg(feature = "basis-universal")] + ImageFormat::Basis => { + basis_buffer_to_image(buffer, supported_compressed_formats, is_srgb) + } + #[cfg(feature = "dds")] + ImageFormat::Dds => dds_buffer_to_image(buffer, supported_compressed_formats, is_srgb), + #[cfg(feature = "ktx2")] + ImageFormat::Ktx2 => { + ktx2_buffer_to_image(buffer, supported_compressed_formats, is_srgb) + } + _ => { + let image_crate_format = format.as_image_crate_format().ok_or_else(|| { + TextureError::UnsupportedTextureFormat(format!("{:?}", format)) + })?; + let dyn_img = image::load_from_memory_with_format(buffer, image_crate_format)?; + Ok(image_to_texture(dyn_img, is_srgb)) + } + } + } + + /// Whether the texture format is compressed or uncompressed + pub fn is_compressed(&self) -> bool { + let format_description = self.texture_descriptor.format.describe(); + format_description + .required_features + .contains(wgpu::Features::TEXTURE_COMPRESSION_ASTC_LDR) + || format_description + .required_features + .contains(wgpu::Features::TEXTURE_COMPRESSION_BC) + || format_description + .required_features + .contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2) } } +#[derive(Clone, Copy, Debug)] +pub enum DataFormat { + R8, + Rg8, + Rgb8, + Rgba8, + Rgba16Float, +} + +#[derive(Clone, Copy, Debug)] +pub enum TranscodeFormat { + Etc1s, + // Has to be transcoded to Rgba8 for use with `wgpu` + Rgb8, + Uastc(DataFormat), +} + /// An error that occurs when loading a texture #[derive(Error, Debug)] pub enum TextureError { - #[error("invalid image mime type")] + #[error("invalid image mime type: {0}")] InvalidImageMimeType(String), - #[error("invalid image extension")] + #[error("invalid image extension: {0}")] InvalidImageExtension(String), #[error("failed to load an image: {0}")] ImageError(#[from] image::ImageError), + #[error("unsupported texture format: {0}")] + UnsupportedTextureFormat(String), + #[error("supercompression not supported: {0}")] + SuperCompressionNotSupported(String), + #[error("failed to load an image: {0}")] + SuperDecompressionError(String), + #[error("invalid data: {0}")] + InvalidData(String), + #[error("transcode error: {0}")] + TranscodeError(String), + #[error("format requires transcoding: {0:?}")] + FormatRequiresTranscodingError(TranscodeFormat), } /// The type of a raw image buffer. @@ -244,6 +384,17 @@ pub enum ImageType<'a> { Extension(&'a str), } +impl<'a> ImageType<'a> { + pub fn to_image_format(&self) -> Result { + match self { + ImageType::MimeType(mime_type) => ImageFormat::from_mime_type(mime_type) + .ok_or_else(|| TextureError::InvalidImageMimeType(mime_type.to_string())), + ImageType::Extension(extension) => ImageFormat::from_extension(extension) + .ok_or_else(|| TextureError::InvalidImageExtension(extension.to_string())), + } + } +} + /// Used to calculate the volume of an item. pub trait Volume { fn volume(&self) -> usize; @@ -387,6 +538,7 @@ impl TextureFormatPixelInfo for TextureFormat { pub struct GpuImage { pub texture: Texture, pub texture_view: TextureView, + pub texture_format: TextureFormat, pub sampler: Sampler, pub size: Size, } @@ -406,49 +558,149 @@ impl RenderAsset for Image { image: Self::ExtractedAsset, (render_device, render_queue): &mut SystemParamItem, ) -> Result> { - let texture = render_device.create_texture(&image.texture_descriptor); - let sampler = render_device.create_sampler(&image.sampler_descriptor); - - let format_size = image.texture_descriptor.format.pixel_size(); - render_queue.write_texture( - ImageCopyTexture { - texture: &texture, - mip_level: 0, - origin: Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - &image.data, - ImageDataLayout { - offset: 0, - bytes_per_row: Some( - std::num::NonZeroU32::new( - image.texture_descriptor.size.width * format_size as u32, - ) - .unwrap(), - ), - rows_per_image: if image.texture_descriptor.size.depth_or_array_layers > 1 { - std::num::NonZeroU32::new(image.texture_descriptor.size.height) - } else { - None + let texture = if image.texture_descriptor.mip_level_count > 1 || image.is_compressed() { + render_device.create_texture_with_data( + render_queue, + &image.texture_descriptor, + &image.data, + ) + } else { + let texture = render_device.create_texture(&image.texture_descriptor); + let format_size = image.texture_descriptor.format.pixel_size(); + render_queue.write_texture( + ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, }, - }, - image.texture_descriptor.size, - ); + &image.data, + ImageDataLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZeroU32::new( + image.texture_descriptor.size.width * format_size as u32, + ) + .unwrap(), + ), + rows_per_image: if image.texture_descriptor.size.depth_or_array_layers > 1 { + std::num::NonZeroU32::new(image.texture_descriptor.size.height) + } else { + None + }, + }, + image.texture_descriptor.size, + ); + texture + }; let texture_view = texture.create_view(&TextureViewDescriptor::default()); let size = Size::new( image.texture_descriptor.size.width as f32, image.texture_descriptor.size.height as f32, ); + let sampler = render_device.create_sampler(&image.sampler_descriptor); Ok(GpuImage { texture, texture_view, + texture_format: image.texture_descriptor.format, sampler, size, }) } } +bitflags::bitflags! { + #[derive(Default)] + #[repr(transparent)] + pub struct CompressedImageFormats: u32 { + const NONE = 0; + const ASTC_LDR = (1 << 0); + const BC = (1 << 1); + const ETC2 = (1 << 2); + } +} + +impl CompressedImageFormats { + pub fn from_features(features: wgpu::Features) -> Self { + let mut supported_compressed_formats = Self::default(); + if features.contains(wgpu::Features::TEXTURE_COMPRESSION_ASTC_LDR) { + supported_compressed_formats |= Self::ASTC_LDR; + } + if features.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) { + supported_compressed_formats |= Self::BC; + } + if features.contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2) { + supported_compressed_formats |= Self::ETC2; + } + supported_compressed_formats + } + + pub fn supports(&self, format: TextureFormat) -> bool { + match format { + TextureFormat::Bc1RgbaUnorm => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc1RgbaUnormSrgb => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc2RgbaUnorm => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc2RgbaUnormSrgb => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc3RgbaUnorm => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc3RgbaUnormSrgb => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc4RUnorm => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc4RSnorm => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc5RgUnorm => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc5RgSnorm => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc6hRgbUfloat => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc6hRgbSfloat => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc7RgbaUnorm => self.contains(CompressedImageFormats::BC), + TextureFormat::Bc7RgbaUnormSrgb => self.contains(CompressedImageFormats::BC), + TextureFormat::Etc2Rgb8Unorm => self.contains(CompressedImageFormats::ETC2), + TextureFormat::Etc2Rgb8UnormSrgb => self.contains(CompressedImageFormats::ETC2), + TextureFormat::Etc2Rgb8A1Unorm => self.contains(CompressedImageFormats::ETC2), + TextureFormat::Etc2Rgb8A1UnormSrgb => self.contains(CompressedImageFormats::ETC2), + TextureFormat::Etc2Rgba8Unorm => self.contains(CompressedImageFormats::ETC2), + TextureFormat::Etc2Rgba8UnormSrgb => self.contains(CompressedImageFormats::ETC2), + TextureFormat::EacR11Unorm => self.contains(CompressedImageFormats::ETC2), + TextureFormat::EacR11Snorm => self.contains(CompressedImageFormats::ETC2), + TextureFormat::EacRg11Unorm => self.contains(CompressedImageFormats::ETC2), + TextureFormat::EacRg11Snorm => self.contains(CompressedImageFormats::ETC2), + TextureFormat::Astc4x4RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc4x4RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc5x4RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc5x4RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc5x5RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc5x5RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc6x5RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc6x5RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc6x6RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc6x6RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc8x5RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc8x5RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc8x6RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc8x6RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc10x5RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc10x5RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc10x6RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc10x6RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc8x8RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc8x8RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc10x8RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc10x8RgbaUnormSrgb => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc10x10RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc10x10RgbaUnormSrgb => { + self.contains(CompressedImageFormats::ASTC_LDR) + } + TextureFormat::Astc12x10RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc12x10RgbaUnormSrgb => { + self.contains(CompressedImageFormats::ASTC_LDR) + } + TextureFormat::Astc12x12RgbaUnorm => self.contains(CompressedImageFormats::ASTC_LDR), + TextureFormat::Astc12x12RgbaUnormSrgb => { + self.contains(CompressedImageFormats::ASTC_LDR) + } + _ => true, + } + } +} + #[cfg(test)] mod test { diff --git a/crates/bevy_render/src/texture/image_texture_conversion.rs b/crates/bevy_render/src/texture/image_texture_conversion.rs index bd52a89f13bb21..1c7e8975550014 100644 --- a/crates/bevy_render/src/texture/image_texture_conversion.rs +++ b/crates/bevy_render/src/texture/image_texture_conversion.rs @@ -4,7 +4,7 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat}; // TODO: fix name? /// Converts a [`DynamicImage`] to an [`Image`]. -pub(crate) fn image_to_texture(dyn_img: DynamicImage) -> Image { +pub(crate) fn image_to_texture(dyn_img: DynamicImage, is_srgb: bool) -> Image { use bevy_core::cast_slice; let width; let height; @@ -17,7 +17,11 @@ pub(crate) fn image_to_texture(dyn_img: DynamicImage) -> Image { let i = DynamicImage::ImageLuma8(i).into_rgba8(); width = i.width(); height = i.height(); - format = TextureFormat::Rgba8UnormSrgb; + format = if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }; data = i.into_raw(); } @@ -25,7 +29,11 @@ pub(crate) fn image_to_texture(dyn_img: DynamicImage) -> Image { let i = DynamicImage::ImageLumaA8(i).into_rgba8(); width = i.width(); height = i.height(); - format = TextureFormat::Rgba8UnormSrgb; + format = if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }; data = i.into_raw(); } @@ -33,14 +41,22 @@ pub(crate) fn image_to_texture(dyn_img: DynamicImage) -> Image { let i = DynamicImage::ImageRgb8(i).into_rgba8(); width = i.width(); height = i.height(); - format = TextureFormat::Rgba8UnormSrgb; + format = if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }; data = i.into_raw(); } DynamicImage::ImageRgba8(i) => { width = i.width(); height = i.height(); - format = TextureFormat::Rgba8UnormSrgb; + format = if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }; data = i.into_raw(); } @@ -49,14 +65,22 @@ pub(crate) fn image_to_texture(dyn_img: DynamicImage) -> Image { width = i.width(); height = i.height(); - format = TextureFormat::Bgra8UnormSrgb; + format = if is_srgb { + TextureFormat::Bgra8UnormSrgb + } else { + TextureFormat::Bgra8Unorm + }; data = i.into_raw(); } DynamicImage::ImageBgra8(i) => { width = i.width(); height = i.height(); - format = TextureFormat::Bgra8UnormSrgb; + format = if is_srgb { + TextureFormat::Bgra8UnormSrgb + } else { + TextureFormat::Bgra8Unorm + }; data = i.into_raw(); } diff --git a/crates/bevy_render/src/texture/image_texture_loader.rs b/crates/bevy_render/src/texture/image_texture_loader.rs index b7f715ccbcfcb3..46be9ba0f9bbcb 100644 --- a/crates/bevy_render/src/texture/image_texture_loader.rs +++ b/crates/bevy_render/src/texture/image_texture_loader.rs @@ -1,15 +1,27 @@ use anyhow::Result; use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; +use bevy_ecs::prelude::{FromWorld, World}; use bevy_utils::BoxedFuture; use thiserror::Error; -use crate::texture::{Image, ImageType, TextureError}; +use crate::{ + renderer::RenderDevice, + texture::{Image, ImageType, TextureError}, +}; + +use super::CompressedImageFormats; /// Loader for images that can be read by the `image` crate. -#[derive(Clone, Default)] -pub struct ImageTextureLoader; +#[derive(Clone)] +pub struct ImageTextureLoader { + supported_compressed_formats: CompressedImageFormats, +} const FILE_EXTENSIONS: &[&str] = &[ + #[cfg(feature = "basis-universal")] + "basis", + #[cfg(feature = "bmp")] + "bmp", #[cfg(feature = "png")] "png", #[cfg(feature = "dds")] @@ -20,8 +32,8 @@ const FILE_EXTENSIONS: &[&str] = &[ "jpg", #[cfg(feature = "jpeg")] "jpeg", - #[cfg(feature = "bmp")] - "bmp", + #[cfg(feature = "ktx2")] + "ktx2", ]; impl AssetLoader for ImageTextureLoader { @@ -34,11 +46,15 @@ impl AssetLoader for ImageTextureLoader { // use the file extension for the image type let ext = load_context.path().extension().unwrap().to_str().unwrap(); - let dyn_img = Image::from_buffer(bytes, ImageType::Extension(ext)).map_err(|err| { - FileTextureError { - error: err, - path: format!("{}", load_context.path().display()), - } + let dyn_img = Image::from_buffer( + bytes, + ImageType::Extension(ext), + self.supported_compressed_formats, + true, + ) + .map_err(|err| FileTextureError { + error: err, + path: format!("{}", load_context.path().display()), })?; load_context.set_default_asset(LoadedAsset::new(dyn_img)); @@ -51,6 +67,16 @@ impl AssetLoader for ImageTextureLoader { } } +impl FromWorld for ImageTextureLoader { + fn from_world(world: &mut World) -> Self { + Self { + supported_compressed_formats: CompressedImageFormats::from_features( + world.resource::().features(), + ), + } + } +} + /// An error that occurs when loading a texture from a file. #[derive(Error, Debug)] pub struct FileTextureError { diff --git a/crates/bevy_render/src/texture/ktx2.rs b/crates/bevy_render/src/texture/ktx2.rs new file mode 100644 index 00000000000000..0bc9eb71c606aa --- /dev/null +++ b/crates/bevy_render/src/texture/ktx2.rs @@ -0,0 +1,1723 @@ +#[cfg(any(feature = "flate2", feature = "ruzstd"))] +use std::io::Read; + +#[cfg(feature = "basis-universal")] +use basis_universal::{ + DecodeFlags, LowLevelUastcTranscoder, SliceParametersUastc, TranscoderBlockFormat, +}; +#[cfg(any(feature = "flate2", feature = "ruzstd"))] +use ktx2::SupercompressionScheme; +use ktx2::{ + BasicDataFormatDescriptor, ChannelTypeQualifiers, ColorModel, DataFormatDescriptorHeader, + Header, SampleInformation, +}; +use wgpu::{Extent3d, TextureDimension, TextureFormat}; + +use super::{CompressedImageFormats, DataFormat, Image, TextureError, TranscodeFormat}; + +pub fn ktx2_buffer_to_image( + buffer: &[u8], + supported_compressed_formats: CompressedImageFormats, + is_srgb: bool, +) -> Result { + let ktx2 = ktx2::Reader::new(buffer).map_err(|err| { + TextureError::InvalidData(format!("Failed to parse ktx2 file: {:?}", err)) + })?; + let Header { + pixel_width: width, + pixel_height: height, + pixel_depth: depth, + layer_count, + level_count, + supercompression_scheme, + .. + } = ktx2.header(); + + // Handle supercompression + let mut levels = Vec::new(); + if let Some(supercompression_scheme) = supercompression_scheme { + for (_level, _level_data) in ktx2.levels().enumerate() { + match supercompression_scheme { + #[cfg(feature = "flate2")] + SupercompressionScheme::ZLIB => { + let mut decoder = flate2::bufread::ZlibDecoder::new(_level_data); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed).map_err(|err| { + TextureError::SuperDecompressionError(format!( + "Failed to decompress {:?} for mip {}: {:?}", + supercompression_scheme, _level, err + )) + })?; + levels.push(decompressed); + } + #[cfg(feature = "ruzstd")] + SupercompressionScheme::Zstandard => { + let mut cursor = std::io::Cursor::new(_level_data); + let mut decoder = ruzstd::StreamingDecoder::new(&mut cursor) + .map_err(TextureError::SuperDecompressionError)?; + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed).map_err(|err| { + TextureError::SuperDecompressionError(format!( + "Failed to decompress {:?} for mip {}: {:?}", + supercompression_scheme, _level, err + )) + })?; + levels.push(decompressed); + } + _ => { + return Err(TextureError::SuperDecompressionError(format!( + "Unsupported supercompression scheme: {:?}", + supercompression_scheme + ))); + } + } + } + } else { + levels = ktx2.levels().map(|level| level.to_vec()).collect(); + } + + // Identify the format + let texture_format = ktx2_get_texture_format(&ktx2, is_srgb).or_else(|error| match error { + // Transcode if needed and supported + TextureError::FormatRequiresTranscodingError(transcode_format) => { + let mut transcoded = Vec::new(); + let texture_format = match transcode_format { + TranscodeFormat::Rgb8 => { + let (mut original_width, mut original_height) = (width, height); + + for level_data in levels.iter() { + let n_pixels = (original_width * original_height) as usize; + + let mut rgba = vec![255u8; n_pixels * 4]; + for i in 0..n_pixels { + rgba[i * 4] = level_data[i * 3]; + rgba[i * 4 + 1] = level_data[i * 3 + 1]; + rgba[i * 4 + 2] = level_data[i * 3 + 2]; + } + transcoded.push(rgba); + + // Next mip dimensions are half the current, minimum 1x1 + original_width = (original_width / 2).max(1); + original_height = (original_height / 2).max(1); + } + + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + } + } + #[cfg(feature = "basis-universal")] + TranscodeFormat::Uastc(data_format) => { + let (transcode_block_format, texture_format) = + get_transcoded_formats(supported_compressed_formats, data_format, is_srgb); + let (mut original_width, mut original_height) = (width, height); + let (block_width_pixels, block_height_pixels) = (4, 4); + + let transcoder = LowLevelUastcTranscoder::new(); + for (level, level_data) in levels.iter().enumerate() { + let slice_parameters = SliceParametersUastc { + num_blocks_x: ((original_width + block_width_pixels - 1) + / block_width_pixels) + .max(1), + num_blocks_y: ((original_height + block_height_pixels - 1) + / block_height_pixels) + .max(1), + has_alpha: false, + original_width, + original_height, + }; + + transcoder + .transcode_slice( + level_data, + slice_parameters, + DecodeFlags::HIGH_QUALITY, + transcode_block_format, + ) + .map(|transcoded_level| transcoded.push(transcoded_level)) + .map_err(|error| { + TextureError::SuperDecompressionError(format!( + "Failed to transcode mip level {} from UASTC to {:?}: {:?}", + level, transcode_block_format, error + )) + })?; + + // Next mip dimensions are half the current, minimum 1x1 + original_width = (original_width / 2).max(1); + original_height = (original_height / 2).max(1); + } + texture_format + } + // ETC1S is a subset of ETC1 which is a subset of ETC2 + // TODO: Implement transcoding + TranscodeFormat::Etc1s => { + let texture_format = if is_srgb { + TextureFormat::Etc2Rgb8UnormSrgb + } else { + TextureFormat::Etc2Rgb8Unorm + }; + if !supported_compressed_formats.supports(texture_format) { + return Err(error); + } + transcoded = levels.to_vec(); + texture_format + } + #[cfg(not(feature = "basis-universal"))] + _ => return Err(error), + }; + levels = transcoded; + Ok(texture_format) + } + _ => Err(error), + })?; + if !supported_compressed_formats.supports(texture_format) { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Format not supported by this GPU: {:?}", + texture_format + ))); + } + + // Assign the data and fill in the rest of the metadata now the possible + // error cases have been handled + let mut image = Image::default(); + image.texture_descriptor.format = texture_format; + image.data = levels.into_iter().flatten().collect::>(); + image.texture_descriptor.size = Extent3d { + width, + height, + depth_or_array_layers: if layer_count > 1 { layer_count } else { depth }.max(1), + }; + image.texture_descriptor.mip_level_count = level_count; + image.texture_descriptor.dimension = if depth > 1 { + TextureDimension::D3 + } else if image.is_compressed() || height > 1 { + TextureDimension::D2 + } else { + TextureDimension::D1 + }; + Ok(image) +} + +#[cfg(feature = "basis-universal")] +pub fn get_transcoded_formats( + supported_compressed_formats: CompressedImageFormats, + data_format: DataFormat, + is_srgb: bool, +) -> (TranscoderBlockFormat, TextureFormat) { + match data_format { + DataFormat::R8 => { + if supported_compressed_formats.contains(CompressedImageFormats::BC) { + (TranscoderBlockFormat::BC4, TextureFormat::Bc4RUnorm) + } else if supported_compressed_formats.contains(CompressedImageFormats::ETC2) { + ( + TranscoderBlockFormat::ETC2_EAC_R11, + TextureFormat::EacR11Unorm, + ) + } else { + (TranscoderBlockFormat::RGBA32, TextureFormat::R8Unorm) + } + } + DataFormat::Rg8 => { + if supported_compressed_formats.contains(CompressedImageFormats::BC) { + (TranscoderBlockFormat::BC5, TextureFormat::Bc5RgUnorm) + } else if supported_compressed_formats.contains(CompressedImageFormats::ETC2) { + ( + TranscoderBlockFormat::ETC2_EAC_RG11, + TextureFormat::EacRg11Unorm, + ) + } else { + (TranscoderBlockFormat::RGBA32, TextureFormat::Rg8Unorm) + } + } + // NOTE: Rgba16Float should be transcoded to BC6H/ASTC_HDR. Neither are supported by + // basis-universal, nor is ASTC_HDR supported by wgpu + DataFormat::Rgb8 | DataFormat::Rgba8 | DataFormat::Rgba16Float => { + // NOTE: UASTC can be losslessly transcoded to ASTC4x4 and ASTC uses the same + // space as BC7 (128-bits per 4x4 texel block) so prefer ASTC over BC for + // transcoding speed and quality. + if supported_compressed_formats.contains(CompressedImageFormats::ASTC_LDR) { + ( + TranscoderBlockFormat::ASTC_4x4, + if is_srgb { + TextureFormat::Astc4x4RgbaUnormSrgb + } else { + TextureFormat::Astc4x4RgbaUnorm + }, + ) + } else if supported_compressed_formats.contains(CompressedImageFormats::BC) { + ( + TranscoderBlockFormat::BC7, + if is_srgb { + TextureFormat::Bc7RgbaUnormSrgb + } else { + TextureFormat::Bc7RgbaUnorm + }, + ) + } else if supported_compressed_formats.contains(CompressedImageFormats::ETC2) { + ( + TranscoderBlockFormat::ETC2_RGBA, + if is_srgb { + TextureFormat::Etc2Rgba8UnormSrgb + } else { + TextureFormat::Etc2Rgba8Unorm + }, + ) + } else { + ( + TranscoderBlockFormat::RGBA32, + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + }, + ) + } + } + } +} + +pub fn ktx2_get_texture_format>( + ktx2: &ktx2::Reader, + is_srgb: bool, +) -> Result { + if let Some(format) = ktx2.header().format { + return ktx2_format_to_texture_format(format, is_srgb); + } + + for data_format_descriptor in ktx2.data_format_descriptors() { + if data_format_descriptor.header == DataFormatDescriptorHeader::BASIC { + let basic_data_format_descriptor = + BasicDataFormatDescriptor::parse(data_format_descriptor.data) + .map_err(|err| TextureError::InvalidData(format!("KTX2: {:?}", err)))?; + let sample_information = basic_data_format_descriptor + .sample_information() + .collect::>(); + return ktx2_dfd_to_texture_format( + &basic_data_format_descriptor, + &sample_information, + is_srgb, + ); + } + } + + Err(TextureError::UnsupportedTextureFormat( + "Unknown".to_string(), + )) +} + +enum DataType { + Unorm, + UnormSrgb, + Snorm, + Float, + Uint, + Sint, +} + +// This can be obtained from std::mem::transmute::(1.0f32). It is used for identifying +// normalized sample types as in Unorm or Snorm. +const F32_1_AS_U32: u32 = 1065353216; + +fn sample_information_to_data_type( + sample: &SampleInformation, + is_srgb: bool, +) -> Result { + // Exponent flag not supported + if sample + .channel_type_qualifiers + .contains(ChannelTypeQualifiers::EXPONENT) + { + return Err(TextureError::UnsupportedTextureFormat( + "Unsupported KTX2 channel type qualifier: exponent".to_string(), + )); + } + Ok( + if sample + .channel_type_qualifiers + .contains(ChannelTypeQualifiers::FLOAT) + { + // If lower bound of range is 0 then unorm, else if upper bound is 1.0f32 as u32 + if sample + .channel_type_qualifiers + .contains(ChannelTypeQualifiers::SIGNED) + { + if sample.upper == F32_1_AS_U32 { + DataType::Snorm + } else { + DataType::Float + } + } else if is_srgb { + DataType::UnormSrgb + } else { + DataType::Unorm + } + } else if sample + .channel_type_qualifiers + .contains(ChannelTypeQualifiers::SIGNED) + { + DataType::Sint + } else { + DataType::Uint + }, + ) +} + +pub fn ktx2_dfd_to_texture_format( + data_format_descriptor: &BasicDataFormatDescriptor, + sample_information: &[SampleInformation], + is_srgb: bool, +) -> Result { + Ok(match data_format_descriptor.color_model { + Some(ColorModel::RGBSDA) => { + match sample_information.len() { + 1 => { + // Only red channel allowed + if sample_information[0].channel_type != 0 { + return Err(TextureError::UnsupportedTextureFormat( + "Only red-component single-component KTX2 RGBSDA formats supported" + .to_string(), + )); + } + + let sample = &sample_information[0]; + let data_type = sample_information_to_data_type(sample, false)?; + match sample.bit_length { + 8 => match data_type { + DataType::Unorm => TextureFormat::R8Unorm, + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for R8".to_string(), + )); + } + DataType::Snorm => TextureFormat::R8Snorm, + DataType::Float => { + return Err(TextureError::UnsupportedTextureFormat( + "Float not supported for R8".to_string(), + )); + } + DataType::Uint => TextureFormat::R8Uint, + DataType::Sint => TextureFormat::R8Sint, + }, + 16 => match data_type { + DataType::Unorm => TextureFormat::R16Unorm, + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for R16".to_string(), + )); + } + DataType::Snorm => TextureFormat::R16Snorm, + DataType::Float => TextureFormat::R16Float, + DataType::Uint => TextureFormat::R16Uint, + DataType::Sint => TextureFormat::R16Sint, + }, + 32 => match data_type { + DataType::Unorm => { + return Err(TextureError::UnsupportedTextureFormat( + "Unorm not supported for R32".to_string(), + )); + } + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for R32".to_string(), + )); + } + DataType::Snorm => { + return Err(TextureError::UnsupportedTextureFormat( + "Snorm not supported for R32".to_string(), + )); + } + DataType::Float => TextureFormat::R32Float, + DataType::Uint => TextureFormat::R32Uint, + DataType::Sint => TextureFormat::R32Sint, + }, + v => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Unsupported sample bit length for RGBSDA 1-channel format: {}", + v + ))); + } + } + } + 2 => { + // Only red and green channels allowed + if sample_information[0].channel_type != 0 + || sample_information[1].channel_type != 1 + { + return Err(TextureError::UnsupportedTextureFormat( + "Only red-green-component two-component KTX2 RGBSDA formats supported" + .to_string(), + )); + } + // Only same bit length for all channels + assert_eq!( + sample_information[0].bit_length, + sample_information[1].bit_length + ); + // Only same channel type qualifiers for all channels + assert_eq!( + sample_information[0].channel_type_qualifiers, + sample_information[1].channel_type_qualifiers + ); + // Only same sample range for all channels + assert_eq!(sample_information[0].lower, sample_information[1].lower); + assert_eq!(sample_information[0].upper, sample_information[1].upper); + + let sample = &sample_information[0]; + let data_type = sample_information_to_data_type(sample, false)?; + match sample.bit_length { + 8 => match data_type { + DataType::Unorm => TextureFormat::Rg8Unorm, + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for Rg8".to_string(), + )); + } + DataType::Snorm => TextureFormat::Rg8Snorm, + DataType::Float => { + return Err(TextureError::UnsupportedTextureFormat( + "Float not supported for Rg8".to_string(), + )); + } + DataType::Uint => TextureFormat::Rg8Uint, + DataType::Sint => TextureFormat::Rg8Sint, + }, + 16 => match data_type { + DataType::Unorm => TextureFormat::Rg16Unorm, + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for Rg16".to_string(), + )); + } + DataType::Snorm => TextureFormat::Rg16Snorm, + DataType::Float => TextureFormat::Rg16Float, + DataType::Uint => TextureFormat::Rg16Uint, + DataType::Sint => TextureFormat::Rg16Sint, + }, + 32 => match data_type { + DataType::Unorm => { + return Err(TextureError::UnsupportedTextureFormat( + "Unorm not supported for Rg32".to_string(), + )); + } + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for Rg32".to_string(), + )); + } + DataType::Snorm => { + return Err(TextureError::UnsupportedTextureFormat( + "Snorm not supported for Rg32".to_string(), + )); + } + DataType::Float => TextureFormat::Rg32Float, + DataType::Uint => TextureFormat::Rg32Uint, + DataType::Sint => TextureFormat::Rg32Sint, + }, + v => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Unsupported sample bit length for RGBSDA 2-channel format: {}", + v + ))); + } + } + } + 3 => { + if sample_information[0].channel_type == 0 + && sample_information[0].bit_length == 11 + && sample_information[1].channel_type == 1 + && sample_information[1].bit_length == 11 + && sample_information[2].channel_type == 2 + && sample_information[2].bit_length == 10 + { + TextureFormat::Rg11b10Float + } else if sample_information[0].channel_type == 0 + && sample_information[0].bit_length == 9 + && sample_information[1].channel_type == 1 + && sample_information[1].bit_length == 9 + && sample_information[2].channel_type == 2 + && sample_information[2].bit_length == 9 + { + TextureFormat::Rgb9e5Ufloat + } else if sample_information[0].channel_type == 0 + && sample_information[0].bit_length == 8 + && sample_information[1].channel_type == 1 + && sample_information[1].bit_length == 8 + && sample_information[2].channel_type == 2 + && sample_information[2].bit_length == 8 + { + return Err(TextureError::FormatRequiresTranscodingError( + TranscodeFormat::Rgb8, + )); + } else { + return Err(TextureError::UnsupportedTextureFormat( + "3-component formats not supported".to_string(), + )); + } + } + 4 => { + // Only RGBA or BGRA channels allowed + let is_rgba = sample_information[0].channel_type == 0; + assert!( + sample_information[0].channel_type == 0 + || sample_information[0].channel_type == 2 + ); + assert_eq!(sample_information[1].channel_type, 1); + assert_eq!( + sample_information[2].channel_type, + if is_rgba { 2 } else { 0 } + ); + assert_eq!(sample_information[3].channel_type, 15); + + // Handle one special packed format + if sample_information[0].bit_length == 10 + && sample_information[1].bit_length == 10 + && sample_information[2].bit_length == 10 + && sample_information[3].bit_length == 2 + { + return Ok(TextureFormat::Rgb10a2Unorm); + } + + // Only same bit length for all channels + assert!( + sample_information[0].bit_length == sample_information[1].bit_length + && sample_information[0].bit_length == sample_information[2].bit_length + && sample_information[0].bit_length == sample_information[3].bit_length + ); + assert!( + sample_information[0].lower == sample_information[1].lower + && sample_information[0].lower == sample_information[2].lower + && sample_information[0].lower == sample_information[3].lower + ); + assert!( + sample_information[0].upper == sample_information[1].upper + && sample_information[0].upper == sample_information[2].upper + && sample_information[0].upper == sample_information[3].upper + ); + + let sample = &sample_information[0]; + let data_type = sample_information_to_data_type(sample, is_srgb)?; + match sample.bit_length { + 8 => match data_type { + DataType::Unorm => { + if is_rgba { + TextureFormat::Rgba8Unorm + } else { + TextureFormat::Bgra8Unorm + } + } + DataType::UnormSrgb => { + if is_rgba { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Bgra8UnormSrgb + } + } + DataType::Snorm => { + if is_rgba { + TextureFormat::Rgba8Snorm + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra8 not supported for Snorm".to_string(), + )); + } + } + DataType::Float => { + return Err(TextureError::UnsupportedTextureFormat( + "Float not supported for Rgba8/Bgra8".to_string(), + )); + } + DataType::Uint => { + if is_rgba { + // NOTE: This is more about how you want to use the data so + // TextureFormat::Rgba8Uint is incorrect here + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + } + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra8 not supported for Uint".to_string(), + )); + } + } + DataType::Sint => { + if is_rgba { + // NOTE: This is more about how you want to use the data so + // TextureFormat::Rgba8Sint is incorrect here + TextureFormat::Rgba8Snorm + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra8 not supported for Sint".to_string(), + )); + } + } + }, + 16 => match data_type { + DataType::Unorm => { + if is_rgba { + TextureFormat::Rgba16Unorm + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra16 not supported for Unorm".to_string(), + )); + } + } + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for Rgba16/Bgra16".to_string(), + )); + } + DataType::Snorm => { + if is_rgba { + TextureFormat::Rgba16Snorm + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra16 not supported for Snorm".to_string(), + )); + } + } + DataType::Float => { + if is_rgba { + TextureFormat::Rgba16Float + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra16 not supported for Float".to_string(), + )); + } + } + DataType::Uint => { + if is_rgba { + TextureFormat::Rgba16Uint + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra16 not supported for Uint".to_string(), + )); + } + } + DataType::Sint => { + if is_rgba { + TextureFormat::Rgba16Sint + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra16 not supported for Sint".to_string(), + )); + } + } + }, + 32 => match data_type { + DataType::Unorm => { + return Err(TextureError::UnsupportedTextureFormat( + "Unorm not supported for Rgba32/Bgra32".to_string(), + )); + } + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for Rgba32/Bgra32".to_string(), + )); + } + DataType::Snorm => { + return Err(TextureError::UnsupportedTextureFormat( + "Snorm not supported for Rgba32/Bgra32".to_string(), + )); + } + DataType::Float => { + if is_rgba { + TextureFormat::Rgba32Float + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra32 not supported for Float".to_string(), + )); + } + } + DataType::Uint => { + if is_rgba { + TextureFormat::Rgba32Uint + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra32 not supported for Uint".to_string(), + )); + } + } + DataType::Sint => { + if is_rgba { + TextureFormat::Rgba32Sint + } else { + return Err(TextureError::UnsupportedTextureFormat( + "Bgra32 not supported for Sint".to_string(), + )); + } + } + }, + v => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Unsupported sample bit length for RGBSDA 4-channel format: {}", + v + ))); + } + } + } + v => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Unsupported channel count for RGBSDA format: {}", + v + ))); + } + } + } + Some(ColorModel::YUVSDA) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::YIQSDA) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::LabSDA) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::CMYKA) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::XYZW) => { + // Same number of channels in both texel block dimensions and sample info descriptions + assert_eq!( + data_format_descriptor.texel_block_dimensions[0] as usize, + sample_information.len() + ); + match sample_information.len() { + 4 => { + // Only RGBA or BGRA channels allowed + assert_eq!(sample_information[0].channel_type, 0); + assert_eq!(sample_information[1].channel_type, 1); + assert_eq!(sample_information[2].channel_type, 2); + assert_eq!(sample_information[3].channel_type, 3); + // Only same bit length for all channels + assert!( + sample_information[0].bit_length == sample_information[1].bit_length + && sample_information[0].bit_length == sample_information[2].bit_length + && sample_information[0].bit_length == sample_information[3].bit_length + ); + // Only same channel type qualifiers for all channels + assert!( + sample_information[0].channel_type_qualifiers + == sample_information[1].channel_type_qualifiers + && sample_information[0].channel_type_qualifiers + == sample_information[2].channel_type_qualifiers + && sample_information[0].channel_type_qualifiers + == sample_information[3].channel_type_qualifiers + ); + // Only same sample range for all channels + assert!( + sample_information[0].lower == sample_information[1].lower + && sample_information[0].lower == sample_information[2].lower + && sample_information[0].lower == sample_information[3].lower + ); + assert!( + sample_information[0].upper == sample_information[1].upper + && sample_information[0].upper == sample_information[2].upper + && sample_information[0].upper == sample_information[3].upper + ); + + let sample = &sample_information[0]; + let data_type = sample_information_to_data_type(sample, false)?; + match sample.bit_length { + 8 => match data_type { + DataType::Unorm => TextureFormat::Rgba8Unorm, + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for XYZW".to_string(), + )); + } + DataType::Snorm => TextureFormat::Rgba8Snorm, + DataType::Float => { + return Err(TextureError::UnsupportedTextureFormat( + "Float not supported for Rgba8/Bgra8".to_string(), + )); + } + DataType::Uint => TextureFormat::Rgba8Uint, + DataType::Sint => TextureFormat::Rgba8Sint, + }, + 16 => match data_type { + DataType::Unorm => TextureFormat::Rgba16Unorm, + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for Rgba16/Bgra16".to_string(), + )); + } + DataType::Snorm => TextureFormat::Rgba16Snorm, + DataType::Float => TextureFormat::Rgba16Float, + DataType::Uint => TextureFormat::Rgba16Uint, + DataType::Sint => TextureFormat::Rgba16Sint, + }, + 32 => match data_type { + DataType::Unorm => { + return Err(TextureError::UnsupportedTextureFormat( + "Unorm not supported for Rgba32/Bgra32".to_string(), + )); + } + DataType::UnormSrgb => { + return Err(TextureError::UnsupportedTextureFormat( + "UnormSrgb not supported for Rgba32/Bgra32".to_string(), + )); + } + DataType::Snorm => { + return Err(TextureError::UnsupportedTextureFormat( + "Snorm not supported for Rgba32/Bgra32".to_string(), + )); + } + DataType::Float => TextureFormat::Rgba32Float, + DataType::Uint => TextureFormat::Rgba32Uint, + DataType::Sint => TextureFormat::Rgba32Sint, + }, + v => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Unsupported sample bit length for XYZW 4-channel format: {}", + v + ))); + } + } + } + v => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Unsupported channel count for XYZW format: {}", + v + ))); + } + } + } + Some(ColorModel::HSVAAng) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::HSLAAng) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::HSVAHex) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::HSLAHex) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::YCgCoA) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::YcCbcCrc) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::ICtCp) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::CIEXYZ) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::CIEXYY) => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + data_format_descriptor.color_model + ))); + } + Some(ColorModel::BC1A) => { + if is_srgb { + TextureFormat::Bc1RgbaUnormSrgb + } else { + TextureFormat::Bc1RgbaUnorm + } + } + Some(ColorModel::BC2) => { + if is_srgb { + TextureFormat::Bc2RgbaUnormSrgb + } else { + TextureFormat::Bc2RgbaUnorm + } + } + Some(ColorModel::BC3) => { + if is_srgb { + TextureFormat::Bc3RgbaUnormSrgb + } else { + TextureFormat::Bc3RgbaUnorm + } + } + Some(ColorModel::BC4) => { + if sample_information[0].lower == 0 { + TextureFormat::Bc4RUnorm + } else { + TextureFormat::Bc4RSnorm + } + } + // FIXME: Red and green channels can be swapped for ATI2n/3Dc + Some(ColorModel::BC5) => { + if sample_information[0].lower == 0 { + TextureFormat::Bc5RgUnorm + } else { + TextureFormat::Bc5RgSnorm + } + } + Some(ColorModel::BC6H) => { + if sample_information[0].lower == 0 { + TextureFormat::Bc6hRgbUfloat + } else { + TextureFormat::Bc6hRgbSfloat + } + } + Some(ColorModel::BC7) => { + if is_srgb { + TextureFormat::Bc7RgbaUnormSrgb + } else { + TextureFormat::Bc7RgbaUnorm + } + } + // ETC1 a subset of ETC2 only supporting Rgb8 + Some(ColorModel::ETC1) => { + if is_srgb { + TextureFormat::Etc2Rgb8UnormSrgb + } else { + TextureFormat::Etc2Rgb8Unorm + } + } + Some(ColorModel::ETC2) => match sample_information.len() { + 1 => { + let sample = &sample_information[0]; + match sample.channel_type { + 0 => { + if sample_information[0] + .channel_type_qualifiers + .contains(ChannelTypeQualifiers::SIGNED) + { + TextureFormat::EacR11Snorm + } else { + TextureFormat::EacR11Unorm + } + } + 2 => { + if is_srgb { + TextureFormat::Etc2Rgb8UnormSrgb + } else { + TextureFormat::Etc2Rgb8Unorm + } + } + _ => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Invalid ETC2 sample channel type: {}", + sample.channel_type + ))) + } + } + } + 2 => { + let sample0 = &sample_information[0]; + let sample1 = &sample_information[1]; + if sample0.channel_type == 0 && sample1.channel_type == 1 { + if sample0 + .channel_type_qualifiers + .contains(ChannelTypeQualifiers::SIGNED) + { + TextureFormat::EacRg11Snorm + } else { + TextureFormat::EacRg11Unorm + } + } else if sample0.channel_type == 2 && sample1.channel_type == 15 { + if is_srgb { + TextureFormat::Etc2Rgb8A1UnormSrgb + } else { + TextureFormat::Etc2Rgb8A1Unorm + } + } else if sample0.channel_type == 15 && sample1.channel_type == 2 { + if is_srgb { + TextureFormat::Etc2Rgba8UnormSrgb + } else { + TextureFormat::Etc2Rgba8Unorm + } + } else { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Invalid ETC2 2-sample channel types: {} {}", + sample0.channel_type, sample1.channel_type + ))); + } + } + v => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Unsupported channel count for ETC2 format: {}", + v + ))); + } + }, + Some(ColorModel::ASTC) => match data_format_descriptor.texel_block_dimensions[0] { + 4 => match data_format_descriptor.texel_block_dimensions[1] { + 4 => { + if is_srgb { + TextureFormat::Astc4x4RgbaUnormSrgb + } else { + TextureFormat::Astc4x4RgbaUnorm + } + } + d => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Invalid ASTC y-dimension: {}", + d + ))) + } + }, + 5 => match data_format_descriptor.texel_block_dimensions[1] { + 4 => { + if is_srgb { + TextureFormat::Astc5x4RgbaUnormSrgb + } else { + TextureFormat::Astc5x4RgbaUnorm + } + } + 5 => { + if is_srgb { + TextureFormat::Astc5x5RgbaUnormSrgb + } else { + TextureFormat::Astc5x5RgbaUnorm + } + } + d => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Invalid ASTC y-dimension: {}", + d + ))) + } + }, + 6 => match data_format_descriptor.texel_block_dimensions[1] { + 5 => { + if is_srgb { + TextureFormat::Astc6x5RgbaUnormSrgb + } else { + TextureFormat::Astc6x5RgbaUnorm + } + } + 6 => { + if is_srgb { + TextureFormat::Astc6x6RgbaUnormSrgb + } else { + TextureFormat::Astc6x6RgbaUnorm + } + } + d => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Invalid ASTC y-dimension: {}", + d + ))) + } + }, + 8 => match data_format_descriptor.texel_block_dimensions[1] { + 5 => { + if is_srgb { + TextureFormat::Astc8x5RgbaUnormSrgb + } else { + TextureFormat::Astc8x5RgbaUnorm + } + } + 6 => { + if is_srgb { + TextureFormat::Astc8x6RgbaUnormSrgb + } else { + TextureFormat::Astc8x6RgbaUnorm + } + } + 8 => { + if is_srgb { + TextureFormat::Astc8x8RgbaUnormSrgb + } else { + TextureFormat::Astc8x8RgbaUnorm + } + } + d => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Invalid ASTC y-dimension: {}", + d + ))) + } + }, + 10 => match data_format_descriptor.texel_block_dimensions[1] { + 5 => { + if is_srgb { + TextureFormat::Astc10x5RgbaUnormSrgb + } else { + TextureFormat::Astc10x5RgbaUnorm + } + } + 6 => { + if is_srgb { + TextureFormat::Astc10x6RgbaUnormSrgb + } else { + TextureFormat::Astc10x6RgbaUnorm + } + } + 8 => { + if is_srgb { + TextureFormat::Astc10x8RgbaUnormSrgb + } else { + TextureFormat::Astc10x8RgbaUnorm + } + } + 10 => { + if is_srgb { + TextureFormat::Astc10x10RgbaUnormSrgb + } else { + TextureFormat::Astc10x10RgbaUnorm + } + } + d => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Invalid ASTC y-dimension: {}", + d + ))) + } + }, + 12 => match data_format_descriptor.texel_block_dimensions[1] { + 10 => { + if is_srgb { + TextureFormat::Astc12x10RgbaUnormSrgb + } else { + TextureFormat::Astc12x10RgbaUnorm + } + } + 12 => { + if is_srgb { + TextureFormat::Astc12x12RgbaUnormSrgb + } else { + TextureFormat::Astc12x12RgbaUnorm + } + } + d => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Invalid ASTC y-dimension: {}", + d + ))) + } + }, + d => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Invalid ASTC x-dimension: {}", + d + ))) + } + }, + Some(ColorModel::ETC1S) => { + return Err(TextureError::FormatRequiresTranscodingError( + TranscodeFormat::Etc1s, + )); + } + Some(ColorModel::PVRTC) => { + return Err(TextureError::UnsupportedTextureFormat( + "PVRTC is not supported".to_string(), + )); + } + Some(ColorModel::PVRTC2) => { + return Err(TextureError::UnsupportedTextureFormat( + "PVRTC2 is not supported".to_string(), + )); + } + Some(ColorModel::UASTC) => { + return Err(TextureError::FormatRequiresTranscodingError( + TranscodeFormat::Uastc(match sample_information.len() { + 1 => DataFormat::R8, + 2 => DataFormat::Rg8, + 3 => DataFormat::Rgb8, + 4 => { + if sample_information[0].bit_length == 8 { + DataFormat::Rgba8 + } else { + DataFormat::Rgba16Float + } + } + _ => DataFormat::Rgba8, + }), + )); + } + None => { + return Err(TextureError::UnsupportedTextureFormat( + "Unspecified KTX2 color model".to_string(), + )); + } + _ => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "Unknown KTX2 color model: {:?}", + data_format_descriptor.color_model + ))); + } + }) +} + +pub fn ktx2_format_to_texture_format( + ktx2_format: ktx2::Format, + is_srgb: bool, +) -> Result { + Ok(match ktx2_format { + ktx2::Format::R8_UNORM => { + if is_srgb { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + ktx2_format + ))); + } else { + TextureFormat::R8Unorm + } + } + ktx2::Format::R8_SNORM => TextureFormat::R8Snorm, + ktx2::Format::R8_UINT => TextureFormat::R8Uint, + ktx2::Format::R8_SINT => TextureFormat::R8Sint, + ktx2::Format::R8_SRGB => { + if is_srgb { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + ktx2_format + ))); + } else { + TextureFormat::R8Unorm + } + } + ktx2::Format::R8G8_UNORM => { + if is_srgb { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + ktx2_format + ))); + } else { + TextureFormat::Rg8Unorm + } + } + ktx2::Format::R8G8_SNORM => TextureFormat::Rg8Snorm, + ktx2::Format::R8G8_UINT => TextureFormat::Rg8Uint, + ktx2::Format::R8G8_SINT => TextureFormat::Rg8Sint, + ktx2::Format::R8G8_SRGB => { + if is_srgb { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + ktx2_format + ))); + } else { + TextureFormat::Rg8Unorm + } + } + ktx2::Format::R8G8B8A8_UNORM => { + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + } + } + ktx2::Format::R8G8B8A8_SNORM => TextureFormat::Rgba8Snorm, + ktx2::Format::R8G8B8A8_UINT => TextureFormat::Rgba8Uint, + ktx2::Format::R8G8B8A8_SINT => TextureFormat::Rgba8Sint, + ktx2::Format::R8G8B8A8_SRGB => { + if is_srgb { + TextureFormat::Rgba8UnormSrgb + } else { + TextureFormat::Rgba8Unorm + } + } + ktx2::Format::B8G8R8A8_UNORM => { + if is_srgb { + TextureFormat::Bgra8UnormSrgb + } else { + TextureFormat::Bgra8Unorm + } + } + ktx2::Format::B8G8R8A8_SRGB => { + if is_srgb { + TextureFormat::Bgra8UnormSrgb + } else { + TextureFormat::Bgra8Unorm + } + } + ktx2::Format::A2R10G10B10_UNORM_PACK32 => TextureFormat::Rgb10a2Unorm, + + ktx2::Format::R16_UNORM => TextureFormat::R16Unorm, + ktx2::Format::R16_SNORM => TextureFormat::R16Snorm, + ktx2::Format::R16_UINT => TextureFormat::R16Uint, + ktx2::Format::R16_SINT => TextureFormat::R16Sint, + ktx2::Format::R16_SFLOAT => TextureFormat::R16Float, + ktx2::Format::R16G16_UNORM => TextureFormat::Rg16Unorm, + ktx2::Format::R16G16_SNORM => TextureFormat::Rg16Snorm, + ktx2::Format::R16G16_UINT => TextureFormat::Rg16Uint, + ktx2::Format::R16G16_SINT => TextureFormat::Rg16Sint, + ktx2::Format::R16G16_SFLOAT => TextureFormat::Rg16Float, + + ktx2::Format::R16G16B16A16_UNORM => TextureFormat::Rgba16Unorm, + ktx2::Format::R16G16B16A16_SNORM => TextureFormat::Rgba16Snorm, + ktx2::Format::R16G16B16A16_UINT => TextureFormat::Rgba16Uint, + ktx2::Format::R16G16B16A16_SINT => TextureFormat::Rgba16Sint, + ktx2::Format::R16G16B16A16_SFLOAT => TextureFormat::Rgba16Float, + ktx2::Format::R32_UINT => TextureFormat::R32Uint, + ktx2::Format::R32_SINT => TextureFormat::R32Sint, + ktx2::Format::R32_SFLOAT => TextureFormat::R32Float, + ktx2::Format::R32G32_UINT => TextureFormat::Rg32Uint, + ktx2::Format::R32G32_SINT => TextureFormat::Rg32Sint, + ktx2::Format::R32G32_SFLOAT => TextureFormat::Rg32Float, + + ktx2::Format::R32G32B32A32_UINT => TextureFormat::Rgba32Uint, + ktx2::Format::R32G32B32A32_SINT => TextureFormat::Rgba32Sint, + ktx2::Format::R32G32B32A32_SFLOAT => TextureFormat::Rgba32Float, + + ktx2::Format::B10G11R11_UFLOAT_PACK32 => TextureFormat::Rg11b10Float, + ktx2::Format::E5B9G9R9_UFLOAT_PACK32 => TextureFormat::Rgb9e5Ufloat, + + ktx2::Format::X8_D24_UNORM_PACK32 => TextureFormat::Depth24Plus, + ktx2::Format::D32_SFLOAT => TextureFormat::Depth32Float, + + ktx2::Format::D24_UNORM_S8_UINT => TextureFormat::Depth24PlusStencil8, + + ktx2::Format::BC1_RGB_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Bc1RgbaUnormSrgb + } else { + TextureFormat::Bc1RgbaUnorm + } + } + ktx2::Format::BC1_RGB_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Bc1RgbaUnormSrgb + } else { + TextureFormat::Bc1RgbaUnorm + } + } + ktx2::Format::BC1_RGBA_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Bc1RgbaUnormSrgb + } else { + TextureFormat::Bc1RgbaUnorm + } + } + ktx2::Format::BC1_RGBA_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Bc1RgbaUnormSrgb + } else { + TextureFormat::Bc1RgbaUnorm + } + } + ktx2::Format::BC2_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Bc2RgbaUnormSrgb + } else { + TextureFormat::Bc2RgbaUnorm + } + } + ktx2::Format::BC2_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Bc2RgbaUnormSrgb + } else { + TextureFormat::Bc2RgbaUnorm + } + } + ktx2::Format::BC3_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Bc3RgbaUnormSrgb + } else { + TextureFormat::Bc3RgbaUnorm + } + } + ktx2::Format::BC3_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Bc3RgbaUnormSrgb + } else { + TextureFormat::Bc3RgbaUnorm + } + } + ktx2::Format::BC4_UNORM_BLOCK => TextureFormat::Bc4RUnorm, + ktx2::Format::BC4_SNORM_BLOCK => TextureFormat::Bc4RSnorm, + ktx2::Format::BC5_UNORM_BLOCK => TextureFormat::Bc5RgUnorm, + ktx2::Format::BC5_SNORM_BLOCK => TextureFormat::Bc5RgSnorm, + ktx2::Format::BC6H_UFLOAT_BLOCK => TextureFormat::Bc6hRgbUfloat, + ktx2::Format::BC6H_SFLOAT_BLOCK => TextureFormat::Bc6hRgbSfloat, + ktx2::Format::BC7_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Bc7RgbaUnormSrgb + } else { + TextureFormat::Bc7RgbaUnorm + } + } + ktx2::Format::BC7_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Bc7RgbaUnormSrgb + } else { + TextureFormat::Bc7RgbaUnorm + } + } + ktx2::Format::ETC2_R8G8B8_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Etc2Rgb8UnormSrgb + } else { + TextureFormat::Etc2Rgb8Unorm + } + } + ktx2::Format::ETC2_R8G8B8_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Etc2Rgb8UnormSrgb + } else { + TextureFormat::Etc2Rgb8Unorm + } + } + ktx2::Format::ETC2_R8G8B8A1_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Etc2Rgb8A1UnormSrgb + } else { + TextureFormat::Etc2Rgb8A1Unorm + } + } + ktx2::Format::ETC2_R8G8B8A1_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Etc2Rgb8A1UnormSrgb + } else { + TextureFormat::Etc2Rgb8A1Unorm + } + } + ktx2::Format::ETC2_R8G8B8A8_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Etc2Rgba8UnormSrgb + } else { + TextureFormat::Etc2Rgba8Unorm + } + } + ktx2::Format::ETC2_R8G8B8A8_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Etc2Rgba8UnormSrgb + } else { + TextureFormat::Etc2Rgba8Unorm + } + } + ktx2::Format::EAC_R11_UNORM_BLOCK => TextureFormat::EacR11Unorm, + ktx2::Format::EAC_R11_SNORM_BLOCK => TextureFormat::EacR11Snorm, + ktx2::Format::EAC_R11G11_UNORM_BLOCK => TextureFormat::EacRg11Unorm, + ktx2::Format::EAC_R11G11_SNORM_BLOCK => TextureFormat::EacRg11Snorm, + ktx2::Format::ASTC_4x4_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc4x4RgbaUnormSrgb + } else { + TextureFormat::Astc4x4RgbaUnorm + } + } + ktx2::Format::ASTC_4x4_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc4x4RgbaUnormSrgb + } else { + TextureFormat::Astc4x4RgbaUnorm + } + } + ktx2::Format::ASTC_5x4_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc5x4RgbaUnormSrgb + } else { + TextureFormat::Astc5x4RgbaUnorm + } + } + ktx2::Format::ASTC_5x4_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc5x4RgbaUnormSrgb + } else { + TextureFormat::Astc5x4RgbaUnorm + } + } + ktx2::Format::ASTC_5x5_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc5x5RgbaUnormSrgb + } else { + TextureFormat::Astc5x5RgbaUnorm + } + } + ktx2::Format::ASTC_5x5_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc5x5RgbaUnormSrgb + } else { + TextureFormat::Astc5x5RgbaUnorm + } + } + ktx2::Format::ASTC_6x5_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc6x5RgbaUnormSrgb + } else { + TextureFormat::Astc6x5RgbaUnorm + } + } + ktx2::Format::ASTC_6x5_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc6x5RgbaUnormSrgb + } else { + TextureFormat::Astc6x5RgbaUnorm + } + } + ktx2::Format::ASTC_6x6_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc6x6RgbaUnormSrgb + } else { + TextureFormat::Astc6x6RgbaUnorm + } + } + ktx2::Format::ASTC_6x6_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc6x6RgbaUnormSrgb + } else { + TextureFormat::Astc6x6RgbaUnorm + } + } + ktx2::Format::ASTC_8x5_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc8x5RgbaUnormSrgb + } else { + TextureFormat::Astc8x5RgbaUnorm + } + } + ktx2::Format::ASTC_8x5_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc8x5RgbaUnormSrgb + } else { + TextureFormat::Astc8x5RgbaUnorm + } + } + ktx2::Format::ASTC_8x6_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc8x6RgbaUnormSrgb + } else { + TextureFormat::Astc8x6RgbaUnorm + } + } + ktx2::Format::ASTC_8x6_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc8x6RgbaUnormSrgb + } else { + TextureFormat::Astc8x6RgbaUnorm + } + } + ktx2::Format::ASTC_8x8_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc8x8RgbaUnormSrgb + } else { + TextureFormat::Astc8x8RgbaUnorm + } + } + ktx2::Format::ASTC_8x8_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc8x8RgbaUnormSrgb + } else { + TextureFormat::Astc8x8RgbaUnorm + } + } + ktx2::Format::ASTC_10x5_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc10x5RgbaUnormSrgb + } else { + TextureFormat::Astc10x5RgbaUnorm + } + } + ktx2::Format::ASTC_10x5_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc10x5RgbaUnormSrgb + } else { + TextureFormat::Astc10x5RgbaUnorm + } + } + ktx2::Format::ASTC_10x6_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc10x6RgbaUnormSrgb + } else { + TextureFormat::Astc10x6RgbaUnorm + } + } + ktx2::Format::ASTC_10x6_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc10x6RgbaUnormSrgb + } else { + TextureFormat::Astc10x6RgbaUnorm + } + } + ktx2::Format::ASTC_10x8_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc10x8RgbaUnormSrgb + } else { + TextureFormat::Astc10x8RgbaUnorm + } + } + ktx2::Format::ASTC_10x8_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc10x8RgbaUnormSrgb + } else { + TextureFormat::Astc10x8RgbaUnorm + } + } + ktx2::Format::ASTC_10x10_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc10x10RgbaUnormSrgb + } else { + TextureFormat::Astc10x10RgbaUnorm + } + } + ktx2::Format::ASTC_10x10_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc10x10RgbaUnormSrgb + } else { + TextureFormat::Astc10x10RgbaUnorm + } + } + ktx2::Format::ASTC_12x10_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc12x10RgbaUnormSrgb + } else { + TextureFormat::Astc12x10RgbaUnorm + } + } + ktx2::Format::ASTC_12x10_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc12x10RgbaUnormSrgb + } else { + TextureFormat::Astc12x10RgbaUnorm + } + } + ktx2::Format::ASTC_12x12_UNORM_BLOCK => { + if is_srgb { + TextureFormat::Astc12x12RgbaUnormSrgb + } else { + TextureFormat::Astc12x12RgbaUnorm + } + } + ktx2::Format::ASTC_12x12_SRGB_BLOCK => { + if is_srgb { + TextureFormat::Astc12x12RgbaUnormSrgb + } else { + TextureFormat::Astc12x12RgbaUnorm + } + } + _ => { + return Err(TextureError::UnsupportedTextureFormat(format!( + "{:?}", + ktx2_format + ))) + } + }) +} diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 3f90ef17de0ece..84bd662747b83f 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -1,13 +1,23 @@ +#[cfg(feature = "basis-universal")] +mod basis; +#[cfg(feature = "dds")] +mod dds; #[cfg(feature = "hdr")] mod hdr_texture_loader; #[allow(clippy::module_inception)] mod image; mod image_texture_loader; +#[cfg(feature = "ktx2")] +mod ktx2; mod texture_cache; pub(crate) mod image_texture_conversion; pub use self::image::*; +#[cfg(feature = "ktx2")] +pub use self::ktx2::*; +#[cfg(feature = "dds")] +pub use dds::*; #[cfg(feature = "hdr")] pub use hdr_texture_loader::*; @@ -29,7 +39,9 @@ impl Plugin for ImagePlugin { feature = "dds", feature = "tga", feature = "jpeg", - feature = "bmp" + feature = "bmp", + feature = "basis-universal", + feature = "ktx2", ))] { app.init_asset_loader::(); diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index d9bd79aab90e88..2c6e31085130aa 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -192,6 +192,7 @@ impl FromWorld for Mesh2dPipeline { GpuImage { texture, texture_view, + texture_format: image.texture_descriptor.format, sampler, size: Size::new( image.texture_descriptor.size.width as f32, diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 409832cde24ce3..9d3f44787e8184 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -26,6 +26,10 @@ |trace_tracy|Enables [Tracy](https://github.com/wolfpld/tracy) as bevy_log output. This allows `Tracy` to connect to and capture profiling data as well as visualize system execution in real-time, present statistics about system execution times, and more.| |wgpu_trace|For tracing wgpu.| |dds|DDS picture format support.| +|ktx2|KTX2 picture format support.| +|zlib|KTX2 Zlib supercompression support.| +|zstd|KTX2 Zstandard supercompression support.| +|basis-universal|Basis Universal picture format support and, if the `ktx2` feature is enabled, also KTX2 UASTC picture format transcoding support.| |tga|TGA picture format support.| |jpeg|JPEG picture format support.| |bmp|BMP picture format support.|