Skip to main content

flow_ngin/data_structures/
texture.rs

1//! GPU textures and texture creation utilities.
2//!
3//! This module provides [`Texture`], a wrapper around WGPU GPU texture resources,
4//! and helper methods for creating depth textures, normal maps, and loading textures
5//! from image data.
6
7use anyhow::*;
8use image::{GenericImageView, ImageFormat, load_from_memory_with_format};
9
10use crate::pipelines::mipmapper::Mipmapper;
11
12/// A GPU texture with a view and optional sampler.
13///
14/// Wraps WGPU texture objects along with associated views and samplers.
15/// Textures are used for color maps, normal maps, depth, and other data
16/// bound to shaders. Typically created via [`from_bytes`](Self::from_bytes) or
17/// via [`create_depth_texture`](Self::create_depth_texture).
18#[derive(Clone, Debug)]
19pub struct Texture {
20    #[allow(unused)]
21    pub texture: wgpu::Texture,
22    pub view: wgpu::TextureView,
23    pub sampler: Option<wgpu::Sampler>,
24}
25
26impl Texture {
27    /// Standard depth buffer texture format (32-bit float).
28    pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
29
30    /// Create a depth texture for depth-testing during rendering.
31    ///
32    /// Depth textures are required for proper depth-testing to determine which objects
33    /// are in front of others. The returned texture is suitable for use as a
34    /// `RENDER_ATTACHMENT` in render passes.
35    ///
36    /// # Arguments
37    ///
38    /// * `size` is [width, height] of the texture in pixels
39    /// * `label` is used as a debug label for the GPU resource
40    pub fn create_msaa_texture(
41        device: &wgpu::Device,
42        config: &wgpu::SurfaceConfiguration,
43        sample_count: u32,
44    ) -> wgpu::TextureView {
45        let texture = device.create_texture(&wgpu::TextureDescriptor {
46            label: Some("MSAA Color Texture"),
47            size: wgpu::Extent3d {
48                width: config.width.max(1),
49                height: config.height.max(1),
50                depth_or_array_layers: 1,
51            },
52            mip_level_count: 1,
53            sample_count,
54            dimension: wgpu::TextureDimension::D2,
55            format: config.format,
56            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
57            view_formats: &[],
58        });
59        texture.create_view(&wgpu::TextureViewDescriptor::default())
60    }
61
62    pub fn create_depth_texture(device: &wgpu::Device, size: [u32; 2], label: &str, sample_count: u32) -> Self {
63        let size = wgpu::Extent3d {
64            width: size[0].max(1),
65            height: size[1].max(1),
66            depth_or_array_layers: 1,
67        };
68        let desc = wgpu::TextureDescriptor {
69            label: Some(label),
70            size,
71            mip_level_count: 1,
72            sample_count,
73            dimension: wgpu::TextureDimension::D2,
74            format: Self::DEPTH_FORMAT,
75            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
76            view_formats: &[Self::DEPTH_FORMAT],
77        };
78        let texture = device.create_texture(&desc);
79        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
80        let sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor {
81            address_mode_u: wgpu::AddressMode::Repeat,
82            address_mode_v: wgpu::AddressMode::Repeat,
83            address_mode_w: wgpu::AddressMode::Repeat,
84            mag_filter: wgpu::FilterMode::Linear,
85            min_filter: wgpu::FilterMode::Linear,
86            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
87            compare: Some(wgpu::CompareFunction::LessEqual),
88            lod_min_clamp: 0.0,
89            lod_max_clamp: 100.0,
90            ..Default::default()
91        }));
92
93        Self {
94            texture,
95            view,
96            sampler,
97        }
98    }
99
100    /// Create a default normal map (neutral blue, representing no deformation).
101    ///
102    /// Returns a solid blue texture suitable as a default when no normal map is provided.
103    /// This avoids the need to change shaders when normal maps are optional.
104    pub fn create_default_normal_map(
105        width: u32,
106        height: u32,
107        device: &wgpu::Device,
108        queue: &wgpu::Queue,
109    ) -> Texture {
110        let size = wgpu::Extent3d {
111            width: width,
112            height: height,
113            depth_or_array_layers: 1,
114        };
115
116        // The blue/purple-ish colour that represents the default for normal maps
117        let data: Vec<u8> = [127, 127, 255, 255]
118            .iter()
119            .cycle()
120            .take(width as usize * height as usize * 4)
121            .map(|&u| u)
122            .collect();
123
124        let texture = device.create_texture(&wgpu::TextureDescriptor {
125            label: Some("default normal map"),
126            size,
127            mip_level_count: 1,
128            sample_count: 1,
129            dimension: wgpu::TextureDimension::D2,
130            format: wgpu::TextureFormat::Rgba8Unorm,
131            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
132            view_formats: &[],
133        });
134
135        queue.write_texture(
136            wgpu::TexelCopyTextureInfo {
137                aspect: wgpu::TextureAspect::All,
138                texture: &texture,
139                mip_level: 0,
140                origin: wgpu::Origin3d::ZERO,
141            },
142            &data,
143            wgpu::TexelCopyBufferLayout {
144                offset: 0,
145                bytes_per_row: Some(width * 4),
146                rows_per_image: Some(height),
147            },
148            size,
149        );
150
151        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
152        let sampler = Some(create_default_sampler(device));
153        Texture {
154            texture,
155            view,
156            sampler,
157        }
158    }
159
160    /// Load a texture from raw byte data (image file contents).
161    ///
162    /// # Arguments
163    ///
164    /// * `bytes` represent raw image file data (PNG, JPEG, etc.)
165    /// * `label` is used as a debug name for the GPU resource
166    /// * `format`  is an optional file format hint (e.g., "png"). If None, auto-detect.
167    /// * `is_normal_map` toggles between sRGB (false) and linear (true) color space
168    pub fn from_bytes(
169        device: &wgpu::Device,
170        queue: &wgpu::Queue,
171        bytes: &[u8],
172        label: &str,
173        format: Option<&str>,
174        is_normal_map: bool,
175    ) -> Result<Self> {
176        let img = match format {
177            None => image::load_from_memory(bytes)?,
178            Some(fmt) => {
179                load_from_memory_with_format(bytes, ImageFormat::from_extension(fmt).unwrap())?
180            }
181        };
182        Self::from_image(device, queue, &img, Some(label), is_normal_map)
183    }
184
185    /// Create a 1×1 solid-colour texture from a raw RGBA byte array.
186    pub fn from_color(rgba: [u8; 4], device: &wgpu::Device, queue: &wgpu::Queue) -> Texture {
187        let size = wgpu::Extent3d {
188            width: 1,
189            height: 1,
190            depth_or_array_layers: 1,
191        };
192
193        let texture = device.create_texture(&wgpu::TextureDescriptor {
194            label: Some("solid color texture"),
195            size,
196            mip_level_count: 1,
197            sample_count: 1,
198            dimension: wgpu::TextureDimension::D2,
199            format: wgpu::TextureFormat::Rgba8UnormSrgb,
200            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
201            view_formats: &[],
202        });
203
204        queue.write_texture(
205            wgpu::TexelCopyTextureInfo {
206                aspect: wgpu::TextureAspect::All,
207                texture: &texture,
208                mip_level: 0,
209                origin: wgpu::Origin3d::ZERO,
210            },
211            &rgba,
212            wgpu::TexelCopyBufferLayout {
213                offset: 0,
214                bytes_per_row: Some(4),
215                rows_per_image: Some(1),
216            },
217            size,
218        );
219
220        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
221        let sampler = Some(create_default_sampler(device));
222        Texture {
223            texture,
224            view,
225            sampler,
226        }
227    }
228
229    pub fn from_image(
230        device: &wgpu::Device,
231        queue: &wgpu::Queue,
232        img: &image::DynamicImage,
233        label: Option<&str>,
234        is_normal_map: bool,
235    ) -> Result<Self> {
236        let dimensions = img.dimensions();
237        let rgba = img.to_rgba8();
238
239        let mip_level_count = dimensions.0.min(dimensions.1).max(1).ilog2() + 1;
240
241        let size = wgpu::Extent3d {
242            width: dimensions.0,
243            height: dimensions.1,
244            depth_or_array_layers: 1,
245        };
246        let format = if is_normal_map {
247            wgpu::TextureFormat::Rgba8Unorm
248        } else {
249            wgpu::TextureFormat::Rgba8UnormSrgb
250        };
251        let texture = device.create_texture(&wgpu::TextureDescriptor {
252            label,
253            size,
254            mip_level_count,
255            sample_count: 1,
256            dimension: wgpu::TextureDimension::D2,
257            format,
258            usage: wgpu::TextureUsages::TEXTURE_BINDING
259                | wgpu::TextureUsages::COPY_DST
260                | wgpu::TextureUsages::COPY_SRC,
261            view_formats: &[],
262        });
263
264        queue.write_texture(
265            wgpu::TexelCopyTextureInfo {
266                aspect: wgpu::TextureAspect::All,
267                texture: &texture,
268                mip_level: 0,
269                origin: wgpu::Origin3d::ZERO,
270            },
271            &rgba,
272            wgpu::TexelCopyBufferLayout {
273                offset: 0,
274                bytes_per_row: Some(4 * dimensions.0),
275                rows_per_image: Some(dimensions.1),
276            },
277            size,
278        );
279
280        let mipmapper = Mipmapper::new(device);
281        mipmapper.generate_mipmaps(device, queue, &texture)?;
282
283        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
284        let sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor {
285            address_mode_u: wgpu::AddressMode::Repeat,
286            address_mode_v: wgpu::AddressMode::Repeat,
287            address_mode_w: wgpu::AddressMode::Repeat,
288            mag_filter: wgpu::FilterMode::Linear,
289            min_filter: wgpu::FilterMode::Linear,
290            mipmap_filter: wgpu::MipmapFilterMode::Linear,
291            ..Default::default()
292        }));
293
294        Ok(Self {
295            texture,
296            view,
297            sampler,
298        })
299    }
300}
301
302pub fn create_default_sampler(device: &wgpu::Device) -> wgpu::Sampler {
303    device.create_sampler(&wgpu::SamplerDescriptor {
304        address_mode_u: wgpu::AddressMode::Repeat,
305        address_mode_v: wgpu::AddressMode::Repeat,
306        address_mode_w: wgpu::AddressMode::Repeat,
307        mag_filter: wgpu::FilterMode::Linear,
308        min_filter: wgpu::FilterMode::Linear,
309        mipmap_filter: wgpu::MipmapFilterMode::Linear,
310        ..Default::default()
311    })
312}