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    ) {
318        // Update camera uniform
319        let camera_uniform = CameraUniform {
320            view_proj: camera.view_proj(),
321        };
322        gpu.queue.write_buffer(
323            &self.camera_buffer,
324            0,
325            bytemuck::cast_slice(&[camera_uniform]),
326        );
327
328        // Update lighting uniform
329        gpu.queue.write_buffer(
330            &self.lighting_buffer,
331            0,
332            bytemuck::cast_slice(&[*lighting]),
333        );
334
335        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
336            label: Some("sprite_render_pass"),
337            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
338                view: target,
339                resolve_target: None,
340                ops: wgpu::Operations {
341                    load: wgpu::LoadOp::Clear(wgpu::Color {
342                        r: 0.1,
343                        g: 0.1,
344                        b: 0.15,
345                        a: 1.0,
346                    }),
347                    store: wgpu::StoreOp::Store,
348                },
349            })],
350            depth_stencil_attachment: None,
351            timestamp_writes: None,
352            occlusion_query_set: None,
353        });
354
355        render_pass.set_pipeline(&self.pipeline);
356        render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
357        render_pass.set_bind_group(2, &self.lighting_bind_group, &[]);
358        render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
359        render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
360
361        // Batch by texture_id
362        let mut i = 0;
363        while i < commands.len() {
364            let tex_id = commands[i].texture_id;
365            let batch_start = i;
366            while i < commands.len() && commands[i].texture_id == tex_id {
367                i += 1;
368            }
369            let batch = &commands[batch_start..i];
370
371            // Get texture bind group
372            let bind_group = match textures.get_bind_group(tex_id) {
373                Some(bg) => bg,
374                None => continue, // skip if texture not loaded
375            };
376
377            // Build instance buffer for this batch
378            let instances: Vec<SpriteInstance> = batch
379                .iter()
380                .map(|cmd| SpriteInstance {
381                    world_pos: [cmd.x, cmd.y],
382                    size: [cmd.w, cmd.h],
383                    uv_offset: [cmd.uv_x, cmd.uv_y],
384                    uv_size: [cmd.uv_w, cmd.uv_h],
385                    tint: [cmd.tint_r, cmd.tint_g, cmd.tint_b, cmd.tint_a],
386                })
387                .collect();
388
389            let instance_buffer =
390                gpu.device
391                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
392                        label: Some("sprite_instance_buffer"),
393                        contents: bytemuck::cast_slice(&instances),
394                        usage: wgpu::BufferUsages::VERTEX,
395                    });
396
397            render_pass.set_bind_group(1, bind_group, &[]);
398            render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
399            render_pass.draw_indexed(0..6, 0, 0..instances.len() as u32);
400        }
401    }
402}