1use bytemuck::{Pod, Zeroable};
2use wgpu::util::DeviceExt;
3
4use super::camera::Camera2D;
5use super::gpu::GpuContext;
6use super::lighting::LightingUniform;
7use super::texture::TextureStore;
8
9pub const BLEND_ALPHA: u8 = 0;
11pub const BLEND_ADDITIVE: u8 = 1;
12pub const BLEND_MULTIPLY: u8 = 2;
13pub const BLEND_SCREEN: u8 = 3;
14
15#[derive(Debug, Clone)]
17pub struct SpriteCommand {
18 pub texture_id: u32,
19 pub x: f32,
20 pub y: f32,
21 pub w: f32,
22 pub h: f32,
23 pub layer: i32,
24 pub uv_x: f32,
25 pub uv_y: f32,
26 pub uv_w: f32,
27 pub uv_h: f32,
28 pub tint_r: f32,
29 pub tint_g: f32,
30 pub tint_b: f32,
31 pub tint_a: f32,
32 pub rotation: f32,
33 pub origin_x: f32,
34 pub origin_y: f32,
35 pub flip_x: bool,
36 pub flip_y: bool,
37 pub opacity: f32,
38 pub blend_mode: u8,
39 pub shader_id: u32,
40}
41
42#[repr(C)]
44#[derive(Copy, Clone, Pod, Zeroable)]
45struct QuadVertex {
46 position: [f32; 2],
47 uv: [f32; 2],
48}
49
50#[repr(C)]
52#[derive(Copy, Clone, Pod, Zeroable)]
53struct SpriteInstance {
54 world_pos: [f32; 2],
55 size: [f32; 2],
56 uv_offset: [f32; 2],
57 uv_size: [f32; 2],
58 tint: [f32; 4],
59 rotation_origin: [f32; 4],
61}
62
63#[repr(C)]
65#[derive(Copy, Clone, Pod, Zeroable)]
66struct CameraUniform {
67 view_proj: [f32; 16],
68}
69
70const QUAD_VERTICES: &[QuadVertex] = &[
72 QuadVertex { position: [0.0, 0.0], uv: [0.0, 0.0] }, QuadVertex { position: [1.0, 0.0], uv: [1.0, 0.0] }, QuadVertex { position: [1.0, 1.0], uv: [1.0, 1.0] }, QuadVertex { position: [0.0, 1.0], uv: [0.0, 1.0] }, ];
77
78const QUAD_INDICES: &[u16] = &[0, 1, 2, 0, 2, 3];
79
80fn blend_state_for(mode: u8) -> wgpu::BlendState {
82 use wgpu::{BlendComponent, BlendFactor, BlendOperation};
83 match mode {
84 BLEND_ALPHA => wgpu::BlendState::ALPHA_BLENDING,
85 BLEND_ADDITIVE => wgpu::BlendState {
86 color: BlendComponent {
87 src_factor: BlendFactor::SrcAlpha,
88 dst_factor: BlendFactor::One,
89 operation: BlendOperation::Add,
90 },
91 alpha: BlendComponent {
92 src_factor: BlendFactor::One,
93 dst_factor: BlendFactor::One,
94 operation: BlendOperation::Add,
95 },
96 },
97 BLEND_MULTIPLY => wgpu::BlendState {
98 color: BlendComponent {
99 src_factor: BlendFactor::Dst,
100 dst_factor: BlendFactor::OneMinusSrcAlpha,
101 operation: BlendOperation::Add,
102 },
103 alpha: BlendComponent {
104 src_factor: BlendFactor::DstAlpha,
105 dst_factor: BlendFactor::OneMinusSrcAlpha,
106 operation: BlendOperation::Add,
107 },
108 },
109 BLEND_SCREEN => wgpu::BlendState {
110 color: BlendComponent {
111 src_factor: BlendFactor::One,
112 dst_factor: BlendFactor::OneMinusSrc,
113 operation: BlendOperation::Add,
114 },
115 alpha: BlendComponent {
116 src_factor: BlendFactor::One,
117 dst_factor: BlendFactor::OneMinusSrcAlpha,
118 operation: BlendOperation::Add,
119 },
120 },
121 _ => wgpu::BlendState::ALPHA_BLENDING, }
123}
124
125pub struct SpritePipeline {
126 pipelines: [wgpu::RenderPipeline; 4],
128 vertex_buffer: wgpu::Buffer,
129 index_buffer: wgpu::Buffer,
130 camera_buffer: wgpu::Buffer,
131 camera_bind_group: wgpu::BindGroup,
132 pub texture_bind_group_layout: wgpu::BindGroupLayout,
133 lighting_buffer: wgpu::Buffer,
134 lighting_bind_group: wgpu::BindGroup,
135}
136
137impl SpritePipeline {
138 pub fn new_headless(
141 device: &wgpu::Device,
142 queue: &wgpu::Queue,
143 format: wgpu::TextureFormat,
144 ) -> Self {
145 Self::new_internal(device, queue, format)
146 }
147
148 pub fn new(gpu: &GpuContext) -> Self {
149 Self::new_internal(&gpu.device, &gpu.queue, gpu.config.format)
150 }
151
152 fn new_internal(
153 device: &wgpu::Device,
154 _queue: &wgpu::Queue,
155 surface_format: wgpu::TextureFormat,
156 ) -> Self {
157 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
158 label: Some("sprite_shader"),
159 source: wgpu::ShaderSource::Wgsl(
160 include_str!("shaders/sprite.wgsl").into(),
161 ),
162 });
163
164 let camera_bind_group_layout =
166 device
167 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
168 label: Some("camera_bind_group_layout"),
169 entries: &[wgpu::BindGroupLayoutEntry {
170 binding: 0,
171 visibility: wgpu::ShaderStages::VERTEX,
172 ty: wgpu::BindingType::Buffer {
173 ty: wgpu::BufferBindingType::Uniform,
174 has_dynamic_offset: false,
175 min_binding_size: None,
176 },
177 count: None,
178 }],
179 });
180
181 let texture_bind_group_layout =
183 device
184 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
185 label: Some("texture_bind_group_layout"),
186 entries: &[
187 wgpu::BindGroupLayoutEntry {
188 binding: 0,
189 visibility: wgpu::ShaderStages::FRAGMENT,
190 ty: wgpu::BindingType::Texture {
191 multisampled: false,
192 view_dimension: wgpu::TextureViewDimension::D2,
193 sample_type: wgpu::TextureSampleType::Float { filterable: true },
194 },
195 count: None,
196 },
197 wgpu::BindGroupLayoutEntry {
198 binding: 1,
199 visibility: wgpu::ShaderStages::FRAGMENT,
200 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
201 count: None,
202 },
203 ],
204 });
205
206 let lighting_bind_group_layout =
208 device
209 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
210 label: Some("lighting_bind_group_layout"),
211 entries: &[wgpu::BindGroupLayoutEntry {
212 binding: 0,
213 visibility: wgpu::ShaderStages::FRAGMENT,
214 ty: wgpu::BindingType::Buffer {
215 ty: wgpu::BufferBindingType::Uniform,
216 has_dynamic_offset: false,
217 min_binding_size: None,
218 },
219 count: None,
220 }],
221 });
222
223 let pipeline_layout =
224 device
225 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
226 label: Some("sprite_pipeline_layout"),
227 bind_group_layouts: &[
228 &camera_bind_group_layout,
229 &texture_bind_group_layout,
230 &lighting_bind_group_layout,
231 ],
232 push_constant_ranges: &[],
233 });
234
235 let vertex_layout = wgpu::VertexBufferLayout {
237 array_stride: std::mem::size_of::<QuadVertex>() as wgpu::BufferAddress,
238 step_mode: wgpu::VertexStepMode::Vertex,
239 attributes: &[
240 wgpu::VertexAttribute {
241 offset: 0,
242 shader_location: 0,
243 format: wgpu::VertexFormat::Float32x2,
244 },
245 wgpu::VertexAttribute {
246 offset: 8,
247 shader_location: 1,
248 format: wgpu::VertexFormat::Float32x2,
249 },
250 ],
251 };
252
253 let instance_layout = wgpu::VertexBufferLayout {
254 array_stride: std::mem::size_of::<SpriteInstance>() as wgpu::BufferAddress,
255 step_mode: wgpu::VertexStepMode::Instance,
256 attributes: &[
257 wgpu::VertexAttribute {
258 offset: 0,
259 shader_location: 2,
260 format: wgpu::VertexFormat::Float32x2, },
262 wgpu::VertexAttribute {
263 offset: 8,
264 shader_location: 3,
265 format: wgpu::VertexFormat::Float32x2, },
267 wgpu::VertexAttribute {
268 offset: 16,
269 shader_location: 4,
270 format: wgpu::VertexFormat::Float32x2, },
272 wgpu::VertexAttribute {
273 offset: 24,
274 shader_location: 5,
275 format: wgpu::VertexFormat::Float32x2, },
277 wgpu::VertexAttribute {
278 offset: 32,
279 shader_location: 6,
280 format: wgpu::VertexFormat::Float32x4, },
282 wgpu::VertexAttribute {
283 offset: 48,
284 shader_location: 7,
285 format: wgpu::VertexFormat::Float32x4, },
287 ],
288 };
289
290 let blend_names = ["alpha", "additive", "multiply", "screen"];
292 let pipelines: Vec<wgpu::RenderPipeline> = (0..4u8)
293 .map(|mode| {
294 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
295 label: Some(&format!("sprite_pipeline_{}", blend_names[mode as usize])),
296 layout: Some(&pipeline_layout),
297 vertex: wgpu::VertexState {
298 module: &shader,
299 entry_point: Some("vs_main"),
300 buffers: &[vertex_layout.clone(), instance_layout.clone()],
301 compilation_options: Default::default(),
302 },
303 fragment: Some(wgpu::FragmentState {
304 module: &shader,
305 entry_point: Some("fs_main"),
306 targets: &[Some(wgpu::ColorTargetState {
307 format: surface_format,
308 blend: Some(blend_state_for(mode)),
309 write_mask: wgpu::ColorWrites::ALL,
310 })],
311 compilation_options: Default::default(),
312 }),
313 primitive: wgpu::PrimitiveState {
314 topology: wgpu::PrimitiveTopology::TriangleList,
315 strip_index_format: None,
316 front_face: wgpu::FrontFace::Ccw,
317 cull_mode: None,
318 polygon_mode: wgpu::PolygonMode::Fill,
319 unclipped_depth: false,
320 conservative: false,
321 },
322 depth_stencil: None,
323 multisample: wgpu::MultisampleState::default(),
324 multiview: None,
325 cache: None,
326 })
327 })
328 .collect();
329
330 let pipelines: [wgpu::RenderPipeline; 4] = pipelines.try_into().unwrap();
331
332 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
333 label: Some("quad_vertex_buffer"),
334 contents: bytemuck::cast_slice(QUAD_VERTICES),
335 usage: wgpu::BufferUsages::VERTEX,
336 });
337
338 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
339 label: Some("quad_index_buffer"),
340 contents: bytemuck::cast_slice(QUAD_INDICES),
341 usage: wgpu::BufferUsages::INDEX,
342 });
343
344 let camera_uniform = CameraUniform {
345 view_proj: Camera2D::default().view_proj(),
346 };
347
348 let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
349 label: Some("camera_uniform_buffer"),
350 contents: bytemuck::cast_slice(&[camera_uniform]),
351 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
352 });
353
354 let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
355 label: Some("camera_bind_group"),
356 layout: &camera_bind_group_layout,
357 entries: &[wgpu::BindGroupEntry {
358 binding: 0,
359 resource: camera_buffer.as_entire_binding(),
360 }],
361 });
362
363 let default_lighting = LightingUniform {
365 ambient: [1.0, 1.0, 1.0],
366 light_count: 0,
367 lights: [super::lighting::LightData {
368 pos_radius: [0.0; 4],
369 color_intensity: [0.0; 4],
370 }; super::lighting::MAX_LIGHTS],
371 };
372
373 let lighting_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
374 label: Some("lighting_uniform_buffer"),
375 contents: bytemuck::cast_slice(&[default_lighting]),
376 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
377 });
378
379 let lighting_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
380 label: Some("lighting_bind_group"),
381 layout: &lighting_bind_group_layout,
382 entries: &[wgpu::BindGroupEntry {
383 binding: 0,
384 resource: lighting_buffer.as_entire_binding(),
385 }],
386 });
387
388 Self {
389 pipelines,
390 vertex_buffer,
391 index_buffer,
392 camera_buffer,
393 camera_bind_group,
394 texture_bind_group_layout,
395 lighting_buffer,
396 lighting_bind_group,
397 }
398 }
399
400 pub fn camera_bind_group(&self) -> &wgpu::BindGroup {
403 &self.camera_bind_group
404 }
405
406 pub fn prepare(
409 &self,
410 device: &wgpu::Device,
411 queue: &wgpu::Queue,
412 camera: &Camera2D,
413 lighting: &LightingUniform,
414 ) {
415 let _ = device; let camera_uniform = CameraUniform {
417 view_proj: camera.view_proj(),
418 };
419 queue.write_buffer(
420 &self.camera_buffer,
421 0,
422 bytemuck::cast_slice(&[camera_uniform]),
423 );
424 queue.write_buffer(
425 &self.lighting_buffer,
426 0,
427 bytemuck::cast_slice(&[*lighting]),
428 );
429 }
430
431 pub fn render(
437 &self,
438 device: &wgpu::Device,
439 _queue: &wgpu::Queue,
440 textures: &TextureStore,
441 shaders: &super::shader::ShaderStore,
442 commands: &[SpriteCommand],
443 target: &wgpu::TextureView,
444 encoder: &mut wgpu::CommandEncoder,
445 clear_color: Option<wgpu::Color>,
446 ) {
447 let load_op = match clear_color {
448 Some(color) => wgpu::LoadOp::Clear(color),
449 None => wgpu::LoadOp::Load,
450 };
451
452 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
453 label: Some("sprite_render_pass"),
454 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
455 view: target,
456 resolve_target: None,
457 ops: wgpu::Operations {
458 load: load_op,
459 store: wgpu::StoreOp::Store,
460 },
461 })],
462 depth_stencil_attachment: None,
463 timestamp_writes: None,
464 occlusion_query_set: None,
465 });
466
467 render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
468 render_pass.set_bind_group(2, &self.lighting_bind_group, &[]);
469 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
470 render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
471
472 let mut current_shader: Option<u32> = None;
474 let mut current_blend: Option<u8> = None;
475 let mut i = 0;
476 while i < commands.len() {
477 let shader = commands[i].shader_id;
478 let blend = commands[i].blend_mode.min(3);
479 let tex_id = commands[i].texture_id;
480 let batch_start = i;
481 while i < commands.len()
482 && commands[i].shader_id == shader
483 && commands[i].blend_mode.min(3) == blend
484 && commands[i].texture_id == tex_id
485 {
486 i += 1;
487 }
488 let batch = &commands[batch_start..i];
489
490 if shader == 0 {
492 if current_shader != Some(0) || current_blend != Some(blend) {
493 render_pass.set_pipeline(&self.pipelines[blend as usize]);
494 current_shader = Some(0);
495 current_blend = Some(blend);
496 }
497 } else if current_shader != Some(shader) {
498 if let Some(pipeline) = shaders.get_pipeline(shader) {
499 render_pass.set_pipeline(pipeline);
500 if let Some(bg) = shaders.get_bind_group(shader) {
501 render_pass.set_bind_group(3, bg, &[]);
502 }
503 current_shader = Some(shader);
504 current_blend = None;
505 } else {
506 continue; }
508 }
509
510 let bind_group = match textures.get_bind_group(tex_id) {
512 Some(bg) => bg,
513 None => continue, };
515
516 let instances: Vec<SpriteInstance> = batch
518 .iter()
519 .map(|cmd| {
520 let mut uv_x = cmd.uv_x;
522 let mut uv_y = cmd.uv_y;
523 let mut uv_w = cmd.uv_w;
524 let mut uv_h = cmd.uv_h;
525 if cmd.flip_x {
526 uv_x += uv_w;
527 uv_w = -uv_w;
528 }
529 if cmd.flip_y {
530 uv_y += uv_h;
531 uv_h = -uv_h;
532 }
533 SpriteInstance {
534 world_pos: [cmd.x, cmd.y],
535 size: [cmd.w, cmd.h],
536 uv_offset: [uv_x, uv_y],
537 uv_size: [uv_w, uv_h],
538 tint: [cmd.tint_r, cmd.tint_g, cmd.tint_b, cmd.tint_a * cmd.opacity],
539 rotation_origin: [cmd.rotation, cmd.origin_x, cmd.origin_y, 0.0],
540 }
541 })
542 .collect();
543
544 let instance_buffer =
545 device
546 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
547 label: Some("sprite_instance_buffer"),
548 contents: bytemuck::cast_slice(&instances),
549 usage: wgpu::BufferUsages::VERTEX,
550 });
551
552 render_pass.set_bind_group(1, bind_group, &[]);
553 render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
554 render_pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
555 }
556 }
557}