Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

170
vendor/bevy_image/src/basis.rs vendored Normal file
View File

@@ -0,0 +1,170 @@
use basis_universal::{
BasisTextureType, DecodeFlags, TranscodeParameters, Transcoder, TranscoderTextureFormat,
};
use wgpu_types::{AstcBlock, AstcChannel, 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 Some(image0_info) = transcoder.image_info(buffer, 0) 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!(
"{basis_texture_format:?} cannot be transcoded to {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 {image_index} has {mip_level_count} but image 0 has {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 {level_index} from {basis_texture_format:?} to {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,
}
.physical_size(texture_format);
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
| BasisTextureType::TextureType2DArray
| BasisTextureType::TextureTypeCubemapArray => TextureDimension::D2,
BasisTextureType::TextureTypeVolume => TextureDimension::D3,
basis_texture_type => {
return Err(TextureError::UnsupportedTextureFormat(format!(
"{basis_texture_type:?}",
)))
}
};
image.data = Some(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,
TextureFormat::Astc {
block: AstcBlock::B4x4,
channel: if is_srgb {
AstcChannel::UnormSrgb
} else {
AstcChannel::Unorm
},
},
)
} 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,74 @@
use crate::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings};
use bevy_asset::saver::{AssetSaver, SavedAsset};
use futures_lite::AsyncWriteExt;
use thiserror::Error;
pub struct CompressedImageSaver;
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum CompressedImageSaverError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Cannot compress an uninitialized image")]
UninitializedImage,
}
impl AssetSaver for CompressedImageSaver {
type Asset = Image;
type Settings = ();
type OutputLoader = ImageLoader;
type Error = CompressedImageSaverError;
async fn save(
&self,
writer: &mut bevy_asset::io::Writer,
image: SavedAsset<'_, Self::Asset>,
_settings: &Self::Settings,
) -> Result<ImageLoaderSettings, Self::Error> {
let is_srgb = image.texture_descriptor.format.is_srgb();
let compressed_basis_data = {
let mut compressor_params = basis_universal::CompressorParams::new();
compressor_params.set_basis_format(basis_universal::BasisTextureFormat::UASTC4x4);
compressor_params.set_generate_mipmaps(true);
let color_space = if is_srgb {
basis_universal::ColorSpace::Srgb
} else {
basis_universal::ColorSpace::Linear
};
compressor_params.set_color_space(color_space);
compressor_params.set_uastc_quality_level(basis_universal::UASTC_QUALITY_DEFAULT);
let mut source_image = compressor_params.source_image_mut(0);
let size = image.size();
let Some(ref data) = image.data else {
return Err(CompressedImageSaverError::UninitializedImage);
};
source_image.init(data, size.x, size.y, 4);
let mut compressor = basis_universal::Compressor::new(4);
#[expect(
unsafe_code,
reason = "The basis-universal compressor cannot be interacted with except through unsafe functions"
)]
// SAFETY: the CompressorParams are "valid" to the best of our knowledge. The basis-universal
// library bindings note that invalid params might produce undefined behavior.
unsafe {
compressor.init(&compressor_params);
compressor.process().unwrap();
}
compressor.basis_file().to_vec()
};
writer.write_all(&compressed_basis_data).await?;
Ok(ImageLoaderSettings {
format: ImageFormatSetting::Format(ImageFormat::Basis),
is_srgb,
sampler: image.sampler.clone(),
asset_usage: image.asset_usage,
})
}
}

418
vendor/bevy_image/src/dds.rs vendored Normal file
View File

@@ -0,0 +1,418 @@
//! [DirectDraw Surface](https://en.wikipedia.org/wiki/DirectDraw_Surface) functionality.
use ddsfile::{Caps2, D3DFormat, Dds, DxgiFormat};
use std::io::Cursor;
use wgpu_types::{
Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor, TextureViewDimension,
};
#[cfg(debug_assertions)]
use {bevy_utils::once, tracing::warn};
use super::{CompressedImageFormats, Image, TextureError, TranscodeFormat};
#[cfg(feature = "dds")]
pub fn dds_buffer_to_image(
#[cfg(debug_assertions)] name: String,
buffer: &[u8],
supported_compressed_formats: CompressedImageFormats,
is_srgb: bool,
) -> Result<Image, TextureError> {
let mut cursor = Cursor::new(buffer);
let dds = Dds::read(&mut cursor)
.map_err(|error| TextureError::InvalidData(format!("Failed to parse DDS file: {error}")))?;
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) {
return Err(TextureError::UnsupportedTextureFormat(format!(
"Format not supported by this GPU: {texture_format:?}",
)));
}
let mut image = Image::default();
let is_cubemap = dds.header.caps2.contains(Caps2::CUBEMAP);
let depth_or_array_layers = if dds.get_num_array_layers() > 1 {
dds.get_num_array_layers()
} else {
dds.get_depth()
};
if is_cubemap
&& !dds.header.caps2.contains(
Caps2::CUBEMAP_NEGATIVEX
| Caps2::CUBEMAP_NEGATIVEY
| Caps2::CUBEMAP_NEGATIVEZ
| Caps2::CUBEMAP_POSITIVEX
| Caps2::CUBEMAP_POSITIVEY
| Caps2::CUBEMAP_POSITIVEZ,
)
{
return Err(TextureError::IncompleteCubemap);
}
image.texture_descriptor.size = Extent3d {
width: dds.get_width(),
height: dds.get_height(),
depth_or_array_layers,
}
.physical_size(texture_format);
let mip_map_level = match dds.get_num_mipmap_levels() {
0 => {
#[cfg(debug_assertions)]
once!(warn!(
"Mipmap levels for texture {} are 0, bumping them to 1",
name
));
1
}
t => t,
};
image.texture_descriptor.mip_level_count = mip_map_level;
image.texture_descriptor.format = texture_format;
image.texture_descriptor.dimension = if dds.get_depth() > 1 {
TextureDimension::D3
// 1x1 textures should generally be interpreted as solid 2D
} else if ((dds.get_width() > 1 || dds.get_height() > 1)
&& !(dds.get_width() > 1 && dds.get_height() > 1))
&& !image.is_compressed()
{
TextureDimension::D1
} else {
TextureDimension::D2
};
if is_cubemap {
let dimension = if image.texture_descriptor.size.depth_or_array_layers > 6 {
TextureViewDimension::CubeArray
} else {
TextureViewDimension::Cube
};
image.texture_view_descriptor = Some(TextureViewDescriptor {
dimension: Some(dimension),
..Default::default()
});
}
// 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)
}
#[cfg(feature = "dds")]
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::R8G8B8 => {
return Err(TextureError::FormatRequiresTranscodingError(TranscodeFormat::Rgb8));
},
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 | D3DFormat::DXT2 => {
if is_srgb {
TextureFormat::Bc2RgbaUnormSrgb
} else {
TextureFormat::Bc2RgbaUnorm
}
}
D3DFormat::DXT5 | D3DFormat::DXT4 => {
if is_srgb {
TextureFormat::Bc3RgbaUnormSrgb
} else {
TextureFormat::Bc3RgbaUnorm
}
}
D3DFormat::A16B16G16R16 => TextureFormat::Rgba16Unorm,
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::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::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 | DxgiFormat::R32G32B32A32_Float => {
TextureFormat::Rgba32Float
}
DxgiFormat::R32G32B32A32_UInt => TextureFormat::Rgba32Uint,
DxgiFormat::R32G32B32A32_SInt => TextureFormat::Rgba32Sint,
DxgiFormat::R16G16B16A16_Typeless | 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 | DxgiFormat::R32G32_Float => TextureFormat::Rg32Float,
DxgiFormat::R32G32_UInt => TextureFormat::Rg32Uint,
DxgiFormat::R32G32_SInt => TextureFormat::Rg32Sint,
DxgiFormat::R10G10B10A2_Typeless | DxgiFormat::R10G10B10A2_UNorm => {
TextureFormat::Rgb10a2Unorm
}
DxgiFormat::R11G11B10_Float => TextureFormat::Rg11b10Ufloat,
DxgiFormat::R8G8B8A8_Typeless
| DxgiFormat::R8G8B8A8_UNorm
| 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 | 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 | DxgiFormat::R32_Float => TextureFormat::R32Float,
DxgiFormat::D32_Float => TextureFormat::Depth32Float,
DxgiFormat::R32_UInt => TextureFormat::R32Uint,
DxgiFormat::R32_SInt => TextureFormat::R32Sint,
DxgiFormat::R24G8_Typeless | DxgiFormat::D24_UNorm_S8_UInt => {
TextureFormat::Depth24PlusStencil8
}
DxgiFormat::R24_UNorm_X8_Typeless => TextureFormat::Depth24Plus,
DxgiFormat::R8G8_Typeless | DxgiFormat::R8G8_UNorm => TextureFormat::Rg8Unorm,
DxgiFormat::R8G8_UInt => TextureFormat::Rg8Uint,
DxgiFormat::R8G8_SNorm => TextureFormat::Rg8Snorm,
DxgiFormat::R8G8_SInt => TextureFormat::Rg8Sint,
DxgiFormat::R16_Typeless | 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 | 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 | DxgiFormat::BC1_UNorm | DxgiFormat::BC1_UNorm_sRGB => {
if is_srgb {
TextureFormat::Bc1RgbaUnormSrgb
} else {
TextureFormat::Bc1RgbaUnorm
}
}
DxgiFormat::BC2_Typeless | DxgiFormat::BC2_UNorm | DxgiFormat::BC2_UNorm_sRGB => {
if is_srgb {
TextureFormat::Bc2RgbaUnormSrgb
} else {
TextureFormat::Bc2RgbaUnorm
}
}
DxgiFormat::BC3_Typeless | DxgiFormat::BC3_UNorm | DxgiFormat::BC3_UNorm_sRGB => {
if is_srgb {
TextureFormat::Bc3RgbaUnormSrgb
} else {
TextureFormat::Bc3RgbaUnorm
}
}
DxgiFormat::BC4_Typeless | DxgiFormat::BC4_UNorm => TextureFormat::Bc4RUnorm,
DxgiFormat::BC4_SNorm => TextureFormat::Bc4RSnorm,
DxgiFormat::BC5_Typeless | DxgiFormat::BC5_UNorm => TextureFormat::Bc5RgUnorm,
DxgiFormat::BC5_SNorm => TextureFormat::Bc5RgSnorm,
DxgiFormat::B8G8R8A8_UNorm
| DxgiFormat::B8G8R8A8_Typeless
| DxgiFormat::B8G8R8A8_UNorm_sRGB => {
if is_srgb {
TextureFormat::Bgra8UnormSrgb
} else {
TextureFormat::Bgra8Unorm
}
}
DxgiFormat::BC6H_Typeless | DxgiFormat::BC6H_UF16 => TextureFormat::Bc6hRgbUfloat,
DxgiFormat::BC6H_SF16 => TextureFormat::Bc6hRgbFloat,
DxgiFormat::BC7_Typeless | DxgiFormat::BC7_UNorm | 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(),
));
})
}
#[cfg(test)]
mod test {
use wgpu_types::{TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat};
use crate::CompressedImageFormats;
use super::dds_buffer_to_image;
/// `wgpu::create_texture_with_data` that reads from data structure but doesn't actually talk to your GPU
fn fake_wgpu_create_texture_with_data(
desc: &TextureDescriptor<Option<&'_ str>, &'_ [TextureFormat]>,
data: &[u8],
) {
// Will return None only if it's a combined depth-stencil format
// If so, default to 4, validation will fail later anyway since the depth or stencil
// aspect needs to be written to individually
let block_size = desc.format.block_copy_size(None).unwrap_or(4);
let (block_width, block_height) = desc.format.block_dimensions();
let layer_iterations = desc.array_layer_count();
let outer_iteration;
let inner_iteration;
match TextureDataOrder::default() {
TextureDataOrder::LayerMajor => {
outer_iteration = layer_iterations;
inner_iteration = desc.mip_level_count;
}
TextureDataOrder::MipMajor => {
outer_iteration = desc.mip_level_count;
inner_iteration = layer_iterations;
}
}
let mut binary_offset = 0;
for outer in 0..outer_iteration {
for inner in 0..inner_iteration {
let (_layer, mip) = match TextureDataOrder::default() {
TextureDataOrder::LayerMajor => (outer, inner),
TextureDataOrder::MipMajor => (inner, outer),
};
let mut mip_size = desc.mip_level_size(mip).unwrap();
// copying layers separately
if desc.dimension != TextureDimension::D3 {
mip_size.depth_or_array_layers = 1;
}
// When uploading mips of compressed textures and the mip is supposed to be
// a size that isn't a multiple of the block size, the mip needs to be uploaded
// as its "physical size" which is the size rounded up to the nearest block size.
let mip_physical = mip_size.physical_size(desc.format);
// All these calculations are performed on the physical size as that's the
// data that exists in the buffer.
let width_blocks = mip_physical.width / block_width;
let height_blocks = mip_physical.height / block_height;
let bytes_per_row = width_blocks * block_size;
let data_size = bytes_per_row * height_blocks * mip_size.depth_or_array_layers;
let end_offset = binary_offset + data_size as usize;
assert!(binary_offset < data.len());
assert!(end_offset <= data.len());
// those asserts match how the data will be accessed by wgpu:
// data[binary_offset..end_offset])
binary_offset = end_offset;
}
}
}
#[test]
fn dds_skybox() {
let buffer: [u8; 224] = [
0x44, 0x44, 0x53, 0x20, 0x7c, 0, 0, 0, 7, 0x10, 0x08, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0x10,
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0x47, 0x49, 0x4d, 0x50, 0x2d, 0x44, 0x44, 0x53, 0x5c,
0x09, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0x20, 0, 0, 0, 4, 0, 0, 0, 0x44, 0x58, 0x54, 0x35, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x08, 0x10, 0, 0, 0, 0xfe, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0x49, 0x92, 0x24, 0x49, 0x92, 0x24, 0xda, 0xd6,
0x2f, 0x5b, 0x8a, 0, 0xff, 0x55, 0xff, 0xff, 0x49, 0x92, 0x24, 0x49, 0x92, 0x24, 0xd5,
0x84, 0x8e, 0x3a, 0xb7, 0, 0xaa, 0x55, 0xff, 0xff, 0x49, 0x92, 0x24, 0x49, 0x92, 0x24,
0xf5, 0x94, 0x6f, 0x32, 0x57, 0xb7, 0x8b, 0, 0xff, 0xff, 0x49, 0x92, 0x24, 0x49, 0x92,
0x24, 0x2c, 0x3a, 0x49, 0x19, 0x28, 0xf7, 0xd7, 0xbe, 0xff, 0xff, 0x49, 0x92, 0x24,
0x49, 0x92, 0x24, 0x16, 0x95, 0xae, 0x42, 0xfc, 0, 0xaa, 0x55, 0xff, 0xff, 0x49, 0x92,
0x24, 0x49, 0x92, 0x24, 0xd8, 0xad, 0xae, 0x42, 0xaf, 0x0a, 0xaa, 0x55,
];
let r = dds_buffer_to_image("".into(), &buffer, CompressedImageFormats::BC, true);
assert!(r.is_ok());
if let Ok(r) = r {
fake_wgpu_create_texture_with_data(&r.texture_descriptor, r.data.as_ref().unwrap());
}
}
}

View File

@@ -0,0 +1,124 @@
use crate::{Image, TextureAtlasLayout, TextureFormatPixelInfo as _};
use bevy_asset::RenderAssetUsages;
use bevy_math::{URect, UVec2};
use guillotiere::{size2, Allocation, AtlasAllocator};
use thiserror::Error;
use tracing::error;
#[derive(Debug, Error)]
pub enum DynamicTextureAtlasBuilderError {
#[error("Couldn't allocate space to add the image requested")]
FailedToAllocateSpace,
/// Attempted to add a texture to an uninitialized atlas
#[error("cannot add texture to uninitialized atlas texture")]
UninitializedAtlas,
/// Attempted to add an uninitialized texture to an atlas
#[error("cannot add uninitialized texture to atlas")]
UninitializedSourceTexture,
}
/// Helper utility to update [`TextureAtlasLayout`] on the fly.
///
/// Helpful in cases when texture is created procedurally,
/// e.g: in a font glyph [`TextureAtlasLayout`], only add the [`Image`] texture for letters to be rendered.
pub struct DynamicTextureAtlasBuilder {
atlas_allocator: AtlasAllocator,
padding: u32,
}
impl DynamicTextureAtlasBuilder {
/// Create a new [`DynamicTextureAtlasBuilder`]
///
/// # Arguments
///
/// * `size` - total size for the atlas
/// * `padding` - gap added between textures in the atlas, both in x axis and y axis
pub fn new(size: UVec2, padding: u32) -> Self {
Self {
atlas_allocator: AtlasAllocator::new(to_size2(size)),
padding,
}
}
/// Add a new texture to `atlas_layout`.
///
/// It is the user's responsibility to pass in the correct [`TextureAtlasLayout`].
/// Also, the asset that `atlas_texture_handle` points to must have a usage matching
/// [`RenderAssetUsages::MAIN_WORLD`].
///
/// # Arguments
///
/// * `atlas_layout` - The atlas layout to add the texture to.
/// * `texture` - The source texture to add to the atlas.
/// * `atlas_texture` - The destination atlas texture to copy the source texture to.
pub fn add_texture(
&mut self,
atlas_layout: &mut TextureAtlasLayout,
texture: &Image,
atlas_texture: &mut Image,
) -> Result<usize, DynamicTextureAtlasBuilderError> {
let allocation = self.atlas_allocator.allocate(size2(
(texture.width() + self.padding).try_into().unwrap(),
(texture.height() + self.padding).try_into().unwrap(),
));
if let Some(allocation) = allocation {
assert!(
atlas_texture.asset_usage.contains(RenderAssetUsages::MAIN_WORLD),
"The atlas_texture image must have the RenderAssetUsages::MAIN_WORLD usage flag set"
);
self.place_texture(atlas_texture, allocation, texture)?;
let mut rect: URect = to_rect(allocation.rectangle);
rect.max = rect.max.saturating_sub(UVec2::splat(self.padding));
Ok(atlas_layout.add_texture(rect))
} else {
Err(DynamicTextureAtlasBuilderError::FailedToAllocateSpace)
}
}
fn place_texture(
&mut self,
atlas_texture: &mut Image,
allocation: Allocation,
texture: &Image,
) -> Result<(), DynamicTextureAtlasBuilderError> {
let mut rect = allocation.rectangle;
rect.max.x -= self.padding as i32;
rect.max.y -= self.padding as i32;
let atlas_width = atlas_texture.width() as usize;
let rect_width = rect.width() as usize;
let format_size = atlas_texture.texture_descriptor.format.pixel_size();
let Some(ref mut atlas_data) = atlas_texture.data else {
return Err(DynamicTextureAtlasBuilderError::UninitializedAtlas);
};
let Some(ref data) = texture.data else {
return Err(DynamicTextureAtlasBuilderError::UninitializedSourceTexture);
};
for (texture_y, bound_y) in (rect.min.y..rect.max.y).map(|i| i as usize).enumerate() {
let begin = (bound_y * atlas_width + rect.min.x as usize) * format_size;
let end = begin + rect_width * format_size;
let texture_begin = texture_y * rect_width * format_size;
let texture_end = texture_begin + rect_width * format_size;
atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);
}
Ok(())
}
}
fn to_rect(rectangle: guillotiere::Rectangle) -> URect {
URect {
min: UVec2::new(
rectangle.min.x.try_into().unwrap(),
rectangle.min.y.try_into().unwrap(),
),
max: UVec2::new(
rectangle.max.x.try_into().unwrap(),
rectangle.max.y.try_into().unwrap(),
),
}
}
fn to_size2(vec2: UVec2) -> guillotiere::Size {
guillotiere::Size::new(vec2.x as i32, vec2.y as i32)
}

View File

@@ -0,0 +1,77 @@
use crate::{Image, TextureFormatPixelInfo};
use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages};
use image::ImageDecoder;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
/// Loads EXR textures as Texture assets
#[derive(Clone, Default)]
#[cfg(feature = "exr")]
pub struct ExrTextureLoader;
#[derive(Serialize, Deserialize, Default, Debug)]
#[cfg(feature = "exr")]
pub struct ExrTextureLoaderSettings {
pub asset_usage: RenderAssetUsages,
}
/// Possible errors that can be produced by [`ExrTextureLoader`]
#[non_exhaustive]
#[derive(Debug, Error)]
#[cfg(feature = "exr")]
pub enum ExrTextureLoaderError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
ImageError(#[from] image::ImageError),
}
impl AssetLoader for ExrTextureLoader {
type Asset = Image;
type Settings = ExrTextureLoaderSettings;
type Error = ExrTextureLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
settings: &Self::Settings,
_load_context: &mut LoadContext<'_>,
) -> Result<Image, Self::Error> {
let format = TextureFormat::Rgba32Float;
debug_assert_eq!(
format.pixel_size(),
4 * 4,
"Format should have 32bit x 4 size"
);
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let decoder = image::codecs::openexr::OpenExrDecoder::with_alpha_preference(
std::io::Cursor::new(bytes),
Some(true),
)?;
let (width, height) = decoder.dimensions();
let total_bytes = decoder.total_bytes() as usize;
let mut buf = vec![0u8; total_bytes];
decoder.read_image(buf.as_mut_slice())?;
Ok(Image::new(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
buf,
format,
settings.asset_usage,
))
}
fn extensions(&self) -> &[&str] {
&["exr"]
}
}

View File

@@ -0,0 +1,79 @@
use crate::{Image, TextureFormatPixelInfo};
use bevy_asset::RenderAssetUsages;
use bevy_asset::{io::Reader, AssetLoader, LoadContext};
use image::DynamicImage;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
/// Loads HDR textures as Texture assets
#[derive(Clone, Default)]
pub struct HdrTextureLoader;
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct HdrTextureLoaderSettings {
pub asset_usage: RenderAssetUsages,
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum HdrTextureLoaderError {
#[error("Could load texture: {0}")]
Io(#[from] std::io::Error),
#[error("Could not extract image: {0}")]
Image(#[from] image::ImageError),
}
impl AssetLoader for HdrTextureLoader {
type Asset = Image;
type Settings = HdrTextureLoaderSettings;
type Error = HdrTextureLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
settings: &Self::Settings,
_load_context: &mut LoadContext<'_>,
) -> Result<Image, Self::Error> {
let format = TextureFormat::Rgba32Float;
debug_assert_eq!(
format.pixel_size(),
4 * 4,
"Format should have 32bit x 4 size"
);
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let decoder = image::codecs::hdr::HdrDecoder::new(bytes.as_slice())?;
let info = decoder.metadata();
let dynamic_image = DynamicImage::from_decoder(decoder)?;
let image_buffer = dynamic_image
.as_rgb32f()
.expect("HDR Image format should be Rgb32F");
let mut rgba_data = Vec::with_capacity(image_buffer.pixels().len() * format.pixel_size());
for rgb in image_buffer.pixels() {
let alpha = 1.0f32;
rgba_data.extend_from_slice(&rgb.0[0].to_le_bytes());
rgba_data.extend_from_slice(&rgb.0[1].to_le_bytes());
rgba_data.extend_from_slice(&rgb.0[2].to_le_bytes());
rgba_data.extend_from_slice(&alpha.to_le_bytes());
}
Ok(Image::new(
Extent3d {
width: info.width,
height: info.height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
rgba_data,
format,
settings.asset_usage,
))
}
fn extensions(&self) -> &[&str] {
&["hdr"]
}
}

1736
vendor/bevy_image/src/image.rs vendored Normal file

File diff suppressed because it is too large Load Diff

179
vendor/bevy_image/src/image_loader.rs vendored Normal file
View File

@@ -0,0 +1,179 @@
use crate::image::{Image, ImageFormat, ImageType, TextureError};
use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages};
use thiserror::Error;
use super::{CompressedImageFormats, ImageSampler};
use serde::{Deserialize, Serialize};
/// Loader for images that can be read by the `image` crate.
#[derive(Clone)]
pub struct ImageLoader {
supported_compressed_formats: CompressedImageFormats,
}
impl ImageLoader {
/// Full list of supported formats.
pub const SUPPORTED_FORMATS: &'static [ImageFormat] = &[
#[cfg(feature = "basis-universal")]
ImageFormat::Basis,
#[cfg(feature = "bmp")]
ImageFormat::Bmp,
#[cfg(feature = "dds")]
ImageFormat::Dds,
#[cfg(feature = "ff")]
ImageFormat::Farbfeld,
#[cfg(feature = "gif")]
ImageFormat::Gif,
#[cfg(feature = "ico")]
ImageFormat::Ico,
#[cfg(feature = "jpeg")]
ImageFormat::Jpeg,
#[cfg(feature = "ktx2")]
ImageFormat::Ktx2,
#[cfg(feature = "png")]
ImageFormat::Png,
#[cfg(feature = "pnm")]
ImageFormat::Pnm,
#[cfg(feature = "qoi")]
ImageFormat::Qoi,
#[cfg(feature = "tga")]
ImageFormat::Tga,
#[cfg(feature = "tiff")]
ImageFormat::Tiff,
#[cfg(feature = "webp")]
ImageFormat::WebP,
];
/// Total count of file extensions, for computing supported file extensions list.
const COUNT_FILE_EXTENSIONS: usize = {
let mut count = 0;
let mut idx = 0;
while idx < Self::SUPPORTED_FORMATS.len() {
count += Self::SUPPORTED_FORMATS[idx].to_file_extensions().len();
idx += 1;
}
count
};
/// Gets the list of file extensions for all formats.
pub const SUPPORTED_FILE_EXTENSIONS: &'static [&'static str] = &{
let mut exts = [""; Self::COUNT_FILE_EXTENSIONS];
let mut ext_idx = 0;
let mut fmt_idx = 0;
while fmt_idx < Self::SUPPORTED_FORMATS.len() {
let mut off = 0;
let fmt_exts = Self::SUPPORTED_FORMATS[fmt_idx].to_file_extensions();
while off < fmt_exts.len() {
exts[ext_idx] = fmt_exts[off];
off += 1;
ext_idx += 1;
}
fmt_idx += 1;
}
exts
};
/// Creates a new image loader that supports the provided formats.
pub fn new(supported_compressed_formats: CompressedImageFormats) -> Self {
Self {
supported_compressed_formats,
}
}
}
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub enum ImageFormatSetting {
#[default]
FromExtension,
Format(ImageFormat),
Guess,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ImageLoaderSettings {
pub format: ImageFormatSetting,
pub is_srgb: bool,
pub sampler: ImageSampler,
pub asset_usage: RenderAssetUsages,
}
impl Default for ImageLoaderSettings {
fn default() -> Self {
Self {
format: ImageFormatSetting::default(),
is_srgb: true,
sampler: ImageSampler::Default,
asset_usage: RenderAssetUsages::default(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum ImageLoaderError {
#[error("Could load shader: {0}")]
Io(#[from] std::io::Error),
#[error("Could not load texture file: {0}")]
FileTexture(#[from] FileTextureError),
}
impl AssetLoader for ImageLoader {
type Asset = Image;
type Settings = ImageLoaderSettings;
type Error = ImageLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
settings: &ImageLoaderSettings,
load_context: &mut LoadContext<'_>,
) -> Result<Image, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let image_type = match settings.format {
ImageFormatSetting::FromExtension => {
// use the file extension for the image type
let ext = load_context.path().extension().unwrap().to_str().unwrap();
ImageType::Extension(ext)
}
ImageFormatSetting::Format(format) => ImageType::Format(format),
ImageFormatSetting::Guess => {
let format = image::guess_format(&bytes).map_err(|err| FileTextureError {
error: err.into(),
path: format!("{}", load_context.path().display()),
})?;
ImageType::Format(ImageFormat::from_image_crate_format(format).ok_or_else(
|| FileTextureError {
error: TextureError::UnsupportedTextureFormat(format!("{format:?}")),
path: format!("{}", load_context.path().display()),
},
)?)
}
};
Ok(Image::from_buffer(
#[cfg(all(debug_assertions, feature = "dds"))]
load_context.path().display().to_string(),
&bytes,
image_type,
self.supported_compressed_formats,
settings.is_srgb,
settings.sampler.clone(),
settings.asset_usage,
)
.map_err(|err| FileTextureError {
error: err,
path: format!("{}", load_context.path().display()),
})?)
}
fn extensions(&self) -> &[&str] {
Self::SUPPORTED_FILE_EXTENSIONS
}
}
/// An error that occurs when loading a texture from a file.
#[derive(Error, Debug)]
#[error("Error reading image file {path}: {error}, this is an error in `bevy_render`.")]
pub struct FileTextureError {
error: TextureError,
path: String,
}

View File

@@ -0,0 +1,243 @@
use crate::{Image, TextureFormatPixelInfo};
use bevy_asset::RenderAssetUsages;
use image::{DynamicImage, ImageBuffer};
use thiserror::Error;
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
impl Image {
/// Converts a [`DynamicImage`] to an [`Image`].
pub fn from_dynamic(
dyn_img: DynamicImage,
is_srgb: bool,
asset_usage: RenderAssetUsages,
) -> Image {
use bytemuck::cast_slice;
let width;
let height;
let data: Vec<u8>;
let format: TextureFormat;
match dyn_img {
DynamicImage::ImageLuma8(image) => {
let i = DynamicImage::ImageLuma8(image).into_rgba8();
width = i.width();
height = i.height();
format = if is_srgb {
TextureFormat::Rgba8UnormSrgb
} else {
TextureFormat::Rgba8Unorm
};
data = i.into_raw();
}
DynamicImage::ImageLumaA8(image) => {
let i = DynamicImage::ImageLumaA8(image).into_rgba8();
width = i.width();
height = i.height();
format = if is_srgb {
TextureFormat::Rgba8UnormSrgb
} else {
TextureFormat::Rgba8Unorm
};
data = i.into_raw();
}
DynamicImage::ImageRgb8(image) => {
let i = DynamicImage::ImageRgb8(image).into_rgba8();
width = i.width();
height = i.height();
format = if is_srgb {
TextureFormat::Rgba8UnormSrgb
} else {
TextureFormat::Rgba8Unorm
};
data = i.into_raw();
}
DynamicImage::ImageRgba8(image) => {
width = image.width();
height = image.height();
format = if is_srgb {
TextureFormat::Rgba8UnormSrgb
} else {
TextureFormat::Rgba8Unorm
};
data = image.into_raw();
}
DynamicImage::ImageLuma16(image) => {
width = image.width();
height = image.height();
format = TextureFormat::R16Uint;
let raw_data = image.into_raw();
data = cast_slice(&raw_data).to_owned();
}
DynamicImage::ImageLumaA16(image) => {
width = image.width();
height = image.height();
format = TextureFormat::Rg16Uint;
let raw_data = image.into_raw();
data = cast_slice(&raw_data).to_owned();
}
DynamicImage::ImageRgb16(image) => {
let i = DynamicImage::ImageRgb16(image).into_rgba16();
width = i.width();
height = i.height();
format = TextureFormat::Rgba16Unorm;
let raw_data = i.into_raw();
data = cast_slice(&raw_data).to_owned();
}
DynamicImage::ImageRgba16(image) => {
width = image.width();
height = image.height();
format = TextureFormat::Rgba16Unorm;
let raw_data = image.into_raw();
data = cast_slice(&raw_data).to_owned();
}
DynamicImage::ImageRgb32F(image) => {
width = image.width();
height = image.height();
format = TextureFormat::Rgba32Float;
let mut local_data =
Vec::with_capacity(width as usize * height as usize * format.pixel_size());
for pixel in image.into_raw().chunks_exact(3) {
// TODO: use the array_chunks method once stabilized
// https://github.com/rust-lang/rust/issues/74985
let r = pixel[0];
let g = pixel[1];
let b = pixel[2];
let a = 1f32;
local_data.extend_from_slice(&r.to_le_bytes());
local_data.extend_from_slice(&g.to_le_bytes());
local_data.extend_from_slice(&b.to_le_bytes());
local_data.extend_from_slice(&a.to_le_bytes());
}
data = local_data;
}
DynamicImage::ImageRgba32F(image) => {
width = image.width();
height = image.height();
format = TextureFormat::Rgba32Float;
let raw_data = image.into_raw();
data = cast_slice(&raw_data).to_owned();
}
// DynamicImage is now non exhaustive, catch future variants and convert them
_ => {
let image = dyn_img.into_rgba8();
width = image.width();
height = image.height();
format = TextureFormat::Rgba8UnormSrgb;
data = image.into_raw();
}
}
Image::new(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data,
format,
asset_usage,
)
}
/// Convert a [`Image`] to a [`DynamicImage`]. Useful for editing image
/// data. Not all [`TextureFormat`] are covered, therefore it will return an
/// error if the format is unsupported. Supported formats are:
/// - `TextureFormat::R8Unorm`
/// - `TextureFormat::Rg8Unorm`
/// - `TextureFormat::Rgba8UnormSrgb`
/// - `TextureFormat::Bgra8UnormSrgb`
///
/// To convert [`Image`] to a different format see: [`Image::convert`].
pub fn try_into_dynamic(self) -> Result<DynamicImage, IntoDynamicImageError> {
let width = self.width();
let height = self.height();
let Some(data) = self.data else {
return Err(IntoDynamicImageError::UninitializedImage);
};
match self.texture_descriptor.format {
TextureFormat::R8Unorm => {
ImageBuffer::from_raw(width, height, data).map(DynamicImage::ImageLuma8)
}
TextureFormat::Rg8Unorm => {
ImageBuffer::from_raw(width, height, data).map(DynamicImage::ImageLumaA8)
}
TextureFormat::Rgba8UnormSrgb => {
ImageBuffer::from_raw(width, height, data).map(DynamicImage::ImageRgba8)
}
// This format is commonly used as the format for the swapchain texture
// This conversion is added here to support screenshots
TextureFormat::Bgra8UnormSrgb | TextureFormat::Bgra8Unorm => {
ImageBuffer::from_raw(width, height, {
let mut data = data;
for bgra in data.chunks_exact_mut(4) {
bgra.swap(0, 2);
}
data
})
.map(DynamicImage::ImageRgba8)
}
// Throw and error if conversion isn't supported
texture_format => return Err(IntoDynamicImageError::UnsupportedFormat(texture_format)),
}
.ok_or(IntoDynamicImageError::UnknownConversionError(
self.texture_descriptor.format,
))
}
}
/// Errors that occur while converting an [`Image`] into a [`DynamicImage`]
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum IntoDynamicImageError {
/// Conversion into dynamic image not supported for source format.
#[error("Conversion into dynamic image not supported for {0:?}.")]
UnsupportedFormat(TextureFormat),
/// Encountered an unknown error during conversion.
#[error("Failed to convert into {0:?}.")]
UnknownConversionError(TextureFormat),
/// Tried to convert an image that has no texture data
#[error("Image has no texture data")]
UninitializedImage,
}
#[cfg(test)]
mod test {
use image::{GenericImage, Rgba};
use super::*;
#[test]
fn two_way_conversion() {
// Check to see if color is preserved through an rgba8 conversion and back.
let mut initial = DynamicImage::new_rgba8(1, 1);
initial.put_pixel(0, 0, Rgba::from([132, 3, 7, 200]));
let image = Image::from_dynamic(initial.clone(), true, RenderAssetUsages::RENDER_WORLD);
// NOTE: Fails if `is_srgb = false` or the dynamic image is of the type rgb8.
assert_eq!(initial, image.try_into_dynamic().unwrap());
}
}

1528
vendor/bevy_image/src/ktx2.rs vendored Normal file

File diff suppressed because it is too large Load Diff

48
vendor/bevy_image/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,48 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
extern crate alloc;
pub mod prelude {
pub use crate::{
dynamic_texture_atlas_builder::DynamicTextureAtlasBuilder,
texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources},
BevyDefault as _, Image, ImageFormat, TextureAtlasBuilder, TextureError,
};
}
mod image;
pub use self::image::*;
#[cfg(feature = "basis-universal")]
mod basis;
#[cfg(feature = "basis-universal")]
mod compressed_image_saver;
#[cfg(feature = "dds")]
mod dds;
mod dynamic_texture_atlas_builder;
#[cfg(feature = "exr")]
mod exr_texture_loader;
#[cfg(feature = "hdr")]
mod hdr_texture_loader;
mod image_loader;
#[cfg(feature = "ktx2")]
mod ktx2;
mod texture_atlas;
mod texture_atlas_builder;
#[cfg(feature = "basis-universal")]
pub use compressed_image_saver::*;
#[cfg(feature = "dds")]
pub use dds::*;
pub use dynamic_texture_atlas_builder::*;
#[cfg(feature = "exr")]
pub use exr_texture_loader::*;
#[cfg(feature = "hdr")]
pub use hdr_texture_loader::*;
pub use image_loader::*;
#[cfg(feature = "ktx2")]
pub use ktx2::*;
pub use texture_atlas::*;
pub use texture_atlas_builder::*;
pub(crate) mod image_texture_conversion;
pub use image_texture_conversion::IntoDynamicImageError;

234
vendor/bevy_image/src/texture_atlas.rs vendored Normal file
View File

@@ -0,0 +1,234 @@
use bevy_app::prelude::*;
use bevy_asset::{Asset, AssetApp as _, AssetId, Assets, Handle};
use bevy_math::{Rect, URect, UVec2};
use bevy_platform::collections::HashMap;
#[cfg(not(feature = "bevy_reflect"))]
use bevy_reflect::TypePath;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
#[cfg(feature = "serialize")]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use crate::Image;
/// Adds support for texture atlases.
pub struct TextureAtlasPlugin;
impl Plugin for TextureAtlasPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<TextureAtlasLayout>();
#[cfg(feature = "bevy_reflect")]
app.register_asset_reflect::<TextureAtlasLayout>()
.register_type::<TextureAtlas>();
}
}
/// Stores a mapping from sub texture handles to the related area index.
///
/// Generated by [`TextureAtlasBuilder`].
///
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
#[derive(Debug)]
pub struct TextureAtlasSources {
/// Maps from a specific image handle to the index in `textures` where they can be found.
pub texture_ids: HashMap<AssetId<Image>, usize>,
}
impl TextureAtlasSources {
/// Retrieves the texture *section* index of the given `texture` handle.
pub fn texture_index(&self, texture: impl Into<AssetId<Image>>) -> Option<usize> {
let id = texture.into();
self.texture_ids.get(&id).cloned()
}
/// Creates a [`TextureAtlas`] handle for the given `texture` handle.
pub fn handle(
&self,
layout: Handle<TextureAtlasLayout>,
texture: impl Into<AssetId<Image>>,
) -> Option<TextureAtlas> {
Some(TextureAtlas {
layout,
index: self.texture_index(texture)?,
})
}
/// Retrieves the texture *section* rectangle of the given `texture` handle in pixels.
pub fn texture_rect(
&self,
layout: &TextureAtlasLayout,
texture: impl Into<AssetId<Image>>,
) -> Option<URect> {
layout.textures.get(self.texture_index(texture)?).cloned()
}
/// Retrieves the texture *section* rectangle of the given `texture` handle in UV coordinates.
/// These are within the range [0..1], as a fraction of the entire texture atlas' size.
pub fn uv_rect(
&self,
layout: &TextureAtlasLayout,
texture: impl Into<AssetId<Image>>,
) -> Option<Rect> {
self.texture_rect(layout, texture).map(|rect| {
let rect = rect.as_rect();
let size = layout.size.as_vec2();
Rect::from_corners(rect.min / size, rect.max / size)
})
}
}
/// Stores a map used to lookup the position of a texture in a [`TextureAtlas`].
/// This can be used to either use and look up a specific section of a texture, or animate frame-by-frame as a sprite sheet.
///
/// Optionally it can store a mapping from sub texture handles to the related area index (see
/// [`TextureAtlasBuilder`]).
///
/// [Example usage animating sprite.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// [Example usage animating sprite in response to an event.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
/// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
///
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
#[derive(Asset, PartialEq, Eq, Debug, Clone)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Clone)
)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
#[cfg_attr(not(feature = "bevy_reflect"), derive(TypePath))]
pub struct TextureAtlasLayout {
/// Total size of texture atlas.
pub size: UVec2,
/// The specific areas of the atlas where each texture can be found
pub textures: Vec<URect>,
}
impl TextureAtlasLayout {
/// Create a new empty layout with custom `dimensions`
pub fn new_empty(dimensions: UVec2) -> Self {
Self {
size: dimensions,
textures: Vec::new(),
}
}
/// Generate a [`TextureAtlasLayout`] as a grid where each
/// `tile_size` by `tile_size` grid-cell is one of the *section* in the
/// atlas. Grid cells are separated by some `padding`, and the grid starts
/// at `offset` pixels from the top left corner. Resulting layout is
/// indexed left to right, top to bottom.
///
/// # Arguments
///
/// * `tile_size` - Each layout grid cell size
/// * `columns` - Grid column count
/// * `rows` - Grid row count
/// * `padding` - Optional padding between cells
/// * `offset` - Optional global grid offset
pub fn from_grid(
tile_size: UVec2,
columns: u32,
rows: u32,
padding: Option<UVec2>,
offset: Option<UVec2>,
) -> Self {
let padding = padding.unwrap_or_default();
let offset = offset.unwrap_or_default();
let mut sprites = Vec::new();
let mut current_padding = UVec2::ZERO;
for y in 0..rows {
if y > 0 {
current_padding.y = padding.y;
}
for x in 0..columns {
if x > 0 {
current_padding.x = padding.x;
}
let cell = UVec2::new(x, y);
let rect_min = (tile_size + current_padding) * cell + offset;
sprites.push(URect {
min: rect_min,
max: rect_min + tile_size,
});
}
}
let grid_size = UVec2::new(columns, rows);
Self {
size: ((tile_size + current_padding) * grid_size) - current_padding,
textures: sprites,
}
}
/// Add a *section* to the list in the layout and returns its index
/// which can be used with [`TextureAtlas`]
///
/// # Arguments
///
/// * `rect` - The section of the texture to be added
///
/// [`TextureAtlas`]: crate::TextureAtlas
pub fn add_texture(&mut self, rect: URect) -> usize {
self.textures.push(rect);
self.textures.len() - 1
}
/// The number of textures in the [`TextureAtlasLayout`]
pub fn len(&self) -> usize {
self.textures.len()
}
pub fn is_empty(&self) -> bool {
self.textures.is_empty()
}
}
/// An index into a [`TextureAtlasLayout`], which corresponds to a specific section of a texture.
///
/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
/// The texture atlas contains various *sections* of a given texture, allowing users to have a single
/// image file for either sprite animation or global mapping.
/// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or display only a *section* of the texture
/// for efficient rendering of related game objects.
///
/// Check the following examples for usage:
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Default, Debug, PartialEq, Hash, Clone)
)]
pub struct TextureAtlas {
/// Texture atlas layout handle
pub layout: Handle<TextureAtlasLayout>,
/// Texture atlas section index
pub index: usize,
}
impl TextureAtlas {
/// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index`
pub fn texture_rect(&self, texture_atlases: &Assets<TextureAtlasLayout>) -> Option<URect> {
let atlas = texture_atlases.get(&self.layout)?;
atlas.textures.get(self.index).copied()
}
}
impl From<Handle<TextureAtlasLayout>> for TextureAtlas {
fn from(texture_atlas: Handle<TextureAtlasLayout>) -> Self {
Self {
layout: texture_atlas,
index: 0,
}
}
}

View File

@@ -0,0 +1,299 @@
use bevy_asset::{AssetId, RenderAssetUsages};
use bevy_math::{URect, UVec2};
use bevy_platform::collections::HashMap;
use rectangle_pack::{
contains_smallest_box, pack_rects, volume_heuristic, GroupedRectsToPlace, PackedLocation,
RectToInsert, TargetBin,
};
use thiserror::Error;
use tracing::{debug, error, warn};
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
use crate::{Image, TextureFormatPixelInfo};
use crate::{TextureAtlasLayout, TextureAtlasSources};
#[derive(Debug, Error)]
pub enum TextureAtlasBuilderError {
#[error("could not pack textures into an atlas within the given bounds")]
NotEnoughSpace,
#[error("added a texture with the wrong format in an atlas")]
WrongFormat,
/// Attempted to add a texture to an uninitialized atlas
#[error("cannot add texture to uninitialized atlas texture")]
UninitializedAtlas,
/// Attempted to add an uninitialized texture to an atlas
#[error("cannot add uninitialized texture to atlas")]
UninitializedSourceTexture,
}
#[derive(Debug)]
#[must_use]
/// A builder which is used to create a texture atlas from many individual
/// sprites.
pub struct TextureAtlasBuilder<'a> {
/// Collection of texture's asset id (optional) and image data to be packed into an atlas
textures_to_place: Vec<(Option<AssetId<Image>>, &'a Image)>,
/// The initial atlas size in pixels.
initial_size: UVec2,
/// The absolute maximum size of the texture atlas in pixels.
max_size: UVec2,
/// The texture format for the textures that will be loaded in the atlas.
format: TextureFormat,
/// Enable automatic format conversion for textures if they are not in the atlas format.
auto_format_conversion: bool,
/// The amount of padding in pixels to add along the right and bottom edges of the texture rects.
padding: UVec2,
}
impl Default for TextureAtlasBuilder<'_> {
fn default() -> Self {
Self {
textures_to_place: Vec::new(),
initial_size: UVec2::splat(256),
max_size: UVec2::splat(2048),
format: TextureFormat::Rgba8UnormSrgb,
auto_format_conversion: true,
padding: UVec2::ZERO,
}
}
}
pub type TextureAtlasBuilderResult<T> = Result<T, TextureAtlasBuilderError>;
impl<'a> TextureAtlasBuilder<'a> {
/// Sets the initial size of the atlas in pixels.
pub fn initial_size(&mut self, size: UVec2) -> &mut Self {
self.initial_size = size;
self
}
/// Sets the max size of the atlas in pixels.
pub fn max_size(&mut self, size: UVec2) -> &mut Self {
self.max_size = size;
self
}
/// Sets the texture format for textures in the atlas.
pub fn format(&mut self, format: TextureFormat) -> &mut Self {
self.format = format;
self
}
/// Control whether the added texture should be converted to the atlas format, if different.
pub fn auto_format_conversion(&mut self, auto_format_conversion: bool) -> &mut Self {
self.auto_format_conversion = auto_format_conversion;
self
}
/// Adds a texture to be copied to the texture atlas.
///
/// Optionally an asset id can be passed that can later be used with the texture layout to retrieve the index of this texture.
/// The insertion order will reflect the index of the added texture in the finished texture atlas.
pub fn add_texture(
&mut self,
image_id: Option<AssetId<Image>>,
texture: &'a Image,
) -> &mut Self {
self.textures_to_place.push((image_id, texture));
self
}
/// Sets the amount of padding in pixels to add between the textures in the texture atlas.
///
/// The `x` value provide will be added to the right edge, while the `y` value will be added to the bottom edge.
pub fn padding(&mut self, padding: UVec2) -> &mut Self {
self.padding = padding;
self
}
fn copy_texture_to_atlas(
atlas_texture: &mut Image,
texture: &Image,
packed_location: &PackedLocation,
padding: UVec2,
) -> TextureAtlasBuilderResult<()> {
let rect_width = (packed_location.width() - padding.x) as usize;
let rect_height = (packed_location.height() - padding.y) as usize;
let rect_x = packed_location.x() as usize;
let rect_y = packed_location.y() as usize;
let atlas_width = atlas_texture.width() as usize;
let format_size = atlas_texture.texture_descriptor.format.pixel_size();
let Some(ref mut atlas_data) = atlas_texture.data else {
return Err(TextureAtlasBuilderError::UninitializedAtlas);
};
let Some(ref data) = texture.data else {
return Err(TextureAtlasBuilderError::UninitializedSourceTexture);
};
for (texture_y, bound_y) in (rect_y..rect_y + rect_height).enumerate() {
let begin = (bound_y * atlas_width + rect_x) * format_size;
let end = begin + rect_width * format_size;
let texture_begin = texture_y * rect_width * format_size;
let texture_end = texture_begin + rect_width * format_size;
atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);
}
Ok(())
}
fn copy_converted_texture(
&self,
atlas_texture: &mut Image,
texture: &Image,
packed_location: &PackedLocation,
) -> TextureAtlasBuilderResult<()> {
if self.format == texture.texture_descriptor.format {
Self::copy_texture_to_atlas(atlas_texture, texture, packed_location, self.padding)?;
} else if let Some(converted_texture) = texture.convert(self.format) {
debug!(
"Converting texture from '{:?}' to '{:?}'",
texture.texture_descriptor.format, self.format
);
Self::copy_texture_to_atlas(
atlas_texture,
&converted_texture,
packed_location,
self.padding,
)?;
} else {
error!(
"Error converting texture from '{:?}' to '{:?}', ignoring",
texture.texture_descriptor.format, self.format
);
}
Ok(())
}
/// Consumes the builder, and returns the newly created texture atlas and
/// the associated atlas layout.
///
/// Assigns indices to the textures based on the insertion order.
/// Internally it copies all rectangles from the textures and copies them
/// into a new texture.
///
/// # Usage
///
/// ```rust
/// # use bevy_ecs::prelude::*;
/// # use bevy_asset::*;
/// # use bevy_image::prelude::*;
///
/// fn my_system(mut textures: ResMut<Assets<Image>>, mut layouts: ResMut<Assets<TextureAtlasLayout>>) {
/// // Declare your builder
/// let mut builder = TextureAtlasBuilder::default();
/// // Customize it
/// // ...
/// // Build your texture and the atlas layout
/// let (atlas_layout, atlas_sources, texture) = builder.build().unwrap();
/// let texture = textures.add(texture);
/// let layout = layouts.add(atlas_layout);
/// }
/// ```
///
/// # Errors
///
/// If there is not enough space in the atlas texture, an error will
/// be returned. It is then recommended to make a larger sprite sheet.
pub fn build(
&mut self,
) -> Result<(TextureAtlasLayout, TextureAtlasSources, Image), TextureAtlasBuilderError> {
let max_width = self.max_size.x;
let max_height = self.max_size.y;
let mut current_width = self.initial_size.x;
let mut current_height = self.initial_size.y;
let mut rect_placements = None;
let mut atlas_texture = Image::default();
let mut rects_to_place = GroupedRectsToPlace::<usize>::new();
// Adds textures to rectangle group packer
for (index, (_, texture)) in self.textures_to_place.iter().enumerate() {
rects_to_place.push_rect(
index,
None,
RectToInsert::new(
texture.width() + self.padding.x,
texture.height() + self.padding.y,
1,
),
);
}
while rect_placements.is_none() {
if current_width > max_width || current_height > max_height {
break;
}
let last_attempt = current_height == max_height && current_width == max_width;
let mut target_bins = alloc::collections::BTreeMap::new();
target_bins.insert(0, TargetBin::new(current_width, current_height, 1));
rect_placements = match pack_rects(
&rects_to_place,
&mut target_bins,
&volume_heuristic,
&contains_smallest_box,
) {
Ok(rect_placements) => {
atlas_texture = Image::new(
Extent3d {
width: current_width,
height: current_height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
vec![
0;
self.format.pixel_size() * (current_width * current_height) as usize
],
self.format,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
);
Some(rect_placements)
}
Err(rectangle_pack::RectanglePackError::NotEnoughBinSpace) => {
current_height = (current_height * 2).clamp(0, max_height);
current_width = (current_width * 2).clamp(0, max_width);
None
}
};
if last_attempt {
break;
}
}
let rect_placements = rect_placements.ok_or(TextureAtlasBuilderError::NotEnoughSpace)?;
let mut texture_rects = Vec::with_capacity(rect_placements.packed_locations().len());
let mut texture_ids = <HashMap<_, _>>::default();
// We iterate through the textures to place to respect the insertion order for the texture indices
for (index, (image_id, texture)) in self.textures_to_place.iter().enumerate() {
let (_, packed_location) = rect_placements.packed_locations().get(&index).unwrap();
let min = UVec2::new(packed_location.x(), packed_location.y());
let max =
min + UVec2::new(packed_location.width(), packed_location.height()) - self.padding;
if let Some(image_id) = image_id {
texture_ids.insert(*image_id, index);
}
texture_rects.push(URect { min, max });
if texture.texture_descriptor.format != self.format && !self.auto_format_conversion {
warn!(
"Loading a texture of format '{:?}' in an atlas with format '{:?}'",
texture.texture_descriptor.format, self.format
);
return Err(TextureAtlasBuilderError::WrongFormat);
}
self.copy_converted_texture(&mut atlas_texture, texture, packed_location)?;
}
Ok((
TextureAtlasLayout {
size: atlas_texture.size(),
textures: texture_rects,
},
TextureAtlasSources { texture_ids },
atlas_texture,
))
}
}