Load and convert RGB8 dds textures (#12952)

# Objective

- Closes #12944.

## Solution

- Load `R8G8B8` textures by transcoding to an rgba format since `wgpu`
does not support texture formats with 3 channels.
- Switch to erroring out instead of panicking on an invalid dds file.

---

## Changelog

### Added

- DDS Textures with the `R8G8B8` format are now supported. They require
an additional conversion step, so using `R8G8B8A8` or a similar format
is preferable for texture loading performance.
This commit is contained in:
Lucien Menassol 2025-02-24 21:45:56 +01:00 committed by GitHub
parent 0855a0e8ad
commit 7c7b1e9fc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 55 additions and 10 deletions

View File

@ -8,7 +8,7 @@ use wgpu_types::{
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
use {bevy_utils::once, tracing::warn}; use {bevy_utils::once, tracing::warn};
use super::{CompressedImageFormats, Image, TextureError}; use super::{CompressedImageFormats, Image, TextureError, TranscodeFormat};
#[cfg(feature = "dds")] #[cfg(feature = "dds")]
pub fn dds_buffer_to_image( pub fn dds_buffer_to_image(
@ -20,7 +20,18 @@ pub fn dds_buffer_to_image(
let mut cursor = Cursor::new(buffer); let mut cursor = Cursor::new(buffer);
let dds = Dds::read(&mut cursor) let dds = Dds::read(&mut cursor)
.map_err(|error| TextureError::InvalidData(format!("Failed to parse DDS file: {error}")))?; .map_err(|error| TextureError::InvalidData(format!("Failed to parse DDS file: {error}")))?;
let texture_format = dds_format_to_texture_format(&dds, is_srgb)?; let (texture_format, transcode_format) = match dds_format_to_texture_format(&dds, is_srgb) {
Ok(format) => (format, None),
Err(TextureError::FormatRequiresTranscodingError(TranscodeFormat::Rgb8)) => {
let format = if is_srgb {
TextureFormat::Bgra8UnormSrgb
} else {
TextureFormat::Bgra8Unorm
};
(format, Some(TranscodeFormat::Rgb8))
}
Err(error) => return Err(error),
};
if !supported_compressed_formats.supports(texture_format) { if !supported_compressed_formats.supports(texture_format) {
return Err(TextureError::UnsupportedTextureFormat(format!( return Err(TextureError::UnsupportedTextureFormat(format!(
"Format not supported by this GPU: {texture_format:?}", "Format not supported by this GPU: {texture_format:?}",
@ -86,7 +97,29 @@ pub fn dds_buffer_to_image(
..Default::default() ..Default::default()
}); });
} }
image.data = Some(dds.data);
// DDS mipmap layout is directly compatible with wgpu's layout (Slice -> Face -> Mip):
// https://learn.microsoft.com/fr-fr/windows/win32/direct3ddds/dx-graphics-dds-reference
image.data = if let Some(transcode_format) = transcode_format {
match transcode_format {
TranscodeFormat::Rgb8 => {
let data = dds
.data
.chunks_exact(3)
.flat_map(|pixel| [pixel[0], pixel[1], pixel[2], u8::MAX])
.collect();
Some(data)
}
_ => {
return Err(TextureError::TranscodeError(format!(
"unsupported transcode from {transcode_format:?} to {texture_format:?}"
)))
}
}
} else {
Some(dds.data)
};
Ok(image) Ok(image)
} }
@ -112,6 +145,9 @@ pub fn dds_format_to_texture_format(
TextureFormat::Bgra8Unorm TextureFormat::Bgra8Unorm
} }
} }
D3DFormat::R8G8B8 => {
return Err(TextureError::FormatRequiresTranscodingError(TranscodeFormat::Rgb8));
},
D3DFormat::G16R16 => TextureFormat::Rg16Uint, D3DFormat::G16R16 => TextureFormat::Rg16Uint,
D3DFormat::A2B10G10R10 => TextureFormat::Rgb10a2Unorm, D3DFormat::A2B10G10R10 => TextureFormat::Rgb10a2Unorm,
D3DFormat::A8L8 => TextureFormat::Rg8Uint, D3DFormat::A8L8 => TextureFormat::Rg8Uint,
@ -153,7 +189,6 @@ pub fn dds_format_to_texture_format(
// FIXME: Map to argb format and user has to know to ignore the alpha channel? // FIXME: Map to argb format and user has to know to ignore the alpha channel?
| D3DFormat::X8B8G8R8 | D3DFormat::X8B8G8R8
| D3DFormat::A2R10G10B10 | D3DFormat::A2R10G10B10
| D3DFormat::R8G8B8
| D3DFormat::X1R5G5B5 | D3DFormat::X1R5G5B5
| D3DFormat::A4R4G4B4 | D3DFormat::A4R4G4B4
| D3DFormat::X4R4G4B4 | D3DFormat::X4R4G4B4

View File

@ -1491,19 +1491,20 @@ pub enum DataFormat {
Rg, Rg,
} }
/// Texture data need to be transcoded from this format for use with `wgpu`.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum TranscodeFormat { pub enum TranscodeFormat {
Etc1s, Etc1s,
Uastc(DataFormat), Uastc(DataFormat),
// Has to be transcoded to R8Unorm for use with `wgpu` // Has to be transcoded to R8Unorm for use with `wgpu`.
R8UnormSrgb, R8UnormSrgb,
// Has to be transcoded to R8G8Unorm for use with `wgpu` // Has to be transcoded to R8G8Unorm for use with `wgpu`.
Rg8UnormSrgb, Rg8UnormSrgb,
// Has to be transcoded to Rgba8 for use with `wgpu` // Has to be transcoded to Rgba8 for use with `wgpu`.
Rgb8, Rgb8,
} }
/// An error that occurs when accessing specific pixels in a texture /// An error that occurs when accessing specific pixels in a texture.
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum TextureAccessError { pub enum TextureAccessError {
#[error("out of bounds (x: {x}, y: {y}, z: {z})")] #[error("out of bounds (x: {x}, y: {y}, z: {z})")]
@ -1514,25 +1515,34 @@ pub enum TextureAccessError {
WrongDimension, WrongDimension,
} }
/// 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 {
/// Image MIME type is invalid.
#[error("invalid image mime type: {0}")] #[error("invalid image mime type: {0}")]
InvalidImageMimeType(String), InvalidImageMimeType(String),
/// Image extension is invalid.
#[error("invalid image extension: {0}")] #[error("invalid image extension: {0}")]
InvalidImageExtension(String), InvalidImageExtension(String),
/// Failed to load an image.
#[error("failed to load an image: {0}")] #[error("failed to load an image: {0}")]
ImageError(#[from] image::ImageError), ImageError(#[from] image::ImageError),
/// Texture format isn't supported.
#[error("unsupported texture format: {0}")] #[error("unsupported texture format: {0}")]
UnsupportedTextureFormat(String), UnsupportedTextureFormat(String),
/// Supercompression isn't supported.
#[error("supercompression not supported: {0}")] #[error("supercompression not supported: {0}")]
SuperCompressionNotSupported(String), SuperCompressionNotSupported(String),
#[error("failed to load an image: {0}")] /// Failed to decompress an image.
#[error("failed to decompress an image: {0}")]
SuperDecompressionError(String), SuperDecompressionError(String),
/// Invalid data.
#[error("invalid data: {0}")] #[error("invalid data: {0}")]
InvalidData(String), InvalidData(String),
/// Transcode error.
#[error("transcode error: {0}")] #[error("transcode error: {0}")]
TranscodeError(String), TranscodeError(String),
/// Format requires transcoding.
#[error("format requires transcoding: {0:?}")] #[error("format requires transcoding: {0:?}")]
FormatRequiresTranscodingError(TranscodeFormat), FormatRequiresTranscodingError(TranscodeFormat),
/// Only cubemaps with six faces are supported. /// Only cubemaps with six faces are supported.