diff --git a/.idea/rg3d-core.iml b/.idea/rg3d-core.iml index 2c7e0802f..56e970693 100644 --- a/.idea/rg3d-core.iml +++ b/.idea/rg3d-core.iml @@ -44,6 +44,7 @@ + diff --git a/Cargo.toml b/Cargo.toml index 58655697d..7fb1866d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ "project-manager", "fyrox-graphics", "fyrox-build-tools" -] +, "fyrox-texture"] resolver = "2" [profile.dev] diff --git a/fyrox-impl/Cargo.toml b/fyrox-impl/Cargo.toml index 42f9a5cca..b134ac678 100644 --- a/fyrox-impl/Cargo.toml +++ b/fyrox-impl/Cargo.toml @@ -23,15 +23,14 @@ fyrox-resource = { path = "../fyrox-resource", version = "0.36.0" } fyrox-animation = { path = "../fyrox-animation", version = "0.36.0" } fyrox-graph = { path = "../fyrox-graph", version = "0.36.0" } fyrox-graphics = { path = "../fyrox-graphics", version = "0.36.0" } +fyrox-texture = { path = "../fyrox-texture", version = "0.36.0" } rapier2d = { version = "0.22", features = ["debug-render"] } rapier3d = { version = "0.22", features = ["debug-render"] } image = { version = "0.25.1", default-features = false, features = ["gif", "jpeg", "png", "tga", "tiff", "bmp"] } inflate = "0.4.5" serde = { version = "1", features = ["derive"] } lazy_static = "1.4.0" -ddsfile = "0.5.0" rayon = "1.5.1" -tbc = "0.3.0" bitflags = "2.2.1" walkdir = "2.3.2" ron = "0.8.0" @@ -41,7 +40,6 @@ strum_macros = "0.26.1" clap = { version = "4", features = ["derive"] } winit = { version = "0.29.2", features = ["serde"] } half = { version = "2.2.1", features = ["bytemuck"] } -fast_image_resize = "4.0.0" base64 = "0.22.1" uvgen = "0.2.0" lightmap = "0.2" diff --git a/fyrox-impl/src/lib.rs b/fyrox-impl/src/lib.rs index 4484bb523..fa7995771 100644 --- a/fyrox-impl/src/lib.rs +++ b/fyrox-impl/src/lib.rs @@ -45,7 +45,6 @@ pub mod utils; pub use crate::core::rand; pub use fxhash; pub use lazy_static; -pub use tbc; pub use walkdir; pub use winit::*; diff --git a/fyrox-impl/src/renderer/cache/texture.rs b/fyrox-impl/src/renderer/cache/texture.rs index 6b03b1431..f92a7c83d 100644 --- a/fyrox-impl/src/renderer/cache/texture.rs +++ b/fyrox-impl/src/renderer/cache/texture.rs @@ -30,7 +30,13 @@ use crate::{ }, resource::texture::{Texture, TextureResource}, }; -use fyrox_graphics::gpu_texture::GpuTextureDescriptor; +use fyrox_graphics::gpu_texture::{ + GpuTextureDescriptor, GpuTextureKind, MagnificationFilter, MinificationFilter, WrapMode, +}; +use fyrox_texture::{ + TextureKind, TextureMagnificationFilter, TextureMinificationFilter, TexturePixelKind, + TextureWrapMode, +}; use std::{cell::RefCell, rc::Rc}; pub(crate) struct TextureRenderData { @@ -43,20 +49,103 @@ pub struct TextureCache { cache: TemporaryCache, } +fn convert_texture_kind(v: TextureKind) -> GpuTextureKind { + match v { + TextureKind::Line { length } => GpuTextureKind::Line { + length: length as usize, + }, + TextureKind::Rectangle { width, height } => GpuTextureKind::Rectangle { + width: width as usize, + height: height as usize, + }, + TextureKind::Cube { width, height } => GpuTextureKind::Cube { + width: width as usize, + height: height as usize, + }, + TextureKind::Volume { + width, + height, + depth, + } => GpuTextureKind::Volume { + width: width as usize, + height: height as usize, + depth: depth as usize, + }, + } +} + +fn convert_pixel_kind(texture_kind: TexturePixelKind) -> PixelKind { + match texture_kind { + TexturePixelKind::R8 => PixelKind::R8, + TexturePixelKind::RGB8 => PixelKind::RGB8, + TexturePixelKind::RGBA8 => PixelKind::RGBA8, + TexturePixelKind::RG8 => PixelKind::RG8, + TexturePixelKind::R16 => PixelKind::R16, + TexturePixelKind::RG16 => PixelKind::RG16, + TexturePixelKind::BGR8 => PixelKind::BGR8, + TexturePixelKind::BGRA8 => PixelKind::BGRA8, + TexturePixelKind::RGB16 => PixelKind::RGB16, + TexturePixelKind::RGBA16 => PixelKind::RGBA16, + TexturePixelKind::RGB16F => PixelKind::RGB16F, + TexturePixelKind::DXT1RGB => PixelKind::DXT1RGB, + TexturePixelKind::DXT1RGBA => PixelKind::DXT1RGBA, + TexturePixelKind::DXT3RGBA => PixelKind::DXT3RGBA, + TexturePixelKind::DXT5RGBA => PixelKind::DXT5RGBA, + TexturePixelKind::R8RGTC => PixelKind::R8RGTC, + TexturePixelKind::RG8RGTC => PixelKind::RG8RGTC, + TexturePixelKind::RGB32F => PixelKind::RGB32F, + TexturePixelKind::RGBA32F => PixelKind::RGBA32F, + TexturePixelKind::Luminance8 => PixelKind::L8, + TexturePixelKind::LuminanceAlpha8 => PixelKind::LA8, + TexturePixelKind::Luminance16 => PixelKind::L16, + TexturePixelKind::LuminanceAlpha16 => PixelKind::LA16, + TexturePixelKind::R32F => PixelKind::R32F, + TexturePixelKind::R16F => PixelKind::R16F, + } +} + +fn convert_magnification_filter(v: TextureMagnificationFilter) -> MagnificationFilter { + match v { + TextureMagnificationFilter::Nearest => MagnificationFilter::Nearest, + TextureMagnificationFilter::Linear => MagnificationFilter::Linear, + } +} + +fn convert_minification_filter(v: TextureMinificationFilter) -> MinificationFilter { + match v { + TextureMinificationFilter::Nearest => MinificationFilter::Nearest, + TextureMinificationFilter::NearestMipMapNearest => MinificationFilter::NearestMipMapNearest, + TextureMinificationFilter::NearestMipMapLinear => MinificationFilter::NearestMipMapLinear, + TextureMinificationFilter::Linear => MinificationFilter::Linear, + TextureMinificationFilter::LinearMipMapNearest => MinificationFilter::LinearMipMapNearest, + TextureMinificationFilter::LinearMipMapLinear => MinificationFilter::LinearMipMapLinear, + } +} + +fn convert_wrap_mode(v: TextureWrapMode) -> WrapMode { + match v { + TextureWrapMode::Repeat => WrapMode::Repeat, + TextureWrapMode::ClampToEdge => WrapMode::ClampToEdge, + TextureWrapMode::ClampToBorder => WrapMode::ClampToBorder, + TextureWrapMode::MirroredRepeat => WrapMode::MirroredRepeat, + TextureWrapMode::MirrorClampToEdge => WrapMode::MirrorClampToEdge, + } +} + fn create_gpu_texture( server: &dyn GraphicsServer, texture: &Texture, ) -> Result { server .create_texture(GpuTextureDescriptor { - kind: texture.kind().into(), - pixel_kind: PixelKind::from(texture.pixel_kind()), - mag_filter: texture.magnification_filter().into(), - min_filter: texture.minification_filter().into(), + kind: convert_texture_kind(texture.kind()), + pixel_kind: convert_pixel_kind(texture.pixel_kind()), + mag_filter: convert_magnification_filter(texture.magnification_filter()), + min_filter: convert_minification_filter(texture.minification_filter()), mip_count: texture.mip_count() as usize, - s_wrap_mode: texture.s_wrap_mode().into(), - t_wrap_mode: texture.t_wrap_mode().into(), - r_wrap_mode: texture.r_wrap_mode().into(), + s_wrap_mode: convert_wrap_mode(texture.s_wrap_mode()), + t_wrap_mode: convert_wrap_mode(texture.t_wrap_mode()), + r_wrap_mode: convert_wrap_mode(texture.r_wrap_mode()), anisotropy: texture.anisotropy_level(), data: Some(texture.data()), base_level: texture.base_level(), @@ -115,8 +204,8 @@ impl TextureCache { if entry.modifications_counter != modifications_count { let mut gpu_texture = entry.gpu_texture.borrow_mut(); if let Err(e) = gpu_texture.set_data( - texture.kind().into(), - texture.pixel_kind().into(), + convert_texture_kind(texture.kind()), + convert_pixel_kind(texture.pixel_kind()), texture.mip_count() as usize, Some(texture.data()), ) { @@ -131,12 +220,13 @@ impl TextureCache { let mut gpu_texture = entry.gpu_texture.borrow_mut(); - let new_mag_filter = texture.magnification_filter().into(); + let new_mag_filter = + convert_magnification_filter(texture.magnification_filter()); if gpu_texture.magnification_filter() != new_mag_filter { gpu_texture.set_magnification_filter(new_mag_filter); } - let new_min_filter = texture.minification_filter().into(); + let new_min_filter = convert_minification_filter(texture.minification_filter()); if gpu_texture.minification_filter() != new_min_filter { gpu_texture.set_minification_filter(new_min_filter); } @@ -145,16 +235,21 @@ impl TextureCache { gpu_texture.set_anisotropy(texture.anisotropy_level()); } - let new_s_wrap_mode = texture.s_wrap_mode().into(); + let new_s_wrap_mode = convert_wrap_mode(texture.s_wrap_mode()); if gpu_texture.wrap_mode(Coordinate::S) != new_s_wrap_mode { gpu_texture.set_wrap(Coordinate::S, new_s_wrap_mode); } - let new_t_wrap_mode = texture.t_wrap_mode().into(); + let new_t_wrap_mode = convert_wrap_mode(texture.t_wrap_mode()); if gpu_texture.wrap_mode(Coordinate::T) != new_t_wrap_mode { gpu_texture.set_wrap(Coordinate::T, new_t_wrap_mode); } + let new_r_wrap_mode = convert_wrap_mode(texture.r_wrap_mode()); + if gpu_texture.wrap_mode(Coordinate::R) != new_r_wrap_mode { + gpu_texture.set_wrap(Coordinate::R, new_r_wrap_mode); + } + return Some(&entry.gpu_texture); } Err(e) => { diff --git a/fyrox-impl/src/renderer/framework/mod.rs b/fyrox-impl/src/renderer/framework/mod.rs index 20888fe61..42cdc39c9 100644 --- a/fyrox-impl/src/renderer/framework/mod.rs +++ b/fyrox-impl/src/renderer/framework/mod.rs @@ -28,112 +28,12 @@ use crate::{ AttributeDefinition, AttributeKind, GeometryBuffer, GeometryBufferDescriptor, VertexBufferData, VertexBufferDescriptor, }, - gpu_texture::{ - GpuTextureKind, MagnificationFilter, MinificationFilter, PixelKind, WrapMode, - }, server::GraphicsServer, }, - resource::texture::{ - TextureKind, TextureMagnificationFilter, TextureMinificationFilter, TexturePixelKind, - TextureWrapMode, - }, scene::mesh::{buffer::VertexAttributeDataType, surface::SurfaceData}, }; pub use fyrox_graphics::*; -impl From for GpuTextureKind { - fn from(v: TextureKind) -> Self { - match v { - TextureKind::Line { length } => GpuTextureKind::Line { - length: length as usize, - }, - TextureKind::Rectangle { width, height } => GpuTextureKind::Rectangle { - width: width as usize, - height: height as usize, - }, - TextureKind::Cube { width, height } => GpuTextureKind::Cube { - width: width as usize, - height: height as usize, - }, - TextureKind::Volume { - width, - height, - depth, - } => GpuTextureKind::Volume { - width: width as usize, - height: height as usize, - depth: depth as usize, - }, - } - } -} - -impl From for PixelKind { - fn from(texture_kind: TexturePixelKind) -> Self { - match texture_kind { - TexturePixelKind::R8 => Self::R8, - TexturePixelKind::RGB8 => Self::RGB8, - TexturePixelKind::RGBA8 => Self::RGBA8, - TexturePixelKind::RG8 => Self::RG8, - TexturePixelKind::R16 => Self::R16, - TexturePixelKind::RG16 => Self::RG16, - TexturePixelKind::BGR8 => Self::BGR8, - TexturePixelKind::BGRA8 => Self::BGRA8, - TexturePixelKind::RGB16 => Self::RGB16, - TexturePixelKind::RGBA16 => Self::RGBA16, - TexturePixelKind::RGB16F => Self::RGB16F, - TexturePixelKind::DXT1RGB => Self::DXT1RGB, - TexturePixelKind::DXT1RGBA => Self::DXT1RGBA, - TexturePixelKind::DXT3RGBA => Self::DXT3RGBA, - TexturePixelKind::DXT5RGBA => Self::DXT5RGBA, - TexturePixelKind::R8RGTC => Self::R8RGTC, - TexturePixelKind::RG8RGTC => Self::RG8RGTC, - TexturePixelKind::RGB32F => Self::RGB32F, - TexturePixelKind::RGBA32F => Self::RGBA32F, - TexturePixelKind::Luminance8 => Self::L8, - TexturePixelKind::LuminanceAlpha8 => Self::LA8, - TexturePixelKind::Luminance16 => Self::L16, - TexturePixelKind::LuminanceAlpha16 => Self::LA16, - TexturePixelKind::R32F => Self::R32F, - TexturePixelKind::R16F => Self::R16F, - } - } -} - -impl From for MagnificationFilter { - fn from(v: TextureMagnificationFilter) -> Self { - match v { - TextureMagnificationFilter::Nearest => Self::Nearest, - TextureMagnificationFilter::Linear => Self::Linear, - } - } -} - -impl From for MinificationFilter { - fn from(v: TextureMinificationFilter) -> Self { - match v { - TextureMinificationFilter::Nearest => Self::Nearest, - TextureMinificationFilter::NearestMipMapNearest => Self::NearestMipMapNearest, - TextureMinificationFilter::NearestMipMapLinear => Self::NearestMipMapLinear, - TextureMinificationFilter::Linear => Self::Linear, - TextureMinificationFilter::LinearMipMapNearest => Self::LinearMipMapNearest, - TextureMinificationFilter::LinearMipMapLinear => Self::LinearMipMapLinear, - } - } -} - -impl From for WrapMode { - fn from(v: TextureWrapMode) -> Self { - match v { - TextureWrapMode::Repeat => WrapMode::Repeat, - TextureWrapMode::ClampToEdge => WrapMode::ClampToEdge, - TextureWrapMode::ClampToBorder => WrapMode::ClampToBorder, - TextureWrapMode::MirroredRepeat => WrapMode::MirroredRepeat, - TextureWrapMode::MirrorClampToEdge => WrapMode::MirrorClampToEdge, - } - } -} - /// Extension trait for [`GeometryBuffer`]. pub trait GeometryBufferExt { /// Creates [`GeometryBuffer`] from [`SurfaceData`]. diff --git a/fyrox-impl/src/resource/texture/mod.rs b/fyrox-impl/src/resource/texture/mod.rs index 9c1be765a..d705f3bde 100644 --- a/fyrox-impl/src/resource/texture/mod.rs +++ b/fyrox-impl/src/resource/texture/mod.rs @@ -18,2011 +18,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -//! Texture is an image that used to fill faces to add details to them. -//! -//! In most cases textures are just 2D images, however there are some exclusions to that - -//! for example cube maps, that may be used for environment mapping. Fyrox supports 1D, 2D, -//! 3D and Cube textures. -//! -//! ## Supported formats -//! -//! To load images and decode them, Fyrox uses image and ddsfile crates. Here is the list of -//! supported formats: png, tga, bmp, dds, jpg, gif, tiff, dds. -//! -//! ## Compressed textures -//! -//! Fyrox supports most commonly used formats of compressed textures: DXT1, DXT3, DXT5. -//! -//! ## Render target -//! -//! Texture can be used as render target to render scene in it. To do this you should use -//! new_render_target method and pass its result to scene's render target property. Renderer -//! will automatically provide you info about metrics of texture, but it won't give you -//! access to pixels of render target. +//! Everything related to textures. See [`fyrox_texture`] crate docs for more info. -use crate::{ - asset::{options::ImportOptions, Resource, ResourceData, TEXTURE_RESOURCE_UUID}, - core::{ - algebra::{Vector2, Vector3}, - futures::io::Error, - io::FileLoadError, - reflect::prelude::*, - uuid::Uuid, - visitor::{PodVecView, Visit, VisitError, VisitResult, Visitor}, - TypeUuidProvider, - }, -}; -use ddsfile::{Caps2, D3DFormat}; -use fast_image_resize as fr; -use fast_image_resize::ResizeOptions; -use fxhash::FxHasher; -use fyrox_core::num_traits::Bounded; -use fyrox_core::sparse::AtomicIndex; -use fyrox_core::uuid_provider; -use fyrox_resource::embedded_data_source; -use fyrox_resource::io::ResourceIo; -use fyrox_resource::manager::BuiltInResource; -use fyrox_resource::untyped::ResourceKind; -use image::{ColorType, DynamicImage, ImageError, ImageFormat, Pixel}; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use std::{ - any::Any, - fmt::{Debug, Display, Formatter}, - hash::{Hash, Hasher}, - io::Cursor, - ops::{Deref, DerefMut, Shr}, - path::Path, -}; -use strum_macros::{AsRefStr, EnumString, VariantNames}; - -pub mod loader; - -/// Texture kind. -#[derive(Copy, Clone, Debug, Reflect)] -pub enum TextureKind { - /// 1D texture. - Line { - /// Length of the texture. - length: u32, - }, - /// 2D texture. - Rectangle { - /// Width of the texture. - width: u32, - /// Height of the texture. - height: u32, - }, - /// Cube texture. - Cube { - /// Width of the cube face. - width: u32, - /// Height of the cube face. - height: u32, - }, - /// Volume texture (3D). - Volume { - /// Width of the volume. - width: u32, - /// Height of the volume. - height: u32, - /// Depth of the volume. - depth: u32, - }, -} - -impl TextureKind { - /// Tries to fetch [`TextureKind::Line`]'s length. - #[inline] - pub fn line_length(&self) -> Option { - if let Self::Line { length } = self { - Some(*length) - } else { - None - } - } - - /// Tries to fetch [`TextureKind::Rectangle`]'s width (x) and height (y). - #[inline] - pub fn rectangle_size(&self) -> Option> { - if let Self::Rectangle { width, height } = self { - Some(Vector2::new(*width, *height)) - } else { - None - } - } - - /// Tries to fetch [`TextureKind::Cube`]'s width (x) and height (y). - #[inline] - pub fn cube_size(&self) -> Option> { - if let Self::Cube { width, height } = self { - Some(Vector2::new(*width, *height)) - } else { - None - } - } - - /// Tries to fetch [`TextureKind::Volume`]'s width (x), height (y), depth (z). - #[inline] - pub fn volume_size(&self) -> Option> { - if let Self::Volume { - width, - height, - depth, - } = self - { - Some(Vector3::new(*width, *height, *depth)) - } else { - None - } - } -} - -impl Default for TextureKind { - fn default() -> Self { - Self::Rectangle { - width: 0, - height: 0, - } - } -} - -impl Visit for TextureKind { - fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { - let mut region = visitor.enter_region(name)?; - - let mut id = match self { - TextureKind::Line { .. } => 0, - TextureKind::Rectangle { .. } => 1, - TextureKind::Cube { .. } => 2, - TextureKind::Volume { .. } => 3, - }; - id.visit("Id", &mut region)?; - if region.is_reading() { - *self = match id { - 0 => TextureKind::Line { length: 0 }, - 1 => TextureKind::Rectangle { - width: 0, - height: 0, - }, - 2 => TextureKind::Cube { - width: 0, - height: 0, - }, - 3 => TextureKind::Volume { - width: 0, - height: 0, - depth: 0, - }, - _ => { - return VisitResult::Err(VisitError::User(format!( - "Invalid texture kind {id}!" - ))) - } - }; - } - match self { - TextureKind::Line { length } => { - length.visit("Length", &mut region)?; - } - TextureKind::Rectangle { width, height } => { - width.visit("Width", &mut region)?; - height.visit("Height", &mut region)?; - } - TextureKind::Cube { width, height } => { - width.visit("Width", &mut region)?; - height.visit("Height", &mut region)?; - } - TextureKind::Volume { - width, - height, - depth, - } => { - width.visit("Width", &mut region)?; - height.visit("Height", &mut region)?; - depth.visit("Depth", &mut region)?; - } - } - - Ok(()) - } -} - -/// Data storage of a texture. -#[derive(Default, Clone, Reflect)] -pub struct TextureBytes(Vec); - -impl Visit for TextureBytes { - fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { - self.0.visit(name, visitor) - } -} - -impl Debug for TextureBytes { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "Texture has {} bytes", self.0.len()) - } -} - -impl From> for TextureBytes { - fn from(bytes: Vec) -> Self { - Self(bytes) - } -} - -impl Deref for TextureBytes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for TextureBytes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -/// Actual texture data. -#[derive(Debug, Clone, Reflect)] -pub struct Texture { - kind: TextureKind, - bytes: TextureBytes, - pixel_kind: TexturePixelKind, - minification_filter: TextureMinificationFilter, - magnification_filter: TextureMagnificationFilter, - s_wrap_mode: TextureWrapMode, - t_wrap_mode: TextureWrapMode, - r_wrap_mode: TextureWrapMode, - base_level: usize, - max_level: usize, - min_lod: f32, - max_lod: f32, - lod_bias: f32, - mip_count: u32, - anisotropy: f32, - modifications_counter: u64, - is_render_target: bool, - #[doc(hidden)] - #[reflect(hidden)] - pub cache_index: Arc, -} - -impl TypeUuidProvider for Texture { - fn type_uuid() -> Uuid { - TEXTURE_RESOURCE_UUID - } -} - -impl ResourceData for Texture { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn type_uuid(&self) -> Uuid { - ::type_uuid() - } - - fn save(&mut self, path: &Path) -> Result<(), Box> { - let color_type = match self.pixel_kind { - TexturePixelKind::R8 => ColorType::L8, - TexturePixelKind::Luminance8 => ColorType::L8, - TexturePixelKind::RGB8 => ColorType::Rgb8, - TexturePixelKind::RGBA8 => ColorType::Rgba8, - TexturePixelKind::RG8 => ColorType::La8, - TexturePixelKind::LuminanceAlpha8 => ColorType::La8, - TexturePixelKind::R16 => ColorType::L16, - TexturePixelKind::Luminance16 => ColorType::L16, - TexturePixelKind::RG16 => ColorType::La16, - TexturePixelKind::LuminanceAlpha16 => ColorType::La16, - TexturePixelKind::RGB16 => ColorType::Rgb16, - TexturePixelKind::RGBA16 => ColorType::Rgba16, - TexturePixelKind::RGB32F => ColorType::Rgb32F, - TexturePixelKind::RGBA32F => ColorType::Rgba32F, - TexturePixelKind::DXT1RGB - | TexturePixelKind::DXT1RGBA - | TexturePixelKind::DXT3RGBA - | TexturePixelKind::DXT5RGBA - | TexturePixelKind::R8RGTC - | TexturePixelKind::RG8RGTC - | TexturePixelKind::BGR8 - | TexturePixelKind::BGRA8 - | TexturePixelKind::RGB16F - | TexturePixelKind::R32F - | TexturePixelKind::R16F => return Err(Box::new(TextureError::UnsupportedFormat)), - }; - if let TextureKind::Rectangle { width, height } = self.kind { - Ok(image::save_buffer( - path, - self.bytes.as_ref(), - width, - height, - color_type, - )?) - } else { - Err(Box::new(TextureError::UnsupportedFormat)) - } - } - - fn can_be_saved(&self) -> bool { - true - } -} - -impl Visit for Texture { - fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { - let mut region = visitor.enter_region(name)?; - - let mut kind = self.pixel_kind.id(); - kind.visit("KindId", &mut region)?; - if region.is_reading() { - self.pixel_kind = TexturePixelKind::new(kind)?; - } - - self.minification_filter - .visit("MinificationFilter", &mut region)?; - self.magnification_filter - .visit("MagnificationFilter", &mut region)?; - self.anisotropy.visit("Anisotropy", &mut region)?; - self.s_wrap_mode.visit("SWrapMode", &mut region)?; - self.t_wrap_mode.visit("TWrapMode", &mut region)?; - let _ = self.t_wrap_mode.visit("RWrapMode", &mut region); - self.mip_count.visit("MipCount", &mut region)?; - self.kind.visit("Kind", &mut region)?; - let mut bytes_view = PodVecView::from_pod_vec(&mut self.bytes); - let _ = bytes_view.visit("Data", &mut region); - - let _ = self.base_level.visit("BaseLevel", &mut region); - let _ = self.max_level.visit("MaxLevel", &mut region); - let _ = self.min_lod.visit("MinLod", &mut region); - let _ = self.max_lod.visit("MaxLod", &mut region); - let _ = self.lod_bias.visit("LodBias", &mut region); - - Ok(()) - } -} - -impl Default for Texture { - /// It is very important to mention that defaults may be different for texture when you're - /// importing them through resource manager, see - /// [TextureImportOptions](../engine/resource_manager/struct.TextureImportOptions.html) for more info. - fn default() -> Self { - Self { - kind: TextureKind::Rectangle { - width: 0, - height: 0, - }, - bytes: Default::default(), - pixel_kind: TexturePixelKind::RGBA8, - minification_filter: TextureMinificationFilter::LinearMipMapLinear, - magnification_filter: TextureMagnificationFilter::Linear, - s_wrap_mode: TextureWrapMode::Repeat, - t_wrap_mode: TextureWrapMode::Repeat, - r_wrap_mode: TextureWrapMode::Repeat, - base_level: 0, - max_level: default_max_level(), - min_lod: default_min_lod(), - max_lod: default_max_lod(), - lod_bias: 0.0, - mip_count: 1, - anisotropy: 16.0, - modifications_counter: 0, - is_render_target: false, - cache_index: Default::default(), - } - } -} - -/// A filter for mip-map generation. -#[derive( - Default, Copy, Clone, Deserialize, Serialize, Debug, Reflect, AsRefStr, EnumString, VariantNames, -)] -pub enum MipFilter { - /// Simple nearest filter, it is the fastest filter available, but it produces noisy mip levels and - /// in most cases it is not advised to use it. Consider its performance as 1x. - Nearest, - /// Bilinear filtration. It has good balance between image quality and speed. It is ~13x times slower - /// than [`Self::Nearest`]. It is default filtering method. - #[default] - Bilinear, - /// Hamming filtration. It has much nicer filtration quality than Bilinear, but the same performance. - Hamming, - /// Catmull-Rom spline filtration. It has very good filtration quality, but it is ~23x times slower - /// than [`Self::Nearest`]. - CatmullRom, - /// Gaussian filtration. It has perfect filtration quality, but it is ~37x times slower than - /// [`Self::Nearest`]. - Lanczos, -} - -uuid_provider!(MipFilter = "8fa17c0e-6889-4540-b396-97db4dc952aa"); - -impl MipFilter { - fn into_filter_type(self) -> fr::FilterType { - match self { - MipFilter::Nearest => fr::FilterType::Box, - MipFilter::Bilinear => fr::FilterType::Bilinear, - MipFilter::CatmullRom => fr::FilterType::CatmullRom, - MipFilter::Hamming => fr::FilterType::Hamming, - MipFilter::Lanczos => fr::FilterType::Lanczos3, - } - } -} - -/// Allows you to define a set of parameters for a texture resource. -/// -/// # Details -/// -/// Usually the content of this structure is stored in a separate file with .options extension. Typical content of -/// a settings file should look like this: -/// -/// ```text -/// ( -/// minification_filter: Linear, -/// magnification_filter: Linear, -/// s_wrap_mode: Repeat, -/// t_wrap_mode: ClampToEdge, -/// anisotropy: 8.0, -/// compression: NoCompression, -/// ) -/// ``` -#[derive(Clone, Deserialize, Serialize, Debug, Reflect)] -pub struct TextureImportOptions { - #[serde(default)] - pub(crate) minification_filter: TextureMinificationFilter, - #[serde(default)] - pub(crate) magnification_filter: TextureMagnificationFilter, - #[serde(default)] - pub(crate) s_wrap_mode: TextureWrapMode, - #[serde(default)] - pub(crate) t_wrap_mode: TextureWrapMode, - #[serde(default)] - pub(crate) r_wrap_mode: TextureWrapMode, - #[serde(default)] - pub(crate) anisotropy: f32, - #[serde(default)] - pub(crate) compression: CompressionOptions, - #[serde(default)] - pub(crate) mip_filter: MipFilter, - #[serde(default)] - pub(crate) flip_green_channel: bool, - #[serde(default)] - pub(crate) base_level: usize, - #[serde(default = "default_max_level")] - pub(crate) max_level: usize, - #[serde(default = "default_min_lod")] - pub(crate) min_lod: f32, - #[serde(default = "default_max_lod")] - pub(crate) max_lod: f32, - #[serde(default)] - pub(crate) lod_bias: f32, -} - -fn default_max_level() -> usize { - 1000 -} - -fn default_min_lod() -> f32 { - -1000.0 -} - -fn default_max_lod() -> f32 { - 1000.0 -} - -impl Default for TextureImportOptions { - fn default() -> Self { - Self { - minification_filter: TextureMinificationFilter::LinearMipMapLinear, - magnification_filter: TextureMagnificationFilter::Linear, - s_wrap_mode: TextureWrapMode::Repeat, - t_wrap_mode: TextureWrapMode::Repeat, - r_wrap_mode: TextureWrapMode::Repeat, - anisotropy: 16.0, - compression: CompressionOptions::default(), - mip_filter: Default::default(), - flip_green_channel: false, - base_level: 0, - max_level: default_max_level(), - min_lod: default_min_lod(), - max_lod: default_max_lod(), - lod_bias: 0.0, - } - } -} - -impl ImportOptions for TextureImportOptions {} - -impl TextureImportOptions { - /// Sets new minification filter which will be applied to every imported texture as - /// default value. - pub fn with_minification_filter( - mut self, - minification_filter: TextureMinificationFilter, - ) -> Self { - self.minification_filter = minification_filter; - self - } - - /// Sets new minification filter which will be applied to every imported texture as - /// default value. - pub fn set_minification_filter(&mut self, minification_filter: TextureMinificationFilter) { - self.minification_filter = minification_filter; - } - - /// Sets new magnification filter which will be applied to every imported texture as - /// default value. - pub fn with_magnification_filter( - mut self, - magnification_filter: TextureMagnificationFilter, - ) -> Self { - self.magnification_filter = magnification_filter; - self - } - - /// Sets new magnification filter which will be applied to every imported texture as - /// default value. - pub fn set_magnification_filter(&mut self, magnification_filter: TextureMagnificationFilter) { - self.magnification_filter = magnification_filter; - } - - /// Sets new S coordinate wrap mode which will be applied to every imported texture as - /// default value. - pub fn with_s_wrap_mode(mut self, s_wrap_mode: TextureWrapMode) -> Self { - self.s_wrap_mode = s_wrap_mode; - self - } - - /// Sets new S coordinate wrap mode which will be applied to every imported texture as - /// default value. - pub fn set_s_wrap_mode(&mut self, s_wrap_mode: TextureWrapMode) { - self.s_wrap_mode = s_wrap_mode; - } - - /// Sets new T coordinate wrap mode which will be applied to every imported texture as - /// default value. - pub fn with_t_wrap_mode(mut self, t_wrap_mode: TextureWrapMode) -> Self { - self.t_wrap_mode = t_wrap_mode; - self - } - - /// Sets new T coordinate wrap mode which will be applied to every imported texture as - /// default value. - pub fn set_t_wrap_mode(&mut self, t_wrap_mode: TextureWrapMode) { - self.t_wrap_mode = t_wrap_mode; - } - - /// Sets new anisotropy level which will be applied to every imported texture as - /// default value. - pub fn with_anisotropy(mut self, anisotropy: f32) -> Self { - self.anisotropy = anisotropy.min(1.0); - self - } - - /// Sets new anisotropy level which will be applied to every imported texture as - /// default value. - pub fn set_anisotropy(&mut self, anisotropy: f32) { - self.anisotropy = anisotropy.min(1.0); - } - - /// Sets desired texture compression. - pub fn with_compression(mut self, compression: CompressionOptions) -> Self { - self.compression = compression; - self - } - - /// Sets desired texture compression. - pub fn set_compression(&mut self, compression: CompressionOptions) { - self.compression = compression; - } - - /// Same effect as [`Texture::set_base_level`]. - pub fn with_base_level(mut self, base_level: usize) -> Self { - self.base_level = base_level; - self - } - - /// Same effect as [`Texture::set_base_level`]. - pub fn set_base_level(&mut self, base_level: usize) { - self.base_level = base_level; - } - - /// Same effect as [`Texture::set_max_level`]. - pub fn with_max_level(mut self, max_level: usize) -> Self { - self.max_level = max_level; - self - } - - /// Same effect as [`Texture::set_max_level`]. - pub fn set_max_level(&mut self, max_level: usize) { - self.max_level = max_level; - } - - /// Same effect as [`Texture::set_min_lod`]. - pub fn with_min_lod(mut self, min_lod: f32) -> Self { - self.min_lod = min_lod; - self - } - - /// Same effect as [`Texture::set_min_lod`]. - pub fn set_min_lod(&mut self, min_lod: f32) { - self.min_lod = min_lod; - } - - /// Same effect as [`Texture::set_max_lod`]. - pub fn with_max_lod(mut self, max_lod: f32) -> Self { - self.max_lod = max_lod; - self - } - - /// Same effect as [`Texture::set_max_lod`]. - pub fn set_max_lod(&mut self, max_lod: f32) { - self.max_lod = max_lod; - } - - /// Same effect as [`Texture::set_lod_bias`]. - pub fn with_lod_bias(mut self, lod_bias: f32) -> Self { - self.lod_bias = lod_bias; - self - } - - /// Same effect as [`Texture::set_lod_bias`]. - pub fn set_lod_bias(&mut self, lod_bias: f32) { - self.lod_bias = lod_bias; - } -} - -lazy_static! { - /// Placeholder texture. - pub static ref PLACEHOLDER: BuiltInResource = BuiltInResource::new(embedded_data_source!("default.png"), - |data| { - TextureResource::load_from_memory( - ResourceKind::External("__PlaceholderTexture".into()), - data, - Default::default() - ) - .unwrap() - }); -} - -/// Type alias for texture resources. -pub type TextureResource = Resource; - -/// Extension trait for texture resources. -pub trait TextureResourceExtension: Sized { - /// Creates new render target for a scene. This method automatically configures GPU texture - /// to correct settings, after render target was created, it must not be modified, otherwise - /// result is undefined. - fn new_render_target(width: u32, height: u32) -> Self; - - /// Tries to load a texture from given data. Use this method if you want to - /// load a texture from embedded data. - /// - /// # On-demand compression - /// - /// The data can be compressed if needed to improve performance on GPU side. - /// - /// # Important notes - /// - /// Textures loaded with this method won't be correctly serialized! It means - /// that if you'll made a scene with textures loaded with this method, and then - /// save a scene, then the engine won't be able to restore the textures if you'll - /// try to load the saved scene. This is essential limitation of this method, - /// because the engine does not know where to get the data of the texture at - /// loading. You should use `ResourceManager::request_texture` in majority of cases! - /// - /// Main use cases for this method are: procedural textures, icons for GUI. - fn load_from_memory( - kind: ResourceKind, - data: &[u8], - import_options: TextureImportOptions, - ) -> Result; - - /// Tries to create new texture from given parameters, it may fail only if size of data passed - /// in does not match with required. - fn from_bytes( - kind: TextureKind, - pixel_kind: TexturePixelKind, - bytes: Vec, - resource_kind: ResourceKind, - ) -> Option; - - /// Creates a deep clone of the texture. Unlike [`TextureResource::clone`], this method clones the actual texture data, - /// which could be slow. - fn deep_clone(&self) -> Self; -} - -impl TextureResourceExtension for TextureResource { - fn new_render_target(width: u32, height: u32) -> Self { - Resource::new_ok( - Default::default(), - Texture { - // Render target will automatically set width and height before rendering. - kind: TextureKind::Rectangle { width, height }, - bytes: Default::default(), - pixel_kind: TexturePixelKind::RGBA8, - minification_filter: TextureMinificationFilter::Linear, - magnification_filter: TextureMagnificationFilter::Linear, - s_wrap_mode: TextureWrapMode::Repeat, - t_wrap_mode: TextureWrapMode::Repeat, - r_wrap_mode: TextureWrapMode::Repeat, - base_level: 0, - max_level: 1000, - min_lod: -1000.0, - max_lod: 1000.0, - lod_bias: 0.0, - mip_count: 1, - anisotropy: 1.0, - modifications_counter: 0, - is_render_target: true, - cache_index: Default::default(), - }, - ) - } - - fn load_from_memory( - kind: ResourceKind, - data: &[u8], - import_options: TextureImportOptions, - ) -> Result { - Ok(Resource::new_ok( - kind, - Texture::load_from_memory(data, import_options)?, - )) - } - - fn from_bytes( - kind: TextureKind, - pixel_kind: TexturePixelKind, - bytes: Vec, - resource_kind: ResourceKind, - ) -> Option { - Some(Resource::new_ok( - resource_kind, - Texture::from_bytes(kind, pixel_kind, bytes)?, - )) - } - - fn deep_clone(&self) -> Self { - let kind = self.header().kind.clone(); - let data = self.data_ref().clone(); - Resource::new_ok(kind, data) - } -} - -/// The texture magnification function is used when the pixel being textured maps to an area -/// less than or equal to one texture element. -#[derive( - Copy, - Clone, - Debug, - Hash, - PartialOrd, - PartialEq, - Deserialize, - Serialize, - Reflect, - VariantNames, - EnumString, - AsRefStr, - Visit, - Eq, -)] -#[repr(u32)] -pub enum TextureMagnificationFilter { - /// Returns the value of the texture element that is nearest to the center of the pixel - /// being textured. - Nearest = 0, - - /// Returns the weighted average of the four texture elements that are closest to the - /// center of the pixel being textured. - Linear = 1, -} - -uuid_provider!(TextureMagnificationFilter = "824f5b6c-8957-42db-9ebc-ef2a5dece5ab"); - -impl Default for TextureMagnificationFilter { - fn default() -> Self { - Self::Linear - } -} - -/// The texture minifying function is used whenever the pixel being textured maps to an area -/// greater than one texture element. -#[derive( - Copy, - Clone, - Debug, - Hash, - PartialOrd, - PartialEq, - Deserialize, - Serialize, - Reflect, - VariantNames, - EnumString, - AsRefStr, - Visit, - Eq, -)] -#[repr(u32)] -pub enum TextureMinificationFilter { - /// Returns the value of the texture element that is nearest to the center of the pixel - /// being textured. - Nearest = 0, - - /// Chooses the mipmap that most closely matches the size of the pixel being textured and - /// uses the Nearest criterion (the texture element nearest to the center of the pixel) - /// to produce a texture value. - NearestMipMapNearest = 1, - - /// Chooses the two mipmaps that most closely match the size of the pixel being textured - /// and uses the Nearest criterion (the texture element nearest to the center of the pixel) - /// to produce a texture value from each mipmap. The final texture value is a weighted average - /// of those two values. - NearestMipMapLinear = 2, - - /// Returns the weighted average of the four texture elements that are closest to the - /// center of the pixel being textured. - Linear = 3, - - /// Chooses the mipmap that most closely matches the size of the pixel being textured and - /// uses the Linear criterion (a weighted average of the four texture elements that are - /// closest to the center of the pixel) to produce a texture value. - LinearMipMapNearest = 4, - - /// Chooses the two mipmaps that most closely match the size of the pixel being textured - /// and uses the Linear criterion (a weighted average of the four texture elements that - /// are closest to the center of the pixel) to produce a texture value from each mipmap. - /// The final texture value is a weighted average of those two values. - LinearMipMapLinear = 5, -} - -uuid_provider!(TextureMinificationFilter = "0ec9e072-6d0a-47b2-a9c2-498cac4de22b"); - -impl TextureMinificationFilter { - /// Returns true if minification filter is using mip mapping, false - otherwise. - pub fn is_using_mip_mapping(self) -> bool { - match self { - TextureMinificationFilter::Nearest | TextureMinificationFilter::Linear => false, - TextureMinificationFilter::NearestMipMapNearest - | TextureMinificationFilter::LinearMipMapLinear - | TextureMinificationFilter::NearestMipMapLinear - | TextureMinificationFilter::LinearMipMapNearest => true, - } - } -} - -impl Default for TextureMinificationFilter { - fn default() -> Self { - Self::LinearMipMapLinear - } -} - -/// Defines a law of texture coordinate modification. -#[derive( - Copy, - Clone, - Debug, - Hash, - PartialOrd, - PartialEq, - Deserialize, - Serialize, - Reflect, - VariantNames, - EnumString, - AsRefStr, - Visit, - Eq, -)] -#[repr(u32)] -pub enum TextureWrapMode { - /// Causes the integer part of a coordinate to be ignored; GPU uses only the fractional part, - /// thereby creating a repeating pattern. - Repeat = 0, - - /// Causes a coordinates to be clamped to the range range, where N is the size of the texture - /// in the direction of clamping - ClampToEdge = 1, - - /// Evaluates a coordinates in a similar manner to ClampToEdge. However, in cases where clamping - /// would have occurred in ClampToEdge mode, the fetched texel data is substituted with the values - /// specified by border color. - ClampToBorder = 2, - - /// Causes the a coordinate to be set to the fractional part of the texture coordinate if the integer - /// part of coordinate is even; if the integer part of coordinate is odd, then the coordinate texture - /// coordinate is set to 1-frac, where frac represents the fractional part of coordinate. - MirroredRepeat = 3, - - /// Causes a coordinate to be repeated as for MirroredRepeat for one repetition of the texture, at - /// which point the coordinate to be clamped as in ClampToEdge. - MirrorClampToEdge = 4, -} - -uuid_provider!(TextureWrapMode = "e360d139-4374-4323-a66d-d192809d9d87"); - -impl Default for TextureWrapMode { - fn default() -> Self { - Self::Repeat - } -} - -/// Texture kind defines pixel format of texture. -#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect)] -#[repr(u32)] -pub enum TexturePixelKind { - /// 1 byte red. - R8 = 0, - - /// Red, green, and blue components, each by 1 byte. - RGB8 = 1, - - /// Red, green, blue, and alpha components, each by 1 byte. - RGBA8 = 2, - - /// Red and green, each by 1 byte. - RG8 = 3, - - /// 2 byte red. - R16 = 4, - - /// Red and green, each by 2 byte. - RG16 = 5, - - /// Blue, green, and red components, each by 1 byte. - BGR8 = 6, - - /// Blue, green, red and alpha components, each by 1 byte. - BGRA8 = 7, - - /// Red, green, and blue components, each by 2 byte. - RGB16 = 8, - - /// Red, green, blue, and alpha components, each by 2 byte. - RGBA16 = 9, - - /// Compressed S3TC DXT1 RGB (no alpha). - DXT1RGB = 10, - - /// Compressed S3TC DXT1 RGBA. - DXT1RGBA = 11, - - /// Compressed S3TC DXT3 RGBA. - DXT3RGBA = 12, - - /// Compressed S3TC DXT5 RGBA. - DXT5RGBA = 13, - - /// Compressed R8 texture (RGTC). - R8RGTC = 14, - - /// Compressed RG8 texture (RGTC). - RG8RGTC = 15, - - /// Floating-point RGB texture with 32bit depth. - RGB32F = 16, - - /// Floating-point RGBA texture with 32bit depth. - RGBA32F = 17, - - /// 1 byte luminance texture where pixels will have (L, L, L, 1.0) value on fetching. - /// - /// # Platform-specific - /// - /// - WebAssembly - not supported, the image will act like [`Self::R8`] format, which - /// will have (R, 0.0, 0.0, 1.0) pixels. - Luminance8 = 18, - - /// 1 byte for luminance and 1 for alpha, where all pixels will have (L, L, L, A) value on fetching. - /// - /// # Platform-specific - /// - /// - WebAssembly - not supported, the image will act like [`Self::RG8`] format, which - /// will have (R, G, R, G) pixels. - LuminanceAlpha8 = 19, - - /// 2 byte luminance texture where pixels will have (L, L, L, 1.0) value on fetching. - /// - /// # Platform-specific - /// - /// - WebAssembly - not supported, the image will act like [`Self::R8`] format, which - /// will have (R, 0.0, 0.0, 1.0) pixels. - Luminance16 = 20, - - /// 2 byte for luminance and 2 for alpha, where all pixels will have (L, L, L, A) value on fetching. - /// - /// # Platform-specific - /// - /// - WebAssembly - not supported, the image will act like [`Self::RG16`] format, which - /// will have (R, G, R, G) pixels. - LuminanceAlpha16 = 21, - - /// Red, green, blue components, each by 2 byte half-precision float. - RGB16F = 22, - - /// Red component as 4-byte, medium-precision float. - R32F = 23, - - /// Red component as 2-byte, half-precision float. - R16F = 24, -} - -impl TexturePixelKind { - fn new(id: u32) -> Result { - match id { - 0 => Ok(Self::R8), - 1 => Ok(Self::RGB8), - 2 => Ok(Self::RGBA8), - 3 => Ok(Self::RG8), - 4 => Ok(Self::R16), - 5 => Ok(Self::RG16), - 6 => Ok(Self::BGR8), - 7 => Ok(Self::BGRA8), - 8 => Ok(Self::RGB16), - 9 => Ok(Self::RGBA16), - 10 => Ok(Self::DXT1RGB), - 11 => Ok(Self::DXT1RGBA), - 12 => Ok(Self::DXT3RGBA), - 13 => Ok(Self::DXT5RGBA), - 14 => Ok(Self::R8RGTC), - 15 => Ok(Self::RG8RGTC), - 16 => Ok(Self::RGB32F), - 17 => Ok(Self::RGBA32F), - 18 => Ok(Self::Luminance8), - 19 => Ok(Self::LuminanceAlpha8), - 20 => Ok(Self::Luminance16), - 21 => Ok(Self::LuminanceAlpha16), - 22 => Ok(Self::RGB16F), - 23 => Ok(Self::R32F), - 24 => Ok(Self::R16F), - _ => Err(format!("Invalid texture kind {id}!")), - } - } - - fn id(self) -> u32 { - self as u32 - } - - /// Tries to get size of the pixel in bytes. Pixels of compressed textures consumes less than a byte, so - /// there's no way to express their size on whole number of bytes, in this case `None` is returned. - pub fn size_in_bytes(&self) -> Option { - match self { - Self::R8 | Self::Luminance8 => Some(1), - Self::RGB8 | Self::BGR8 => Some(3), - Self::RGBA8 | Self::RG16 | Self::BGRA8 | Self::LuminanceAlpha16 | Self::R32F => Some(4), - Self::RG8 | Self::R16 | Self::LuminanceAlpha8 | Self::Luminance16 | Self::R16F => { - Some(2) - } - Self::RGB16 | Self::RGB16F => Some(6), - Self::RGBA16 => Some(8), - Self::RGB32F => Some(12), - Self::RGBA32F => Some(16), - // Pixels of compressed textures consumes less than a byte, so there's no way to express - // their size on whole number of bytes. - Self::DXT1RGB - | Self::DXT1RGBA - | Self::DXT3RGBA - | Self::DXT5RGBA - | Self::R8RGTC - | Self::RG8RGTC => None, - } - } -} - -/// An error that may occur during texture operations. -#[derive(Debug)] -pub enum TextureError { - /// Format (pixel format, dimensions) is not supported. - UnsupportedFormat, - /// An io error. - Io(std::io::Error), - /// Internal image crate error. - Image(image::ImageError), - /// An error occurred during file loading. - FileLoadError(FileLoadError), -} - -impl Display for TextureError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - TextureError::UnsupportedFormat => { - write!(f, "Unsupported format!") - } - TextureError::Io(v) => { - write!(f, "An i/o error has occurred: {v}") - } - TextureError::Image(v) => { - write!(f, "Image loading error {v}") - } - TextureError::FileLoadError(v) => { - write!(f, "A file load error has occurred {v:?}") - } - } - } -} - -impl std::error::Error for TextureError {} - -impl From for TextureError { - fn from(v: FileLoadError) -> Self { - Self::FileLoadError(v) - } -} - -impl From for TextureError { - fn from(v: ImageError) -> Self { - Self::Image(v) - } -} - -impl From for TextureError { - fn from(v: Error) -> Self { - Self::Io(v) - } -} - -fn ceil_div_4(x: u32) -> u32 { - (x + 3) / 4 -} - -/// Texture compression options. -/// -/// # Notes -/// -/// Try to avoid using compression for normal maps, normals maps usually has smooth -/// gradients, but compression algorithms used by Fyrox cannot preserve good quality -/// of such gradients. -#[derive( - Copy, - Clone, - Deserialize, - Serialize, - PartialEq, - Eq, - Debug, - Reflect, - VariantNames, - EnumString, - AsRefStr, -)] -#[repr(u32)] -pub enum CompressionOptions { - /// An image will be stored without compression if it is not already compressed. - NoCompression = 0, - - /// An image will be encoded via DXT1 (BC1) compression with low quality if is not - /// already compressed. - /// Compression ratio is 1:8 (without alpha) or 1:6 (with 1-bit alpha). - /// This option provides maximum speed by having lowest requirements of memory - /// bandwidth. - Speed = 1, - - /// An image will be encoded via DXT5 (BC5) compression with high quality if it is - /// not already compressed. - /// Compression ratio is 1:4 (including alpha) - /// This option is faster than `NoCompression` speed by lower requirements of memory - /// bandwidth. - Quality = 2, -} - -uuid_provider!(CompressionOptions = "fbdcc081-d0b8-4b62-9925-2de6c013fbf5"); - -impl Default for CompressionOptions { - fn default() -> Self { - Self::NoCompression - } -} - -fn transmute_slice(bytes: &[u8]) -> &'_ [T] { - // SAFETY: This is absolutely safe because `image` crate's Rgb8/Rgba8/etc. and `tbc`s Rgb8/Rgba8/etc. - // have exactly the same memory layout. - unsafe { - std::slice::from_raw_parts( - bytes.as_ptr() as *const T, - bytes.len() / std::mem::size_of::(), - ) - } -} - -fn transmute_slice_mut(bytes: &mut [u8]) -> &'_ mut [T] { - // SAFETY: This is absolutely safe because `image` crate's Rgb8/Rgba8/etc. and `tbc`s Rgb8/Rgba8/etc. - // have exactly the same memory layout. - unsafe { - std::slice::from_raw_parts_mut( - bytes.as_ptr() as *mut T, - bytes.len() / std::mem::size_of::(), - ) - } -} - -fn compress_bc1(bytes: &[u8], width: usize, height: usize) -> Vec { - tbc::encode_image_bc1_conv_u8::(transmute_slice::(bytes), width, height) -} - -fn compress_bc3(bytes: &[u8], width: usize, height: usize) -> Vec { - tbc::encode_image_bc3_conv_u8::(transmute_slice::(bytes), width, height) -} - -fn compress_r8_bc4(bytes: &[u8], width: usize, height: usize) -> Vec { - tbc::encode_image_bc4_r8_conv_u8::(transmute_slice::(bytes), width, height) -} - -fn compress_rg8_bc4( - bytes: &[u8], - width: usize, - height: usize, -) -> Vec { - tbc::encode_image_bc4_rg8_conv_u8::(transmute_slice::(bytes), width, height) -} - -fn data_hash(data: &[u8]) -> u64 { - let mut hasher = FxHasher::default(); - data.hash(&mut hasher); - hasher.finish() -} - -fn try_compress( - pixel_kind: TexturePixelKind, - bytes: &[u8], - w: usize, - h: usize, - compression: CompressionOptions, -) -> Option<(Vec, TexturePixelKind)> { - match (pixel_kind, compression) { - (TexturePixelKind::RGB8, CompressionOptions::Speed) => Some(( - compress_bc1::(bytes, w, h), - TexturePixelKind::DXT1RGB, - )), - (TexturePixelKind::RGB8, CompressionOptions::Quality) => Some(( - compress_bc3::(bytes, w, h), - TexturePixelKind::DXT5RGBA, - )), - (TexturePixelKind::RGBA8, CompressionOptions::Speed) => Some(( - compress_bc1::(bytes, w, h), - TexturePixelKind::DXT1RGBA, - )), - (TexturePixelKind::RGBA8, CompressionOptions::Quality) => Some(( - compress_bc3::(bytes, w, h), - TexturePixelKind::DXT5RGBA, - )), - (TexturePixelKind::R8, CompressionOptions::Speed) - | (TexturePixelKind::R8, CompressionOptions::Quality) - | (TexturePixelKind::Luminance8, CompressionOptions::Speed) - | (TexturePixelKind::Luminance8, CompressionOptions::Quality) => Some(( - compress_r8_bc4::(bytes, w, h), - TexturePixelKind::R8RGTC, - )), - (TexturePixelKind::RG8, CompressionOptions::Speed) - | (TexturePixelKind::RG8, CompressionOptions::Quality) - | (TexturePixelKind::LuminanceAlpha8, CompressionOptions::Speed) - | (TexturePixelKind::LuminanceAlpha8, CompressionOptions::Quality) => Some(( - compress_rg8_bc4::(bytes, w, h), - TexturePixelKind::RG8RGTC, - )), - _ => None, - } -} - -fn bytes_in_mip_level(kind: TextureKind, pixel_kind: TexturePixelKind, mip: usize) -> u32 { - let pixel_count = match kind { - TextureKind::Line { length } => length.shr(mip), - TextureKind::Rectangle { width, height } => width.shr(mip) * height.shr(mip), - TextureKind::Cube { width, height } => 6 * width.shr(mip) * height.shr(mip), - TextureKind::Volume { - width, - height, - depth, - } => width.shr(mip) * height.shr(mip) * depth.shr(mip), - }; - match pixel_kind { - // Uncompressed formats. - TexturePixelKind::R8 | TexturePixelKind::Luminance8 => pixel_count, - TexturePixelKind::R16 - | TexturePixelKind::LuminanceAlpha8 - | TexturePixelKind::Luminance16 - | TexturePixelKind::RG8 - | TexturePixelKind::R16F => 2 * pixel_count, - TexturePixelKind::RGB8 | TexturePixelKind::BGR8 => 3 * pixel_count, - TexturePixelKind::RGBA8 - | TexturePixelKind::BGRA8 - | TexturePixelKind::RG16 - | TexturePixelKind::LuminanceAlpha16 - | TexturePixelKind::R32F => 4 * pixel_count, - TexturePixelKind::RGB16 | TexturePixelKind::RGB16F => 6 * pixel_count, - TexturePixelKind::RGBA16 => 8 * pixel_count, - TexturePixelKind::RGB32F => 12 * pixel_count, - TexturePixelKind::RGBA32F => 16 * pixel_count, - - // Compressed formats. - TexturePixelKind::DXT1RGB - | TexturePixelKind::DXT1RGBA - | TexturePixelKind::DXT3RGBA - | TexturePixelKind::DXT5RGBA - | TexturePixelKind::R8RGTC - | TexturePixelKind::RG8RGTC => { - let block_size = match pixel_kind { - TexturePixelKind::DXT1RGB - | TexturePixelKind::DXT1RGBA - | TexturePixelKind::R8RGTC => 8, - TexturePixelKind::DXT3RGBA - | TexturePixelKind::DXT5RGBA - | TexturePixelKind::RG8RGTC => 16, - _ => unreachable!(), - }; - match kind { - TextureKind::Line { length } => ceil_div_4(length) * block_size, - TextureKind::Rectangle { width, height } => { - ceil_div_4(width) * ceil_div_4(height) * block_size - } - TextureKind::Cube { width, height } => { - 6 * ceil_div_4(width) * ceil_div_4(height) * block_size - } - TextureKind::Volume { - width, - height, - depth, - } => ceil_div_4(width) * ceil_div_4(height) * ceil_div_4(depth) * block_size, - } - } - } -} - -fn mip_byte_offset(kind: TextureKind, pixel_kind: TexturePixelKind, mut mip: usize) -> usize { - // TODO: This could be done without loop. - let mut offset = 0; - loop { - offset += bytes_in_mip_level(kind, pixel_kind, mip) as usize; - mip = mip.saturating_sub(1); - if mip == 0 { - break; - } - } - offset -} - -fn convert_pixel_type_enum(pixel_kind: TexturePixelKind) -> fr::PixelType { - match pixel_kind { - TexturePixelKind::R8 | TexturePixelKind::Luminance8 => fr::PixelType::U8, - TexturePixelKind::RGB8 | TexturePixelKind::BGR8 => fr::PixelType::U8x3, - TexturePixelKind::RGBA8 | TexturePixelKind::BGRA8 => fr::PixelType::U8x4, - TexturePixelKind::RG8 | TexturePixelKind::LuminanceAlpha8 => fr::PixelType::U8x2, - TexturePixelKind::R16 | TexturePixelKind::Luminance16 => fr::PixelType::U16, - TexturePixelKind::RG16 | TexturePixelKind::LuminanceAlpha16 => fr::PixelType::U16x2, - TexturePixelKind::RGB16 => fr::PixelType::U16x3, - TexturePixelKind::RGBA16 => fr::PixelType::U16x4, - TexturePixelKind::R32F => fr::PixelType::F32, - _ => unreachable!(), - } -} - -fn flip_green_channel<'a, P>(pixels: impl Iterator) -where - P: Pixel + 'a, -{ - for pixel in pixels { - let green = &mut pixel.channels_mut()[1]; - let inverted = P::Subpixel::max_value() - *green; - *green = inverted; - } -} - -impl Texture { - /// Tries to load a texture from given data in one of the following formats: PNG, BMP, TGA, JPG, DDS, GIF. Use - /// this method if you want to load a texture from embedded data. - /// - /// # On-demand compression and mip-map generation - /// - /// The data can be compressed if needed to improve performance on GPU side. Mip-maps can be generated as well. - /// **CAVEAT:** Compression and mip-map generation **won't** be taken into account in case of **DDS** textures, - /// because DDS can already contain such data, you should generate mips and compress DDS textures manually using - /// some offline tool like DirectXTexTool or similar. - /// - /// # Important notes - /// - /// Textures loaded with this method won't be correctly serialized! It means that if you'll made a scene with - /// textures loaded with this method, and then save a scene, then the engine won't be able to restore the textures - /// if you'll try to load the saved scene. This is essential limitation of this method, because the engine does - /// not know where to get the data of the texture at loading. You should use `ResourceManager::request_texture` - /// in majority of cases! - /// - /// # Use cases - /// - /// Main use cases for this method are: procedural textures, icons for GUI. - pub fn load_from_memory( - data: &[u8], - import_options: TextureImportOptions, - ) -> Result { - // DDS is special. It can contain various kinds of textures as well as textures with - // various pixel formats. - // - // TODO: Add support for DXGI formats. - if let Ok(dds) = ddsfile::Dds::read(&mut Cursor::new(data)) { - let d3dformat = dds - .get_d3d_format() - .ok_or(TextureError::UnsupportedFormat)?; - let mip_count = dds.get_num_mipmap_levels(); - let mut bytes = dds.data; - - // Try to use as much formats as possible. - let pixel_kind = match d3dformat { - D3DFormat::DXT1 => TexturePixelKind::DXT1RGBA, - D3DFormat::DXT3 => TexturePixelKind::DXT3RGBA, - D3DFormat::DXT5 => TexturePixelKind::DXT5RGBA, - D3DFormat::L8 | D3DFormat::A8 => TexturePixelKind::R8, - D3DFormat::L16 => TexturePixelKind::R16, - D3DFormat::R8G8B8 => TexturePixelKind::RGB8, - D3DFormat::A8L8 => TexturePixelKind::RG8, - D3DFormat::A8R8G8B8 => { - // // ARGB8 -> RGBA8 - // assert_eq!(bytes.len() % 4, 0); - // for chunk in bytes.chunks_exact_mut(4) { - // let a = chunk[0]; - // let r = chunk[1]; - // let g = chunk[2]; - // let b = chunk[3]; - // chunk[0] = r; - // chunk[1] = g; - // chunk[2] = b; - // chunk[3] = a; - // } - TexturePixelKind::RGBA8 - } - D3DFormat::G16R16 => { - // GR16 -> RG16 - assert_eq!(bytes.len() % 4, 0); - for chunk in bytes.chunks_exact_mut(4) { - // Red Hi + Lo bytes - let gh = chunk[0]; - let gl = chunk[1]; - // Green Hi + Lo bytes - let rh = chunk[2]; - let rl = chunk[3]; - // Swap - chunk[0] = rh; - chunk[1] = rl; - chunk[2] = gh; - chunk[3] = gl; - } - TexturePixelKind::RG16 - } - _ => return Err(TextureError::UnsupportedFormat), - }; - - Ok(Self { - pixel_kind, - modifications_counter: 0, - minification_filter: import_options.minification_filter, - magnification_filter: import_options.magnification_filter, - s_wrap_mode: import_options.s_wrap_mode, - t_wrap_mode: import_options.t_wrap_mode, - r_wrap_mode: import_options.r_wrap_mode, - base_level: import_options.base_level, - max_level: import_options.max_level, - min_lod: import_options.min_lod, - max_lod: import_options.max_lod, - anisotropy: import_options.anisotropy, - mip_count, - bytes: bytes.into(), - kind: if dds.header.caps2 & Caps2::CUBEMAP == Caps2::CUBEMAP { - TextureKind::Cube { - width: dds.header.width, - height: dds.header.height, - } - } else if dds.header.caps2 & Caps2::VOLUME == Caps2::VOLUME { - TextureKind::Volume { - width: dds.header.width, - height: dds.header.height, - depth: dds.header.depth.unwrap(), - } - } else { - TextureKind::Rectangle { - width: dds.header.width, - height: dds.header.height, - } - }, - is_render_target: false, - cache_index: Default::default(), - lod_bias: import_options.lod_bias, - }) - } else { - // Commonly used formats are all rectangle textures. - let mut dyn_img = image::load_from_memory(data) - // Try to load as TGA, this is needed because TGA is badly designed format and does not - // have an identifier in the beginning of the file (so called "magic") that allows quickly - // check if the file is really contains expected data. - .or_else(|_| image::load_from_memory_with_format(data, ImageFormat::Tga))?; - - let width = dyn_img.width(); - let height = dyn_img.height(); - - if import_options.flip_green_channel { - match dyn_img { - DynamicImage::ImageRgb8(ref mut img) => flip_green_channel(img.pixels_mut()), - DynamicImage::ImageRgba8(ref mut img) => flip_green_channel(img.pixels_mut()), - DynamicImage::ImageRgb16(ref mut img) => flip_green_channel(img.pixels_mut()), - DynamicImage::ImageRgba16(ref mut img) => flip_green_channel(img.pixels_mut()), - DynamicImage::ImageRgb32F(ref mut img) => flip_green_channel(img.pixels_mut()), - DynamicImage::ImageRgba32F(ref mut img) => flip_green_channel(img.pixels_mut()), - _ => (), - } - } - - let src_pixel_kind = match dyn_img { - DynamicImage::ImageLuma8(_) => TexturePixelKind::Luminance8, - DynamicImage::ImageLumaA8(_) => TexturePixelKind::LuminanceAlpha8, - DynamicImage::ImageRgb8(_) => TexturePixelKind::RGB8, - DynamicImage::ImageRgba8(_) => TexturePixelKind::RGBA8, - DynamicImage::ImageLuma16(_) => TexturePixelKind::Luminance16, - DynamicImage::ImageLumaA16(_) => TexturePixelKind::LuminanceAlpha16, - DynamicImage::ImageRgb16(_) => TexturePixelKind::RGB16, - DynamicImage::ImageRgba16(_) => TexturePixelKind::RGBA16, - DynamicImage::ImageRgb32F(_) => TexturePixelKind::RGB32F, - DynamicImage::ImageRgba32F(_) => TexturePixelKind::RGBA32F, - _ => return Err(TextureError::UnsupportedFormat), - }; - let mut final_pixel_kind = src_pixel_kind; - - let mut mip_count = 0; - let mut bytes = Vec::with_capacity( - width as usize * height as usize * src_pixel_kind.size_in_bytes().unwrap_or(4), - ); - - if import_options.minification_filter.is_using_mip_mapping() { - let src_pixel_type = convert_pixel_type_enum(src_pixel_kind); - let mut level_width = width; - let mut level_height = height; - let mut current_level = fr::images::Image::from_vec_u8( - level_width, - level_height, - dyn_img.as_bytes().to_vec(), - src_pixel_type, - ) - .map_err(|_| TextureError::UnsupportedFormat)?; - - while level_width != 0 && level_height != 0 { - if mip_count != 0 { - let mut dst_img = - fr::images::Image::new(level_width, level_height, src_pixel_type); - - let mut resizer = fr::Resizer::new(); - - resizer - .resize( - ¤t_level, - &mut dst_img, - Some(&ResizeOptions { - algorithm: fr::ResizeAlg::Convolution( - import_options.mip_filter.into_filter_type(), - ), - cropping: Default::default(), - mul_div_alpha: true, - }), - ) - .expect("Pixel types must match!"); - - current_level = dst_img; - } - - mip_count += 1; - - if import_options.compression == CompressionOptions::NoCompression { - bytes.extend_from_slice(current_level.buffer()) - } else if let Some((compressed_data, new_pixel_kind)) = try_compress( - src_pixel_kind, - current_level.buffer(), - level_width as usize, - level_height as usize, - import_options.compression, - ) { - final_pixel_kind = new_pixel_kind; - bytes.extend_from_slice(&compressed_data); - } else { - bytes.extend_from_slice(current_level.buffer()) - } - - level_width = level_width.checked_shr(1).unwrap_or_default(); - level_height = level_height.checked_shr(1).unwrap_or_default(); - } - } else { - mip_count = 1; - - if import_options.compression == CompressionOptions::NoCompression { - bytes.extend_from_slice(dyn_img.as_bytes()); - } else if let Some((compressed_data, new_pixel_kind)) = try_compress( - src_pixel_kind, - dyn_img.as_bytes(), - width as usize, - height as usize, - import_options.compression, - ) { - final_pixel_kind = new_pixel_kind; - bytes.extend_from_slice(&compressed_data); - } else { - bytes.extend_from_slice(dyn_img.as_bytes()) - } - } - - Ok(Self { - pixel_kind: final_pixel_kind, - kind: TextureKind::Rectangle { width, height }, - modifications_counter: 0, - bytes: bytes.into(), - mip_count, - minification_filter: import_options.minification_filter, - magnification_filter: import_options.magnification_filter, - s_wrap_mode: import_options.s_wrap_mode, - t_wrap_mode: import_options.t_wrap_mode, - r_wrap_mode: import_options.r_wrap_mode, - base_level: import_options.base_level, - max_level: import_options.max_level, - min_lod: import_options.min_lod, - max_lod: import_options.max_lod, - anisotropy: import_options.anisotropy, - is_render_target: false, - cache_index: Default::default(), - lod_bias: import_options.lod_bias, - }) - } - } - - /// Tries to load a texture from a file. - /// - /// # Notes - /// - /// It is **not** public because you must use resource manager to load textures from external - /// resources. - pub(crate) async fn load_from_file>( - path: P, - io: &dyn ResourceIo, - import_options: TextureImportOptions, - ) -> Result { - let data = io.load_file(path.as_ref()).await?; - Self::load_from_memory(&data, import_options) - } - - /// Creates new texture instance from given parameters. - /// - /// # Limitations - /// - /// Currently textures with only one mip level are supported! - pub fn from_bytes( - kind: TextureKind, - pixel_kind: TexturePixelKind, - bytes: Vec, - ) -> Option { - if bytes_in_mip_level(kind, pixel_kind, 0) != bytes.len() as u32 { - None - } else { - Some(Self { - kind, - modifications_counter: 0, - bytes: bytes.into(), - pixel_kind, - ..Default::default() - }) - } - } - - /// Sets new minification filter. It is used when texture becomes smaller. - #[inline] - pub fn set_minification_filter(&mut self, filter: TextureMinificationFilter) { - self.minification_filter = filter; - } - - /// Returns current minification filter. - #[inline] - pub fn minification_filter(&self) -> TextureMinificationFilter { - self.minification_filter - } - - /// Sets new magnification filter. It is used when texture is "stretching". - #[inline] - pub fn set_magnification_filter(&mut self, filter: TextureMagnificationFilter) { - self.magnification_filter = filter; - } - - /// Returns current magnification filter. - #[inline] - pub fn magnification_filter(&self) -> TextureMagnificationFilter { - self.magnification_filter - } - - /// Sets new S coordinate wrap mode. - #[inline] - pub fn set_s_wrap_mode(&mut self, s_wrap_mode: TextureWrapMode) { - self.s_wrap_mode = s_wrap_mode; - } - - /// Returns current S coordinate wrap mode. - #[inline] - pub fn s_wrap_mode(&self) -> TextureWrapMode { - self.s_wrap_mode - } - - /// Sets new T coordinate wrap mode. - #[inline] - pub fn set_t_wrap_mode(&mut self, t_wrap_mode: TextureWrapMode) { - self.t_wrap_mode = t_wrap_mode; - } - - /// Returns current T coordinate wrap mode. - #[inline] - pub fn t_wrap_mode(&self) -> TextureWrapMode { - self.t_wrap_mode - } - - /// Sets new R coordinate wrap mode. - #[inline] - pub fn set_r_wrap_mode(&mut self, r_wrap_mode: TextureWrapMode) { - self.r_wrap_mode = r_wrap_mode; - } - - /// Returns current T coordinate wrap mode. - #[inline] - pub fn r_wrap_mode(&self) -> TextureWrapMode { - self.r_wrap_mode - } - - /// Returns total mip count. - #[inline] - pub fn mip_count(&self) -> u32 { - self.mip_count - } - - /// Returns texture kind. - #[inline] - pub fn kind(&self) -> TextureKind { - self.kind - } - - /// Returns total amount of modifications done on the texture. - #[inline] - pub fn modifications_count(&self) -> u64 { - self.modifications_counter - } - - /// Calculates current data hash. - #[inline] - pub fn calculate_data_hash(&self) -> u64 { - data_hash(&self.bytes.0) - } - - /// Returns current pixel kind. - #[inline] - pub fn pixel_kind(&self) -> TexturePixelKind { - self.pixel_kind - } - - /// Returns the index of the lowest defined mipmap level. - #[inline] - pub fn base_level(&self) -> usize { - self.base_level - } - - /// Specifies the index of the lowest defined mipmap level. Keep in mind, that the texture data - /// should provide the actual mip map level defined by the provided value, otherwise the - /// rendering will be incorrect (probably just black on majority of implementations) and glitchy. - #[inline] - pub fn set_base_level(&mut self, base_level: usize) { - self.base_level = base_level; - } - - /// Returns the index of the highest defined mipmap level. - #[inline] - pub fn max_level(&self) -> usize { - self.max_level - } - - /// Sets the index of the highest defined mipmap level. Keep in mind, that the texture data - /// should provide the actual mip map level defined by the provided value, otherwise the - /// rendering will be incorrect (probably just black on majority of implementations) and glitchy. - #[inline] - pub fn set_max_level(&mut self, max_level: usize) { - self.max_level = max_level; - } - - /// Returns the minimum level-of-detail parameter. See [`Self::set_min_lod`] for more info. - #[inline] - pub fn min_lod(&self) -> f32 { - self.min_lod - } - - /// Sets the minimum level-of-detail parameter. This floating-point value limits the selection - /// of highest resolution mipmap (lowest mipmap level). The initial value is -1000.0. - #[inline] - pub fn set_min_lod(&mut self, min_lod: f32) { - self.min_lod = min_lod; - } - - /// Returns the maximum level-of-detail parameter. See [`Self::set_max_lod`] for more info. - #[inline] - pub fn max_lod(&self) -> f32 { - self.max_lod - } - - /// Sets the maximum level-of-detail parameter. This floating-point value limits the selection - /// of the lowest resolution mipmap (highest mipmap level). The initial value is 1000. - #[inline] - pub fn set_max_lod(&mut self, max_lod: f32) { - self.max_lod = max_lod; - } - - /// Returns a fixed bias value that is to be added to the level-of-detail parameter for the - /// texture before texture sampling. See [`Self::set_lod_bias`] for more info. - #[inline] - pub fn lod_bias(&self) -> f32 { - self.lod_bias - } - - /// Specifies a fixed bias value that is to be added to the level-of-detail parameter for the - /// texture before texture sampling. The specified value is added to the shader-supplied bias - /// value (if any) and subsequently clamped into the implementation-defined range - /// `−bias_max..bias_max`, where `bias_max` is the value that can be fetched from the current - /// graphics server. The initial value is 0.0. - #[inline] - pub fn set_lod_bias(&mut self, lod_bias: f32) { - self.lod_bias = lod_bias; - } - - /// Returns current data as immutable slice. - pub fn data(&self) -> &[u8] { - &self.bytes - } - - /// Tries to cast the internal data buffer to the given type. Type casting will succeed only if the the - /// size of the type `T` is equal with the size of the pixel. - /// - /// ## Important notes - /// - /// While this function is safe, there's no guarantee that the actual type-casted data will match the layout - /// of your data structure. For example, you could have a pixel of type `RG16`, where each pixel consumes 2 - /// bytes (4 in total) and cast it to the structure `struct Rgba8 { r: u8, g: u8, b: u8, a: u8 }` which is - /// safe in terms of memory access (both are 4 bytes total), but not ok in terms of actual data. Be careful - /// when using this method. - /// - /// Keep in mind, that this method will return a reference to **all** data in the texture, including all mip - /// levels. If you need to get typed data from specific mip level use [`Self::mip_level_data_of_type`]. - pub fn data_of_type(&self) -> Option<&[T]> { - if let Some(pixel_size) = self.pixel_kind.size_in_bytes() { - if pixel_size == std::mem::size_of::() { - return Some(transmute_slice(&self.bytes)); - } - } - None - } - - /// Returns data of the given mip level. - #[inline] - pub fn mip_level_data(&self, mip: usize) -> &[u8] { - let mip_begin = mip - .checked_sub(1) - .map(|prev| mip_byte_offset(self.kind, self.pixel_kind, prev)) - .unwrap_or_default(); - let mip_end = mip_byte_offset(self.kind, self.pixel_kind, mip); - &self.bytes[mip_begin..mip_end] - } - - /// Tries to cast the specific mip level data of the internal data buffer to the given type. Type casting - /// will succeed only if the the size of the type `T` is equal with the size of the pixel. - /// - /// ## Important notes - /// - /// While this function is safe, there's no guarantee that the actual type-casted data will match the layout - /// of your data structure. For example, you could have a pixel of type `RG16`, where each pixel consumes 2 - /// bytes (4 in total) and cast it to the structure `struct Rgba8 { r: u8, g: u8, b: u8, a: u8 }` which is - /// safe in terms of memory access (both are 4 bytes total), but not ok in terms of actual data. Be careful - /// when using this method. - pub fn mip_level_data_of_type(&self, mip: usize) -> Option<&[T]> { - if let Some(pixel_size) = self.pixel_kind.size_in_bytes() { - if pixel_size == std::mem::size_of::() { - return Some(transmute_slice(self.mip_level_data(mip))); - } - } - None - } - - /// Returns true if the texture is used as render target. - #[inline] - pub fn is_render_target(&self) -> bool { - self.is_render_target - } - - /// Max samples for anisotropic filtering. Default value is 16.0 (max). - /// However real value passed to GPU will be clamped to maximum supported - /// by current GPU. To disable anisotropic filtering set this to 1.0. - /// Typical values are 2.0, 4.0, 8.0, 16.0. - #[inline] - pub fn set_anisotropy_level(&mut self, anisotropy: f32) { - self.anisotropy = anisotropy.max(1.0); - } - - /// Returns current anisotropy level. - #[inline] - pub fn anisotropy_level(&self) -> f32 { - self.anisotropy - } - - /// Returns a special reference holder that provides mutable access to content of the - /// texture and automatically calculates hash of the data in its destructor. - #[inline] - pub fn modify(&mut self) -> TextureDataRefMut<'_> { - TextureDataRefMut { texture: self } - } -} - -/// A special reference holder that provides mutable access to content of the -/// texture and automatically calculates hash of the data in its destructor. -pub struct TextureDataRefMut<'a> { - texture: &'a mut Texture, -} - -impl Drop for TextureDataRefMut<'_> { - fn drop(&mut self) { - self.texture.modifications_counter += 1; - } -} - -impl Deref for TextureDataRefMut<'_> { - type Target = Texture; - - fn deref(&self) -> &Self::Target { - self.texture - } -} - -impl DerefMut for TextureDataRefMut<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.texture - } -} - -impl TextureDataRefMut<'_> { - /// Returns mutable reference to the data of the texture. - pub fn data_mut(&mut self) -> &mut [u8] { - &mut self.texture.bytes - } - - /// Tries to cast the internal data buffer to the given type. Type casting will succeed only if the the - /// size of the type `T` is equal with the size of the pixel. **WARNING:** While this function is safe, there's - /// no guarantee that the actual type-casted data will match the layout of your data structure. For example, - /// you could have a pixel of type `RG16`, where each pixel consumes 2 bytes (4 in total) and cast it to the - /// structure `struct Rgba8 { r: u8, g: u8, b: u8, a: u8 }` which is safe in terms of memory access (both are 4 - /// bytes total), but not ok in terms of actual data. Be careful when using the method. - pub fn data_mut_of_type(&mut self) -> Option<&mut [T]> { - if let Some(pixel_size) = self.texture.pixel_kind.size_in_bytes() { - if pixel_size == std::mem::size_of::() { - return Some(transmute_slice_mut(&mut self.texture.bytes)); - } - } - None - } -} - -#[cfg(test)] -#[allow(missing_docs)] -pub mod test { - use crate::resource::texture::{ - TextureKind, TexturePixelKind, TextureResource, TextureResourceExtension, - }; - - pub fn create_test_texture() -> TextureResource { - TextureResource::from_bytes( - TextureKind::Rectangle { - width: 1, - height: 1, - }, - TexturePixelKind::RGBA8, - vec![1, 1, 1, 1], - Default::default(), - ) - .unwrap() - } -} +pub use fyrox_texture::*; diff --git a/fyrox-texture/Cargo.toml b/fyrox-texture/Cargo.toml new file mode 100644 index 000000000..360fe6c54 --- /dev/null +++ b/fyrox-texture/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fyrox-texture" +version = "0.36.0" +edition = "2021" + +[dependencies] +fyrox-core = { path = "../fyrox-core", version = "0.36.0" } +fyrox-resource = { path = "../fyrox-resource", version = "0.36.0" } +ddsfile = "0.5.0" +fast_image_resize = "4.0.0" +serde = { version = "1", features = ["derive"] } +lazy_static = "1.4.0" +fxhash = "0.2.1" +strum = "0.26.1" +strum_macros = "0.26.1" +tbc = "0.3.0" +image = { version = "0.25.1", default-features = false, features = ["gif", "jpeg", "png", "tga", "tiff", "bmp"] } \ No newline at end of file diff --git a/fyrox-impl/src/resource/texture/default.png b/fyrox-texture/src/default.png similarity index 100% rename from fyrox-impl/src/resource/texture/default.png rename to fyrox-texture/src/default.png diff --git a/fyrox-texture/src/lib.rs b/fyrox-texture/src/lib.rs new file mode 100644 index 000000000..3a8de7d72 --- /dev/null +++ b/fyrox-texture/src/lib.rs @@ -0,0 +1,2004 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +//! Texture is an image that used to fill faces to add details to them. +//! +//! In most cases textures are just 2D images, however there are some exclusions to that - +//! for example cube maps, that may be used for environment mapping. Fyrox supports 1D, 2D, +//! 3D and Cube textures. +//! +//! ## Supported formats +//! +//! To load images and decode them, Fyrox uses image and ddsfile crates. Here is the list of +//! supported formats: png, tga, bmp, dds, jpg, gif, tiff, dds. +//! +//! ## Compressed textures +//! +//! Fyrox supports most commonly used formats of compressed textures: DXT1, DXT3, DXT5. +//! +//! ## Render target +//! +//! Texture can be used as render target to render scene in it. To do this you should use +//! new_render_target method and pass its result to scene's render target property. Renderer +//! will automatically provide you info about metrics of texture, but it won't give you +//! access to pixels of render target. + +use ddsfile::{Caps2, D3DFormat}; +use fast_image_resize as fr; +use fast_image_resize::ResizeOptions; +use fxhash::FxHasher; +use fyrox_core::{ + algebra::{Vector2, Vector3}, + futures::io::Error, + io::FileLoadError, + num_traits::Bounded, + reflect::prelude::*, + sparse::AtomicIndex, + uuid::Uuid, + uuid_provider, + visitor::{PodVecView, Visit, VisitError, VisitResult, Visitor}, + TypeUuidProvider, +}; +use fyrox_resource::{ + embedded_data_source, io::ResourceIo, manager::BuiltInResource, options::ImportOptions, + untyped::ResourceKind, Resource, ResourceData, TEXTURE_RESOURCE_UUID, +}; +use image::{ColorType, DynamicImage, ImageError, ImageFormat, Pixel}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::{ + any::Any, + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + io::Cursor, + ops::{Deref, DerefMut, Shr}, + path::Path, + sync::Arc, +}; +use strum_macros::{AsRefStr, EnumString, VariantNames}; + +pub mod loader; + +/// Texture kind. +#[derive(Copy, Clone, Debug, Reflect)] +pub enum TextureKind { + /// 1D texture. + Line { + /// Length of the texture. + length: u32, + }, + /// 2D texture. + Rectangle { + /// Width of the texture. + width: u32, + /// Height of the texture. + height: u32, + }, + /// Cube texture. + Cube { + /// Width of the cube face. + width: u32, + /// Height of the cube face. + height: u32, + }, + /// Volume texture (3D). + Volume { + /// Width of the volume. + width: u32, + /// Height of the volume. + height: u32, + /// Depth of the volume. + depth: u32, + }, +} + +impl TextureKind { + /// Tries to fetch [`TextureKind::Line`]'s length. + #[inline] + pub fn line_length(&self) -> Option { + if let Self::Line { length } = self { + Some(*length) + } else { + None + } + } + + /// Tries to fetch [`TextureKind::Rectangle`]'s width (x) and height (y). + #[inline] + pub fn rectangle_size(&self) -> Option> { + if let Self::Rectangle { width, height } = self { + Some(Vector2::new(*width, *height)) + } else { + None + } + } + + /// Tries to fetch [`TextureKind::Cube`]'s width (x) and height (y). + #[inline] + pub fn cube_size(&self) -> Option> { + if let Self::Cube { width, height } = self { + Some(Vector2::new(*width, *height)) + } else { + None + } + } + + /// Tries to fetch [`TextureKind::Volume`]'s width (x), height (y), depth (z). + #[inline] + pub fn volume_size(&self) -> Option> { + if let Self::Volume { + width, + height, + depth, + } = self + { + Some(Vector3::new(*width, *height, *depth)) + } else { + None + } + } +} + +impl Default for TextureKind { + fn default() -> Self { + Self::Rectangle { + width: 0, + height: 0, + } + } +} + +impl Visit for TextureKind { + fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { + let mut region = visitor.enter_region(name)?; + + let mut id = match self { + TextureKind::Line { .. } => 0, + TextureKind::Rectangle { .. } => 1, + TextureKind::Cube { .. } => 2, + TextureKind::Volume { .. } => 3, + }; + id.visit("Id", &mut region)?; + if region.is_reading() { + *self = match id { + 0 => TextureKind::Line { length: 0 }, + 1 => TextureKind::Rectangle { + width: 0, + height: 0, + }, + 2 => TextureKind::Cube { + width: 0, + height: 0, + }, + 3 => TextureKind::Volume { + width: 0, + height: 0, + depth: 0, + }, + _ => { + return VisitResult::Err(VisitError::User(format!( + "Invalid texture kind {id}!" + ))) + } + }; + } + match self { + TextureKind::Line { length } => { + length.visit("Length", &mut region)?; + } + TextureKind::Rectangle { width, height } => { + width.visit("Width", &mut region)?; + height.visit("Height", &mut region)?; + } + TextureKind::Cube { width, height } => { + width.visit("Width", &mut region)?; + height.visit("Height", &mut region)?; + } + TextureKind::Volume { + width, + height, + depth, + } => { + width.visit("Width", &mut region)?; + height.visit("Height", &mut region)?; + depth.visit("Depth", &mut region)?; + } + } + + Ok(()) + } +} + +/// Data storage of a texture. +#[derive(Default, Clone, Reflect)] +pub struct TextureBytes(Vec); + +impl Visit for TextureBytes { + fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { + self.0.visit(name, visitor) + } +} + +impl Debug for TextureBytes { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Texture has {} bytes", self.0.len()) + } +} + +impl From> for TextureBytes { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl Deref for TextureBytes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for TextureBytes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Actual texture data. +#[derive(Debug, Clone, Reflect)] +pub struct Texture { + kind: TextureKind, + bytes: TextureBytes, + pixel_kind: TexturePixelKind, + minification_filter: TextureMinificationFilter, + magnification_filter: TextureMagnificationFilter, + s_wrap_mode: TextureWrapMode, + t_wrap_mode: TextureWrapMode, + r_wrap_mode: TextureWrapMode, + base_level: usize, + max_level: usize, + min_lod: f32, + max_lod: f32, + lod_bias: f32, + mip_count: u32, + anisotropy: f32, + modifications_counter: u64, + is_render_target: bool, + #[doc(hidden)] + #[reflect(hidden)] + pub cache_index: Arc, +} + +impl TypeUuidProvider for Texture { + fn type_uuid() -> Uuid { + TEXTURE_RESOURCE_UUID + } +} + +impl ResourceData for Texture { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn type_uuid(&self) -> Uuid { + ::type_uuid() + } + + fn save(&mut self, path: &Path) -> Result<(), Box> { + let color_type = match self.pixel_kind { + TexturePixelKind::R8 => ColorType::L8, + TexturePixelKind::Luminance8 => ColorType::L8, + TexturePixelKind::RGB8 => ColorType::Rgb8, + TexturePixelKind::RGBA8 => ColorType::Rgba8, + TexturePixelKind::RG8 => ColorType::La8, + TexturePixelKind::LuminanceAlpha8 => ColorType::La8, + TexturePixelKind::R16 => ColorType::L16, + TexturePixelKind::Luminance16 => ColorType::L16, + TexturePixelKind::RG16 => ColorType::La16, + TexturePixelKind::LuminanceAlpha16 => ColorType::La16, + TexturePixelKind::RGB16 => ColorType::Rgb16, + TexturePixelKind::RGBA16 => ColorType::Rgba16, + TexturePixelKind::RGB32F => ColorType::Rgb32F, + TexturePixelKind::RGBA32F => ColorType::Rgba32F, + TexturePixelKind::DXT1RGB + | TexturePixelKind::DXT1RGBA + | TexturePixelKind::DXT3RGBA + | TexturePixelKind::DXT5RGBA + | TexturePixelKind::R8RGTC + | TexturePixelKind::RG8RGTC + | TexturePixelKind::BGR8 + | TexturePixelKind::BGRA8 + | TexturePixelKind::RGB16F + | TexturePixelKind::R32F + | TexturePixelKind::R16F => return Err(Box::new(TextureError::UnsupportedFormat)), + }; + if let TextureKind::Rectangle { width, height } = self.kind { + Ok(image::save_buffer( + path, + self.bytes.as_ref(), + width, + height, + color_type, + )?) + } else { + Err(Box::new(TextureError::UnsupportedFormat)) + } + } + + fn can_be_saved(&self) -> bool { + true + } +} + +impl Visit for Texture { + fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult { + let mut region = visitor.enter_region(name)?; + + let mut kind = self.pixel_kind.id(); + kind.visit("KindId", &mut region)?; + if region.is_reading() { + self.pixel_kind = TexturePixelKind::new(kind)?; + } + + self.minification_filter + .visit("MinificationFilter", &mut region)?; + self.magnification_filter + .visit("MagnificationFilter", &mut region)?; + self.anisotropy.visit("Anisotropy", &mut region)?; + self.s_wrap_mode.visit("SWrapMode", &mut region)?; + self.t_wrap_mode.visit("TWrapMode", &mut region)?; + let _ = self.t_wrap_mode.visit("RWrapMode", &mut region); + self.mip_count.visit("MipCount", &mut region)?; + self.kind.visit("Kind", &mut region)?; + let mut bytes_view = PodVecView::from_pod_vec(&mut self.bytes); + let _ = bytes_view.visit("Data", &mut region); + + let _ = self.base_level.visit("BaseLevel", &mut region); + let _ = self.max_level.visit("MaxLevel", &mut region); + let _ = self.min_lod.visit("MinLod", &mut region); + let _ = self.max_lod.visit("MaxLod", &mut region); + let _ = self.lod_bias.visit("LodBias", &mut region); + + Ok(()) + } +} + +impl Default for Texture { + /// It is very important to mention that defaults may be different for texture when you're + /// importing them through resource manager, see + /// [TextureImportOptions](../engine/resource_manager/struct.TextureImportOptions.html) for more info. + fn default() -> Self { + Self { + kind: TextureKind::Rectangle { + width: 0, + height: 0, + }, + bytes: Default::default(), + pixel_kind: TexturePixelKind::RGBA8, + minification_filter: TextureMinificationFilter::LinearMipMapLinear, + magnification_filter: TextureMagnificationFilter::Linear, + s_wrap_mode: TextureWrapMode::Repeat, + t_wrap_mode: TextureWrapMode::Repeat, + r_wrap_mode: TextureWrapMode::Repeat, + base_level: 0, + max_level: default_max_level(), + min_lod: default_min_lod(), + max_lod: default_max_lod(), + lod_bias: 0.0, + mip_count: 1, + anisotropy: 16.0, + modifications_counter: 0, + is_render_target: false, + cache_index: Default::default(), + } + } +} + +/// A filter for mip-map generation. +#[derive( + Default, Copy, Clone, Deserialize, Serialize, Debug, Reflect, AsRefStr, EnumString, VariantNames, +)] +pub enum MipFilter { + /// Simple nearest filter, it is the fastest filter available, but it produces noisy mip levels and + /// in most cases it is not advised to use it. Consider its performance as 1x. + Nearest, + /// Bilinear filtration. It has good balance between image quality and speed. It is ~13x times slower + /// than [`Self::Nearest`]. It is default filtering method. + #[default] + Bilinear, + /// Hamming filtration. It has much nicer filtration quality than Bilinear, but the same performance. + Hamming, + /// Catmull-Rom spline filtration. It has very good filtration quality, but it is ~23x times slower + /// than [`Self::Nearest`]. + CatmullRom, + /// Gaussian filtration. It has perfect filtration quality, but it is ~37x times slower than + /// [`Self::Nearest`]. + Lanczos, +} + +uuid_provider!(MipFilter = "8fa17c0e-6889-4540-b396-97db4dc952aa"); + +impl MipFilter { + fn into_filter_type(self) -> fr::FilterType { + match self { + MipFilter::Nearest => fr::FilterType::Box, + MipFilter::Bilinear => fr::FilterType::Bilinear, + MipFilter::CatmullRom => fr::FilterType::CatmullRom, + MipFilter::Hamming => fr::FilterType::Hamming, + MipFilter::Lanczos => fr::FilterType::Lanczos3, + } + } +} + +/// Allows you to define a set of parameters for a texture resource. +/// +/// # Details +/// +/// Usually the content of this structure is stored in a separate file with .options extension. Typical content of +/// a settings file should look like this: +/// +/// ```text +/// ( +/// minification_filter: Linear, +/// magnification_filter: Linear, +/// s_wrap_mode: Repeat, +/// t_wrap_mode: ClampToEdge, +/// anisotropy: 8.0, +/// compression: NoCompression, +/// ) +/// ``` +#[derive(Clone, Deserialize, Serialize, Debug, Reflect)] +pub struct TextureImportOptions { + #[serde(default)] + pub(crate) minification_filter: TextureMinificationFilter, + #[serde(default)] + pub(crate) magnification_filter: TextureMagnificationFilter, + #[serde(default)] + pub(crate) s_wrap_mode: TextureWrapMode, + #[serde(default)] + pub(crate) t_wrap_mode: TextureWrapMode, + #[serde(default)] + pub(crate) r_wrap_mode: TextureWrapMode, + #[serde(default)] + pub(crate) anisotropy: f32, + #[serde(default)] + pub(crate) compression: CompressionOptions, + #[serde(default)] + pub(crate) mip_filter: MipFilter, + #[serde(default)] + pub(crate) flip_green_channel: bool, + #[serde(default)] + pub(crate) base_level: usize, + #[serde(default = "default_max_level")] + pub(crate) max_level: usize, + #[serde(default = "default_min_lod")] + pub(crate) min_lod: f32, + #[serde(default = "default_max_lod")] + pub(crate) max_lod: f32, + #[serde(default)] + pub(crate) lod_bias: f32, +} + +fn default_max_level() -> usize { + 1000 +} + +fn default_min_lod() -> f32 { + -1000.0 +} + +fn default_max_lod() -> f32 { + 1000.0 +} + +impl Default for TextureImportOptions { + fn default() -> Self { + Self { + minification_filter: TextureMinificationFilter::LinearMipMapLinear, + magnification_filter: TextureMagnificationFilter::Linear, + s_wrap_mode: TextureWrapMode::Repeat, + t_wrap_mode: TextureWrapMode::Repeat, + r_wrap_mode: TextureWrapMode::Repeat, + anisotropy: 16.0, + compression: CompressionOptions::default(), + mip_filter: Default::default(), + flip_green_channel: false, + base_level: 0, + max_level: default_max_level(), + min_lod: default_min_lod(), + max_lod: default_max_lod(), + lod_bias: 0.0, + } + } +} + +impl ImportOptions for TextureImportOptions {} + +impl TextureImportOptions { + /// Sets new minification filter which will be applied to every imported texture as + /// default value. + pub fn with_minification_filter( + mut self, + minification_filter: TextureMinificationFilter, + ) -> Self { + self.minification_filter = minification_filter; + self + } + + /// Sets new minification filter which will be applied to every imported texture as + /// default value. + pub fn set_minification_filter(&mut self, minification_filter: TextureMinificationFilter) { + self.minification_filter = minification_filter; + } + + /// Sets new magnification filter which will be applied to every imported texture as + /// default value. + pub fn with_magnification_filter( + mut self, + magnification_filter: TextureMagnificationFilter, + ) -> Self { + self.magnification_filter = magnification_filter; + self + } + + /// Sets new magnification filter which will be applied to every imported texture as + /// default value. + pub fn set_magnification_filter(&mut self, magnification_filter: TextureMagnificationFilter) { + self.magnification_filter = magnification_filter; + } + + /// Sets new S coordinate wrap mode which will be applied to every imported texture as + /// default value. + pub fn with_s_wrap_mode(mut self, s_wrap_mode: TextureWrapMode) -> Self { + self.s_wrap_mode = s_wrap_mode; + self + } + + /// Sets new S coordinate wrap mode which will be applied to every imported texture as + /// default value. + pub fn set_s_wrap_mode(&mut self, s_wrap_mode: TextureWrapMode) { + self.s_wrap_mode = s_wrap_mode; + } + + /// Sets new T coordinate wrap mode which will be applied to every imported texture as + /// default value. + pub fn with_t_wrap_mode(mut self, t_wrap_mode: TextureWrapMode) -> Self { + self.t_wrap_mode = t_wrap_mode; + self + } + + /// Sets new T coordinate wrap mode which will be applied to every imported texture as + /// default value. + pub fn set_t_wrap_mode(&mut self, t_wrap_mode: TextureWrapMode) { + self.t_wrap_mode = t_wrap_mode; + } + + /// Sets new anisotropy level which will be applied to every imported texture as + /// default value. + pub fn with_anisotropy(mut self, anisotropy: f32) -> Self { + self.anisotropy = anisotropy.min(1.0); + self + } + + /// Sets new anisotropy level which will be applied to every imported texture as + /// default value. + pub fn set_anisotropy(&mut self, anisotropy: f32) { + self.anisotropy = anisotropy.min(1.0); + } + + /// Sets desired texture compression. + pub fn with_compression(mut self, compression: CompressionOptions) -> Self { + self.compression = compression; + self + } + + /// Sets desired texture compression. + pub fn set_compression(&mut self, compression: CompressionOptions) { + self.compression = compression; + } + + /// Same effect as [`Texture::set_base_level`]. + pub fn with_base_level(mut self, base_level: usize) -> Self { + self.base_level = base_level; + self + } + + /// Same effect as [`Texture::set_base_level`]. + pub fn set_base_level(&mut self, base_level: usize) { + self.base_level = base_level; + } + + /// Same effect as [`Texture::set_max_level`]. + pub fn with_max_level(mut self, max_level: usize) -> Self { + self.max_level = max_level; + self + } + + /// Same effect as [`Texture::set_max_level`]. + pub fn set_max_level(&mut self, max_level: usize) { + self.max_level = max_level; + } + + /// Same effect as [`Texture::set_min_lod`]. + pub fn with_min_lod(mut self, min_lod: f32) -> Self { + self.min_lod = min_lod; + self + } + + /// Same effect as [`Texture::set_min_lod`]. + pub fn set_min_lod(&mut self, min_lod: f32) { + self.min_lod = min_lod; + } + + /// Same effect as [`Texture::set_max_lod`]. + pub fn with_max_lod(mut self, max_lod: f32) -> Self { + self.max_lod = max_lod; + self + } + + /// Same effect as [`Texture::set_max_lod`]. + pub fn set_max_lod(&mut self, max_lod: f32) { + self.max_lod = max_lod; + } + + /// Same effect as [`Texture::set_lod_bias`]. + pub fn with_lod_bias(mut self, lod_bias: f32) -> Self { + self.lod_bias = lod_bias; + self + } + + /// Same effect as [`Texture::set_lod_bias`]. + pub fn set_lod_bias(&mut self, lod_bias: f32) { + self.lod_bias = lod_bias; + } +} + +lazy_static! { + /// Placeholder texture. + pub static ref PLACEHOLDER: BuiltInResource = BuiltInResource::new(embedded_data_source!("default.png"), + |data| { + TextureResource::load_from_memory( + ResourceKind::External("__PlaceholderTexture".into()), + data, + Default::default() + ) + .unwrap() + }); +} + +/// Type alias for texture resources. +pub type TextureResource = Resource; + +/// Extension trait for texture resources. +pub trait TextureResourceExtension: Sized { + /// Creates new render target for a scene. This method automatically configures GPU texture + /// to correct settings, after render target was created, it must not be modified, otherwise + /// result is undefined. + fn new_render_target(width: u32, height: u32) -> Self; + + /// Tries to load a texture from given data. Use this method if you want to + /// load a texture from embedded data. + /// + /// # On-demand compression + /// + /// The data can be compressed if needed to improve performance on GPU side. + /// + /// # Important notes + /// + /// Textures loaded with this method won't be correctly serialized! It means + /// that if you'll made a scene with textures loaded with this method, and then + /// save a scene, then the engine won't be able to restore the textures if you'll + /// try to load the saved scene. This is essential limitation of this method, + /// because the engine does not know where to get the data of the texture at + /// loading. You should use `ResourceManager::request_texture` in majority of cases! + /// + /// Main use cases for this method are: procedural textures, icons for GUI. + fn load_from_memory( + kind: ResourceKind, + data: &[u8], + import_options: TextureImportOptions, + ) -> Result; + + /// Tries to create new texture from given parameters, it may fail only if size of data passed + /// in does not match with required. + fn from_bytes( + kind: TextureKind, + pixel_kind: TexturePixelKind, + bytes: Vec, + resource_kind: ResourceKind, + ) -> Option; + + /// Creates a deep clone of the texture. Unlike [`TextureResource::clone`], this method clones the actual texture data, + /// which could be slow. + fn deep_clone(&self) -> Self; +} + +impl TextureResourceExtension for TextureResource { + fn new_render_target(width: u32, height: u32) -> Self { + Resource::new_ok( + Default::default(), + Texture { + // Render target will automatically set width and height before rendering. + kind: TextureKind::Rectangle { width, height }, + bytes: Default::default(), + pixel_kind: TexturePixelKind::RGBA8, + minification_filter: TextureMinificationFilter::Linear, + magnification_filter: TextureMagnificationFilter::Linear, + s_wrap_mode: TextureWrapMode::Repeat, + t_wrap_mode: TextureWrapMode::Repeat, + r_wrap_mode: TextureWrapMode::Repeat, + base_level: 0, + max_level: 1000, + min_lod: -1000.0, + max_lod: 1000.0, + lod_bias: 0.0, + mip_count: 1, + anisotropy: 1.0, + modifications_counter: 0, + is_render_target: true, + cache_index: Default::default(), + }, + ) + } + + fn load_from_memory( + kind: ResourceKind, + data: &[u8], + import_options: TextureImportOptions, + ) -> Result { + Ok(Resource::new_ok( + kind, + Texture::load_from_memory(data, import_options)?, + )) + } + + fn from_bytes( + kind: TextureKind, + pixel_kind: TexturePixelKind, + bytes: Vec, + resource_kind: ResourceKind, + ) -> Option { + Some(Resource::new_ok( + resource_kind, + Texture::from_bytes(kind, pixel_kind, bytes)?, + )) + } + + fn deep_clone(&self) -> Self { + let kind = self.header().kind.clone(); + let data = self.data_ref().clone(); + Resource::new_ok(kind, data) + } +} + +/// The texture magnification function is used when the pixel being textured maps to an area +/// less than or equal to one texture element. +#[derive( + Copy, + Clone, + Debug, + Hash, + PartialOrd, + PartialEq, + Deserialize, + Serialize, + Reflect, + VariantNames, + EnumString, + AsRefStr, + Visit, + Eq, +)] +#[repr(u32)] +pub enum TextureMagnificationFilter { + /// Returns the value of the texture element that is nearest to the center of the pixel + /// being textured. + Nearest = 0, + + /// Returns the weighted average of the four texture elements that are closest to the + /// center of the pixel being textured. + Linear = 1, +} + +uuid_provider!(TextureMagnificationFilter = "824f5b6c-8957-42db-9ebc-ef2a5dece5ab"); + +impl Default for TextureMagnificationFilter { + fn default() -> Self { + Self::Linear + } +} + +/// The texture minifying function is used whenever the pixel being textured maps to an area +/// greater than one texture element. +#[derive( + Copy, + Clone, + Debug, + Hash, + PartialOrd, + PartialEq, + Deserialize, + Serialize, + Reflect, + VariantNames, + EnumString, + AsRefStr, + Visit, + Eq, +)] +#[repr(u32)] +pub enum TextureMinificationFilter { + /// Returns the value of the texture element that is nearest to the center of the pixel + /// being textured. + Nearest = 0, + + /// Chooses the mipmap that most closely matches the size of the pixel being textured and + /// uses the Nearest criterion (the texture element nearest to the center of the pixel) + /// to produce a texture value. + NearestMipMapNearest = 1, + + /// Chooses the two mipmaps that most closely match the size of the pixel being textured + /// and uses the Nearest criterion (the texture element nearest to the center of the pixel) + /// to produce a texture value from each mipmap. The final texture value is a weighted average + /// of those two values. + NearestMipMapLinear = 2, + + /// Returns the weighted average of the four texture elements that are closest to the + /// center of the pixel being textured. + Linear = 3, + + /// Chooses the mipmap that most closely matches the size of the pixel being textured and + /// uses the Linear criterion (a weighted average of the four texture elements that are + /// closest to the center of the pixel) to produce a texture value. + LinearMipMapNearest = 4, + + /// Chooses the two mipmaps that most closely match the size of the pixel being textured + /// and uses the Linear criterion (a weighted average of the four texture elements that + /// are closest to the center of the pixel) to produce a texture value from each mipmap. + /// The final texture value is a weighted average of those two values. + LinearMipMapLinear = 5, +} + +uuid_provider!(TextureMinificationFilter = "0ec9e072-6d0a-47b2-a9c2-498cac4de22b"); + +impl TextureMinificationFilter { + /// Returns true if minification filter is using mip mapping, false - otherwise. + pub fn is_using_mip_mapping(self) -> bool { + match self { + TextureMinificationFilter::Nearest | TextureMinificationFilter::Linear => false, + TextureMinificationFilter::NearestMipMapNearest + | TextureMinificationFilter::LinearMipMapLinear + | TextureMinificationFilter::NearestMipMapLinear + | TextureMinificationFilter::LinearMipMapNearest => true, + } + } +} + +impl Default for TextureMinificationFilter { + fn default() -> Self { + Self::LinearMipMapLinear + } +} + +/// Defines a law of texture coordinate modification. +#[derive( + Copy, + Clone, + Debug, + Hash, + PartialOrd, + PartialEq, + Deserialize, + Serialize, + Reflect, + VariantNames, + EnumString, + AsRefStr, + Visit, + Eq, +)] +#[repr(u32)] +pub enum TextureWrapMode { + /// Causes the integer part of a coordinate to be ignored; GPU uses only the fractional part, + /// thereby creating a repeating pattern. + Repeat = 0, + + /// Causes a coordinates to be clamped to the range range, where N is the size of the texture + /// in the direction of clamping + ClampToEdge = 1, + + /// Evaluates a coordinates in a similar manner to ClampToEdge. However, in cases where clamping + /// would have occurred in ClampToEdge mode, the fetched texel data is substituted with the values + /// specified by border color. + ClampToBorder = 2, + + /// Causes the a coordinate to be set to the fractional part of the texture coordinate if the integer + /// part of coordinate is even; if the integer part of coordinate is odd, then the coordinate texture + /// coordinate is set to 1-frac, where frac represents the fractional part of coordinate. + MirroredRepeat = 3, + + /// Causes a coordinate to be repeated as for MirroredRepeat for one repetition of the texture, at + /// which point the coordinate to be clamped as in ClampToEdge. + MirrorClampToEdge = 4, +} + +uuid_provider!(TextureWrapMode = "e360d139-4374-4323-a66d-d192809d9d87"); + +impl Default for TextureWrapMode { + fn default() -> Self { + Self::Repeat + } +} + +/// Texture kind defines pixel format of texture. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect)] +#[repr(u32)] +pub enum TexturePixelKind { + /// 1 byte red. + R8 = 0, + + /// Red, green, and blue components, each by 1 byte. + RGB8 = 1, + + /// Red, green, blue, and alpha components, each by 1 byte. + RGBA8 = 2, + + /// Red and green, each by 1 byte. + RG8 = 3, + + /// 2 byte red. + R16 = 4, + + /// Red and green, each by 2 byte. + RG16 = 5, + + /// Blue, green, and red components, each by 1 byte. + BGR8 = 6, + + /// Blue, green, red and alpha components, each by 1 byte. + BGRA8 = 7, + + /// Red, green, and blue components, each by 2 byte. + RGB16 = 8, + + /// Red, green, blue, and alpha components, each by 2 byte. + RGBA16 = 9, + + /// Compressed S3TC DXT1 RGB (no alpha). + DXT1RGB = 10, + + /// Compressed S3TC DXT1 RGBA. + DXT1RGBA = 11, + + /// Compressed S3TC DXT3 RGBA. + DXT3RGBA = 12, + + /// Compressed S3TC DXT5 RGBA. + DXT5RGBA = 13, + + /// Compressed R8 texture (RGTC). + R8RGTC = 14, + + /// Compressed RG8 texture (RGTC). + RG8RGTC = 15, + + /// Floating-point RGB texture with 32bit depth. + RGB32F = 16, + + /// Floating-point RGBA texture with 32bit depth. + RGBA32F = 17, + + /// 1 byte luminance texture where pixels will have (L, L, L, 1.0) value on fetching. + /// + /// # Platform-specific + /// + /// - WebAssembly - not supported, the image will act like [`Self::R8`] format, which + /// will have (R, 0.0, 0.0, 1.0) pixels. + Luminance8 = 18, + + /// 1 byte for luminance and 1 for alpha, where all pixels will have (L, L, L, A) value on fetching. + /// + /// # Platform-specific + /// + /// - WebAssembly - not supported, the image will act like [`Self::RG8`] format, which + /// will have (R, G, R, G) pixels. + LuminanceAlpha8 = 19, + + /// 2 byte luminance texture where pixels will have (L, L, L, 1.0) value on fetching. + /// + /// # Platform-specific + /// + /// - WebAssembly - not supported, the image will act like [`Self::R8`] format, which + /// will have (R, 0.0, 0.0, 1.0) pixels. + Luminance16 = 20, + + /// 2 byte for luminance and 2 for alpha, where all pixels will have (L, L, L, A) value on fetching. + /// + /// # Platform-specific + /// + /// - WebAssembly - not supported, the image will act like [`Self::RG16`] format, which + /// will have (R, G, R, G) pixels. + LuminanceAlpha16 = 21, + + /// Red, green, blue components, each by 2 byte half-precision float. + RGB16F = 22, + + /// Red component as 4-byte, medium-precision float. + R32F = 23, + + /// Red component as 2-byte, half-precision float. + R16F = 24, +} + +impl TexturePixelKind { + fn new(id: u32) -> Result { + match id { + 0 => Ok(Self::R8), + 1 => Ok(Self::RGB8), + 2 => Ok(Self::RGBA8), + 3 => Ok(Self::RG8), + 4 => Ok(Self::R16), + 5 => Ok(Self::RG16), + 6 => Ok(Self::BGR8), + 7 => Ok(Self::BGRA8), + 8 => Ok(Self::RGB16), + 9 => Ok(Self::RGBA16), + 10 => Ok(Self::DXT1RGB), + 11 => Ok(Self::DXT1RGBA), + 12 => Ok(Self::DXT3RGBA), + 13 => Ok(Self::DXT5RGBA), + 14 => Ok(Self::R8RGTC), + 15 => Ok(Self::RG8RGTC), + 16 => Ok(Self::RGB32F), + 17 => Ok(Self::RGBA32F), + 18 => Ok(Self::Luminance8), + 19 => Ok(Self::LuminanceAlpha8), + 20 => Ok(Self::Luminance16), + 21 => Ok(Self::LuminanceAlpha16), + 22 => Ok(Self::RGB16F), + 23 => Ok(Self::R32F), + 24 => Ok(Self::R16F), + _ => Err(format!("Invalid texture kind {id}!")), + } + } + + fn id(self) -> u32 { + self as u32 + } + + /// Tries to get size of the pixel in bytes. Pixels of compressed textures consumes less than a byte, so + /// there's no way to express their size on whole number of bytes, in this case `None` is returned. + pub fn size_in_bytes(&self) -> Option { + match self { + Self::R8 | Self::Luminance8 => Some(1), + Self::RGB8 | Self::BGR8 => Some(3), + Self::RGBA8 | Self::RG16 | Self::BGRA8 | Self::LuminanceAlpha16 | Self::R32F => Some(4), + Self::RG8 | Self::R16 | Self::LuminanceAlpha8 | Self::Luminance16 | Self::R16F => { + Some(2) + } + Self::RGB16 | Self::RGB16F => Some(6), + Self::RGBA16 => Some(8), + Self::RGB32F => Some(12), + Self::RGBA32F => Some(16), + // Pixels of compressed textures consumes less than a byte, so there's no way to express + // their size on whole number of bytes. + Self::DXT1RGB + | Self::DXT1RGBA + | Self::DXT3RGBA + | Self::DXT5RGBA + | Self::R8RGTC + | Self::RG8RGTC => None, + } + } +} + +/// An error that may occur during texture operations. +#[derive(Debug)] +pub enum TextureError { + /// Format (pixel format, dimensions) is not supported. + UnsupportedFormat, + /// An io error. + Io(std::io::Error), + /// Internal image crate error. + Image(image::ImageError), + /// An error occurred during file loading. + FileLoadError(FileLoadError), +} + +impl Display for TextureError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TextureError::UnsupportedFormat => { + write!(f, "Unsupported format!") + } + TextureError::Io(v) => { + write!(f, "An i/o error has occurred: {v}") + } + TextureError::Image(v) => { + write!(f, "Image loading error {v}") + } + TextureError::FileLoadError(v) => { + write!(f, "A file load error has occurred {v:?}") + } + } + } +} + +impl std::error::Error for TextureError {} + +impl From for TextureError { + fn from(v: FileLoadError) -> Self { + Self::FileLoadError(v) + } +} + +impl From for TextureError { + fn from(v: ImageError) -> Self { + Self::Image(v) + } +} + +impl From for TextureError { + fn from(v: Error) -> Self { + Self::Io(v) + } +} + +fn ceil_div_4(x: u32) -> u32 { + (x + 3) / 4 +} + +/// Texture compression options. +/// +/// # Notes +/// +/// Try to avoid using compression for normal maps, normals maps usually has smooth +/// gradients, but compression algorithms used by Fyrox cannot preserve good quality +/// of such gradients. +#[derive( + Copy, + Clone, + Deserialize, + Serialize, + PartialEq, + Eq, + Debug, + Reflect, + VariantNames, + EnumString, + AsRefStr, +)] +#[repr(u32)] +pub enum CompressionOptions { + /// An image will be stored without compression if it is not already compressed. + NoCompression = 0, + + /// An image will be encoded via DXT1 (BC1) compression with low quality if is not + /// already compressed. + /// Compression ratio is 1:8 (without alpha) or 1:6 (with 1-bit alpha). + /// This option provides maximum speed by having lowest requirements of memory + /// bandwidth. + Speed = 1, + + /// An image will be encoded via DXT5 (BC5) compression with high quality if it is + /// not already compressed. + /// Compression ratio is 1:4 (including alpha) + /// This option is faster than `NoCompression` speed by lower requirements of memory + /// bandwidth. + Quality = 2, +} + +uuid_provider!(CompressionOptions = "fbdcc081-d0b8-4b62-9925-2de6c013fbf5"); + +impl Default for CompressionOptions { + fn default() -> Self { + Self::NoCompression + } +} + +fn transmute_slice(bytes: &[u8]) -> &'_ [T] { + // SAFETY: This is absolutely safe because `image` crate's Rgb8/Rgba8/etc. and `tbc`s Rgb8/Rgba8/etc. + // have exactly the same memory layout. + unsafe { + std::slice::from_raw_parts( + bytes.as_ptr() as *const T, + bytes.len() / std::mem::size_of::(), + ) + } +} + +fn transmute_slice_mut(bytes: &mut [u8]) -> &'_ mut [T] { + // SAFETY: This is absolutely safe because `image` crate's Rgb8/Rgba8/etc. and `tbc`s Rgb8/Rgba8/etc. + // have exactly the same memory layout. + unsafe { + std::slice::from_raw_parts_mut( + bytes.as_ptr() as *mut T, + bytes.len() / std::mem::size_of::(), + ) + } +} + +fn compress_bc1(bytes: &[u8], width: usize, height: usize) -> Vec { + tbc::encode_image_bc1_conv_u8::(transmute_slice::(bytes), width, height) +} + +fn compress_bc3(bytes: &[u8], width: usize, height: usize) -> Vec { + tbc::encode_image_bc3_conv_u8::(transmute_slice::(bytes), width, height) +} + +fn compress_r8_bc4(bytes: &[u8], width: usize, height: usize) -> Vec { + tbc::encode_image_bc4_r8_conv_u8::(transmute_slice::(bytes), width, height) +} + +fn compress_rg8_bc4( + bytes: &[u8], + width: usize, + height: usize, +) -> Vec { + tbc::encode_image_bc4_rg8_conv_u8::(transmute_slice::(bytes), width, height) +} + +fn data_hash(data: &[u8]) -> u64 { + let mut hasher = FxHasher::default(); + data.hash(&mut hasher); + hasher.finish() +} + +fn try_compress( + pixel_kind: TexturePixelKind, + bytes: &[u8], + w: usize, + h: usize, + compression: CompressionOptions, +) -> Option<(Vec, TexturePixelKind)> { + match (pixel_kind, compression) { + (TexturePixelKind::RGB8, CompressionOptions::Speed) => Some(( + compress_bc1::(bytes, w, h), + TexturePixelKind::DXT1RGB, + )), + (TexturePixelKind::RGB8, CompressionOptions::Quality) => Some(( + compress_bc3::(bytes, w, h), + TexturePixelKind::DXT5RGBA, + )), + (TexturePixelKind::RGBA8, CompressionOptions::Speed) => Some(( + compress_bc1::(bytes, w, h), + TexturePixelKind::DXT1RGBA, + )), + (TexturePixelKind::RGBA8, CompressionOptions::Quality) => Some(( + compress_bc3::(bytes, w, h), + TexturePixelKind::DXT5RGBA, + )), + (TexturePixelKind::R8, CompressionOptions::Speed) + | (TexturePixelKind::R8, CompressionOptions::Quality) + | (TexturePixelKind::Luminance8, CompressionOptions::Speed) + | (TexturePixelKind::Luminance8, CompressionOptions::Quality) => Some(( + compress_r8_bc4::(bytes, w, h), + TexturePixelKind::R8RGTC, + )), + (TexturePixelKind::RG8, CompressionOptions::Speed) + | (TexturePixelKind::RG8, CompressionOptions::Quality) + | (TexturePixelKind::LuminanceAlpha8, CompressionOptions::Speed) + | (TexturePixelKind::LuminanceAlpha8, CompressionOptions::Quality) => Some(( + compress_rg8_bc4::(bytes, w, h), + TexturePixelKind::RG8RGTC, + )), + _ => None, + } +} + +fn bytes_in_mip_level(kind: TextureKind, pixel_kind: TexturePixelKind, mip: usize) -> u32 { + let pixel_count = match kind { + TextureKind::Line { length } => length.shr(mip), + TextureKind::Rectangle { width, height } => width.shr(mip) * height.shr(mip), + TextureKind::Cube { width, height } => 6 * width.shr(mip) * height.shr(mip), + TextureKind::Volume { + width, + height, + depth, + } => width.shr(mip) * height.shr(mip) * depth.shr(mip), + }; + match pixel_kind { + // Uncompressed formats. + TexturePixelKind::R8 | TexturePixelKind::Luminance8 => pixel_count, + TexturePixelKind::R16 + | TexturePixelKind::LuminanceAlpha8 + | TexturePixelKind::Luminance16 + | TexturePixelKind::RG8 + | TexturePixelKind::R16F => 2 * pixel_count, + TexturePixelKind::RGB8 | TexturePixelKind::BGR8 => 3 * pixel_count, + TexturePixelKind::RGBA8 + | TexturePixelKind::BGRA8 + | TexturePixelKind::RG16 + | TexturePixelKind::LuminanceAlpha16 + | TexturePixelKind::R32F => 4 * pixel_count, + TexturePixelKind::RGB16 | TexturePixelKind::RGB16F => 6 * pixel_count, + TexturePixelKind::RGBA16 => 8 * pixel_count, + TexturePixelKind::RGB32F => 12 * pixel_count, + TexturePixelKind::RGBA32F => 16 * pixel_count, + + // Compressed formats. + TexturePixelKind::DXT1RGB + | TexturePixelKind::DXT1RGBA + | TexturePixelKind::DXT3RGBA + | TexturePixelKind::DXT5RGBA + | TexturePixelKind::R8RGTC + | TexturePixelKind::RG8RGTC => { + let block_size = match pixel_kind { + TexturePixelKind::DXT1RGB + | TexturePixelKind::DXT1RGBA + | TexturePixelKind::R8RGTC => 8, + TexturePixelKind::DXT3RGBA + | TexturePixelKind::DXT5RGBA + | TexturePixelKind::RG8RGTC => 16, + _ => unreachable!(), + }; + match kind { + TextureKind::Line { length } => ceil_div_4(length) * block_size, + TextureKind::Rectangle { width, height } => { + ceil_div_4(width) * ceil_div_4(height) * block_size + } + TextureKind::Cube { width, height } => { + 6 * ceil_div_4(width) * ceil_div_4(height) * block_size + } + TextureKind::Volume { + width, + height, + depth, + } => ceil_div_4(width) * ceil_div_4(height) * ceil_div_4(depth) * block_size, + } + } + } +} + +fn mip_byte_offset(kind: TextureKind, pixel_kind: TexturePixelKind, mut mip: usize) -> usize { + // TODO: This could be done without loop. + let mut offset = 0; + loop { + offset += bytes_in_mip_level(kind, pixel_kind, mip) as usize; + mip = mip.saturating_sub(1); + if mip == 0 { + break; + } + } + offset +} + +fn convert_pixel_type_enum(pixel_kind: TexturePixelKind) -> fr::PixelType { + match pixel_kind { + TexturePixelKind::R8 | TexturePixelKind::Luminance8 => fr::PixelType::U8, + TexturePixelKind::RGB8 | TexturePixelKind::BGR8 => fr::PixelType::U8x3, + TexturePixelKind::RGBA8 | TexturePixelKind::BGRA8 => fr::PixelType::U8x4, + TexturePixelKind::RG8 | TexturePixelKind::LuminanceAlpha8 => fr::PixelType::U8x2, + TexturePixelKind::R16 | TexturePixelKind::Luminance16 => fr::PixelType::U16, + TexturePixelKind::RG16 | TexturePixelKind::LuminanceAlpha16 => fr::PixelType::U16x2, + TexturePixelKind::RGB16 => fr::PixelType::U16x3, + TexturePixelKind::RGBA16 => fr::PixelType::U16x4, + TexturePixelKind::R32F => fr::PixelType::F32, + _ => unreachable!(), + } +} + +fn flip_green_channel<'a, P>(pixels: impl Iterator) +where + P: Pixel + 'a, +{ + for pixel in pixels { + let green = &mut pixel.channels_mut()[1]; + let inverted = P::Subpixel::max_value() - *green; + *green = inverted; + } +} + +impl Texture { + /// Tries to load a texture from given data in one of the following formats: PNG, BMP, TGA, JPG, DDS, GIF. Use + /// this method if you want to load a texture from embedded data. + /// + /// # On-demand compression and mip-map generation + /// + /// The data can be compressed if needed to improve performance on GPU side. Mip-maps can be generated as well. + /// **CAVEAT:** Compression and mip-map generation **won't** be taken into account in case of **DDS** textures, + /// because DDS can already contain such data, you should generate mips and compress DDS textures manually using + /// some offline tool like DirectXTexTool or similar. + /// + /// # Important notes + /// + /// Textures loaded with this method won't be correctly serialized! It means that if you'll made a scene with + /// textures loaded with this method, and then save a scene, then the engine won't be able to restore the textures + /// if you'll try to load the saved scene. This is essential limitation of this method, because the engine does + /// not know where to get the data of the texture at loading. You should use `ResourceManager::request_texture` + /// in majority of cases! + /// + /// # Use cases + /// + /// Main use cases for this method are: procedural textures, icons for GUI. + pub fn load_from_memory( + data: &[u8], + import_options: TextureImportOptions, + ) -> Result { + // DDS is special. It can contain various kinds of textures as well as textures with + // various pixel formats. + // + // TODO: Add support for DXGI formats. + if let Ok(dds) = ddsfile::Dds::read(&mut Cursor::new(data)) { + let d3dformat = dds + .get_d3d_format() + .ok_or(TextureError::UnsupportedFormat)?; + let mip_count = dds.get_num_mipmap_levels(); + let mut bytes = dds.data; + + // Try to use as much formats as possible. + let pixel_kind = match d3dformat { + D3DFormat::DXT1 => TexturePixelKind::DXT1RGBA, + D3DFormat::DXT3 => TexturePixelKind::DXT3RGBA, + D3DFormat::DXT5 => TexturePixelKind::DXT5RGBA, + D3DFormat::L8 | D3DFormat::A8 => TexturePixelKind::R8, + D3DFormat::L16 => TexturePixelKind::R16, + D3DFormat::R8G8B8 => TexturePixelKind::RGB8, + D3DFormat::A8L8 => TexturePixelKind::RG8, + D3DFormat::A8R8G8B8 => { + // // ARGB8 -> RGBA8 + // assert_eq!(bytes.len() % 4, 0); + // for chunk in bytes.chunks_exact_mut(4) { + // let a = chunk[0]; + // let r = chunk[1]; + // let g = chunk[2]; + // let b = chunk[3]; + // chunk[0] = r; + // chunk[1] = g; + // chunk[2] = b; + // chunk[3] = a; + // } + TexturePixelKind::RGBA8 + } + D3DFormat::G16R16 => { + // GR16 -> RG16 + assert_eq!(bytes.len() % 4, 0); + for chunk in bytes.chunks_exact_mut(4) { + // Red Hi + Lo bytes + let gh = chunk[0]; + let gl = chunk[1]; + // Green Hi + Lo bytes + let rh = chunk[2]; + let rl = chunk[3]; + // Swap + chunk[0] = rh; + chunk[1] = rl; + chunk[2] = gh; + chunk[3] = gl; + } + TexturePixelKind::RG16 + } + _ => return Err(TextureError::UnsupportedFormat), + }; + + Ok(Self { + pixel_kind, + modifications_counter: 0, + minification_filter: import_options.minification_filter, + magnification_filter: import_options.magnification_filter, + s_wrap_mode: import_options.s_wrap_mode, + t_wrap_mode: import_options.t_wrap_mode, + r_wrap_mode: import_options.r_wrap_mode, + base_level: import_options.base_level, + max_level: import_options.max_level, + min_lod: import_options.min_lod, + max_lod: import_options.max_lod, + anisotropy: import_options.anisotropy, + mip_count, + bytes: bytes.into(), + kind: if dds.header.caps2 & Caps2::CUBEMAP == Caps2::CUBEMAP { + TextureKind::Cube { + width: dds.header.width, + height: dds.header.height, + } + } else if dds.header.caps2 & Caps2::VOLUME == Caps2::VOLUME { + TextureKind::Volume { + width: dds.header.width, + height: dds.header.height, + depth: dds.header.depth.unwrap(), + } + } else { + TextureKind::Rectangle { + width: dds.header.width, + height: dds.header.height, + } + }, + is_render_target: false, + cache_index: Default::default(), + lod_bias: import_options.lod_bias, + }) + } else { + // Commonly used formats are all rectangle textures. + let mut dyn_img = image::load_from_memory(data) + // Try to load as TGA, this is needed because TGA is badly designed format and does not + // have an identifier in the beginning of the file (so called "magic") that allows quickly + // check if the file is really contains expected data. + .or_else(|_| image::load_from_memory_with_format(data, ImageFormat::Tga))?; + + let width = dyn_img.width(); + let height = dyn_img.height(); + + if import_options.flip_green_channel { + match dyn_img { + DynamicImage::ImageRgb8(ref mut img) => flip_green_channel(img.pixels_mut()), + DynamicImage::ImageRgba8(ref mut img) => flip_green_channel(img.pixels_mut()), + DynamicImage::ImageRgb16(ref mut img) => flip_green_channel(img.pixels_mut()), + DynamicImage::ImageRgba16(ref mut img) => flip_green_channel(img.pixels_mut()), + DynamicImage::ImageRgb32F(ref mut img) => flip_green_channel(img.pixels_mut()), + DynamicImage::ImageRgba32F(ref mut img) => flip_green_channel(img.pixels_mut()), + _ => (), + } + } + + let src_pixel_kind = match dyn_img { + DynamicImage::ImageLuma8(_) => TexturePixelKind::Luminance8, + DynamicImage::ImageLumaA8(_) => TexturePixelKind::LuminanceAlpha8, + DynamicImage::ImageRgb8(_) => TexturePixelKind::RGB8, + DynamicImage::ImageRgba8(_) => TexturePixelKind::RGBA8, + DynamicImage::ImageLuma16(_) => TexturePixelKind::Luminance16, + DynamicImage::ImageLumaA16(_) => TexturePixelKind::LuminanceAlpha16, + DynamicImage::ImageRgb16(_) => TexturePixelKind::RGB16, + DynamicImage::ImageRgba16(_) => TexturePixelKind::RGBA16, + DynamicImage::ImageRgb32F(_) => TexturePixelKind::RGB32F, + DynamicImage::ImageRgba32F(_) => TexturePixelKind::RGBA32F, + _ => return Err(TextureError::UnsupportedFormat), + }; + let mut final_pixel_kind = src_pixel_kind; + + let mut mip_count = 0; + let mut bytes = Vec::with_capacity( + width as usize * height as usize * src_pixel_kind.size_in_bytes().unwrap_or(4), + ); + + if import_options.minification_filter.is_using_mip_mapping() { + let src_pixel_type = convert_pixel_type_enum(src_pixel_kind); + let mut level_width = width; + let mut level_height = height; + let mut current_level = fr::images::Image::from_vec_u8( + level_width, + level_height, + dyn_img.as_bytes().to_vec(), + src_pixel_type, + ) + .map_err(|_| TextureError::UnsupportedFormat)?; + + while level_width != 0 && level_height != 0 { + if mip_count != 0 { + let mut dst_img = + fr::images::Image::new(level_width, level_height, src_pixel_type); + + let mut resizer = fr::Resizer::new(); + + resizer + .resize( + ¤t_level, + &mut dst_img, + Some(&ResizeOptions { + algorithm: fr::ResizeAlg::Convolution( + import_options.mip_filter.into_filter_type(), + ), + cropping: Default::default(), + mul_div_alpha: true, + }), + ) + .expect("Pixel types must match!"); + + current_level = dst_img; + } + + mip_count += 1; + + if import_options.compression == CompressionOptions::NoCompression { + bytes.extend_from_slice(current_level.buffer()) + } else if let Some((compressed_data, new_pixel_kind)) = try_compress( + src_pixel_kind, + current_level.buffer(), + level_width as usize, + level_height as usize, + import_options.compression, + ) { + final_pixel_kind = new_pixel_kind; + bytes.extend_from_slice(&compressed_data); + } else { + bytes.extend_from_slice(current_level.buffer()) + } + + level_width = level_width.checked_shr(1).unwrap_or_default(); + level_height = level_height.checked_shr(1).unwrap_or_default(); + } + } else { + mip_count = 1; + + if import_options.compression == CompressionOptions::NoCompression { + bytes.extend_from_slice(dyn_img.as_bytes()); + } else if let Some((compressed_data, new_pixel_kind)) = try_compress( + src_pixel_kind, + dyn_img.as_bytes(), + width as usize, + height as usize, + import_options.compression, + ) { + final_pixel_kind = new_pixel_kind; + bytes.extend_from_slice(&compressed_data); + } else { + bytes.extend_from_slice(dyn_img.as_bytes()) + } + } + + Ok(Self { + pixel_kind: final_pixel_kind, + kind: TextureKind::Rectangle { width, height }, + modifications_counter: 0, + bytes: bytes.into(), + mip_count, + minification_filter: import_options.minification_filter, + magnification_filter: import_options.magnification_filter, + s_wrap_mode: import_options.s_wrap_mode, + t_wrap_mode: import_options.t_wrap_mode, + r_wrap_mode: import_options.r_wrap_mode, + base_level: import_options.base_level, + max_level: import_options.max_level, + min_lod: import_options.min_lod, + max_lod: import_options.max_lod, + anisotropy: import_options.anisotropy, + is_render_target: false, + cache_index: Default::default(), + lod_bias: import_options.lod_bias, + }) + } + } + + /// Tries to load a texture from a file. + /// + /// # Notes + /// + /// It is **not** public because you must use resource manager to load textures from external + /// resources. + pub(crate) async fn load_from_file>( + path: P, + io: &dyn ResourceIo, + import_options: TextureImportOptions, + ) -> Result { + let data = io.load_file(path.as_ref()).await?; + Self::load_from_memory(&data, import_options) + } + + /// Creates new texture instance from given parameters. + /// + /// # Limitations + /// + /// Currently textures with only one mip level are supported! + pub fn from_bytes( + kind: TextureKind, + pixel_kind: TexturePixelKind, + bytes: Vec, + ) -> Option { + if bytes_in_mip_level(kind, pixel_kind, 0) != bytes.len() as u32 { + None + } else { + Some(Self { + kind, + modifications_counter: 0, + bytes: bytes.into(), + pixel_kind, + ..Default::default() + }) + } + } + + /// Sets new minification filter. It is used when texture becomes smaller. + #[inline] + pub fn set_minification_filter(&mut self, filter: TextureMinificationFilter) { + self.minification_filter = filter; + } + + /// Returns current minification filter. + #[inline] + pub fn minification_filter(&self) -> TextureMinificationFilter { + self.minification_filter + } + + /// Sets new magnification filter. It is used when texture is "stretching". + #[inline] + pub fn set_magnification_filter(&mut self, filter: TextureMagnificationFilter) { + self.magnification_filter = filter; + } + + /// Returns current magnification filter. + #[inline] + pub fn magnification_filter(&self) -> TextureMagnificationFilter { + self.magnification_filter + } + + /// Sets new S coordinate wrap mode. + #[inline] + pub fn set_s_wrap_mode(&mut self, s_wrap_mode: TextureWrapMode) { + self.s_wrap_mode = s_wrap_mode; + } + + /// Returns current S coordinate wrap mode. + #[inline] + pub fn s_wrap_mode(&self) -> TextureWrapMode { + self.s_wrap_mode + } + + /// Sets new T coordinate wrap mode. + #[inline] + pub fn set_t_wrap_mode(&mut self, t_wrap_mode: TextureWrapMode) { + self.t_wrap_mode = t_wrap_mode; + } + + /// Returns current T coordinate wrap mode. + #[inline] + pub fn t_wrap_mode(&self) -> TextureWrapMode { + self.t_wrap_mode + } + + /// Sets new R coordinate wrap mode. + #[inline] + pub fn set_r_wrap_mode(&mut self, r_wrap_mode: TextureWrapMode) { + self.r_wrap_mode = r_wrap_mode; + } + + /// Returns current T coordinate wrap mode. + #[inline] + pub fn r_wrap_mode(&self) -> TextureWrapMode { + self.r_wrap_mode + } + + /// Returns total mip count. + #[inline] + pub fn mip_count(&self) -> u32 { + self.mip_count + } + + /// Returns texture kind. + #[inline] + pub fn kind(&self) -> TextureKind { + self.kind + } + + /// Returns total amount of modifications done on the texture. + #[inline] + pub fn modifications_count(&self) -> u64 { + self.modifications_counter + } + + /// Calculates current data hash. + #[inline] + pub fn calculate_data_hash(&self) -> u64 { + data_hash(&self.bytes.0) + } + + /// Returns current pixel kind. + #[inline] + pub fn pixel_kind(&self) -> TexturePixelKind { + self.pixel_kind + } + + /// Returns the index of the lowest defined mipmap level. + #[inline] + pub fn base_level(&self) -> usize { + self.base_level + } + + /// Specifies the index of the lowest defined mipmap level. Keep in mind, that the texture data + /// should provide the actual mip map level defined by the provided value, otherwise the + /// rendering will be incorrect (probably just black on majority of implementations) and glitchy. + #[inline] + pub fn set_base_level(&mut self, base_level: usize) { + self.base_level = base_level; + } + + /// Returns the index of the highest defined mipmap level. + #[inline] + pub fn max_level(&self) -> usize { + self.max_level + } + + /// Sets the index of the highest defined mipmap level. Keep in mind, that the texture data + /// should provide the actual mip map level defined by the provided value, otherwise the + /// rendering will be incorrect (probably just black on majority of implementations) and glitchy. + #[inline] + pub fn set_max_level(&mut self, max_level: usize) { + self.max_level = max_level; + } + + /// Returns the minimum level-of-detail parameter. See [`Self::set_min_lod`] for more info. + #[inline] + pub fn min_lod(&self) -> f32 { + self.min_lod + } + + /// Sets the minimum level-of-detail parameter. This floating-point value limits the selection + /// of highest resolution mipmap (lowest mipmap level). The initial value is -1000.0. + #[inline] + pub fn set_min_lod(&mut self, min_lod: f32) { + self.min_lod = min_lod; + } + + /// Returns the maximum level-of-detail parameter. See [`Self::set_max_lod`] for more info. + #[inline] + pub fn max_lod(&self) -> f32 { + self.max_lod + } + + /// Sets the maximum level-of-detail parameter. This floating-point value limits the selection + /// of the lowest resolution mipmap (highest mipmap level). The initial value is 1000. + #[inline] + pub fn set_max_lod(&mut self, max_lod: f32) { + self.max_lod = max_lod; + } + + /// Returns a fixed bias value that is to be added to the level-of-detail parameter for the + /// texture before texture sampling. See [`Self::set_lod_bias`] for more info. + #[inline] + pub fn lod_bias(&self) -> f32 { + self.lod_bias + } + + /// Specifies a fixed bias value that is to be added to the level-of-detail parameter for the + /// texture before texture sampling. The specified value is added to the shader-supplied bias + /// value (if any) and subsequently clamped into the implementation-defined range + /// `−bias_max..bias_max`, where `bias_max` is the value that can be fetched from the current + /// graphics server. The initial value is 0.0. + #[inline] + pub fn set_lod_bias(&mut self, lod_bias: f32) { + self.lod_bias = lod_bias; + } + + /// Returns current data as immutable slice. + pub fn data(&self) -> &[u8] { + &self.bytes + } + + /// Tries to cast the internal data buffer to the given type. Type casting will succeed only if the the + /// size of the type `T` is equal with the size of the pixel. + /// + /// ## Important notes + /// + /// While this function is safe, there's no guarantee that the actual type-casted data will match the layout + /// of your data structure. For example, you could have a pixel of type `RG16`, where each pixel consumes 2 + /// bytes (4 in total) and cast it to the structure `struct Rgba8 { r: u8, g: u8, b: u8, a: u8 }` which is + /// safe in terms of memory access (both are 4 bytes total), but not ok in terms of actual data. Be careful + /// when using this method. + /// + /// Keep in mind, that this method will return a reference to **all** data in the texture, including all mip + /// levels. If you need to get typed data from specific mip level use [`Self::mip_level_data_of_type`]. + pub fn data_of_type(&self) -> Option<&[T]> { + if let Some(pixel_size) = self.pixel_kind.size_in_bytes() { + if pixel_size == std::mem::size_of::() { + return Some(transmute_slice(&self.bytes)); + } + } + None + } + + /// Returns data of the given mip level. + #[inline] + pub fn mip_level_data(&self, mip: usize) -> &[u8] { + let mip_begin = mip + .checked_sub(1) + .map(|prev| mip_byte_offset(self.kind, self.pixel_kind, prev)) + .unwrap_or_default(); + let mip_end = mip_byte_offset(self.kind, self.pixel_kind, mip); + &self.bytes[mip_begin..mip_end] + } + + /// Tries to cast the specific mip level data of the internal data buffer to the given type. Type casting + /// will succeed only if the the size of the type `T` is equal with the size of the pixel. + /// + /// ## Important notes + /// + /// While this function is safe, there's no guarantee that the actual type-casted data will match the layout + /// of your data structure. For example, you could have a pixel of type `RG16`, where each pixel consumes 2 + /// bytes (4 in total) and cast it to the structure `struct Rgba8 { r: u8, g: u8, b: u8, a: u8 }` which is + /// safe in terms of memory access (both are 4 bytes total), but not ok in terms of actual data. Be careful + /// when using this method. + pub fn mip_level_data_of_type(&self, mip: usize) -> Option<&[T]> { + if let Some(pixel_size) = self.pixel_kind.size_in_bytes() { + if pixel_size == std::mem::size_of::() { + return Some(transmute_slice(self.mip_level_data(mip))); + } + } + None + } + + /// Returns true if the texture is used as render target. + #[inline] + pub fn is_render_target(&self) -> bool { + self.is_render_target + } + + /// Max samples for anisotropic filtering. Default value is 16.0 (max). + /// However real value passed to GPU will be clamped to maximum supported + /// by current GPU. To disable anisotropic filtering set this to 1.0. + /// Typical values are 2.0, 4.0, 8.0, 16.0. + #[inline] + pub fn set_anisotropy_level(&mut self, anisotropy: f32) { + self.anisotropy = anisotropy.max(1.0); + } + + /// Returns current anisotropy level. + #[inline] + pub fn anisotropy_level(&self) -> f32 { + self.anisotropy + } + + /// Returns a special reference holder that provides mutable access to content of the + /// texture and automatically calculates hash of the data in its destructor. + #[inline] + pub fn modify(&mut self) -> TextureDataRefMut<'_> { + TextureDataRefMut { texture: self } + } +} + +/// A special reference holder that provides mutable access to content of the +/// texture and automatically calculates hash of the data in its destructor. +pub struct TextureDataRefMut<'a> { + texture: &'a mut Texture, +} + +impl Drop for TextureDataRefMut<'_> { + fn drop(&mut self) { + self.texture.modifications_counter += 1; + } +} + +impl Deref for TextureDataRefMut<'_> { + type Target = Texture; + + fn deref(&self) -> &Self::Target { + self.texture + } +} + +impl DerefMut for TextureDataRefMut<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.texture + } +} + +impl TextureDataRefMut<'_> { + /// Returns mutable reference to the data of the texture. + pub fn data_mut(&mut self) -> &mut [u8] { + &mut self.texture.bytes + } + + /// Tries to cast the internal data buffer to the given type. Type casting will succeed only if the the + /// size of the type `T` is equal with the size of the pixel. **WARNING:** While this function is safe, there's + /// no guarantee that the actual type-casted data will match the layout of your data structure. For example, + /// you could have a pixel of type `RG16`, where each pixel consumes 2 bytes (4 in total) and cast it to the + /// structure `struct Rgba8 { r: u8, g: u8, b: u8, a: u8 }` which is safe in terms of memory access (both are 4 + /// bytes total), but not ok in terms of actual data. Be careful when using the method. + pub fn data_mut_of_type(&mut self) -> Option<&mut [T]> { + if let Some(pixel_size) = self.texture.pixel_kind.size_in_bytes() { + if pixel_size == std::mem::size_of::() { + return Some(transmute_slice_mut(&mut self.texture.bytes)); + } + } + None + } +} diff --git a/fyrox-impl/src/resource/texture/loader.rs b/fyrox-texture/src/loader.rs similarity index 86% rename from fyrox-impl/src/resource/texture/loader.rs rename to fyrox-texture/src/loader.rs index 48606f1d1..22e331a11 100644 --- a/fyrox-impl/src/resource/texture/loader.rs +++ b/fyrox-texture/src/loader.rs @@ -20,17 +20,12 @@ //! Texture loader. -use crate::{ - asset::{ - io::ResourceIo, - loader::{ - BoxedImportOptionsLoaderFuture, BoxedLoaderFuture, LoaderPayload, ResourceLoader, - }, - options::{try_get_import_settings, try_get_import_settings_opaque, BaseImportOptions}, - state::LoadError, - }, - core::{uuid::Uuid, TypeUuidProvider}, - resource::texture::{Texture, TextureImportOptions}, +use crate::{Texture, TextureImportOptions}; +use fyrox_core::{uuid::Uuid, TypeUuidProvider}; +use fyrox_resource::{ + io::ResourceIo, loader::BoxedImportOptionsLoaderFuture, loader::BoxedLoaderFuture, + loader::LoaderPayload, loader::ResourceLoader, options::try_get_import_settings, + options::try_get_import_settings_opaque, options::BaseImportOptions, state::LoadError, }; use std::{path::PathBuf, sync::Arc};