Skip to main content

arcane_engine/renderer/
sprite.rs

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
9/// A sprite draw command queued from TypeScript.
10#[derive(Debug, Clone)]
11pub struct SpriteCommand {
12    pub texture_id: u32,
13    pub x: f32,
14    pub y: f32,
15    pub w: f32,
16    pub h: f32,
17    pub layer: i32,
18    pub uv_x: f32,
19    pub uv_y: f32,
20    pub uv_w: f32,
21    pub uv_h: f32,
22    pub tint_r: f32,
23    pub tint_g: f32,
24    pub tint_b: f32,
25    pub tint_a: f32,
26}
27
28/// Per-vertex data for the unit quad.
29#[repr(C)]
30#[derive(Copy, Clone, Pod, Zeroable)]
31struct QuadVertex {
32    position: [f32; 2],
33    uv: [f32; 2],
34}
35
36/// Per-instance data for each sprite.
37#[repr(C)]
38#[derive(Copy, Clone, Pod, Zeroable)]
39struct SpriteInstance {
40    world_pos: [f32; 2],
41    size: [f32; 2],
42    uv_offset: [f32; 2],
43    uv_size: [f32; 2],
44    tint: [f32; 4],
45}
46
47/// Camera uniform buffer data.
48#[repr(C)]
49#[derive(Copy, Clone, Pod, Zeroable)]
50struct CameraUniform {
51    view_proj: [f32; 16],
52}
53
54// Unit quad: two triangles forming a 1x1 square at origin
55const QUAD_VERTICES: &[QuadVertex] = &[
56    QuadVertex { position: [0.0, 0.0], uv: [0.0, 0.0] }, // top-left
57    QuadVertex { position: [1.0, 0.0], uv: [1.0, 0.0] }, // top-right
58    QuadVertex { position: [1.0, 1.0], uv: [1.0, 1.0] }, // bottom-right
59    QuadVertex { position: [0.0, 1.0], uv: [0.0, 1.0] }, // bottom-left
60];
61
62const QUAD_INDICES: &[u16] = &[0, 1, 2, 0, 2, 3];
63
64pub struct SpritePipeline {
65    pipeline: wgpu::RenderPipeline,
66    vertex_buffer: wgpu::Buffer,
67    index_buffer: wgpu::Buffer,
68    camera_buffer: wgpu::Buffer,
69    camera_bind_group: wgpu::BindGroup,
70    pub texture_bind_group_layout: wgpu::BindGroupLayout,
71    lighting_buffer: wgpu::Buffer,
72    lighting_bind_group: wgpu::BindGroup,
73}
74
75impl SpritePipeline {
76    pub fn new(gpu: &GpuContext) -> Self {
77        let shader = gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor {
78            label: Some("sprite_shader"),
79            source: wgpu::ShaderSource::Wgsl(
80                include_str!("shaders/sprite.wgsl").into(),
81            ),
82        });
83
84        // Camera uniform bind group layout (group 0)
85        let camera_bind_group_layout =
86            gpu.device
87                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
88                    label: Some("camera_bind_group_layout"),
89                    entries: &[wgpu::BindGroupLayoutEntry {
90                        binding: 0,
91                        visibility: wgpu::ShaderStages::VERTEX,
92                        ty: wgpu::BindingType::Buffer {
93                            ty: wgpu::BufferBindingType::Uniform,
94                            has_dynamic_offset: false,
95                            min_binding_size: None,
96                        },
97                        count: None,
98                    }],
99                });
100
101        // Texture bind group layout (group 1)
102        let texture_bind_group_layout =
103            gpu.device
104                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
105                    label: Some("texture_bind_group_layout"),
106                    entries: &[
107                        wgpu::BindGroupLayoutEntry {
108                            binding: 0,
109                            visibility: wgpu::ShaderStages::FRAGMENT,
110                            ty: wgpu::BindingType::Texture {
111                                multisampled: false,
112                                view_dimension: wgpu::TextureViewDimension::D2,
113                                sample_type: wgpu::TextureSampleType::Float { filterable: true },
114                            },
115                            count: None,
116                        },
117                        wgpu::BindGroupLayoutEntry {
118                            binding: 1,
119                            visibility: wgpu::ShaderStages::FRAGMENT,
120                            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
121                            count: None,
122                        },
123                    ],
124                });
125
126        // Lighting uniform bind group layout (group 2)
127        let lighting_bind_group_layout =
128            gpu.device
129                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
130                    label: Some("lighting_bind_group_layout"),
131                    entries: &[wgpu::BindGroupLayoutEntry {
132                        binding: 0,
133                        visibility: wgpu::ShaderStages::FRAGMENT,
134                        ty: wgpu::BindingType::Buffer {
135                            ty: wgpu::BufferBindingType::Uniform,
136                            has_dynamic_offset: false,
137                            min_binding_size: None,
138                        },
139                        count: None,
140                    }],
141                });
142
143        let pipeline_layout =
144            gpu.device
145                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
146                    label: Some("sprite_pipeline_layout"),
147                    bind_group_layouts: &[
148                        &camera_bind_group_layout,
149                        &texture_bind_group_layout,
150                        &lighting_bind_group_layout,
151                    ],
152                    push_constant_ranges: &[],
153                });
154
155        // Vertex buffer layouts
156        let vertex_layout = wgpu::VertexBufferLayout {
157            array_stride: std::mem::size_of::<QuadVertex>() as wgpu::BufferAddress,
158            step_mode: wgpu::VertexStepMode::Vertex,
159            attributes: &[
160                wgpu::VertexAttribute {
161                    offset: 0,
162                    shader_location: 0,
163                    format: wgpu::VertexFormat::Float32x2,
164                },
165                wgpu::VertexAttribute {
166                    offset: 8,
167                    shader_location: 1,
168                    format: wgpu::VertexFormat::Float32x2,
169                },
170            ],
171        };
172
173        let instance_layout = wgpu::VertexBufferLayout {
174            array_stride: std::mem::size_of::<SpriteInstance>() as wgpu::BufferAddress,
175            step_mode: wgpu::VertexStepMode::Instance,
176            attributes: &[
177                wgpu::VertexAttribute {
178                    offset: 0,
179                    shader_location: 2,
180                    format: wgpu::VertexFormat::Float32x2, // world_pos
181                },
182                wgpu::VertexAttribute {
183                    offset: 8,
184                    shader_location: 3,
185                    format: wgpu::VertexFormat::Float32x2, // size
186                },
187                wgpu::VertexAttribute {
188                    offset: 16,
189                    shader_location: 4,
190                    format: wgpu::VertexFormat::Float32x2, // uv_offset
191                },
192                wgpu::VertexAttribute {
193                    offset: 24,
194                    shader_location: 5,
195                    format: wgpu::VertexFormat::Float32x2, // uv_size
196                },
197                wgpu::VertexAttribute {
198                    offset: 32,
199                    shader_location: 6,
200                    format: wgpu::VertexFormat::Float32x4, // tint
201                },
202            ],
203        };
204
205        let pipeline = gpu.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
206            label: Some("sprite_pipeline"),
207            layout: Some(&pipeline_layout),
208            vertex: wgpu::VertexState {
209                module: &shader,
210                entry_point: Some("vs_main"),
211                buffers: &[vertex_layout, instance_layout],
212                compilation_options: Default::default(),
213            },
214            fragment: Some(wgpu::FragmentState {
215                module: &shader,
216                entry_point: Some("fs_main"),
217                targets: &[Some(wgpu::ColorTargetState {
218                    format: gpu.config.format,
219                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
220                    write_mask: wgpu::ColorWrites::ALL,
221                })],
222                compilation_options: Default::default(),
223            }),
224            primitive: wgpu::PrimitiveState {
225                topology: wgpu::PrimitiveTopology::TriangleList,
226                strip_index_format: None,
227                front_face: wgpu::FrontFace::Ccw,
228                cull_mode: None,
229                polygon_mode: wgpu::PolygonMode::Fill,
230                unclipped_depth: false,
231                conservative: false,
232            },
233            depth_stencil: None,
234            multisample: wgpu::MultisampleState::default(),
235            multiview: None,
236            cache: None,
237        });
238
239        let vertex_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
240            label: Some("quad_vertex_buffer"),
241            contents: bytemuck::cast_slice(QUAD_VERTICES),
242            usage: wgpu::BufferUsages::VERTEX,
243        });
244
245        let index_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
246            label: Some("quad_index_buffer"),
247            contents: bytemuck::cast_slice(QUAD_INDICES),
248            usage: wgpu::BufferUsages::INDEX,
249        });
250
251        let camera_uniform = CameraUniform {
252            view_proj: Camera2D::default().view_proj(),
253        };
254
255        let camera_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
256            label: Some("camera_uniform_buffer"),
257            contents: bytemuck::cast_slice(&[camera_uniform]),
258            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
259        });
260
261        let camera_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
262            label: Some("camera_bind_group"),
263            layout: &camera_bind_group_layout,
264            entries: &[wgpu::BindGroupEntry {
265                binding: 0,
266                resource: camera_buffer.as_entire_binding(),
267            }],
268        });
269
270        // Lighting uniform buffer (272 bytes = LightingUniform size)
271        let default_lighting = LightingUniform {
272            ambient: [1.0, 1.0, 1.0],
273            light_count: 0,
274            lights: [super::lighting::LightData {
275                pos_radius: [0.0; 4],
276                color_intensity: [0.0; 4],
277            }; super::lighting::MAX_LIGHTS],
278        };
279
280        let lighting_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
281            label: Some("lighting_uniform_buffer"),
282            contents: bytemuck::cast_slice(&[default_lighting]),
283            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
284        });
285
286        let lighting_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
287            label: Some("lighting_bind_group"),
288            layout: &lighting_bind_group_layout,
289            entries: &[wgpu::BindGroupEntry {
290                binding: 0,
291                resource: lighting_buffer.as_entire_binding(),
292            }],
293        });
294
295        Self {
296            pipeline,
297            vertex_buffer,
298            index_buffer,
299            camera_buffer,
300            camera_bind_group,
301            texture_bind_group_layout,
302            lighting_buffer,
303            lighting_bind_group,
304        }
305    }
306
307    /// Render a sorted list of sprite commands. Commands should be sorted by layer then texture_id.
308    pub fn render(
309        &self,
310        gpu: &GpuContext,
311        textures: &TextureStore,
312        camera: &Camera2D,
313        lighting: &LightingUniform,
314        commands: &[SpriteCommand],
315        target: &wgpu::TextureView,
316        encoder: &mut wgpu::CommandEncoder,
317        clear_color: wgpu::Color,
318    ) {
319        // Update camera uniform
320        let camera_uniform = CameraUniform {
321            view_proj: camera.view_proj(),
322        };
323        gpu.queue.write_buffer(
324            &self.camera_buffer,
325            0,
326            bytemuck::cast_slice(&[camera_uniform]),
327        );
328
329        // Update lighting uniform
330        gpu.queue.write_buffer(
331            &self.lighting_buffer,
332            0,
333            bytemuck::cast_slice(&[*lighting]),
334        );
335
336        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
337            label: Some("sprite_render_pass"),
338            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
339                view: target,
340                resolve_target: None,
341                ops: wgpu::Operations {
342                    load: wgpu::LoadOp::Clear(clear_color),
343                    store: wgpu::StoreOp::Store,
344                },
345            })],
346            depth_stencil_attachment: None,
347            timestamp_writes: None,
348            occlusion_query_set: None,
349        });
350
351        render_pass.set_pipeline(&self.pipeline);
352        render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
353        render_pass.set_bind_group(2, &self.lighting_bind_group, &[]);
354        render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
355        render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
356
357        // Batch by texture_id
358        let mut i = 0;
359        while i < commands.len() {
360            let tex_id = commands[i].texture_id;
361            let batch_start = i;
362            while i < commands.len() && commands[i].texture_id == tex_id {
363                i += 1;
364            }
365            let batch = &commands[batch_start..i];
366
367            // Get texture bind group
368            let bind_group = match textures.get_bind_group(tex_id) {
369                Some(bg) => bg,
370                None => continue, // skip if texture not loaded
371            };
372
373            // Build instance buffer for this batch
374            let instances: Vec<SpriteInstance> = batch
375                .iter()
376                .map(|cmd| SpriteInstance {
377                    world_pos: [cmd.x, cmd.y],
378                    size: [cmd.w, cmd.h],
379                    uv_offset: [cmd.uv_x, cmd.uv_y],
380                    uv_size: [cmd.uv_w, cmd.uv_h],
381                    tint: [cmd.tint_r, cmd.tint_g, cmd.tint_b, cmd.tint_a],
382                })
383                .collect();
384
385            let instance_buffer =
386                gpu.device
387                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
388                        label: Some("sprite_instance_buffer"),
389                        contents: bytemuck::cast_slice(&instances),
390                        usage: wgpu::BufferUsages::VERTEX,
391                    });
392
393            render_pass.set_bind_group(1, bind_group, &[]);
394            render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
395            render_pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
396        }
397    }
398}