KTX2/DDS/.basis compressed texture support (#3884)

# Objective

- Support compressed textures including 'universal' formats (ETC1S, UASTC) and transcoding of them to 
- Support `.dds`, `.ktx2`, and `.basis` files

## Solution

- Fixes https://github.com/bevyengine/bevy/issues/3608 Look there for more details.
- Note that the functionality is all enabled through non-default features. If it is desirable to enable some by default, I can do that.
- The `basis-universal` crate, used for `.basis` file support and for transcoding, is built on bindings against a C++ library. It's not feasible to rewrite in Rust in a short amount of time. There are no Rust alternatives of which I am aware and it's specialised code. In its current state it doesn't support the wasm target, but I don't know for sure. However, it is possible to build the upstream C++ library with emscripten, so there is perhaps a way to add support for web too with some shenanigans.
- There's no support for transcoding from BasisLZ/ETC1S in KTX2 files as it was quite non-trivial to implement and didn't feel important given people could use `.basis` files for ETC1S.
This commit is contained in:
Robert Swain 2022-03-15 22:26:46 +00:00
parent 9dfd4e4b08
commit 0529f633f9
19 changed files with 2749 additions and 89 deletions

View file

@ -64,10 +64,15 @@ wgpu_trace = ["bevy_internal/wgpu_trace"]
# Image format support for texture loading (PNG and HDR are enabled by default) # Image format support for texture loading (PNG and HDR are enabled by default)
hdr = ["bevy_internal/hdr"] hdr = ["bevy_internal/hdr"]
png = ["bevy_internal/png"] png = ["bevy_internal/png"]
dds = ["bevy_internal/dds"]
tga = ["bevy_internal/tga"] tga = ["bevy_internal/tga"]
jpeg = ["bevy_internal/jpeg"] jpeg = ["bevy_internal/jpeg"]
bmp = ["bevy_internal/bmp"] 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) # Audio format support (vorbis is enabled by default)
flac = ["bevy_internal/flac"] flac = ["bevy_internal/flac"]

View file

@ -3,7 +3,7 @@ use bevy_asset::{
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset, AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset,
}; };
use bevy_core::Name; use bevy_core::Name;
use bevy_ecs::world::World; use bevy_ecs::{prelude::FromWorld, world::World};
use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder}; use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder};
use bevy_log::warn; use bevy_log::warn;
use bevy_math::{Mat4, Vec3}; use bevy_math::{Mat4, Vec3};
@ -18,10 +18,9 @@ use bevy_render::{
color::Color, color::Color,
mesh::{Indices, Mesh, VertexAttributeValues}, mesh::{Indices, Mesh, VertexAttributeValues},
primitives::{Aabb, Frustum}, primitives::{Aabb, Frustum},
render_resource::{ render_resource::{AddressMode, FilterMode, PrimitiveTopology, SamplerDescriptor},
AddressMode, FilterMode, PrimitiveTopology, SamplerDescriptor, TextureFormat, renderer::RenderDevice,
}, texture::{CompressedImageFormats, Image, ImageType, TextureError},
texture::{Image, ImageType, TextureError},
view::VisibleEntities, view::VisibleEntities,
}; };
use bevy_scene::Scene; use bevy_scene::Scene;
@ -60,8 +59,9 @@ pub enum GltfError {
} }
/// Loads glTF files with all of their data as their corresponding bevy representations. /// 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 { impl AssetLoader for GltfLoader {
fn load<'a>( fn load<'a>(
@ -69,7 +69,9 @@ impl AssetLoader for GltfLoader {
bytes: &'a [u8], bytes: &'a [u8],
load_context: &'a mut LoadContext, load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<()>> { ) -> 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] { 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::<RenderDevice>().features(),
),
}
}
}
/// Loads an entire glTF file. /// Loads an entire glTF file.
async fn load_gltf<'a, 'b>( async fn load_gltf<'a, 'b>(
bytes: &'a [u8], bytes: &'a [u8],
load_context: &'a mut LoadContext<'b>, load_context: &'a mut LoadContext<'b>,
supported_compressed_formats: CompressedImageFormats,
) -> Result<(), GltfError> { ) -> Result<(), GltfError> {
let gltf = gltf::Gltf::from_slice(bytes)?; let gltf = gltf::Gltf::from_slice(bytes)?;
let buffer_data = load_buffers(&gltf, load_context, load_context.path()).await?; 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 // to avoid https://github.com/bevyengine/bevy/pull/2725
if gltf.textures().len() == 1 || cfg!(target_arch = "wasm32") { if gltf.textures().len() == 1 || cfg!(target_arch = "wasm32") {
for gltf_texture in gltf.textures() { for gltf_texture in gltf.textures() {
let (texture, label) = let (texture, label) = load_texture(
load_texture(gltf_texture, &buffer_data, &linear_textures, load_context).await?; gltf_texture,
&buffer_data,
&linear_textures,
load_context,
supported_compressed_formats,
)
.await?;
load_context.set_labeled_asset(&label, LoadedAsset::new(texture)); load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
} }
} else { } else {
@ -265,7 +284,14 @@ async fn load_gltf<'a, 'b>(
let load_context: &LoadContext = load_context; let load_context: &LoadContext = load_context;
let buffer_data = &buffer_data; let buffer_data = &buffer_data;
scope.spawn(async move { 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<u8>], buffer_data: &[Vec<u8>],
linear_textures: &HashSet<usize>, linear_textures: &HashSet<usize>,
load_context: &LoadContext<'a>, load_context: &LoadContext<'a>,
supported_compressed_formats: CompressedImageFormats,
) -> Result<(Image, String), GltfError> { ) -> Result<(Image, String), GltfError> {
let is_srgb = !linear_textures.contains(&gltf_texture.index());
let mut texture = match gltf_texture.source().source() { let mut texture = match gltf_texture.source().source() {
gltf::image::Source::View { view, mime_type } => { gltf::image::Source::View { view, mime_type } => {
let start = view.offset() as usize; let start = view.offset() as usize;
let end = (view.offset() + view.length()) as usize; let end = (view.offset() + view.length()) as usize;
let buffer = &buffer_data[view.buffer().index()][start..end]; 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 } => { gltf::image::Source::Uri { uri, mime_type } => {
let uri = percent_encoding::percent_decode_str(uri) let uri = percent_encoding::percent_decode_str(uri)
@ -363,13 +396,12 @@ async fn load_texture<'a>(
Image::from_buffer( Image::from_buffer(
&bytes, &bytes,
mime_type.map(ImageType::MimeType).unwrap_or(image_type), mime_type.map(ImageType::MimeType).unwrap_or(image_type),
supported_compressed_formats,
is_srgb,
)? )?
} }
}; };
texture.sampler_descriptor = texture_sampler(&gltf_texture); 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))) Ok((texture, texture_label(&gltf_texture)))
} }

View file

@ -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) # Image format support for texture loading (PNG and HDR are enabled by default)
hdr = ["bevy_render/hdr"] hdr = ["bevy_render/hdr"]
png = ["bevy_render/png"] png = ["bevy_render/png"]
dds = ["bevy_render/dds"]
tga = ["bevy_render/tga"] tga = ["bevy_render/tga"]
jpeg = ["bevy_render/jpeg"] jpeg = ["bevy_render/jpeg"]
bmp = ["bevy_render/bmp"] 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) # Audio format support (vorbis is enabled by default)
flac = ["bevy_audio/flac"] flac = ["bevy_audio/flac"]

View file

@ -58,6 +58,8 @@ impl PluginGroup for DefaultPlugins {
#[cfg(feature = "bevy_pbr")] #[cfg(feature = "bevy_pbr")]
group.add(bevy_pbr::PbrPlugin::default()); 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")] #[cfg(feature = "bevy_gltf")]
group.add(bevy_gltf::GltfPlugin::default()); group.add(bevy_gltf::GltfPlugin::default());

View file

@ -123,6 +123,7 @@ bitflags::bitflags! {
const ALPHA_MODE_OPAQUE = (1 << 6); const ALPHA_MODE_OPAQUE = (1 << 6);
const ALPHA_MODE_MASK = (1 << 7); const ALPHA_MODE_MASK = (1 << 7);
const ALPHA_MODE_BLEND = (1 << 8); const ALPHA_MODE_BLEND = (1 << 8);
const TWO_COMPONENT_NORMAL_MAP = (1 << 9);
const NONE = 0; const NONE = 0;
const UNINITIALIZED = 0xFFFF; const UNINITIALIZED = 0xFFFF;
} }
@ -246,6 +247,22 @@ impl RenderAsset for StandardMaterial {
flags |= StandardMaterialFlags::UNLIT; flags |= StandardMaterialFlags::UNLIT;
} }
let has_normal_map = material.normal_map_texture.is_some(); 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? // NOTE: 0.5 is from the glTF default - do we want this?
let mut alpha_cutoff = 0.5; let mut alpha_cutoff = 0.5;
match material.alpha_mode { match material.alpha_mode {

View file

@ -327,6 +327,7 @@ impl FromWorld for MeshPipeline {
GpuImage { GpuImage {
texture, texture,
texture_view, texture_view,
texture_format: image.texture_descriptor.format,
sampler, sampler,
size: Size::new( size: Size::new(
image.texture_descriptor.size.width as f32, image.texture_descriptor.size.width as f32,

View file

@ -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_OPAQUE: u32 = 64u;
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 128u; let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 128u;
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 256u; let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 256u;
let STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 512u;
[[group(1), binding(0)]] [[group(1), binding(0)]]
var<uniform> material: StandardMaterial; var<uniform> material: StandardMaterial;
@ -515,7 +516,16 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
#ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS
#ifdef STANDARDMATERIAL_NORMAL_MAP #ifdef STANDARDMATERIAL_NORMAL_MAP
let TBN = mat3x3<f32>(T, B, N); let TBN = mat3x3<f32>(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<f32>;
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<f32>(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
#endif #endif

View file

@ -11,10 +11,15 @@ keywords = ["bevy"]
[features] [features]
png = ["image/png"] png = ["image/png"]
hdr = ["image/hdr"] hdr = ["image/hdr"]
dds = ["image/dds"]
tga = ["image/tga"] tga = ["image/tga"]
jpeg = ["image/jpeg"] jpeg = ["image/jpeg"]
bmp = ["image/bmp"] bmp = ["image/bmp"]
dds = ["ddsfile"]
# For ktx2 supercompression
zlib = ["flate2"]
zstd = ["ruzstd"]
trace = [] trace = []
wgpu_trace = ["wgpu/trace"] wgpu_trace = ["wgpu/trace"]
ci_limits = [] ci_limits = []
@ -54,3 +59,10 @@ hexasphere = "7.0.0"
parking_lot = "0.11.0" parking_lot = "0.11.0"
regex = "1.5" regex = "1.5"
copyless = "0.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 }

View file

@ -289,6 +289,8 @@ impl Plugin for RenderPlugin {
.add_plugin(CameraPlugin) .add_plugin(CameraPlugin)
.add_plugin(ViewPlugin) .add_plugin(ViewPlugin)
.add_plugin(MeshPlugin) .add_plugin(MeshPlugin)
// NOTE: Load this after renderer initialization so that it knows about the supported
// compressed texture formats
.add_plugin(ImagePlugin); .add_plugin(ImagePlugin);
} }
} }

View file

@ -6,6 +6,8 @@ use futures_lite::future;
use std::sync::Arc; use std::sync::Arc;
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
use super::RenderQueue;
/// This GPU device is responsible for the creation of most rendering and compute resources. /// This GPU device is responsible for the creation of most rendering and compute resources.
#[derive(Clone)] #[derive(Clone)]
pub struct RenderDevice { pub struct RenderDevice {
@ -121,6 +123,22 @@ impl RenderDevice {
Buffer::from(wgpu_buffer) 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`]. /// Creates a new [`Texture`].
/// ///
/// `desc` specifies the general format of the texture. /// `desc` specifies the general format of the texture.

View file

@ -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<Image, TextureError> {
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
},
)
}
}

View file

@ -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<Image, TextureError> {
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<TextureFormat, TextureError> {
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(),
));
})
}

View file

@ -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 super::image_texture_conversion::image_to_texture;
use crate::{ use crate::{
render_asset::{PrepareAssetError, RenderAsset}, render_asset::{PrepareAssetError, RenderAsset},
@ -20,6 +27,82 @@ pub const SAMPLER_ASSET_INDEX: u64 = 1;
pub const DEFAULT_IMAGE_HANDLE: HandleUntyped = pub const DEFAULT_IMAGE_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Image::TYPE_UUID, 13148262314052771789); 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<Self> {
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<Self> {
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<image::ImageFormat> {
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)] #[derive(Debug, Clone, TypeUuid)]
#[uuid = "6ea26da6-6cf8-4ea2-9986-1d7bf6c17d6f"] #[uuid = "6ea26da6-6cf8-4ea2-9986-1d7bf6c17d6f"]
pub struct Image { pub struct Image {
@ -181,38 +264,35 @@ impl Image {
pub fn convert(&self, new_format: TextureFormat) -> Option<Self> { pub fn convert(&self, new_format: TextureFormat) -> Option<Self> {
super::image_texture_conversion::texture_to_image(self) super::image_texture_conversion::texture_to_image(self)
.and_then(|img| match new_format { .and_then(|img| match new_format {
TextureFormat::R8Unorm => Some(image::DynamicImage::ImageLuma8(img.into_luma8())), TextureFormat::R8Unorm => {
TextureFormat::Rg8Unorm => { Some((image::DynamicImage::ImageLuma8(img.into_luma8()), false))
Some(image::DynamicImage::ImageLumaA8(img.into_luma_alpha8()))
} }
TextureFormat::Rg8Unorm => Some((
image::DynamicImage::ImageLumaA8(img.into_luma_alpha8()),
false,
)),
TextureFormat::Rgba8UnormSrgb => { TextureFormat::Rgba8UnormSrgb => {
Some(image::DynamicImage::ImageRgba8(img.into_rgba8())) Some((image::DynamicImage::ImageRgba8(img.into_rgba8()), true))
} }
TextureFormat::Bgra8UnormSrgb => { TextureFormat::Bgra8UnormSrgb => {
Some(image::DynamicImage::ImageBgra8(img.into_bgra8())) Some((image::DynamicImage::ImageBgra8(img.into_bgra8()), true))
} }
_ => None, _ => 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` /// Load a bytes buffer in a [`Image`], according to type `image_type`, using the `image`
/// crate /// crate
pub fn from_buffer(buffer: &[u8], image_type: ImageType) -> Result<Image, TextureError> { pub fn from_buffer(
let format = match image_type { buffer: &[u8],
ImageType::MimeType(mime_type) => match mime_type { image_type: ImageType,
"image/png" => Ok(image::ImageFormat::Png), #[allow(unused_variables)] supported_compressed_formats: CompressedImageFormats,
"image/vnd-ms.dds" => Ok(image::ImageFormat::Dds), is_srgb: bool,
"image/x-targa" => Ok(image::ImageFormat::Tga), ) -> Result<Image, TextureError> {
"image/x-tga" => Ok(image::ImageFormat::Tga), let format = image_type.to_image_format()?;
"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())),
}?;
// Load the image in the expected format. // Load the image in the expected format.
// Some formats like PNG allow for R or RG textures too, so the texture // 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 // needs to be added, so the image data needs to be converted in those
// cases. // cases.
let dyn_img = image::load_from_memory_with_format(buffer, format)?; match format {
Ok(image_to_texture(dyn_img)) #[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 /// An error that occurs when loading a texture
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum TextureError { pub enum TextureError {
#[error("invalid image mime type")] #[error("invalid image mime type: {0}")]
InvalidImageMimeType(String), InvalidImageMimeType(String),
#[error("invalid image extension")] #[error("invalid image extension: {0}")]
InvalidImageExtension(String), InvalidImageExtension(String),
#[error("failed to load an image: {0}")] #[error("failed to load an image: {0}")]
ImageError(#[from] image::ImageError), 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. /// The type of a raw image buffer.
@ -244,6 +384,17 @@ pub enum ImageType<'a> {
Extension(&'a str), Extension(&'a str),
} }
impl<'a> ImageType<'a> {
pub fn to_image_format(&self) -> Result<ImageFormat, TextureError> {
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. /// Used to calculate the volume of an item.
pub trait Volume { pub trait Volume {
fn volume(&self) -> usize; fn volume(&self) -> usize;
@ -387,6 +538,7 @@ impl TextureFormatPixelInfo for TextureFormat {
pub struct GpuImage { pub struct GpuImage {
pub texture: Texture, pub texture: Texture,
pub texture_view: TextureView, pub texture_view: TextureView,
pub texture_format: TextureFormat,
pub sampler: Sampler, pub sampler: Sampler,
pub size: Size, pub size: Size,
} }
@ -406,9 +558,14 @@ impl RenderAsset for Image {
image: Self::ExtractedAsset, image: Self::ExtractedAsset,
(render_device, render_queue): &mut SystemParamItem<Self::Param>, (render_device, render_queue): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> { ) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
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 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(); let format_size = image.texture_descriptor.format.pixel_size();
render_queue.write_texture( render_queue.write_texture(
ImageCopyTexture { ImageCopyTexture {
@ -434,21 +591,116 @@ impl RenderAsset for Image {
}, },
image.texture_descriptor.size, image.texture_descriptor.size,
); );
texture
};
let texture_view = texture.create_view(&TextureViewDescriptor::default()); let texture_view = texture.create_view(&TextureViewDescriptor::default());
let size = Size::new( let size = Size::new(
image.texture_descriptor.size.width as f32, image.texture_descriptor.size.width as f32,
image.texture_descriptor.size.height as f32, image.texture_descriptor.size.height as f32,
); );
let sampler = render_device.create_sampler(&image.sampler_descriptor);
Ok(GpuImage { Ok(GpuImage {
texture, texture,
texture_view, texture_view,
texture_format: image.texture_descriptor.format,
sampler, sampler,
size, 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)] #[cfg(test)]
mod test { mod test {

View file

@ -4,7 +4,7 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat};
// TODO: fix name? // TODO: fix name?
/// Converts a [`DynamicImage`] to an [`Image`]. /// 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; use bevy_core::cast_slice;
let width; let width;
let height; let height;
@ -17,7 +17,11 @@ pub(crate) fn image_to_texture(dyn_img: DynamicImage) -> Image {
let i = DynamicImage::ImageLuma8(i).into_rgba8(); let i = DynamicImage::ImageLuma8(i).into_rgba8();
width = i.width(); width = i.width();
height = i.height(); height = i.height();
format = TextureFormat::Rgba8UnormSrgb; format = if is_srgb {
TextureFormat::Rgba8UnormSrgb
} else {
TextureFormat::Rgba8Unorm
};
data = i.into_raw(); 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(); let i = DynamicImage::ImageLumaA8(i).into_rgba8();
width = i.width(); width = i.width();
height = i.height(); height = i.height();
format = TextureFormat::Rgba8UnormSrgb; format = if is_srgb {
TextureFormat::Rgba8UnormSrgb
} else {
TextureFormat::Rgba8Unorm
};
data = i.into_raw(); 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(); let i = DynamicImage::ImageRgb8(i).into_rgba8();
width = i.width(); width = i.width();
height = i.height(); height = i.height();
format = TextureFormat::Rgba8UnormSrgb; format = if is_srgb {
TextureFormat::Rgba8UnormSrgb
} else {
TextureFormat::Rgba8Unorm
};
data = i.into_raw(); data = i.into_raw();
} }
DynamicImage::ImageRgba8(i) => { DynamicImage::ImageRgba8(i) => {
width = i.width(); width = i.width();
height = i.height(); height = i.height();
format = TextureFormat::Rgba8UnormSrgb; format = if is_srgb {
TextureFormat::Rgba8UnormSrgb
} else {
TextureFormat::Rgba8Unorm
};
data = i.into_raw(); data = i.into_raw();
} }
@ -49,14 +65,22 @@ pub(crate) fn image_to_texture(dyn_img: DynamicImage) -> Image {
width = i.width(); width = i.width();
height = i.height(); height = i.height();
format = TextureFormat::Bgra8UnormSrgb; format = if is_srgb {
TextureFormat::Bgra8UnormSrgb
} else {
TextureFormat::Bgra8Unorm
};
data = i.into_raw(); data = i.into_raw();
} }
DynamicImage::ImageBgra8(i) => { DynamicImage::ImageBgra8(i) => {
width = i.width(); width = i.width();
height = i.height(); height = i.height();
format = TextureFormat::Bgra8UnormSrgb; format = if is_srgb {
TextureFormat::Bgra8UnormSrgb
} else {
TextureFormat::Bgra8Unorm
};
data = i.into_raw(); data = i.into_raw();
} }

View file

@ -1,15 +1,27 @@
use anyhow::Result; use anyhow::Result;
use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; use bevy_asset::{AssetLoader, LoadContext, LoadedAsset};
use bevy_ecs::prelude::{FromWorld, World};
use bevy_utils::BoxedFuture; use bevy_utils::BoxedFuture;
use thiserror::Error; 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. /// Loader for images that can be read by the `image` crate.
#[derive(Clone, Default)] #[derive(Clone)]
pub struct ImageTextureLoader; pub struct ImageTextureLoader {
supported_compressed_formats: CompressedImageFormats,
}
const FILE_EXTENSIONS: &[&str] = &[ const FILE_EXTENSIONS: &[&str] = &[
#[cfg(feature = "basis-universal")]
"basis",
#[cfg(feature = "bmp")]
"bmp",
#[cfg(feature = "png")] #[cfg(feature = "png")]
"png", "png",
#[cfg(feature = "dds")] #[cfg(feature = "dds")]
@ -20,8 +32,8 @@ const FILE_EXTENSIONS: &[&str] = &[
"jpg", "jpg",
#[cfg(feature = "jpeg")] #[cfg(feature = "jpeg")]
"jpeg", "jpeg",
#[cfg(feature = "bmp")] #[cfg(feature = "ktx2")]
"bmp", "ktx2",
]; ];
impl AssetLoader for ImageTextureLoader { impl AssetLoader for ImageTextureLoader {
@ -34,11 +46,15 @@ impl AssetLoader for ImageTextureLoader {
// use the file extension for the image type // use the file extension for the image type
let ext = load_context.path().extension().unwrap().to_str().unwrap(); let ext = load_context.path().extension().unwrap().to_str().unwrap();
let dyn_img = Image::from_buffer(bytes, ImageType::Extension(ext)).map_err(|err| { let dyn_img = Image::from_buffer(
FileTextureError { bytes,
ImageType::Extension(ext),
self.supported_compressed_formats,
true,
)
.map_err(|err| FileTextureError {
error: err, error: err,
path: format!("{}", load_context.path().display()), path: format!("{}", load_context.path().display()),
}
})?; })?;
load_context.set_default_asset(LoadedAsset::new(dyn_img)); 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::<RenderDevice>().features(),
),
}
}
}
/// An error that occurs when loading a texture from a file. /// An error that occurs when loading a texture from a file.
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub struct FileTextureError { pub struct FileTextureError {

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,23 @@
#[cfg(feature = "basis-universal")]
mod basis;
#[cfg(feature = "dds")]
mod dds;
#[cfg(feature = "hdr")] #[cfg(feature = "hdr")]
mod hdr_texture_loader; mod hdr_texture_loader;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod image; mod image;
mod image_texture_loader; mod image_texture_loader;
#[cfg(feature = "ktx2")]
mod ktx2;
mod texture_cache; mod texture_cache;
pub(crate) mod image_texture_conversion; pub(crate) mod image_texture_conversion;
pub use self::image::*; pub use self::image::*;
#[cfg(feature = "ktx2")]
pub use self::ktx2::*;
#[cfg(feature = "dds")]
pub use dds::*;
#[cfg(feature = "hdr")] #[cfg(feature = "hdr")]
pub use hdr_texture_loader::*; pub use hdr_texture_loader::*;
@ -29,7 +39,9 @@ impl Plugin for ImagePlugin {
feature = "dds", feature = "dds",
feature = "tga", feature = "tga",
feature = "jpeg", feature = "jpeg",
feature = "bmp" feature = "bmp",
feature = "basis-universal",
feature = "ktx2",
))] ))]
{ {
app.init_asset_loader::<ImageTextureLoader>(); app.init_asset_loader::<ImageTextureLoader>();

View file

@ -192,6 +192,7 @@ impl FromWorld for Mesh2dPipeline {
GpuImage { GpuImage {
texture, texture,
texture_view, texture_view,
texture_format: image.texture_descriptor.format,
sampler, sampler,
size: Size::new( size: Size::new(
image.texture_descriptor.size.width as f32, image.texture_descriptor.size.width as f32,

View file

@ -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.| |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.| |wgpu_trace|For tracing wgpu.|
|dds|DDS picture format support.| |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.| |tga|TGA picture format support.|
|jpeg|JPEG picture format support.| |jpeg|JPEG picture format support.|
|bmp|BMP picture format support.| |bmp|BMP picture format support.|