Skip to main content

flake/model/
texture.rs

1use crate::prelude::{update_texture, Rect, Vector2, WHITE, Image};
2use anyhow::*;
3use image::GenericImageView;
4use wgpu::*;
5
6/// # FilterType
7///
8/// Tells whether a texture should be drawn with pixelated (Nearest) or smooth (Linear) edges.
9/// Linear is the default filter type.
10///
11/// ## Examples
12///
13/// ```
14/// let pixelated_texture = load_texture("src/Cupcake.png").with_filter_type(FitlerType::Nearest);
15/// ```
16#[repr(C)]
17#[derive(Clone, Copy, Debug, Default)]
18pub enum FilterType {
19    /// No antialiasing, sharp pixelated edges
20    Nearest,
21
22    /// Antialiasing, smooth interpolated edges
23    #[default]
24    Linear,
25}
26
27impl FilterType {
28    pub fn to_wgpu_filter(&self) -> FilterMode {
29        match self {
30            FilterType::Nearest => FilterMode::Nearest,
31            FilterType::Linear => FilterMode::Linear,
32        }
33    }
34
35    pub fn to_wgpu_mipmap_filter(&self) -> MipmapFilterMode {
36        match self {
37            FilterType::Nearest => MipmapFilterMode::Nearest,
38            FilterType::Linear => MipmapFilterMode::Linear,
39        }
40    }
41}
42
43#[repr(C)]
44#[derive(Clone, Copy, Debug, Default)]
45pub struct Texture {
46    pub(crate) id: usize,
47    pub(crate) width: u32,
48    pub(crate) height: u32,
49}
50
51impl Texture {
52    pub fn width(&self) -> u32 {
53        self.width
54    }
55
56    pub fn height(&self) -> u32 {
57        self.height
58    }
59
60    pub fn update(&self, image: &Image) {
61        update_texture(*self, image);
62    }
63}
64
65pub(crate) struct TextureEntry {
66    pub gpu: GpuTexture,
67    pub bind_group: BindGroup,
68    pub premultiplied: bool,
69}
70
71#[derive(Clone, Debug)]
72pub struct GpuTexture {
73    #[allow(unused)]
74    pub texture: wgpu::Texture,
75    pub view: TextureView,
76    pub sampler: Sampler,
77}
78
79impl GpuTexture {
80    #[allow(unused)]
81    pub fn empty(
82        device: &Device,
83        width: u32,
84        height: u32,
85        filter_type: FilterType,
86        format: TextureFormat,
87    ) -> Result<Self> {
88        let size = Extent3d { width, height, depth_or_array_layers: 1 };
89
90        let texture = device.create_texture(&TextureDescriptor {
91            label: Some("render target"),
92            size,
93            mip_level_count: 1, // render attachments must be single-mip
94            sample_count: 1,
95            dimension: TextureDimension::D2,
96            format,             // was Rgba8UnormSrgb
97            usage: TextureUsages::TEXTURE_BINDING
98                | TextureUsages::COPY_DST
99                | TextureUsages::RENDER_ATTACHMENT,
100            view_formats: &[],
101        });
102
103        let filter_mode = filter_type.to_wgpu_filter();
104        let view = texture.create_view(&TextureViewDescriptor::default());
105        let sampler = device.create_sampler(&SamplerDescriptor {
106            address_mode_u: AddressMode::ClampToEdge,
107            address_mode_v: AddressMode::ClampToEdge,
108            address_mode_w: AddressMode::ClampToEdge,
109            mag_filter: filter_mode,
110            min_filter: filter_mode,
111            mipmap_filter: filter_type.to_wgpu_mipmap_filter(),
112            ..Default::default()
113        });
114
115        Ok(Self { texture, view, sampler })
116    }
117
118    #[allow(unused)]
119    pub fn from_bytes(
120        device: &Device,
121        queue: &Queue,
122        bytes: &[u8],
123        filter_type: FilterType,
124        label: &str,
125    ) -> Result<Self> {
126        let img = image::load_from_memory(bytes)?;
127        Self::from_image(device, queue, &img, filter_type, Some(label))
128    }
129
130    #[allow(unused)]
131    pub fn from_image(
132        device: &Device,
133        queue: &Queue,
134        img: &image::DynamicImage,
135        filter_type: FilterType,
136        label: Option<&str>,
137    ) -> Result<Self> {
138        let rgba = img.to_rgba8();
139        let dimensions = img.dimensions();
140
141        let size = Extent3d {
142            width: dimensions.0,
143            height: dimensions.1,
144            depth_or_array_layers: 1,
145        };
146        let mip_level_count = if matches!(filter_type, FilterType::Nearest) {
147            1 // Disable mipmaps for pixel art
148        } else {
149            dimensions.0.max(dimensions.1).ilog2() + 1
150        };
151        let texture = device.create_texture(&TextureDescriptor {
152            label,
153            size,
154            mip_level_count,
155            sample_count: 1,
156            dimension: TextureDimension::D2,
157            format: TextureFormat::Rgba8UnormSrgb,
158            usage: TextureUsages::TEXTURE_BINDING
159                | TextureUsages::COPY_DST
160                | TextureUsages::RENDER_ATTACHMENT,
161            view_formats: &[],
162        });
163
164        queue.write_texture(
165            TexelCopyTextureInfo {
166                aspect: TextureAspect::All,
167                texture: &texture,
168                mip_level: 0,
169                origin: Origin3d::ZERO,
170            },
171            &rgba,
172            TexelCopyBufferLayout {
173                offset: 0,
174                bytes_per_row: Some(4 * dimensions.0),
175                rows_per_image: Some(dimensions.1),
176            },
177            size,
178        );
179        Self::generate_mipmaps(device, queue, &texture, mip_level_count);
180
181        let filter_mode = filter_type.to_wgpu_filter();
182        let mipmap_filter = filter_type.to_wgpu_mipmap_filter();
183
184        let view = texture.create_view(&TextureViewDescriptor::default());
185        let sampler = device.create_sampler(&SamplerDescriptor {
186            address_mode_u: AddressMode::ClampToEdge,
187            address_mode_v: AddressMode::ClampToEdge,
188            address_mode_w: AddressMode::ClampToEdge,
189            mag_filter: filter_mode,
190            min_filter: filter_mode,
191            mipmap_filter,
192            ..Default::default()
193        });
194
195        Ok(Self {
196            texture,
197            view,
198            sampler,
199        })
200    }
201
202    pub fn generate_mipmaps(
203        device: &Device,
204        queue: &Queue,
205        texture: &wgpu::Texture,
206        mip_count: u32,
207    ) {
208        let shader = device.create_shader_module(include_wgsl!("../shaders/blit.wgsl"));
209
210        let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
211            label: Some("mip blit"),
212            layout: None,
213            vertex: VertexState {
214                module: &shader,
215                entry_point: Some("vs"),
216                buffers: &[],
217                compilation_options: Default::default(),
218            },
219            fragment: Some(FragmentState {
220                module: &shader,
221                entry_point: Some("fs"),
222                targets: &[Some(TextureFormat::Rgba8UnormSrgb.into())],
223                compilation_options: Default::default(),
224            }),
225            primitive: PrimitiveState::default(),
226            depth_stencil: None,
227            multisample: MultisampleState::default(),
228            multiview_mask: None,
229            cache: None,
230        });
231
232        let sampler = device.create_sampler(&SamplerDescriptor {
233            mag_filter: FilterMode::Linear,
234            min_filter: FilterMode::Linear,
235            ..Default::default()
236        });
237
238        let layout = pipeline.get_bind_group_layout(0);
239        let mut encoder = device.create_command_encoder(&Default::default());
240
241        for i in 1..mip_count {
242            let src = texture.create_view(&TextureViewDescriptor {
243                base_mip_level: i - 1,
244                mip_level_count: Some(1),
245                ..Default::default()
246            });
247            let dst = texture.create_view(&TextureViewDescriptor {
248                base_mip_level: i,
249                mip_level_count: Some(1),
250                ..Default::default()
251            });
252
253            let bind_group = device.create_bind_group(&BindGroupDescriptor {
254                label: None,
255                layout: &layout,
256                entries: &[
257                    BindGroupEntry {
258                        binding: 0,
259                        resource: BindingResource::TextureView(&src),
260                    },
261                    BindGroupEntry {
262                        binding: 1,
263                        resource: BindingResource::Sampler(&sampler),
264                    },
265                ],
266            });
267
268            let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
269                label: None,
270                color_attachments: &[Some(RenderPassColorAttachment {
271                    view: &dst,
272                    resolve_target: None,
273                    depth_slice: None,
274                    ops: Operations {
275                        load: LoadOp::Clear(Color::TRANSPARENT),
276                        store: StoreOp::Store,
277                    },
278                })],
279                ..Default::default()
280            });
281            pass.set_pipeline(&pipeline);
282            pass.set_bind_group(0, &bind_group, &[]);
283            pass.draw(0..3, 0..1);
284        }
285
286        queue.submit(std::iter::once(encoder.finish()));
287    }
288
289    /// # Depth Texture
290    /// Stores the depth of each pixel for depth testing.
291    pub const DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float;
292
293    pub fn create_depth_texture(
294        device: &Device,
295        width: u32,
296        height: u32,
297        label: &str,
298    ) -> Self {
299        let size = Extent3d {
300            width: width.max(1),
301            height: height.max(1),
302            depth_or_array_layers: 1,
303        };
304
305        let desc = TextureDescriptor {
306            label: Some(label),
307            size,
308            mip_level_count: 1,
309            sample_count: 4,
310            dimension: TextureDimension::D2,
311            format: Self::DEPTH_FORMAT,
312            usage: TextureUsages::RENDER_ATTACHMENT
313                | TextureUsages::TEXTURE_BINDING
314                | TextureUsages::COPY_DST,
315            view_formats: &[],
316        };
317
318        let texture = device.create_texture(&desc);
319        let view = texture.create_view(&TextureViewDescriptor::default());
320        let sampler = device.create_sampler(&SamplerDescriptor {
321            address_mode_u: AddressMode::ClampToEdge,
322            address_mode_v: AddressMode::ClampToEdge,
323            address_mode_w: AddressMode::ClampToEdge,
324            mag_filter: FilterMode::Linear,
325            min_filter: FilterMode::Linear,
326            mipmap_filter: MipmapFilterMode::Nearest,
327            compare: Some(CompareFunction::LessEqual),
328            lod_min_clamp: 0.0,
329            lod_max_clamp: 100.0,
330            ..Default::default()
331        });
332
333        Self {
334            texture,
335            view,
336            sampler,
337        }
338    }
339}
340
341/// # DrawTextureParams
342///
343/// Parameters used in `draw_texture_ex()` which give more flexibility to drawing textures, such as
344/// - How large the texture to be drawn is
345/// - The source rect to draw
346/// - Rotation
347/// - Flip on X
348/// - Flip on Y
349/// - Pivot
350pub struct DrawTextureParams {
351    pub dest_size: Option<Vector2>,
352    pub source: Option<Rect>, // x, y, w, h in pixels
353    pub rotation: f32,        // radians
354    pub flip_x: bool,
355    pub flip_y: bool,
356    pub pivot: Option<Vector2>, // world coords, default = center
357    pub color: crate::prelude::Color,
358}
359
360impl Default for DrawTextureParams {
361    fn default() -> Self {
362        Self {
363            dest_size: None,
364            source: None,
365            rotation: 0.0,
366            flip_x: false,
367            flip_y: false,
368            pivot: None,
369            color: WHITE,
370        }
371    }
372}