1use astrelis_core::logging;
11use astrelis_render::{
12 GraphicsContext, RenderPassBuilder, RenderTarget, RenderableWindow, WindowContextDescriptor,
13 SpriteSheet, SpriteSheetDescriptor, SpriteAnimation,
14};
15use astrelis_winit::{
16 WindowId,
17 app::run_app,
18 event::PhysicalSize,
19 window::{WindowBackend, WindowDescriptor, Window},
20};
21use std::collections::HashMap;
22use std::time::Instant;
23use wgpu::util::DeviceExt;
24
25const SHADER: &str = r#"
27struct Uniforms {
28 mvp: mat4x4<f32>,
29}
30
31@group(0) @binding(0) var<uniform> uniforms: Uniforms;
32@group(0) @binding(1) var sprite_texture: texture_2d<f32>;
33@group(0) @binding(2) var sprite_sampler: sampler;
34
35struct VertexInput {
36 @location(0) position: vec2<f32>,
37 @location(1) uv: vec2<f32>,
38}
39
40struct VertexOutput {
41 @builtin(position) clip_position: vec4<f32>,
42 @location(0) uv: vec2<f32>,
43}
44
45@vertex
46fn vs_main(in: VertexInput) -> VertexOutput {
47 var out: VertexOutput;
48 out.clip_position = uniforms.mvp * vec4<f32>(in.position, 0.0, 1.0);
49 out.uv = in.uv;
50 return out;
51}
52
53@fragment
54fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
55 return textureSample(sprite_texture, sprite_sampler, in.uv);
56}
57"#;
58
59#[repr(C)]
60#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
61struct Vertex {
62 position: [f32; 2],
63 uv: [f32; 2],
64}
65
66#[repr(C)]
67#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
68struct Uniforms {
69 mvp: [[f32; 4]; 4],
70}
71
72fn generate_sprite_sheet_data() -> (Vec<u8>, u32, u32) {
74 const SPRITE_SIZE: u32 = 64;
75 const COLUMNS: u32 = 4;
76 const ROWS: u32 = 1;
77
78 let width = SPRITE_SIZE * COLUMNS;
79 let height = SPRITE_SIZE * ROWS;
80 let mut pixels = vec![0u8; (width * height * 4) as usize];
81
82 for frame in 0..4 {
84 let base_x = frame * SPRITE_SIZE;
85 let center = SPRITE_SIZE as f32 / 2.0;
86 let radius = SPRITE_SIZE as f32 / 2.0 - 4.0;
87
88 for y in 0..SPRITE_SIZE {
89 for x in 0..SPRITE_SIZE {
90 let px = (base_x + x) as usize;
91 let py = y as usize;
92 let idx = (py * width as usize + px) * 4;
93
94 let dx = x as f32 - center;
95 let dy = y as f32 - center;
96 let dist = (dx * dx + dy * dy).sqrt();
97 let angle = dy.atan2(dx);
98
99 if (dist - radius).abs() < 3.0 {
101 let segment_angle = std::f32::consts::PI / 2.0 * frame as f32;
103 let mut rel_angle = angle - segment_angle;
104 while rel_angle < 0.0 {
105 rel_angle += std::f32::consts::PI * 2.0;
106 }
107 while rel_angle > std::f32::consts::PI * 2.0 {
108 rel_angle -= std::f32::consts::PI * 2.0;
109 }
110
111 let brightness = 1.0 - (rel_angle / (std::f32::consts::PI * 2.0));
113 let r = (100.0 + 155.0 * brightness) as u8;
114 let g = (150.0 + 105.0 * brightness) as u8;
115 let b = 255;
116
117 pixels[idx] = r;
118 pixels[idx + 1] = g;
119 pixels[idx + 2] = b;
120 pixels[idx + 3] = 255;
121 } else if dist < radius - 3.0 {
122 let alpha = ((radius - 3.0 - dist) / 10.0).clamp(0.0, 0.3);
124 pixels[idx] = 100;
125 pixels[idx + 1] = 150;
126 pixels[idx + 2] = 200;
127 pixels[idx + 3] = (alpha * 255.0) as u8;
128 }
129 }
130 }
131 }
132
133 (pixels, width, height)
134}
135
136struct App {
137 _context: &'static GraphicsContext,
138 windows: HashMap<WindowId, RenderableWindow>,
139 pipeline: wgpu::RenderPipeline,
140 bind_group: wgpu::BindGroup,
141 vertex_buffer: wgpu::Buffer,
142 uniform_buffer: wgpu::Buffer,
143 sprite_sheet: SpriteSheet,
144 animation: SpriteAnimation,
145 last_update: Instant,
146}
147
148fn main() {
149 logging::init();
150
151 run_app(|ctx| {
152 let graphics_ctx = GraphicsContext::new_sync();
153 let mut windows = HashMap::new();
154
155 let scale = Window::platform_dpi() as f32;
156 let window = ctx
157 .create_window(WindowDescriptor {
158 title: "Sprite Sheet Animation Example".to_string(),
159 size: Some(PhysicalSize::new(400.0 * scale, 400.0 * scale)),
160 ..Default::default()
161 })
162 .expect("Failed to create window");
163
164 let renderable_window = RenderableWindow::new_with_descriptor(
165 window,
166 graphics_ctx,
167 WindowContextDescriptor {
168 format: Some(wgpu::TextureFormat::Bgra8UnormSrgb),
169 ..Default::default()
170 },
171 );
172
173 let window_id = renderable_window.id();
174 windows.insert(window_id, renderable_window);
175
176 let (sprite_data, tex_width, tex_height) = generate_sprite_sheet_data();
178 let sprite_sheet = SpriteSheet::from_data(
179 graphics_ctx,
180 &sprite_data,
181 tex_width,
182 tex_height,
183 SpriteSheetDescriptor {
184 sprite_width: 64,
185 sprite_height: 64,
186 columns: 4,
187 rows: 1,
188 ..Default::default()
189 },
190 );
191
192 let animation = SpriteAnimation::new(4, 8.0);
194
195 let shader = graphics_ctx.device.create_shader_module(wgpu::ShaderModuleDescriptor {
197 label: Some("Sprite Shader"),
198 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
199 });
200
201 let bind_group_layout = graphics_ctx.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
203 label: Some("Sprite Bind Group Layout"),
204 entries: &[
205 wgpu::BindGroupLayoutEntry {
206 binding: 0,
207 visibility: wgpu::ShaderStages::VERTEX,
208 ty: wgpu::BindingType::Buffer {
209 ty: wgpu::BufferBindingType::Uniform,
210 has_dynamic_offset: false,
211 min_binding_size: None,
212 },
213 count: None,
214 },
215 wgpu::BindGroupLayoutEntry {
216 binding: 1,
217 visibility: wgpu::ShaderStages::FRAGMENT,
218 ty: wgpu::BindingType::Texture {
219 sample_type: wgpu::TextureSampleType::Float { filterable: true },
220 view_dimension: wgpu::TextureViewDimension::D2,
221 multisampled: false,
222 },
223 count: None,
224 },
225 wgpu::BindGroupLayoutEntry {
226 binding: 2,
227 visibility: wgpu::ShaderStages::FRAGMENT,
228 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
229 count: None,
230 },
231 ],
232 });
233
234 let pipeline_layout = graphics_ctx.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
236 label: Some("Sprite Pipeline Layout"),
237 bind_group_layouts: &[&bind_group_layout],
238 push_constant_ranges: &[],
239 });
240
241 let pipeline = graphics_ctx.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
243 label: Some("Sprite Pipeline"),
244 layout: Some(&pipeline_layout),
245 vertex: wgpu::VertexState {
246 module: &shader,
247 entry_point: Some("vs_main"),
248 buffers: &[wgpu::VertexBufferLayout {
249 array_stride: std::mem::size_of::<Vertex>() as u64,
250 step_mode: wgpu::VertexStepMode::Vertex,
251 attributes: &[
252 wgpu::VertexAttribute {
253 offset: 0,
254 shader_location: 0,
255 format: wgpu::VertexFormat::Float32x2,
256 },
257 wgpu::VertexAttribute {
258 offset: 8,
259 shader_location: 1,
260 format: wgpu::VertexFormat::Float32x2,
261 },
262 ],
263 }],
264 compilation_options: wgpu::PipelineCompilationOptions::default(),
265 },
266 fragment: Some(wgpu::FragmentState {
267 module: &shader,
268 entry_point: Some("fs_main"),
269 targets: &[Some(wgpu::ColorTargetState {
270 format: wgpu::TextureFormat::Bgra8UnormSrgb,
271 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
272 write_mask: wgpu::ColorWrites::ALL,
273 })],
274 compilation_options: wgpu::PipelineCompilationOptions::default(),
275 }),
276 primitive: wgpu::PrimitiveState {
277 topology: wgpu::PrimitiveTopology::TriangleList,
278 ..Default::default()
279 },
280 depth_stencil: None,
281 multisample: wgpu::MultisampleState::default(),
282 multiview: None,
283 cache: None,
284 });
285
286 let uniforms = Uniforms {
288 mvp: [
289 [1.0, 0.0, 0.0, 0.0],
290 [0.0, 1.0, 0.0, 0.0],
291 [0.0, 0.0, 1.0, 0.0],
292 [0.0, 0.0, 0.0, 1.0],
293 ],
294 };
295 let uniform_buffer = graphics_ctx.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
296 label: Some("Uniform Buffer"),
297 contents: bytemuck::cast_slice(&[uniforms]),
298 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
299 });
300
301 let sampler = graphics_ctx.device.create_sampler(&wgpu::SamplerDescriptor {
303 label: Some("Sprite Sampler"),
304 mag_filter: wgpu::FilterMode::Linear,
305 min_filter: wgpu::FilterMode::Linear,
306 ..Default::default()
307 });
308
309 let bind_group = graphics_ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
311 label: Some("Sprite Bind Group"),
312 layout: &bind_group_layout,
313 entries: &[
314 wgpu::BindGroupEntry {
315 binding: 0,
316 resource: uniform_buffer.as_entire_binding(),
317 },
318 wgpu::BindGroupEntry {
319 binding: 1,
320 resource: wgpu::BindingResource::TextureView(sprite_sheet.view()),
321 },
322 wgpu::BindGroupEntry {
323 binding: 2,
324 resource: wgpu::BindingResource::Sampler(&sampler),
325 },
326 ],
327 });
328
329 let vertices = create_quad_vertices(0.0, 0.0, 1.0, 1.0);
331 let vertex_buffer = graphics_ctx.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
332 label: Some("Vertex Buffer"),
333 contents: bytemuck::cast_slice(&vertices),
334 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
335 });
336
337 Box::new(App {
338 _context: graphics_ctx,
339 windows,
340 pipeline,
341 bind_group,
342 vertex_buffer,
343 uniform_buffer,
344 sprite_sheet,
345 animation,
346 last_update: Instant::now(),
347 })
348 });
349}
350
351fn create_quad_vertices(u_min: f32, v_min: f32, u_max: f32, v_max: f32) -> [Vertex; 6] {
352 [
353 Vertex { position: [-0.5, -0.5], uv: [u_min, v_max] },
354 Vertex { position: [0.5, -0.5], uv: [u_max, v_max] },
355 Vertex { position: [0.5, 0.5], uv: [u_max, v_min] },
356 Vertex { position: [-0.5, -0.5], uv: [u_min, v_max] },
357 Vertex { position: [0.5, 0.5], uv: [u_max, v_min] },
358 Vertex { position: [-0.5, 0.5], uv: [u_min, v_min] },
359 ]
360}
361
362impl astrelis_winit::app::App for App {
363 fn update(&mut self, _ctx: &mut astrelis_winit::app::AppCtx) {
364 let now = Instant::now();
365 let dt = now.duration_since(self.last_update).as_secs_f32();
366 self.last_update = now;
367
368 if self.animation.update(dt) {
370 let frame = self.animation.current_frame();
372 let uv = self.sprite_sheet.sprite_uv(frame);
373 let vertices = create_quad_vertices(uv.u_min, uv.v_min, uv.u_max, uv.v_max);
374
375 if let Some(window) = self.windows.values().next() {
377 window.context().graphics_context().queue.write_buffer(
378 &self.vertex_buffer,
379 0,
380 bytemuck::cast_slice(&vertices),
381 );
382 }
383 }
384 }
385
386 fn render(
387 &mut self,
388 _ctx: &mut astrelis_winit::app::AppCtx,
389 window_id: WindowId,
390 events: &mut astrelis_winit::event::EventBatch,
391 ) {
392 let Some(window) = self.windows.get_mut(&window_id) else {
393 return;
394 };
395
396 events.dispatch(|event| {
398 if let astrelis_winit::event::Event::WindowResized(size) = event {
399 window.resized(*size);
400 astrelis_winit::event::HandleStatus::consumed()
401 } else {
402 astrelis_winit::event::HandleStatus::ignored()
403 }
404 });
405
406 let mut frame = window.begin_drawing();
407
408 {
409 let mut render_pass = RenderPassBuilder::new()
410 .label("Sprite Render Pass")
411 .target(RenderTarget::Surface)
412 .clear_color(wgpu::Color {
413 r: 0.1,
414 g: 0.1,
415 b: 0.15,
416 a: 1.0,
417 })
418 .build(&mut frame);
419
420 let pass = render_pass.descriptor();
421 pass.set_pipeline(&self.pipeline);
422 pass.set_bind_group(0, &self.bind_group, &[]);
423 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
424 pass.draw(0..6, 0..1);
425 }
426
427 frame.finish();
428 }
429}