1use bytemuck::{Pod, Zeroable};
10use glam::Vec2;
11use wgpu::util::DeviceExt;
12
13use crate::camera::Camera;
14use crate::texture::Texture;
15
16const MAX_SPRITES: usize = 4096;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum BlendMode {
21 #[default]
22 Alpha,
23 Additive,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Rect {
29 pub x: f32,
30 pub y: f32,
31 pub w: f32,
32 pub h: f32,
33}
34
35impl Rect {
36 pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
37 Self { x, y, w, h }
38 }
39
40 pub fn from_pos_size(pos: Vec2, size: Vec2) -> Self {
42 Self {
43 x: pos.x,
44 y: pos.y,
45 w: size.x,
46 h: size.y,
47 }
48 }
49}
50
51#[repr(C)]
53#[derive(Copy, Clone, Debug, Pod, Zeroable)]
54pub struct SpriteInstance {
55 position: [f32; 2],
56 scale: [f32; 2],
57 color: [f32; 4],
58 uv_offset: [f32; 2], uv_size: [f32; 2], }
61
62impl SpriteInstance {
63 pub fn new(
64 position: Vec2,
65 scale: Vec2,
66 color: [f32; 4],
67 uv_offset: Vec2,
68 uv_size: Vec2,
69 ) -> Self {
70 Self {
71 position: position.to_array(),
72 scale: scale.to_array(),
73 color,
74 uv_offset: uv_offset.to_array(),
75 uv_size: uv_size.to_array(),
76 }
77 }
78}
79
80pub struct Sprite {
82 pub position: Vec2,
83 pub size: Vec2,
84 pub color: [f32; 4],
85 pub source_rect: Option<Rect>, pub flip_x: bool,
87 pub texture_id: usize, pub blend_mode: BlendMode,
89}
90
91impl Sprite {
93 pub fn new(position: Vec2, size: Vec2, color: [f32; 4]) -> Self {
94 Self {
95 position,
96 size,
97 color,
98 source_rect: None,
99 flip_x: false,
100 texture_id: 0, blend_mode: BlendMode::Alpha,
102 }
103 }
104
105 pub fn with_source(mut self, rect: Rect) -> Self {
106 self.source_rect = Some(rect);
107 self
108 }
109
110 pub fn with_flip(mut self, flip_x: bool) -> Self {
111 self.flip_x = flip_x;
112 self
113 }
114
115 pub fn with_texture_id(mut self, texture_id: usize) -> Self {
116 self.texture_id = texture_id;
117 self
118 }
119
120 pub fn with_blend_mode(mut self, blend_mode: BlendMode) -> Self {
121 self.blend_mode = blend_mode;
122 self
123 }
124
125 fn to_instance(&self, texture_width: f32, texture_height: f32) -> SpriteInstance {
129 let (uv_offset, uv_size) = if let Some(src) = self.source_rect {
130 let u = src.x / texture_width;
132 let v = src.y / texture_height;
133 let uw = src.w / texture_width;
134 let vh = src.h / texture_height;
135 (Vec2::new(u, v), Vec2::new(uw, vh))
136 } else {
137 (Vec2::ZERO, Vec2::ONE)
139 };
140
141 let (uv_offset, uv_size) = if self.flip_x {
146 (
147 Vec2::new(uv_offset.x + uv_size.x, uv_offset.y),
148 Vec2::new(-uv_size.x, uv_size.y),
149 )
150 } else {
151 (uv_offset, uv_size)
152 };
153
154 SpriteInstance::new(self.position, self.size, self.color, uv_offset, uv_size)
155 }
156}
157
158pub struct SpriteRenderer {
160 pipeline: wgpu::RenderPipeline,
161 additive_pipeline: wgpu::RenderPipeline,
162 camera_buffer: wgpu::Buffer,
163 camera_bind_group: wgpu::BindGroup,
164 texture_bind_group_layout: wgpu::BindGroupLayout,
165 #[allow(dead_code)]
166 default_texture: Texture,
167 default_bind_group: wgpu::BindGroup,
168 rect_instance_buffer: wgpu::Buffer,
169 rect_instance_data: Vec<SpriteInstance>,
170 sprite_instance_buffer: wgpu::Buffer,
171
172 batch_ranges: Vec<(usize, std::ops::Range<u32>)>,
174 additive_batch_ranges: Vec<(usize, std::ops::Range<u32>)>,
175
176 texture_batches: Vec<Vec<SpriteInstance>>,
178 additive_texture_batches: Vec<Vec<SpriteInstance>>,
179}
180
181impl SpriteRenderer {
182 pub fn new(
183 device: &wgpu::Device,
184 queue: &wgpu::Queue,
185 format: wgpu::TextureFormat,
186 camera: &Camera,
187 ) -> Self {
188 let default_texture = Texture::white_pixel(device, queue);
190
191 let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
193 label: Some("Camera Uniform Buffer"),
194 contents: bytemuck::cast_slice(&[*camera.uniform()]),
195 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
196 });
197
198 let camera_bind_group_layout =
200 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
201 label: Some("Camera Bind Group Layout"),
202 entries: &[wgpu::BindGroupLayoutEntry {
203 binding: 0,
204 visibility: wgpu::ShaderStages::VERTEX,
205 ty: wgpu::BindingType::Buffer {
206 ty: wgpu::BufferBindingType::Uniform,
207 has_dynamic_offset: false,
208 min_binding_size: None,
209 },
210 count: None,
211 }],
212 });
213
214 let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
216 label: Some("Camera Bind Group"),
217 layout: &camera_bind_group_layout,
218 entries: &[wgpu::BindGroupEntry {
219 binding: 0,
220 resource: camera_buffer.as_entire_binding(),
221 }],
222 });
223
224 let texture_bind_group_layout =
226 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
227 label: Some("Sprite Texture Bind Group Layout"),
228 entries: &[
229 wgpu::BindGroupLayoutEntry {
230 binding: 0,
231 visibility: wgpu::ShaderStages::FRAGMENT,
232 ty: wgpu::BindingType::Texture {
233 sample_type: wgpu::TextureSampleType::Float { filterable: true },
234 view_dimension: wgpu::TextureViewDimension::D2,
235 multisampled: false,
236 },
237 count: None,
238 },
239 wgpu::BindGroupLayoutEntry {
240 binding: 1,
241 visibility: wgpu::ShaderStages::FRAGMENT,
242 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
243 count: None,
244 },
245 ],
246 });
247
248 let default_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
250 label: Some("Default Sprite Texture Bind Group"),
251 layout: &texture_bind_group_layout,
252 entries: &[
253 wgpu::BindGroupEntry {
254 binding: 0,
255 resource: wgpu::BindingResource::TextureView(&default_texture.view),
256 },
257 wgpu::BindGroupEntry {
258 binding: 1,
259 resource: wgpu::BindingResource::Sampler(&default_texture.sampler),
260 },
261 ],
262 });
263
264 let rect_instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
266 label: Some("Rect Instance Buffer"),
267 size: (MAX_SPRITES * std::mem::size_of::<SpriteInstance>()) as u64,
268 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
269 mapped_at_creation: false,
270 });
271
272 let sprite_instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
273 label: Some("Sprite Instance Buffer"),
274 size: (MAX_SPRITES * std::mem::size_of::<SpriteInstance>()) as u64,
275 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
276 mapped_at_creation: false,
277 });
278
279 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
281 label: Some("Sprite Shader"),
282 source: wgpu::ShaderSource::Wgsl(
283 include_str!("../assets/shaders/shader_sprite.wgsl").into(),
284 ),
285 });
286
287 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
288 label: Some("Sprite Pipeline Layout"),
289 bind_group_layouts: &[&camera_bind_group_layout, &texture_bind_group_layout],
290 push_constant_ranges: &[],
291 });
292
293 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
295 label: Some("Sprite Pipeline"),
296 layout: Some(&pipeline_layout),
297 vertex: wgpu::VertexState {
298 module: &shader,
299 entry_point: Some("vs_main"),
300 buffers: &[wgpu::VertexBufferLayout {
301 array_stride: std::mem::size_of::<SpriteInstance>() as u64,
302 step_mode: wgpu::VertexStepMode::Instance,
303 attributes: &[
307 wgpu::VertexAttribute {
309 format: wgpu::VertexFormat::Float32x2,
310 offset: 0,
311 shader_location: 0,
312 },
313 wgpu::VertexAttribute {
315 format: wgpu::VertexFormat::Float32x2,
316 offset: 8,
317 shader_location: 1,
318 },
319 wgpu::VertexAttribute {
321 format: wgpu::VertexFormat::Float32x4,
322 offset: 16,
323 shader_location: 2,
324 },
325 wgpu::VertexAttribute {
327 format: wgpu::VertexFormat::Float32x2,
328 offset: 32,
329 shader_location: 3,
330 },
331 wgpu::VertexAttribute {
333 format: wgpu::VertexFormat::Float32x2,
334 offset: 40,
335 shader_location: 4,
336 },
337 ],
338 }],
339 compilation_options: Default::default(),
340 },
341 fragment: Some(wgpu::FragmentState {
343 module: &shader,
344 entry_point: Some("fs_main"),
345 targets: &[Some(wgpu::ColorTargetState {
346 format,
347 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
348 write_mask: wgpu::ColorWrites::ALL,
349 })],
350 compilation_options: Default::default(),
351 }),
352 primitive: wgpu::PrimitiveState {
354 topology: wgpu::PrimitiveTopology::TriangleList,
355 ..Default::default()
356 },
357 depth_stencil: None,
358 multisample: wgpu::MultisampleState::default(),
359 multiview: None,
360 cache: None,
361 });
362
363 let vertex_buffer_layout = wgpu::VertexBufferLayout {
364 array_stride: std::mem::size_of::<SpriteInstance>() as u64,
365 step_mode: wgpu::VertexStepMode::Instance,
366 attributes: &[
367 wgpu::VertexAttribute {
368 format: wgpu::VertexFormat::Float32x2,
369 offset: 0,
370 shader_location: 0,
371 },
372 wgpu::VertexAttribute {
373 format: wgpu::VertexFormat::Float32x2,
374 offset: 8,
375 shader_location: 1,
376 },
377 wgpu::VertexAttribute {
378 format: wgpu::VertexFormat::Float32x4,
379 offset: 16,
380 shader_location: 2,
381 },
382 wgpu::VertexAttribute {
383 format: wgpu::VertexFormat::Float32x2,
384 offset: 32,
385 shader_location: 3,
386 },
387 wgpu::VertexAttribute {
388 format: wgpu::VertexFormat::Float32x2,
389 offset: 40,
390 shader_location: 4,
391 },
392 ],
393 };
394
395 let additive_blend = wgpu::BlendState {
397 color: wgpu::BlendComponent {
398 src_factor: wgpu::BlendFactor::SrcAlpha,
399 dst_factor: wgpu::BlendFactor::One,
400 operation: wgpu::BlendOperation::Add,
401 },
402 alpha: wgpu::BlendComponent {
403 src_factor: wgpu::BlendFactor::One,
404 dst_factor: wgpu::BlendFactor::One,
405 operation: wgpu::BlendOperation::Add,
406 },
407 };
408
409 let additive_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
410 label: Some("Sprite Additive Pipeline"),
411 layout: Some(&pipeline_layout),
412 vertex: wgpu::VertexState {
413 module: &shader,
414 entry_point: Some("vs_main"),
415 buffers: &[vertex_buffer_layout],
416 compilation_options: Default::default(),
417 },
418 fragment: Some(wgpu::FragmentState {
419 module: &shader,
420 entry_point: Some("fs_main"),
421 targets: &[Some(wgpu::ColorTargetState {
422 format,
423 blend: Some(additive_blend),
424 write_mask: wgpu::ColorWrites::ALL,
425 })],
426 compilation_options: Default::default(),
427 }),
428 primitive: wgpu::PrimitiveState {
429 topology: wgpu::PrimitiveTopology::TriangleList,
430 ..Default::default()
431 },
432 depth_stencil: None,
433 multisample: wgpu::MultisampleState::default(),
434 multiview: None,
435 cache: None,
436 });
437
438 Self {
440 pipeline,
441 additive_pipeline,
442 camera_buffer,
443 camera_bind_group,
444 texture_bind_group_layout,
445 default_texture,
446 default_bind_group,
447 rect_instance_buffer,
448 rect_instance_data: Vec::with_capacity(MAX_SPRITES),
449 sprite_instance_buffer,
450 batch_ranges: Vec::new(),
451 additive_batch_ranges: Vec::new(),
452 texture_batches: Vec::new(),
453 additive_texture_batches: Vec::new(),
454 }
455 }
456
457 pub fn create_texture_bind_group(
459 &self,
460 device: &wgpu::Device,
461 texture: &Texture,
462 ) -> wgpu::BindGroup {
463 device.create_bind_group(&wgpu::BindGroupDescriptor {
464 label: Some("Custom Sprite Texture Bind Group"),
465 layout: &self.texture_bind_group_layout,
466 entries: &[
469 wgpu::BindGroupEntry {
470 binding: 0,
471 resource: wgpu::BindingResource::TextureView(&texture.view),
472 },
473 wgpu::BindGroupEntry {
474 binding: 1,
475 resource: wgpu::BindingResource::Sampler(&texture.sampler),
476 },
477 ],
478 })
479 }
480
481 pub fn update_camera(&self, queue: &wgpu::Queue, camera: &Camera) {
483 queue.write_buffer(
484 &self.camera_buffer,
485 0,
486 bytemuck::cast_slice(&[*camera.uniform()]),
487 );
488 }
489
490 pub fn prepare(
492 &mut self,
493 queue: &wgpu::Queue,
494 sprites: &[Sprite],
495 texture_sizes: &[(f32, f32)],
496 ) {
497 self.rect_instance_data.clear();
498 self.batch_ranges.clear();
499 self.additive_batch_ranges.clear();
500
501 for batch in &mut self.texture_batches {
503 batch.clear();
504 }
505 for batch in &mut self.additive_texture_batches {
506 batch.clear();
507 }
508
509 if sprites.len() > MAX_SPRITES {
512 log::warn!(
513 "Sprite overflow: {} submitted, capped at {}",
514 sprites.len(),
515 MAX_SPRITES
516 );
517 }
518 for sprite in sprites.iter().take(MAX_SPRITES) {
519 if sprite.source_rect.is_some() {
520 let (tex_width, tex_height) = texture_sizes
521 .get(sprite.texture_id)
522 .copied()
523 .unwrap_or((1.0, 1.0));
524
525 let instance = sprite.to_instance(tex_width, tex_height);
526
527 let batches = match sprite.blend_mode {
528 BlendMode::Alpha => &mut self.texture_batches,
529 BlendMode::Additive => &mut self.additive_texture_batches,
530 };
531
532 if sprite.texture_id >= batches.len() {
533 batches.resize_with(sprite.texture_id + 1, Vec::new);
534 }
535 batches[sprite.texture_id].push(instance);
536 } else {
537 self.rect_instance_data.push(sprite.to_instance(1.0, 1.0));
538 }
539 }
540
541 if !self.rect_instance_data.is_empty() {
543 queue.write_buffer(
544 &self.rect_instance_buffer,
545 0,
546 bytemuck::cast_slice(&self.rect_instance_data),
547 );
548 }
549
550 let mut all_instances: Vec<SpriteInstance> = Vec::new();
552
553 for (tex_id, instances) in self.texture_batches.iter().enumerate() {
554 if instances.is_empty() {
555 continue;
556 }
557 let start = all_instances.len() as u32;
558 all_instances.extend_from_slice(instances);
559 self.batch_ranges
560 .push((tex_id, start..all_instances.len() as u32));
561 }
562
563 for (tex_id, instances) in self.additive_texture_batches.iter().enumerate() {
564 if instances.is_empty() {
565 continue;
566 }
567 let start = all_instances.len() as u32;
568 all_instances.extend_from_slice(instances);
569 self.additive_batch_ranges
570 .push((tex_id, start..all_instances.len() as u32));
571 }
572
573 if !all_instances.is_empty() {
574 queue.write_buffer(
575 &self.sprite_instance_buffer,
576 0,
577 bytemuck::cast_slice(&all_instances),
578 );
579 }
580 }
581
582 pub fn render_multi<'rpass>(
586 &'rpass self,
587 render_pass: &mut wgpu::RenderPass<'rpass>,
588 bind_groups: &'rpass [wgpu::BindGroup],
589 ) {
590 render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
591
592 if !self.rect_instance_data.is_empty() {
594 render_pass.set_pipeline(&self.pipeline);
595 render_pass.set_bind_group(1, &self.default_bind_group, &[]);
596 render_pass.set_vertex_buffer(0, self.rect_instance_buffer.slice(..));
597 render_pass.draw(0..6, 0..self.rect_instance_data.len() as u32);
598 }
599
600 let has_alpha = !self.batch_ranges.is_empty();
602 let has_additive = !self.additive_batch_ranges.is_empty();
603
604 if has_alpha || has_additive {
605 render_pass.set_vertex_buffer(0, self.sprite_instance_buffer.slice(..));
606 }
607
608 if has_alpha {
609 render_pass.set_pipeline(&self.pipeline);
610
611 for &(texture_id, ref range) in &self.batch_ranges {
612 let bind_group = bind_groups
613 .get(texture_id)
614 .unwrap_or(&self.default_bind_group);
615
616 render_pass.set_bind_group(1, bind_group, &[]);
617 render_pass.draw(0..6, range.clone());
618 }
619 }
620
621 if has_additive {
623 render_pass.set_pipeline(&self.additive_pipeline);
624
625 for &(texture_id, ref range) in &self.additive_batch_ranges {
626 let bind_group = bind_groups
627 .get(texture_id)
628 .unwrap_or(&self.default_bind_group);
629
630 render_pass.set_bind_group(1, bind_group, &[]);
631 render_pass.draw(0..6, range.clone());
632 }
633 }
634 }
635}