Skip to main content

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