wde_renderer/assets/
texture.rs

1use wde_logger::prelude::*;
2
3use bevy::{
4    asset::{AssetLoader, LoadContext, io::Reader},
5    ecs::system::lifetimeless::SRes,
6    prelude::*
7};
8use image::GenericImageView;
9use serde::{Deserialize, Serialize};
10use std::io::{Error, ErrorKind};
11use thiserror::Error;
12
13use crate::core::RenderInstance;
14
15use super::asset::{PrepareAssetError, RenderAsset};
16
17// Reexport structs
18pub use wde_wgpu::texture::{
19    DEPTH_FORMAT, FilterMode, SWAPCHAIN_FORMAT, TextureFormat, TextureUsages
20};
21
22/// Stores a CPU texture with raw pixel data and metadata for GPU allocation.
23/// If loaded from a file, the texture should have a `.png` or `.jpg` extension.
24/// This texture will be uploaded to the GPU, represented by a [`GpuTexture`] asset.
25#[derive(Asset, TypePath, Clone, Debug)]
26pub struct Texture {
27    pub label: String,
28    /// Width and height in pixels of the CPU buffer.
29    pub size: (u32, u32),
30
31    /// GPU texture format requested when allocating the resource. Defaults to `Rgba8Unorm`.
32    pub format: TextureFormat,
33    /// GPU usage flags (sampling, storage, copy, etc.). Defaults to `TEXTURE_BINDING` (sampled texture).
34    pub usages: TextureUsages,
35
36    /// Sample count for the texture (1 for no MSAA, etc.). Defaults to 1.
37    pub sample_count: u32,
38    /// Number of layers for array textures (1 for non-arrays, etc.). Defaults to 1.
39    pub layer_count: u32,
40    /// Number of mip levels (1 = no mipmaps, 0 = auto-calculate max levels). Defaults to 1.
41    pub mip_level_count: u32,
42
43    /// Raw pixel data for the texture, in the format specified by `format`. Defaults to empty.
44    pub data: Vec<u8>
45}
46impl Default for Texture {
47    fn default() -> Self {
48        Texture {
49            label: "Unknown Texture".to_string(),
50            size: (1, 1),
51            format: TextureFormat::Rgba8Unorm,
52            usages: TextureUsages::TEXTURE_BINDING,
53            sample_count: 1,
54            layer_count: 1,
55            mip_level_count: 1,
56            data: Vec::new()
57        }
58    }
59}
60
61/// Represents a GPU texture resource allocated from a CPU [`Texture`] asset.
62pub struct GpuTexture {
63    /// Human readable identifier applied to the GPU resource label. This is not necessarily unique.
64    pub label: String,
65    /// Handle to the GPU texture allocated via `wde-wgpu`.
66    pub texture: wde_wgpu::texture::Texture
67}
68impl RenderAsset for GpuTexture {
69    type SourceAsset = Texture;
70    type Params = SRes<RenderInstance>;
71
72    fn prepare(
73        asset: Self::SourceAsset,
74        render_instance: &mut bevy::ecs::system::SystemParamItem<Self::Params>
75    ) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
76        trace!(asset.label, "Preparing GPU texture asset.");
77
78        let render_instance = render_instance.0.as_ref().read().unwrap();
79
80        // Create the texture with mip levels
81        let texture = wde_wgpu::texture::Texture::new(
82            &render_instance,
83            &asset.label,
84            (asset.size.0, asset.size.1),
85            asset.format,
86            asset.usages,
87            asset.sample_count,
88            asset.layer_count,
89            asset.mip_level_count
90        );
91
92        // Copy the texture data
93        if !asset.data.is_empty() {
94            texture.copy_from_buffer(&render_instance, asset.format, &asset.data);
95        }
96        Ok(GpuTexture {
97            label: asset.label,
98            texture
99        })
100    }
101
102    fn label(&self) -> &str {
103        &self.label
104    }
105}
106
107/// Settings for loading a texture asset while creating a [`Texture`].
108#[derive(Serialize, Deserialize)]
109pub struct TextureLoaderSettings {
110    /// Human readable identifier applied to the GPU resource label. This is not necessarily unique.
111    pub label: String,
112    /// GPU texture format requested when allocating the resource. Defaults to `Rgba8Unorm`.
113    pub format: TextureFormat,
114    /// GPU usage flags (sampling, storage, copy, etc.). Defaults to `TEXTURE_BINDING` (sampled texture).
115    pub usages: TextureUsages
116}
117impl Default for TextureLoaderSettings {
118    fn default() -> Self {
119        Self {
120            label: "Unknown Texture".to_string(),
121            format: TextureFormat::Rgba8Unorm,
122            usages: TextureUsages::TEXTURE_BINDING
123        }
124    }
125}
126
127#[derive(Debug, Error)]
128pub(crate) enum TextureLoaderError {
129    #[error("Could not load texture: {0}")]
130    Io(#[from] std::io::Error)
131}
132#[derive(Default, TypePath)]
133pub(crate) struct TextureLoader;
134impl AssetLoader for TextureLoader {
135    type Asset = Texture;
136    type Settings = TextureLoaderSettings;
137    type Error = TextureLoaderError;
138
139    async fn load(
140        &self,
141        reader: &mut dyn Reader,
142        settings: &TextureLoaderSettings,
143        load_context: &mut LoadContext<'_>
144    ) -> Result<Self::Asset, Self::Error> {
145        debug!("Loading texture {}.", load_context.path());
146
147        // Read the texture data bytes
148        let mut bytes = Vec::new();
149        reader.read_to_end(&mut bytes).await?;
150
151        // Load the image
152        let image = match image::load_from_memory(&bytes) {
153            Ok(image) => image,
154            Err(err) => {
155                error!("Could not load texture: {}", err);
156                return Err(TextureLoaderError::Io(Error::new(
157                    ErrorKind::InvalidData,
158                    err
159                )));
160            }
161        };
162        let size = image.dimensions();
163
164        // Convert to right format pixel size
165        let format_properties = get_format_properties(settings.format).unwrap();
166        let data = match format_properties.0 {
167            8 => from_channels(&image.into_rgba8(), format_properties.1),
168            16 => bytemuck::cast_slice(&from_channels(&image.into_rgba16(), format_properties.1))
169                .to_vec(),
170            21 => bytemuck::cast_slice(&from_channels(&image.into_rgba32f(), format_properties.1))
171                .to_vec(),
172            _ => unreachable!()
173        };
174
175        Ok(Texture {
176            label: settings.label.clone(),
177            format: settings.format,
178            usages: settings.usages,
179            size,
180            sample_count: 1,
181            layer_count: 1,
182            mip_level_count: 1,
183            data
184        })
185    }
186
187    fn extensions(&self) -> &[&str] {
188        &["png", "jpg"]
189    }
190}
191
192/// Get the properties of a texture format.
193/// - `None` if the format is not supported.
194/// - `Some` with the properties of the format:
195///    - `bits`: Can be 8 bits, 16 bits or 32 bits.
196///    - `channels`: The number of channels in the format (1 to 4).
197fn get_format_properties(texture_format: TextureFormat) -> Option<(u32, u32)> {
198    match texture_format {
199        TextureFormat::R8Unorm
200        | TextureFormat::R8Uint
201        | TextureFormat::R8Snorm
202        | TextureFormat::R8Sint => Some((8, 1)),
203        TextureFormat::R16Unorm
204        | TextureFormat::R16Uint
205        | TextureFormat::R16Snorm
206        | TextureFormat::R16Sint
207        | TextureFormat::R16Float => Some((16, 1)),
208        TextureFormat::R32Uint | TextureFormat::R32Sint | TextureFormat::R32Float => Some((32, 1)),
209        TextureFormat::Rg8Unorm
210        | TextureFormat::Rg8Uint
211        | TextureFormat::Rg8Snorm
212        | TextureFormat::Rg8Sint => Some((8, 2)),
213        TextureFormat::Rg16Unorm
214        | TextureFormat::Rg16Uint
215        | TextureFormat::Rg16Snorm
216        | TextureFormat::Rg16Sint
217        | TextureFormat::Rg16Float => Some((16, 2)),
218        TextureFormat::Rg32Uint | TextureFormat::Rg32Sint | TextureFormat::Rg32Float => {
219            Some((32, 2))
220        }
221        TextureFormat::Rgba8Unorm
222        | TextureFormat::Rgba8UnormSrgb
223        | TextureFormat::Rgba8Uint
224        | TextureFormat::Rgba8Snorm
225        | TextureFormat::Rgba8Sint => Some((8, 4)),
226        TextureFormat::Rgba16Unorm
227        | TextureFormat::Rgba16Uint
228        | TextureFormat::Rgba16Snorm
229        | TextureFormat::Rgba16Sint
230        | TextureFormat::Rgba16Float => Some((16, 4)),
231        TextureFormat::Rgba32Uint | TextureFormat::Rgba32Sint | TextureFormat::Rgba32Float => {
232            Some((32, 4))
233        }
234        _ => None
235    }
236}
237
238/// Convert an image to a pixel buffer.
239fn from_channels<T: Clone + Copy + bytemuck::NoUninit + bytemuck::Pod>(
240    data: &[T],
241    channels: u32
242) -> Vec<T> {
243    let inv_channels = [4, 3, 2, 1];
244    if channels == 4 {
245        return data.to_vec();
246    }
247    let inv_channel = inv_channels[channels as usize - 1];
248
249    // Extract channels
250    let mut buffer: Vec<T> = Vec::with_capacity(data.len() / inv_channel as usize);
251    for i in 0..data.len() / inv_channel as usize {
252        buffer.push(data[i * inv_channel as usize]);
253    }
254    buffer
255}