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

View File

@@ -0,0 +1 @@
{"files":{"Cargo.lock":"1fdec542b5c7903ada5cdad7ab339bc9a3645b93df52558a3b2f1ddd1135bff6","Cargo.toml":"14047f846328db0ef5a2c7671d2f4634ba5b29172481516374a573b03cd9cc64","LICENSE-APACHE":"a6cba85bc92e0cff7a450b1d873c0eaa2e9fc96bf472df0247a26bec77bf3ff9","LICENSE-MIT":"508a77d2e7b51d98adeed32648ad124b7b30241a8e70b2e72c99f92d8e5874d1","src/basis.rs":"45b7c7d68ea76317a651b07a2659718a5a621cd0b00ce09ddfe1e41636a35ba4","src/compressed_image_saver.rs":"c347c04fa5c1aeaf5aefa6a27588cbd51223b77a77424f97b748a7ed7dce09b2","src/dds.rs":"c6f17ab0973a749bb699c991c12cc2b05bfec24416d2ed1b6e0ecdec33c20ed0","src/dynamic_texture_atlas_builder.rs":"bb1af32de96568c53c30b887e2d61bf05027dd63931c3ad0f551daf7c4a6fd15","src/exr_texture_loader.rs":"2250a0199d7e713c3277d898fb6de1f55bebed8c4e2edbc6b54d71fac8072ac0","src/hdr_texture_loader.rs":"df4a6718d1d89bebbc4376222eb1a54697fa5cc86d3499e0ac672bf3496b017a","src/image.rs":"703ce3ba6ceed9699745ccaf860eaeaf000f596b000c82191d415e8cf2784b85","src/image_loader.rs":"aec8d7f78f5dc108f84ef72e29b58f52d24d742f3e4dbdb2ea508657a3188e7f","src/image_texture_conversion.rs":"c1cde1ada0703541abddee0517551135d91255e4b12f4abb801d7ed3b87d5753","src/ktx2.rs":"e438b564a37a72fedd9f1289daec81607d3a151445b7f8a60d8eb024e97eea47","src/lib.rs":"9db0f3df93ead3dc5d0c0d74d8651e8cb7d3d2edfe5adf4c14184086334b10e2","src/texture_atlas.rs":"a5ec2a102793ece9d4b499287902150e06672e668689aa312d083efa86822f73","src/texture_atlas_builder.rs":"2fd397b6099cb159404e1d9c6cb860c0b411a55a6cb7e1f7e6a673b67e82e4e5"},"package":"65e6e900cfecadbc3149953169e36b9e26f922ed8b002d62339d8a9dc6129328"}

2189
vendor/bevy_image/Cargo.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

192
vendor/bevy_image/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,192 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "bevy_image"
version = "0.16.1"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Provides image types for Bevy Engine"
homepage = "https://bevyengine.org"
readme = false
keywords = ["bevy"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/bevyengine/bevy"
resolver = "2"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = [
"-Zunstable-options",
"--generate-link-to-definition",
]
[features]
basis-universal = ["dep:basis-universal"]
bevy_reflect = ["bevy_math/bevy_reflect"]
bmp = ["image/bmp"]
dds = ["ddsfile"]
default = ["bevy_reflect"]
exr = ["image/exr"]
ff = ["image/ff"]
gif = ["image/gif"]
hdr = ["image/hdr"]
ico = ["image/ico"]
jpeg = ["image/jpeg"]
ktx2 = ["dep:ktx2"]
png = ["image/png"]
pnm = ["image/pnm"]
qoi = ["image/qoi"]
serialize = [
"bevy_reflect",
"bevy_platform/serialize",
"bevy_utils/serde",
]
tga = ["image/tga"]
tiff = ["image/tiff"]
webp = ["image/webp"]
zlib = ["flate2"]
zstd = ["ruzstd"]
[lib]
name = "bevy_image"
path = "src/lib.rs"
[dependencies.basis-universal]
version = "0.3.0"
optional = true
[dependencies.bevy_app]
version = "0.16.1"
[dependencies.bevy_asset]
version = "0.16.1"
[dependencies.bevy_color]
version = "0.16.2"
features = [
"serialize",
"wgpu-types",
]
[dependencies.bevy_math]
version = "0.16.1"
[dependencies.bevy_platform]
version = "0.16.1"
features = ["std"]
default-features = false
[dependencies.bevy_reflect]
version = "0.16.1"
[dependencies.bevy_utils]
version = "0.16.1"
[dependencies.bitflags]
version = "2.3"
features = ["serde"]
[dependencies.bytemuck]
version = "1.5"
[dependencies.ddsfile]
version = "0.5.2"
optional = true
[dependencies.flate2]
version = "1.0.22"
optional = true
[dependencies.futures-lite]
version = "2.0.1"
[dependencies.guillotiere]
version = "0.6.0"
[dependencies.half]
version = "2.4.1"
[dependencies.image]
version = "0.25.2"
default-features = false
[dependencies.ktx2]
version = "0.3.0"
optional = true
[dependencies.rectangle-pack]
version = "0.4"
[dependencies.ruzstd]
version = "0.8.0"
optional = true
[dependencies.serde]
version = "1"
features = ["derive"]
[dependencies.thiserror]
version = "2"
default-features = false
[dependencies.tracing]
version = "0.1"
features = ["std"]
default-features = false
[dependencies.wgpu-types]
version = "24"
default-features = false
[dev-dependencies.bevy_ecs]
version = "0.16.1"
[lints.clippy]
alloc_instead_of_core = "warn"
allow_attributes = "warn"
allow_attributes_without_reason = "warn"
doc_markdown = "warn"
manual_let_else = "warn"
match_same_arms = "warn"
needless_lifetimes = "allow"
nonstandard_macro_braces = "warn"
print_stderr = "warn"
print_stdout = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
redundant_closure_for_method_calls = "warn"
redundant_else = "warn"
ref_as_ptr = "warn"
semicolon_if_nothing_returned = "warn"
std_instead_of_alloc = "warn"
std_instead_of_core = "warn"
too_long_first_doc_paragraph = "allow"
too_many_arguments = "allow"
type_complexity = "allow"
undocumented_unsafe_blocks = "warn"
unwrap_or_default = "warn"
[lints.rust]
missing_docs = "warn"
unsafe_code = "deny"
unsafe_op_in_unsafe_fn = "warn"
unused_qualifications = "warn"
[lints.rust.unexpected_cfgs]
level = "warn"
priority = 0
check-cfg = ["cfg(docsrs_dep)"]

176
vendor/bevy_image/LICENSE-APACHE vendored Normal file
View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

19
vendor/bevy_image/LICENSE-MIT vendored Normal file
View File

@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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,
))
}
}