bevy/crates/bevy_render/src/texture/image.rs
Mike 6903a9411b get pixel size from wgpu (#6820)
# Objective

- Get rid of giant match statement to get PixelInfo. 
- This will allow for supporting any texture that is uncompressed, instead of people needing to PR in any textures that are supported in wgpu, but not bevy.

## Solution

- More conservative alternative to https://github.com/bevyengine/bevy/pull/6788, where we don't try to make some of the calculations correct for compressed types.
- Delete `PixelInfo` and get the pixel_size directly from wgpu. Data from wgpu is here: https://docs.rs/wgpu-types/0.14.0/src/wgpu_types/lib.rs.html#2359
- Panic if the texture is a compressed type. An integer byte size of a pixel is no longer a valid concept when talking about compressed textures.
- All internal usages use `pixel_size` and not `pixel_info` and are on uncompressed formats. Most of these usages are on either explicit texture formats or slightly indirectly through `TextureFormat::bevy_default()`. The other uses are in `TextureAtlas` and have other calculations that assumes the texture is uncompressed. 

## Changelog

- remove `PixelInfo` and get `pixel_size` from wgpu

## Migration Guide

`PixelInfo` has been removed. `PixelInfo::components` is equivalent to `texture_format.describe().components`. `PixelInfo::type_size` can be gotten from `texture_format.describe().block_size/ texture_format.describe().components`. But note this can yield incorrect results for some texture types like Rg11b10Float.
2022-12-11 18:22:07 +00:00

631 lines
22 KiB
Rust

#[cfg(feature = "basis-universal")]
use super::basis::*;
#[cfg(feature = "dds")]
use super::dds::*;
#[cfg(feature = "ktx2")]
use super::ktx2::*;
use crate::{
render_asset::{PrepareAssetError, RenderAsset},
render_resource::{Sampler, Texture, TextureView},
renderer::{RenderDevice, RenderQueue},
texture::BevyDefault,
};
use bevy_asset::HandleUntyped;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::system::{lifetimeless::SRes, Resource, SystemParamItem};
use bevy_math::Vec2;
use bevy_reflect::{FromReflect, Reflect, TypeUuid};
use std::hash::Hash;
use thiserror::Error;
use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor};
pub const TEXTURE_ASSET_INDEX: u64 = 0;
pub const SAMPLER_ASSET_INDEX: u64 = 1;
pub const DEFAULT_IMAGE_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Image::TYPE_UUID, 13148262314052771789);
#[derive(Debug)]
pub enum ImageFormat {
Avif,
Basis,
Bmp,
Dds,
Farbfeld,
Gif,
Hdr,
Ico,
Jpeg,
Ktx2,
Png,
Pnm,
Tga,
Tiff,
WebP,
}
impl ImageFormat {
pub fn from_mime_type(mime_type: &str) -> Option<Self> {
Some(match mime_type.to_ascii_lowercase().as_str() {
"image/bmp" | "image/x-bmp" => ImageFormat::Bmp,
"image/vnd-ms.dds" => ImageFormat::Dds,
"image/jpeg" => ImageFormat::Jpeg,
"image/ktx2" => ImageFormat::Ktx2,
"image/png" => ImageFormat::Png,
"image/x-targa" | "image/x-tga" => ImageFormat::Tga,
_ => return None,
})
}
pub fn from_extension(extension: &str) -> Option<Self> {
Some(match extension.to_ascii_lowercase().as_str() {
"avif" => ImageFormat::Avif,
"basis" => ImageFormat::Basis,
"bmp" => ImageFormat::Bmp,
"dds" => ImageFormat::Dds,
"ff" | "farbfeld" => ImageFormat::Farbfeld,
"gif" => ImageFormat::Gif,
"hdr" => ImageFormat::Hdr,
"ico" => ImageFormat::Ico,
"jpg" | "jpeg" => ImageFormat::Jpeg,
"ktx2" => ImageFormat::Ktx2,
"pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm,
"png" => ImageFormat::Png,
"tga" => ImageFormat::Tga,
"tif" | "tiff" => ImageFormat::Tiff,
"webp" => ImageFormat::WebP,
_ => return None,
})
}
pub fn as_image_crate_format(&self) -> Option<image::ImageFormat> {
Some(match self {
ImageFormat::Avif => image::ImageFormat::Avif,
ImageFormat::Bmp => image::ImageFormat::Bmp,
ImageFormat::Dds => image::ImageFormat::Dds,
ImageFormat::Farbfeld => image::ImageFormat::Farbfeld,
ImageFormat::Gif => image::ImageFormat::Gif,
ImageFormat::Hdr => image::ImageFormat::Hdr,
ImageFormat::Ico => image::ImageFormat::Ico,
ImageFormat::Jpeg => image::ImageFormat::Jpeg,
ImageFormat::Png => image::ImageFormat::Png,
ImageFormat::Pnm => image::ImageFormat::Pnm,
ImageFormat::Tga => image::ImageFormat::Tga,
ImageFormat::Tiff => image::ImageFormat::Tiff,
ImageFormat::WebP => image::ImageFormat::WebP,
ImageFormat::Basis | ImageFormat::Ktx2 => return None,
})
}
}
#[derive(Reflect, FromReflect, Debug, Clone, TypeUuid)]
#[uuid = "6ea26da6-6cf8-4ea2-9986-1d7bf6c17d6f"]
#[reflect_value]
pub struct Image {
pub data: Vec<u8>,
// TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors
pub texture_descriptor: wgpu::TextureDescriptor<'static>,
/// The [`ImageSampler`] to use during rendering.
pub sampler_descriptor: ImageSampler,
pub texture_view_descriptor: Option<wgpu::TextureViewDescriptor<'static>>,
}
/// Used in [`Image`], this determines what image sampler to use when rendering. The default setting,
/// [`ImageSampler::Default`], will read the sampler from the [`ImagePlugin`](super::ImagePlugin) at setup.
/// Setting this to [`ImageSampler::Descriptor`] will override the global default descriptor for this [`Image`].
#[derive(Debug, Default, Clone)]
pub enum ImageSampler {
/// Default image sampler, derived from the [`ImagePlugin`](super::ImagePlugin) setup.
#[default]
Default,
/// Custom sampler for this image which will override global default.
Descriptor(wgpu::SamplerDescriptor<'static>),
}
impl ImageSampler {
/// Returns an image sampler with `Linear` min and mag filters
#[inline]
pub fn linear() -> ImageSampler {
ImageSampler::Descriptor(Self::linear_descriptor())
}
/// Returns an image sampler with `nearest` min and mag filters
#[inline]
pub fn nearest() -> ImageSampler {
ImageSampler::Descriptor(Self::nearest_descriptor())
}
/// Returns a sampler descriptor with `Linear` min and mag filters
#[inline]
pub fn linear_descriptor() -> wgpu::SamplerDescriptor<'static> {
wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
}
}
/// Returns a sampler descriptor with `Nearest` min and mag filters
#[inline]
pub fn nearest_descriptor() -> wgpu::SamplerDescriptor<'static> {
wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
}
}
}
/// A rendering resource for the default image sampler which is set during renderer
/// initialization.
///
/// The [`ImagePlugin`](super::ImagePlugin) can be set during app initialization to change the default
/// image sampler.
#[derive(Resource, Debug, Clone, Deref, DerefMut)]
pub struct DefaultImageSampler(pub(crate) Sampler);
impl Default for Image {
fn default() -> Self {
let format = wgpu::TextureFormat::bevy_default();
let data = vec![255; format.pixel_size()];
Image {
data,
texture_descriptor: wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
format,
dimension: wgpu::TextureDimension::D2,
label: None,
mip_level_count: 1,
sample_count: 1,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
},
sampler_descriptor: ImageSampler::Default,
texture_view_descriptor: None,
}
}
}
impl Image {
/// Creates a new image from raw binary data and the corresponding metadata.
///
/// # Panics
/// Panics if the length of the `data`, volume of the `size` and the size of the `format`
/// do not match.
pub fn new(
size: Extent3d,
dimension: TextureDimension,
data: Vec<u8>,
format: TextureFormat,
) -> Self {
debug_assert_eq!(
size.volume() * format.pixel_size(),
data.len(),
"Pixel data, size and format have to match",
);
let mut image = Self {
data,
..Default::default()
};
image.texture_descriptor.dimension = dimension;
image.texture_descriptor.size = size;
image.texture_descriptor.format = format;
image
}
/// Creates a new image from raw binary data and the corresponding metadata, by filling
/// the image data with the `pixel` data repeated multiple times.
///
/// # Panics
/// Panics if the size of the `format` is not a multiple of the length of the `pixel` data.
/// do not match.
pub fn new_fill(
size: Extent3d,
dimension: TextureDimension,
pixel: &[u8],
format: TextureFormat,
) -> Self {
let mut value = Image::default();
value.texture_descriptor.format = format;
value.texture_descriptor.dimension = dimension;
value.resize(size);
debug_assert_eq!(
pixel.len() % format.pixel_size(),
0,
"Must not have incomplete pixel data."
);
debug_assert!(
pixel.len() <= value.data.len(),
"Fill data must fit within pixel buffer."
);
for current_pixel in value.data.chunks_exact_mut(pixel.len()) {
current_pixel.copy_from_slice(pixel);
}
value
}
/// Returns the aspect ratio (height/width) of a 2D image.
pub fn aspect_2d(&self) -> f32 {
self.texture_descriptor.size.height as f32 / self.texture_descriptor.size.width as f32
}
/// Returns the size of a 2D image.
pub fn size(&self) -> Vec2 {
Vec2::new(
self.texture_descriptor.size.width as f32,
self.texture_descriptor.size.height as f32,
)
}
/// Resizes the image to the new size, by removing information or appending 0 to the `data`.
/// Does not properly resize the contents of the image, but only its internal `data` buffer.
pub fn resize(&mut self, size: Extent3d) {
self.texture_descriptor.size = size;
self.data.resize(
size.volume() * self.texture_descriptor.format.pixel_size(),
0,
);
}
/// Changes the `size`, asserting that the total number of data elements (pixels) remains the
/// same.
///
/// # Panics
/// Panics if the `new_size` does not have the same volume as to old one.
pub fn reinterpret_size(&mut self, new_size: Extent3d) {
assert!(
new_size.volume() == self.texture_descriptor.size.volume(),
"Incompatible sizes: old = {:?} new = {:?}",
self.texture_descriptor.size,
new_size
);
self.texture_descriptor.size = new_size;
}
/// Takes a 2D image containing vertically stacked images of the same size, and reinterprets
/// it as a 2D array texture, where each of the stacked images becomes one layer of the
/// array. This is primarily for use with the `texture2DArray` shader uniform type.
///
/// # Panics
/// Panics if the texture is not 2D, has more than one layers or is not evenly dividable into
/// the `layers`.
pub fn reinterpret_stacked_2d_as_array(&mut self, layers: u32) {
// Must be a stacked image, and the height must be divisible by layers.
assert!(self.texture_descriptor.dimension == TextureDimension::D2);
assert!(self.texture_descriptor.size.depth_or_array_layers == 1);
assert_eq!(self.texture_descriptor.size.height % layers, 0);
self.reinterpret_size(Extent3d {
width: self.texture_descriptor.size.width,
height: self.texture_descriptor.size.height / layers,
depth_or_array_layers: layers,
});
}
/// Convert a texture from a format to another. Only a few formats are
/// supported as input and output:
/// - `TextureFormat::R8Unorm`
/// - `TextureFormat::Rg8Unorm`
/// - `TextureFormat::Rgba8UnormSrgb`
///
/// To get [`Image`] as a [`image::DynamicImage`] see:
/// [`Image::try_into_dynamic`].
pub fn convert(&self, new_format: TextureFormat) -> Option<Self> {
self.clone()
.try_into_dynamic()
.ok()
.and_then(|img| match new_format {
TextureFormat::R8Unorm => {
Some((image::DynamicImage::ImageLuma8(img.into_luma8()), false))
}
TextureFormat::Rg8Unorm => Some((
image::DynamicImage::ImageLumaA8(img.into_luma_alpha8()),
false,
)),
TextureFormat::Rgba8UnormSrgb => {
Some((image::DynamicImage::ImageRgba8(img.into_rgba8()), true))
}
_ => None,
})
.map(|(dyn_img, is_srgb)| Self::from_dynamic(dyn_img, is_srgb))
}
/// Load a bytes buffer in a [`Image`], according to type `image_type`, using the `image`
/// crate
pub fn from_buffer(
buffer: &[u8],
image_type: ImageType,
#[allow(unused_variables)] supported_compressed_formats: CompressedImageFormats,
is_srgb: bool,
) -> Result<Image, TextureError> {
let format = image_type.to_image_format()?;
// Load the image in the expected format.
// Some formats like PNG allow for R or RG textures too, so the texture
// format needs to be determined. For RGB textures an alpha channel
// needs to be added, so the image data needs to be converted in those
// cases.
match format {
#[cfg(feature = "basis-universal")]
ImageFormat::Basis => {
basis_buffer_to_image(buffer, supported_compressed_formats, is_srgb)
}
#[cfg(feature = "dds")]
ImageFormat::Dds => dds_buffer_to_image(buffer, supported_compressed_formats, is_srgb),
#[cfg(feature = "ktx2")]
ImageFormat::Ktx2 => {
ktx2_buffer_to_image(buffer, supported_compressed_formats, is_srgb)
}
_ => {
let image_crate_format = format
.as_image_crate_format()
.ok_or_else(|| TextureError::UnsupportedTextureFormat(format!("{format:?}")))?;
let mut reader = image::io::Reader::new(std::io::Cursor::new(buffer));
reader.set_format(image_crate_format);
reader.no_limits();
let dyn_img = reader.decode()?;
Ok(Self::from_dynamic(dyn_img, is_srgb))
}
}
}
/// Whether the texture format is compressed or uncompressed
pub fn is_compressed(&self) -> bool {
let format_description = self.texture_descriptor.format.describe();
format_description
.required_features
.contains(wgpu::Features::TEXTURE_COMPRESSION_ASTC_LDR)
|| format_description
.required_features
.contains(wgpu::Features::TEXTURE_COMPRESSION_BC)
|| format_description
.required_features
.contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2)
}
}
#[derive(Clone, Copy, Debug)]
pub enum DataFormat {
Rgb,
Rgba,
Rrr,
Rrrg,
Rg,
}
#[derive(Clone, Copy, Debug)]
pub enum TranscodeFormat {
Etc1s,
// Has to be transcoded to Rgba8 for use with `wgpu`
Rgb8,
Uastc(DataFormat),
}
/// An error that occurs when loading a texture
#[derive(Error, Debug)]
pub enum TextureError {
#[error("invalid image mime type: {0}")]
InvalidImageMimeType(String),
#[error("invalid image extension: {0}")]
InvalidImageExtension(String),
#[error("failed to load an image: {0}")]
ImageError(#[from] image::ImageError),
#[error("unsupported texture format: {0}")]
UnsupportedTextureFormat(String),
#[error("supercompression not supported: {0}")]
SuperCompressionNotSupported(String),
#[error("failed to load an image: {0}")]
SuperDecompressionError(String),
#[error("invalid data: {0}")]
InvalidData(String),
#[error("transcode error: {0}")]
TranscodeError(String),
#[error("format requires transcoding: {0:?}")]
FormatRequiresTranscodingError(TranscodeFormat),
}
/// The type of a raw image buffer.
pub enum ImageType<'a> {
/// The mime type of an image, for example `"image/png"`.
MimeType(&'a str),
/// The extension of an image file, for example `"png"`.
Extension(&'a str),
}
impl<'a> ImageType<'a> {
pub fn to_image_format(&self) -> Result<ImageFormat, TextureError> {
match self {
ImageType::MimeType(mime_type) => ImageFormat::from_mime_type(mime_type)
.ok_or_else(|| TextureError::InvalidImageMimeType(mime_type.to_string())),
ImageType::Extension(extension) => ImageFormat::from_extension(extension)
.ok_or_else(|| TextureError::InvalidImageExtension(extension.to_string())),
}
}
}
/// Used to calculate the volume of an item.
pub trait Volume {
fn volume(&self) -> usize;
}
impl Volume for Extent3d {
/// Calculates the volume of the [`Extent3d`].
fn volume(&self) -> usize {
(self.width * self.height * self.depth_or_array_layers) as usize
}
}
/// Extends the wgpu [`TextureFormat`] with information about the pixel.
pub trait TextureFormatPixelInfo {
/// Returns the size of a pixel in bytes of the format.
fn pixel_size(&self) -> usize;
}
impl TextureFormatPixelInfo for TextureFormat {
fn pixel_size(&self) -> usize {
let info = self.describe();
match info.block_dimensions {
(1, 1) => info.block_size.into(),
_ => panic!("Using pixel_size for compressed textures is invalid"),
}
}
}
/// The GPU-representation of an [`Image`].
/// Consists of the [`Texture`], its [`TextureView`] and the corresponding [`Sampler`], and the texture's size.
#[derive(Debug, Clone)]
pub struct GpuImage {
pub texture: Texture,
pub texture_view: TextureView,
pub texture_format: TextureFormat,
pub sampler: Sampler,
pub size: Vec2,
}
impl RenderAsset for Image {
type ExtractedAsset = Image;
type PreparedAsset = GpuImage;
type Param = (
SRes<RenderDevice>,
SRes<RenderQueue>,
SRes<DefaultImageSampler>,
);
/// Clones the Image.
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
}
/// Converts the extracted image into a [`GpuImage`].
fn prepare_asset(
image: Self::ExtractedAsset,
(render_device, render_queue, default_sampler): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
let texture = render_device.create_texture_with_data(
render_queue,
&image.texture_descriptor,
&image.data,
);
let texture_view = texture.create_view(
image
.texture_view_descriptor
.or_else(|| Some(TextureViewDescriptor::default()))
.as_ref()
.unwrap(),
);
let size = Vec2::new(
image.texture_descriptor.size.width as f32,
image.texture_descriptor.size.height as f32,
);
let sampler = match image.sampler_descriptor {
ImageSampler::Default => (***default_sampler).clone(),
ImageSampler::Descriptor(descriptor) => render_device.create_sampler(&descriptor),
};
Ok(GpuImage {
texture,
texture_view,
texture_format: image.texture_descriptor.format,
sampler,
size,
})
}
}
bitflags::bitflags! {
#[derive(Default)]
#[repr(transparent)]
pub struct CompressedImageFormats: u32 {
const NONE = 0;
const ASTC_LDR = (1 << 0);
const BC = (1 << 1);
const ETC2 = (1 << 2);
}
}
impl CompressedImageFormats {
pub fn from_features(features: wgpu::Features) -> Self {
let mut supported_compressed_formats = Self::default();
if features.contains(wgpu::Features::TEXTURE_COMPRESSION_ASTC_LDR) {
supported_compressed_formats |= Self::ASTC_LDR;
}
if features.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) {
supported_compressed_formats |= Self::BC;
}
if features.contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2) {
supported_compressed_formats |= Self::ETC2;
}
supported_compressed_formats
}
pub fn supports(&self, format: TextureFormat) -> bool {
match format {
TextureFormat::Bc1RgbaUnorm
| TextureFormat::Bc1RgbaUnormSrgb
| TextureFormat::Bc2RgbaUnorm
| TextureFormat::Bc2RgbaUnormSrgb
| TextureFormat::Bc3RgbaUnorm
| TextureFormat::Bc3RgbaUnormSrgb
| TextureFormat::Bc4RUnorm
| TextureFormat::Bc4RSnorm
| TextureFormat::Bc5RgUnorm
| TextureFormat::Bc5RgSnorm
| TextureFormat::Bc6hRgbUfloat
| TextureFormat::Bc6hRgbSfloat
| TextureFormat::Bc7RgbaUnorm
| TextureFormat::Bc7RgbaUnormSrgb => self.contains(CompressedImageFormats::BC),
TextureFormat::Etc2Rgb8Unorm
| TextureFormat::Etc2Rgb8UnormSrgb
| TextureFormat::Etc2Rgb8A1Unorm
| TextureFormat::Etc2Rgb8A1UnormSrgb
| TextureFormat::Etc2Rgba8Unorm
| TextureFormat::Etc2Rgba8UnormSrgb
| TextureFormat::EacR11Unorm
| TextureFormat::EacR11Snorm
| TextureFormat::EacRg11Unorm
| TextureFormat::EacRg11Snorm => self.contains(CompressedImageFormats::ETC2),
TextureFormat::Astc { .. } => self.contains(CompressedImageFormats::ASTC_LDR),
_ => true,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn image_size() {
let size = Extent3d {
width: 200,
height: 100,
depth_or_array_layers: 1,
};
let image = Image::new_fill(
size,
TextureDimension::D2,
&[0, 0, 0, 255],
TextureFormat::Rgba8Unorm,
);
assert_eq!(
Vec2::new(size.width as f32, size.height as f32),
image.size()
);
}
#[test]
fn image_default_size() {
let image = Image::default();
assert_eq!(Vec2::ONE, image.size());
}
}