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