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