1use wgpu::util::DeviceExt;
2
3pub const SPRITE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6pub enum SpriteFilter {
7 #[default]
8 Linear,
9 Nearest,
10}
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct SpriteImage {
14 pub width: u32,
15 pub height: u32,
16 pub rgba: Vec<u8>,
17 pub filter: SpriteFilter,
18}
19
20impl SpriteImage {
21 pub fn new(
22 width: u32,
23 height: u32,
24 rgba: Vec<u8>,
25 filter: SpriteFilter,
26 ) -> Result<Self, String> {
27 if width == 0 || height == 0 {
28 return Err("a sprite atlas needs a size".into());
29 }
30 if rgba.len() != width as usize * height as usize * 4 {
31 return Err(format!(
32 "a {width}x{height} sprite atlas needs {} bytes, not {}",
33 width as usize * height as usize * 4,
34 rgba.len()
35 ));
36 }
37 Ok(Self {
38 width,
39 height,
40 rgba,
41 filter,
42 })
43 }
44
45 fn texel(&self, x: i64, y: i64) -> [f32; 4] {
46 let x = x.clamp(0, i64::from(self.width) - 1) as usize;
47 let y = y.clamp(0, i64::from(self.height) - 1) as usize;
48 let at = (y * self.width as usize + x) * 4;
49 std::array::from_fn(|channel| f32::from(self.rgba[at + channel]) / 255.0)
50 }
51
52 pub fn sample(&self, uv: [f32; 2]) -> [f32; 4] {
53 let x = uv[0] * self.width as f32;
54 let y = uv[1] * self.height as f32;
55 match self.filter {
56 SpriteFilter::Nearest => self.texel(x.floor() as i64, y.floor() as i64),
57 SpriteFilter::Linear => {
58 let fx = x - 0.5;
59 let fy = y - 0.5;
60 let x0 = fx.floor();
61 let y0 = fy.floor();
62 let tx = fx - x0;
63 let ty = fy - y0;
64 let (x0, y0) = (x0 as i64, y0 as i64);
65 let a = self.texel(x0, y0);
66 let b = self.texel(x0 + 1, y0);
67 let c = self.texel(x0, y0 + 1);
68 let d = self.texel(x0 + 1, y0 + 1);
69 std::array::from_fn(|k| {
70 let top = a[k] + (b[k] - a[k]) * tx;
71 let bottom = c[k] + (d[k] - c[k]) * tx;
72 top + (bottom - top) * ty
73 })
74 }
75 }
76 }
77}
78
79pub struct FlatSprites {
80 pub image: SpriteImage,
81 _texture: wgpu::Texture,
82 pub(crate) view: wgpu::TextureView,
83 pub(crate) sampler: wgpu::Sampler,
84}
85
86impl FlatSprites {
87 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, image: SpriteImage) -> Self {
88 let texture = device.create_texture_with_data(
89 queue,
90 &wgpu::TextureDescriptor {
91 label: Some("flat sprites"),
92 size: wgpu::Extent3d {
93 width: image.width,
94 height: image.height,
95 depth_or_array_layers: 1,
96 },
97 mip_level_count: 1,
98 sample_count: 1,
99 dimension: wgpu::TextureDimension::D2,
100 format: SPRITE_FORMAT,
101 usage: wgpu::TextureUsages::TEXTURE_BINDING,
102 view_formats: &[],
103 },
104 wgpu::util::TextureDataOrder::LayerMajor,
105 &image.rgba,
106 );
107 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
108 let sampler = sampler(device, image.filter);
109 Self {
110 image,
111 _texture: texture,
112 view,
113 sampler,
114 }
115 }
116
117 pub fn view(&self) -> &wgpu::TextureView {
118 &self.view
119 }
120}
121
122pub(crate) fn sampler(device: &wgpu::Device, filter: SpriteFilter) -> wgpu::Sampler {
123 let filter = match filter {
124 SpriteFilter::Linear => wgpu::FilterMode::Linear,
125 SpriteFilter::Nearest => wgpu::FilterMode::Nearest,
126 };
127 device.create_sampler(&wgpu::SamplerDescriptor {
128 label: Some("flat sprites"),
129 address_mode_u: wgpu::AddressMode::ClampToEdge,
130 address_mode_v: wgpu::AddressMode::ClampToEdge,
131 mag_filter: filter,
132 min_filter: filter,
133 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
134 ..Default::default()
135 })
136}