swamp_wgpu_sprites/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/swamp
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use bytemuck::{Pod, Zeroable};
6use image::RgbaImage;
7use limnus_wgpu_math::{Matrix4, Vec4};
8use wgpu::BufferBindingType;
9use wgpu::util::DeviceExt;
10use wgpu::{
11    BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
12    BindGroupLayoutEntry, BindingType, Buffer, BufferAddress, BufferDescriptor, BufferUsages,
13    Device, Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, PipelineLayout,
14    PipelineLayoutDescriptor, Queue, RenderPipeline, RenderPipelineDescriptor, Sampler,
15    SamplerBindingType, ShaderModule, ShaderStages, Texture, TextureAspect, TextureDescriptor,
16    TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureViewDescriptor,
17    TextureViewDimension, VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode,
18};
19use wgpu::{BindingResource, PipelineCompilationOptions};
20use wgpu::{
21    BlendState, ColorTargetState, ColorWrites, Face, FrontFace, MultisampleState, PolygonMode,
22    PrimitiveState, PrimitiveTopology, util,
23};
24
25#[repr(C)]
26#[derive(Copy, Clone, Debug)]
27struct Vertex {
28    position: [f32; 2],   // 2D position of the vertex
29    tex_coords: [f32; 2], // Texture coordinates
30}
31
32// Implement Zeroable manually
33unsafe impl Zeroable for Vertex {}
34
35// Implement Pod manually
36unsafe impl Pod for Vertex {}
37
38impl Vertex {
39    const ATTRIBUTES: [VertexAttribute; 2] =
40        wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2];
41
42    pub const fn desc() -> VertexBufferLayout<'static> {
43        VertexBufferLayout {
44            array_stride: size_of::<Self>() as BufferAddress,
45            step_mode: VertexStepMode::Vertex,
46            attributes: &Self::ATTRIBUTES,
47        }
48    }
49}
50
51/// Buffer that stores a model and texture coordinates for one sprite
52// model: Mx4
53// tex_coords: V4
54#[must_use]
55pub fn create_sprite_uniform_buffer(device: &Device, label: &str) -> Buffer {
56    device.create_buffer_init(&util::BufferInitDescriptor {
57        label: Some(label),
58        contents: bytemuck::cast_slice(&[SpriteInstanceUniform {
59            model: Matrix4::identity(),
60            tex_coords_mul_add: Vec4([0.0, 0.0, 1.0, 1.0]),
61            rotation: 0,
62            color: Vec4([1.0, 0.0, 1.0, 1.0]),
63            use_texture: 0,
64        }]),
65        usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
66    })
67}
68
69#[repr(C)]
70#[derive(Copy, Clone)]
71pub struct CameraUniform {
72    pub view_proj: Matrix4,
73}
74
75unsafe impl Pod for CameraUniform {}
76unsafe impl Zeroable for CameraUniform {}
77
78#[repr(C)]
79#[derive(Copy, Clone)]
80pub struct SpriteInstanceUniform {
81    pub model: Matrix4, // Model Transformation matrix
82    pub tex_coords_mul_add: Vec4,
83    pub rotation: u32,
84    pub color: Vec4,
85    pub use_texture: u32,
86}
87
88unsafe impl Pod for SpriteInstanceUniform {}
89unsafe impl Zeroable for SpriteInstanceUniform {}
90
91impl SpriteInstanceUniform {
92    #[must_use]
93    pub const fn new(
94        model: Matrix4,
95        tex_coords_mul_add: Vec4,
96        rotation: u32,
97        color: Vec4,
98        use_texture: bool,
99    ) -> Self {
100        Self {
101            model,
102            tex_coords_mul_add,
103            rotation,
104            color,
105            use_texture: use_texture as u32,
106        }
107    }
108}
109
110impl SpriteInstanceUniform {
111    const fn desc<'a>() -> VertexBufferLayout<'a> {
112        VertexBufferLayout {
113            array_stride: size_of::<Self>() as BufferAddress,
114            step_mode: VertexStepMode::Instance,
115            attributes: &[
116                // Model Matrix. There is unfortunately no Matrix type, so you have to define it as 4 attributes of Float32x4.
117                VertexAttribute {
118                    offset: 0,
119                    shader_location: 2,
120                    format: VertexFormat::Float32x4,
121                },
122                VertexAttribute {
123                    offset: 16,
124                    shader_location: 3,
125                    format: VertexFormat::Float32x4,
126                },
127                VertexAttribute {
128                    offset: 32,
129                    shader_location: 4,
130                    format: VertexFormat::Float32x4,
131                },
132                VertexAttribute {
133                    offset: 48,
134                    shader_location: 5,
135                    format: VertexFormat::Float32x4,
136                },
137                // Texture multiplier and add
138                VertexAttribute {
139                    offset: 64,
140                    shader_location: 6,
141                    format: VertexFormat::Float32x4,
142                },
143                // Rotation
144                VertexAttribute {
145                    offset: 80,
146                    shader_location: 7,
147                    format: VertexFormat::Uint32,
148                },
149                // color (RGBA)
150                VertexAttribute {
151                    offset: 84,
152                    shader_location: 8,
153                    format: VertexFormat::Float32x4,
154                },
155                // use_texture (bool)
156                VertexAttribute {
157                    offset: 84 + 4 * 4,
158                    shader_location: 9,
159                    format: VertexFormat::Uint32,
160                },
161            ],
162        }
163    }
164}
165
166// wgpu has, for very unknown reasons, put coordinate texture origo at top-left(!)
167const RIGHT: f32 = 1.0;
168const DOWN: f32 = 1.0;
169
170const IDENTITY_QUAD_VERTICES: &[Vertex] = &[
171    Vertex {
172        position: [0.0, 0.0],
173        tex_coords: [0.0, DOWN],
174    }, // Bottom left
175    Vertex {
176        position: [1.0, 0.0],
177        tex_coords: [RIGHT, DOWN],
178    }, // Bottom right
179    Vertex {
180        position: [1.0, 1.0],
181        tex_coords: [RIGHT, 0.0],
182    }, // Top right
183    Vertex {
184        position: [0.0, 1.0],
185        tex_coords: [0.0, 0.0],
186    }, // Top left
187];
188
189// u16 is the smallest index buffer supported by wgpu // IndexFormat
190pub const INDICES: &[u16] = &[0, 1, 2, 0, 2, 3];
191
192#[derive(Debug)]
193pub struct SpriteInfo {
194    pub sprite_pipeline: RenderPipeline,
195
196    pub sampler: Sampler,
197    pub vertex_buffer: Buffer,
198    pub index_buffer: Buffer,
199
200    // Camera - Group 0
201    pub camera_bind_group_layout: BindGroupLayout,
202
203    pub camera_uniform_buffer: Buffer,
204    pub camera_bind_group: BindGroup,
205
206    // Texture and Sampler - Group 1
207    pub sprite_texture_sampler_bind_group_layout: BindGroupLayout,
208
209    // Vertex Instances - Group 1
210    pub quad_matrix_and_uv_instance_buffer: Buffer,
211}
212
213const MAX_RENDER_SPRITE_COUNT: usize = 10_000;
214
215impl SpriteInfo {
216    #[must_use]
217    pub fn new(
218        device: &Device,
219        surface_texture_format: TextureFormat,
220        vertex_shader_source: &str,
221        fragment_shader_source: &str,
222        view_proj_matrix: Matrix4,
223    ) -> Self {
224        let vertex_shader =
225            swamp_wgpu::create_shader_module(device, "sprite vertex", vertex_shader_source);
226        let fragment_shader =
227            swamp_wgpu::create_shader_module(device, "sprite fragment", fragment_shader_source);
228
229        let index_buffer = create_sprite_index_buffer(device, "identity quad index buffer");
230        let vertex_buffer = create_sprite_vertex_buffer(device, "identity quad vertex buffer");
231
232        // ------------------------------- Camera View Projection Matrix in Group 0 --------------------------
233        let camera_uniform_buffer = create_camera_uniform_buffer(
234            device,
235            view_proj_matrix,
236            "view and projection matrix (camera)",
237        );
238
239        let camera_bind_group_layout =
240            create_camera_uniform_bind_group_layout(device, "camera bind group layout");
241
242        let camera_bind_group = create_camera_uniform_bind_group(
243            device,
244            &camera_bind_group_layout,
245            &camera_uniform_buffer,
246            "camera matrix",
247        );
248
249        // -------------------------- Texture and Sampler in Group 1 -----------------------------------------------
250        let sprite_texture_sampler_bind_group_layout = create_sprite_texture_sampler_group_layout(
251            device,
252            "texture and sampler bind group layout",
253        );
254
255        // -------------------------- Sprite Instance in Group 2 -----------------------------------------------
256        let quad_matrix_and_uv_instance_buffer = create_quad_matrix_and_uv_instance_buffer(
257            device,
258            MAX_RENDER_SPRITE_COUNT,
259            "sprite_instance buffer",
260        );
261
262        let sprite_pipeline_layout = create_sprite_pipeline_layout(
263            device,
264            &camera_bind_group_layout,
265            &sprite_texture_sampler_bind_group_layout,
266            "sprite pipeline layout",
267        );
268
269        let sprite_pipeline = create_sprite_pipeline(
270            device,
271            surface_texture_format,
272            &sprite_pipeline_layout,
273            &vertex_shader,
274            &fragment_shader,
275        );
276
277        let sampler = swamp_wgpu::create_nearest_sampler(device, "sprite nearest sampler");
278
279        Self {
280            sprite_pipeline,
281            sampler,
282            vertex_buffer,
283            index_buffer,
284            camera_bind_group_layout,
285            camera_uniform_buffer,
286            camera_bind_group,
287            sprite_texture_sampler_bind_group_layout,
288            quad_matrix_and_uv_instance_buffer,
289        }
290    }
291}
292
293/// Creates the view - projection matrix (Camera)
294fn create_camera_uniform_buffer(device: &Device, view_proj: Matrix4, label: &str) -> Buffer {
295    let camera_uniform = CameraUniform { view_proj };
296
297    device.create_buffer_init(&util::BufferInitDescriptor {
298        label: Some(label),
299        contents: bytemuck::cast_slice(&[camera_uniform]),
300        usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
301    })
302}
303
304/// Camera is just one binding, the view projection camera matrix
305fn create_camera_uniform_bind_group_layout(device: &Device, label: &str) -> BindGroupLayout {
306    device.create_bind_group_layout(&BindGroupLayoutDescriptor {
307        label: Some(label),
308        entries: &[BindGroupLayoutEntry {
309            binding: 0,
310            visibility: ShaderStages::VERTEX,
311            ty: BindingType::Buffer {
312                ty: BufferBindingType::Uniform,
313                has_dynamic_offset: false,
314                min_binding_size: None,
315            },
316            count: None,
317        }],
318    })
319}
320
321fn create_camera_uniform_bind_group(
322    device: &Device,
323    bind_group_layout: &BindGroupLayout,
324    uniform_buffer: &Buffer,
325    label: &str,
326) -> BindGroup {
327    device.create_bind_group(&BindGroupDescriptor {
328        label: Some(label),
329        layout: bind_group_layout,
330        entries: &[BindGroupEntry {
331            binding: 0,
332            resource: uniform_buffer.as_entire_binding(),
333        }],
334    })
335}
336
337#[must_use]
338pub fn load_texture_from_memory(
339    device: &Device,
340    queue: &Queue,
341    img: &RgbaImage,
342    label: &str,
343) -> Texture {
344    let (width, height) = img.dimensions();
345
346    // Create the texture and upload the data (same as before)
347    let texture = device.create_texture(&TextureDescriptor {
348        label: Some(label),
349        size: Extent3d {
350            width,
351            height,
352            depth_or_array_layers: 1,
353        },
354        mip_level_count: 1,
355        sample_count: 1,
356        dimension: TextureDimension::D2,
357        format: TextureFormat::Rgba8UnormSrgb,
358        usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
359        view_formats: &[TextureFormat::Rgba8UnormSrgb],
360    });
361
362    queue.write_texture(
363        ImageCopyTexture {
364            texture: &texture,
365            mip_level: 0,
366            origin: Origin3d::ZERO,
367            aspect: TextureAspect::All,
368        },
369        img,
370        ImageDataLayout {
371            offset: 0,
372            bytes_per_row: Some(4 * width),
373            rows_per_image: Some(height),
374        },
375        Extent3d {
376            width,
377            height,
378            depth_or_array_layers: 1,
379        },
380    );
381
382    texture
383}
384
385#[must_use]
386pub fn create_sprite_vertex_buffer(device: &Device, label: &str) -> Buffer {
387    device.create_buffer_init(&util::BufferInitDescriptor {
388        label: Some(label),
389        contents: bytemuck::cast_slice(IDENTITY_QUAD_VERTICES),
390        usage: BufferUsages::VERTEX,
391    })
392}
393
394#[must_use]
395pub fn create_sprite_index_buffer(device: &Device, label: &str) -> Buffer {
396    device.create_buffer_init(&util::BufferInitDescriptor {
397        label: Some(label),
398        contents: bytemuck::cast_slice(INDICES),
399        usage: BufferUsages::INDEX,
400    })
401}
402
403/// Binding0: Texture
404/// Binding1: Sampler
405#[must_use]
406pub fn create_sprite_texture_sampler_group_layout(device: &Device, label: &str) -> BindGroupLayout {
407    device.create_bind_group_layout(&BindGroupLayoutDescriptor {
408        label: Some(label),
409        entries: &[
410            BindGroupLayoutEntry {
411                binding: 0,
412                visibility: ShaderStages::FRAGMENT,
413                ty: BindingType::Texture {
414                    multisampled: false,
415                    view_dimension: TextureViewDimension::D2,
416                    sample_type: TextureSampleType::Float { filterable: true },
417                },
418                count: None,
419            },
420            BindGroupLayoutEntry {
421                binding: 1,
422                visibility: ShaderStages::FRAGMENT,
423                ty: BindingType::Sampler(SamplerBindingType::Filtering),
424                count: None,
425            },
426        ],
427    })
428}
429
430#[must_use]
431pub fn create_sprite_texture_and_sampler_bind_group(
432    device: &Device,
433    bind_group_layout: &BindGroupLayout,
434    texture: &Texture,
435    sampler: &Sampler,
436    label: &str,
437) -> BindGroup {
438    let texture_view = texture.create_view(&TextureViewDescriptor::default());
439    device.create_bind_group(&BindGroupDescriptor {
440        layout: bind_group_layout,
441        entries: &[
442            BindGroupEntry {
443                binding: 0,
444                resource: BindingResource::TextureView(&texture_view),
445            },
446            BindGroupEntry {
447                binding: 1,
448                resource: BindingResource::Sampler(sampler),
449            },
450        ],
451        label: Some(label),
452    })
453}
454
455#[must_use]
456pub fn create_quad_matrix_and_uv_instance_buffer(
457    device: &Device,
458    max_instances: usize,
459    label: &str,
460) -> Buffer {
461    let buffer_size = (size_of::<SpriteInstanceUniform>() * max_instances) as BufferAddress;
462
463    device.create_buffer(&BufferDescriptor {
464        label: Some(label),
465        size: buffer_size,
466        usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
467        mapped_at_creation: false,
468    })
469}
470
471fn create_sprite_pipeline_layout(
472    device: &Device,
473    camera_bind_group_layout: &BindGroupLayout,
474    texture_sampler_group_layout: &BindGroupLayout,
475    label: &str,
476) -> PipelineLayout {
477    device.create_pipeline_layout(&PipelineLayoutDescriptor {
478        label: Some(label),
479        bind_group_layouts: &[camera_bind_group_layout, texture_sampler_group_layout],
480        push_constant_ranges: &[],
481    })
482}
483
484fn create_sprite_pipeline(
485    device: &Device,
486    format: TextureFormat,
487    pipeline_layout: &PipelineLayout,
488    vertex_shader: &ShaderModule,
489    fragment_shader: &ShaderModule,
490) -> RenderPipeline {
491    device.create_render_pipeline(&RenderPipelineDescriptor {
492        label: Some("sprite alpha blend pipeline"),
493        layout: Some(pipeline_layout),
494        vertex: wgpu::VertexState {
495            module: vertex_shader,
496            entry_point: Some("vs_main"),
497            compilation_options: PipelineCompilationOptions::default(),
498            buffers: &[Vertex::desc(), SpriteInstanceUniform::desc()],
499        },
500        fragment: Some(wgpu::FragmentState {
501            module: fragment_shader,
502            entry_point: Some("fs_main"),
503            compilation_options: PipelineCompilationOptions::default(),
504            targets: &[Some(ColorTargetState {
505                format,
506                blend: Some(BlendState::ALPHA_BLENDING),
507                write_mask: ColorWrites::ALL,
508            })],
509        }),
510        primitive: PrimitiveState {
511            topology: PrimitiveTopology::TriangleList,
512            strip_index_format: None,
513            front_face: FrontFace::Ccw,
514            cull_mode: Some(Face::Back),
515            unclipped_depth: false,
516            polygon_mode: PolygonMode::Fill,
517            conservative: false,
518        },
519
520        depth_stencil: None,
521        multisample: MultisampleState::default(),
522        multiview: None,
523        cache: None,
524    })
525}