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(gpu: &GpuContext) -> Self {
139 let shader = gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor {
140 label: Some("sprite_shader"),
141 source: wgpu::ShaderSource::Wgsl(
142 include_str!("shaders/sprite.wgsl").into(),
143 ),
144 });
145
146 let camera_bind_group_layout =
148 gpu.device
149 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
150 label: Some("camera_bind_group_layout"),
151 entries: &[wgpu::BindGroupLayoutEntry {
152 binding: 0,
153 visibility: wgpu::ShaderStages::VERTEX,
154 ty: wgpu::BindingType::Buffer {
155 ty: wgpu::BufferBindingType::Uniform,
156 has_dynamic_offset: false,
157 min_binding_size: None,
158 },
159 count: None,
160 }],
161 });
162
163 let texture_bind_group_layout =
165 gpu.device
166 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
167 label: Some("texture_bind_group_layout"),
168 entries: &[
169 wgpu::BindGroupLayoutEntry {
170 binding: 0,
171 visibility: wgpu::ShaderStages::FRAGMENT,
172 ty: wgpu::BindingType::Texture {
173 multisampled: false,
174 view_dimension: wgpu::TextureViewDimension::D2,
175 sample_type: wgpu::TextureSampleType::Float { filterable: true },
176 },
177 count: None,
178 },
179 wgpu::BindGroupLayoutEntry {
180 binding: 1,
181 visibility: wgpu::ShaderStages::FRAGMENT,
182 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
183 count: None,
184 },
185 ],
186 });
187
188 let lighting_bind_group_layout =
190 gpu.device
191 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
192 label: Some("lighting_bind_group_layout"),
193 entries: &[wgpu::BindGroupLayoutEntry {
194 binding: 0,
195 visibility: wgpu::ShaderStages::FRAGMENT,
196 ty: wgpu::BindingType::Buffer {
197 ty: wgpu::BufferBindingType::Uniform,
198 has_dynamic_offset: false,
199 min_binding_size: None,
200 },
201 count: None,
202 }],
203 });
204
205 let pipeline_layout =
206 gpu.device
207 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
208 label: Some("sprite_pipeline_layout"),
209 bind_group_layouts: &[
210 &camera_bind_group_layout,
211 &texture_bind_group_layout,
212 &lighting_bind_group_layout,
213 ],
214 push_constant_ranges: &[],
215 });
216
217 let vertex_layout = wgpu::VertexBufferLayout {
219 array_stride: std::mem::size_of::<QuadVertex>() as wgpu::BufferAddress,
220 step_mode: wgpu::VertexStepMode::Vertex,
221 attributes: &[
222 wgpu::VertexAttribute {
223 offset: 0,
224 shader_location: 0,
225 format: wgpu::VertexFormat::Float32x2,
226 },
227 wgpu::VertexAttribute {
228 offset: 8,
229 shader_location: 1,
230 format: wgpu::VertexFormat::Float32x2,
231 },
232 ],
233 };
234
235 let instance_layout = wgpu::VertexBufferLayout {
236 array_stride: std::mem::size_of::<SpriteInstance>() as wgpu::BufferAddress,
237 step_mode: wgpu::VertexStepMode::Instance,
238 attributes: &[
239 wgpu::VertexAttribute {
240 offset: 0,
241 shader_location: 2,
242 format: wgpu::VertexFormat::Float32x2, },
244 wgpu::VertexAttribute {
245 offset: 8,
246 shader_location: 3,
247 format: wgpu::VertexFormat::Float32x2, },
249 wgpu::VertexAttribute {
250 offset: 16,
251 shader_location: 4,
252 format: wgpu::VertexFormat::Float32x2, },
254 wgpu::VertexAttribute {
255 offset: 24,
256 shader_location: 5,
257 format: wgpu::VertexFormat::Float32x2, },
259 wgpu::VertexAttribute {
260 offset: 32,
261 shader_location: 6,
262 format: wgpu::VertexFormat::Float32x4, },
264 wgpu::VertexAttribute {
265 offset: 48,
266 shader_location: 7,
267 format: wgpu::VertexFormat::Float32x4, },
269 ],
270 };
271
272 let blend_names = ["alpha", "additive", "multiply", "screen"];
274 let pipelines: Vec<wgpu::RenderPipeline> = (0..4u8)
275 .map(|mode| {
276 gpu.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
277 label: Some(&format!("sprite_pipeline_{}", blend_names[mode as usize])),
278 layout: Some(&pipeline_layout),
279 vertex: wgpu::VertexState {
280 module: &shader,
281 entry_point: Some("vs_main"),
282 buffers: &[vertex_layout.clone(), instance_layout.clone()],
283 compilation_options: Default::default(),
284 },
285 fragment: Some(wgpu::FragmentState {
286 module: &shader,
287 entry_point: Some("fs_main"),
288 targets: &[Some(wgpu::ColorTargetState {
289 format: gpu.config.format,
290 blend: Some(blend_state_for(mode)),
291 write_mask: wgpu::ColorWrites::ALL,
292 })],
293 compilation_options: Default::default(),
294 }),
295 primitive: wgpu::PrimitiveState {
296 topology: wgpu::PrimitiveTopology::TriangleList,
297 strip_index_format: None,
298 front_face: wgpu::FrontFace::Ccw,
299 cull_mode: None,
300 polygon_mode: wgpu::PolygonMode::Fill,
301 unclipped_depth: false,
302 conservative: false,
303 },
304 depth_stencil: None,
305 multisample: wgpu::MultisampleState::default(),
306 multiview: None,
307 cache: None,
308 })
309 })
310 .collect();
311
312 let pipelines: [wgpu::RenderPipeline; 4] = pipelines.try_into().unwrap();
313
314 let vertex_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
315 label: Some("quad_vertex_buffer"),
316 contents: bytemuck::cast_slice(QUAD_VERTICES),
317 usage: wgpu::BufferUsages::VERTEX,
318 });
319
320 let index_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
321 label: Some("quad_index_buffer"),
322 contents: bytemuck::cast_slice(QUAD_INDICES),
323 usage: wgpu::BufferUsages::INDEX,
324 });
325
326 let camera_uniform = CameraUniform {
327 view_proj: Camera2D::default().view_proj(),
328 };
329
330 let camera_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
331 label: Some("camera_uniform_buffer"),
332 contents: bytemuck::cast_slice(&[camera_uniform]),
333 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
334 });
335
336 let camera_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
337 label: Some("camera_bind_group"),
338 layout: &camera_bind_group_layout,
339 entries: &[wgpu::BindGroupEntry {
340 binding: 0,
341 resource: camera_buffer.as_entire_binding(),
342 }],
343 });
344
345 let default_lighting = LightingUniform {
347 ambient: [1.0, 1.0, 1.0],
348 light_count: 0,
349 lights: [super::lighting::LightData {
350 pos_radius: [0.0; 4],
351 color_intensity: [0.0; 4],
352 }; super::lighting::MAX_LIGHTS],
353 };
354
355 let lighting_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
356 label: Some("lighting_uniform_buffer"),
357 contents: bytemuck::cast_slice(&[default_lighting]),
358 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
359 });
360
361 let lighting_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
362 label: Some("lighting_bind_group"),
363 layout: &lighting_bind_group_layout,
364 entries: &[wgpu::BindGroupEntry {
365 binding: 0,
366 resource: lighting_buffer.as_entire_binding(),
367 }],
368 });
369
370 Self {
371 pipelines,
372 vertex_buffer,
373 index_buffer,
374 camera_buffer,
375 camera_bind_group,
376 texture_bind_group_layout,
377 lighting_buffer,
378 lighting_bind_group,
379 }
380 }
381
382 pub fn render(
385 &self,
386 gpu: &GpuContext,
387 textures: &TextureStore,
388 shaders: &super::shader::ShaderStore,
389 camera: &Camera2D,
390 lighting: &LightingUniform,
391 commands: &[SpriteCommand],
392 target: &wgpu::TextureView,
393 encoder: &mut wgpu::CommandEncoder,
394 clear_color: wgpu::Color,
395 ) {
396 let camera_uniform = CameraUniform {
398 view_proj: camera.view_proj(),
399 };
400 gpu.queue.write_buffer(
401 &self.camera_buffer,
402 0,
403 bytemuck::cast_slice(&[camera_uniform]),
404 );
405
406 gpu.queue.write_buffer(
408 &self.lighting_buffer,
409 0,
410 bytemuck::cast_slice(&[*lighting]),
411 );
412
413 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
414 label: Some("sprite_render_pass"),
415 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
416 view: target,
417 resolve_target: None,
418 ops: wgpu::Operations {
419 load: wgpu::LoadOp::Clear(clear_color),
420 store: wgpu::StoreOp::Store,
421 },
422 })],
423 depth_stencil_attachment: None,
424 timestamp_writes: None,
425 occlusion_query_set: None,
426 });
427
428 render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
429 render_pass.set_bind_group(2, &self.lighting_bind_group, &[]);
430 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
431 render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
432
433 let mut current_shader: Option<u32> = None;
435 let mut current_blend: Option<u8> = None;
436 let mut i = 0;
437 while i < commands.len() {
438 let shader = commands[i].shader_id;
439 let blend = commands[i].blend_mode.min(3);
440 let tex_id = commands[i].texture_id;
441 let batch_start = i;
442 while i < commands.len()
443 && commands[i].shader_id == shader
444 && commands[i].blend_mode.min(3) == blend
445 && commands[i].texture_id == tex_id
446 {
447 i += 1;
448 }
449 let batch = &commands[batch_start..i];
450
451 if shader == 0 {
453 if current_shader != Some(0) || current_blend != Some(blend) {
454 render_pass.set_pipeline(&self.pipelines[blend as usize]);
455 current_shader = Some(0);
456 current_blend = Some(blend);
457 }
458 } else if current_shader != Some(shader) {
459 if let Some(pipeline) = shaders.get_pipeline(shader) {
460 render_pass.set_pipeline(pipeline);
461 if let Some(bg) = shaders.get_bind_group(shader) {
462 render_pass.set_bind_group(3, bg, &[]);
463 }
464 current_shader = Some(shader);
465 current_blend = None;
466 } else {
467 continue; }
469 }
470
471 let bind_group = match textures.get_bind_group(tex_id) {
473 Some(bg) => bg,
474 None => continue, };
476
477 let instances: Vec<SpriteInstance> = batch
479 .iter()
480 .map(|cmd| {
481 let mut uv_x = cmd.uv_x;
483 let mut uv_y = cmd.uv_y;
484 let mut uv_w = cmd.uv_w;
485 let mut uv_h = cmd.uv_h;
486 if cmd.flip_x {
487 uv_x += uv_w;
488 uv_w = -uv_w;
489 }
490 if cmd.flip_y {
491 uv_y += uv_h;
492 uv_h = -uv_h;
493 }
494 SpriteInstance {
495 world_pos: [cmd.x, cmd.y],
496 size: [cmd.w, cmd.h],
497 uv_offset: [uv_x, uv_y],
498 uv_size: [uv_w, uv_h],
499 tint: [cmd.tint_r, cmd.tint_g, cmd.tint_b, cmd.tint_a * cmd.opacity],
500 rotation_origin: [cmd.rotation, cmd.origin_x, cmd.origin_y, 0.0],
501 }
502 })
503 .collect();
504
505 let instance_buffer =
506 gpu.device
507 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
508 label: Some("sprite_instance_buffer"),
509 contents: bytemuck::cast_slice(&instances),
510 usage: wgpu::BufferUsages::VERTEX,
511 });
512
513 render_pass.set_bind_group(1, bind_group, &[]);
514 render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
515 render_pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
516 }
517 }
518}