1use astrelis_core::logging;
11use astrelis_render::{
12 Color, GraphicsContext, RenderWindow, RenderWindowBuilder, SpriteAnimation, SpriteSheet,
13 SpriteSheetDescriptor,
14};
15use astrelis_winit::{
16 WindowId,
17 app::run_app,
18 window::{Window, WindowDescriptor, WinitPhysicalSize},
19};
20use std::collections::HashMap;
21use std::sync::Arc;
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 {
85 let base_x = frame * SPRITE_SIZE;
86 let center = SPRITE_SIZE as f32 / 2.0;
87 let radius = SPRITE_SIZE as f32 / 2.0 - 4.0; for y in 0..SPRITE_SIZE {
90 for x in 0..SPRITE_SIZE {
91 let px = (base_x + x) as usize;
92 let py = y as usize;
93 let idx = (py * width as usize + px) * 4;
94
95 let dx = x as f32 - center;
96 let dy = y as f32 - center;
97 let dist = (dx * dx + dy * dy).sqrt();
98 let angle = dy.atan2(dx);
99
100 if (dist - radius).abs() < 3.0 {
102 let segment_angle = std::f32::consts::PI / 2.0 * frame as f32;
104 let mut rel_angle = angle - segment_angle;
106 while rel_angle < 0.0 {
107 rel_angle += std::f32::consts::PI * 2.0;
108 }
109 while rel_angle > std::f32::consts::PI * 2.0 {
110 rel_angle -= std::f32::consts::PI * 2.0;
111 }
112
113 let brightness = 1.0 - (rel_angle / (std::f32::consts::PI * 2.0));
116 let r = (100.0 + 155.0 * brightness) as u8; let g = (150.0 + 105.0 * brightness) as u8; let b = 255; pixels[idx] = r;
121 pixels[idx + 1] = g;
122 pixels[idx + 2] = b;
123 pixels[idx + 3] = 255;
124 } else if dist < radius - 3.0 {
125 let alpha = ((radius - 3.0 - dist) / 10.0).clamp(0.0, 0.3);
128 pixels[idx] = 100;
129 pixels[idx + 1] = 150;
130 pixels[idx + 2] = 200;
131 pixels[idx + 3] = (alpha * 255.0) as u8;
132 }
133 }
134 }
135 }
136
137 (pixels, width, height)
138}
139
140struct App {
141 _context: Arc<GraphicsContext>,
142 windows: HashMap<WindowId, RenderWindow>,
143 pipeline: wgpu::RenderPipeline,
144 bind_group: wgpu::BindGroup,
145 vertex_buffer: wgpu::Buffer,
146 _uniform_buffer: wgpu::Buffer,
147 sprite_sheet: SpriteSheet,
148 animation: SpriteAnimation,
149 last_update: Instant,
150}
151
152fn main() {
153 logging::init();
154
155 run_app(|ctx| {
156 let graphics_ctx =
157 GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
158 let mut windows = HashMap::new();
159
160 let scale = Window::platform_dpi() as f32;
161 let window = ctx
162 .create_window(WindowDescriptor {
163 title: "Sprite Sheet Animation Example".to_string(),
164 size: Some(WinitPhysicalSize::new(400.0 * scale, 400.0 * scale)),
165 ..Default::default()
166 })
167 .expect("Failed to create window");
168
169 let renderable_window = RenderWindowBuilder::new()
170 .color_format(wgpu::TextureFormat::Bgra8UnormSrgb)
171 .with_depth_default()
172 .build(window, graphics_ctx.clone())
173 .expect("Failed to create render window");
174
175 let window_id = renderable_window.id();
176 windows.insert(window_id, renderable_window);
177
178 let (sprite_data, tex_width, tex_height) = generate_sprite_sheet_data();
180 let sprite_sheet = SpriteSheet::from_data(
181 &graphics_ctx,
182 &sprite_data,
183 tex_width,
184 tex_height,
185 SpriteSheetDescriptor {
186 sprite_width: 64,
187 sprite_height: 64,
188 columns: 4,
189 rows: 1,
190 ..Default::default()
191 },
192 );
193
194 let animation = SpriteAnimation::new(4, 8.0);
196
197 let shader = graphics_ctx
199 .device()
200 .create_shader_module(wgpu::ShaderModuleDescriptor {
201 label: Some("Sprite Shader"),
202 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
203 });
204
205 let bind_group_layout =
207 graphics_ctx
208 .device()
209 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
210 label: Some("Sprite Bind Group Layout"),
211 entries: &[
212 wgpu::BindGroupLayoutEntry {
213 binding: 0,
214 visibility: wgpu::ShaderStages::VERTEX,
215 ty: wgpu::BindingType::Buffer {
216 ty: wgpu::BufferBindingType::Uniform,
217 has_dynamic_offset: false,
218 min_binding_size: None,
219 },
220 count: None,
221 },
222 wgpu::BindGroupLayoutEntry {
223 binding: 1,
224 visibility: wgpu::ShaderStages::FRAGMENT,
225 ty: wgpu::BindingType::Texture {
226 sample_type: wgpu::TextureSampleType::Float { filterable: true },
227 view_dimension: wgpu::TextureViewDimension::D2,
228 multisampled: false,
229 },
230 count: None,
231 },
232 wgpu::BindGroupLayoutEntry {
233 binding: 2,
234 visibility: wgpu::ShaderStages::FRAGMENT,
235 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
236 count: None,
237 },
238 ],
239 });
240
241 let pipeline_layout =
243 graphics_ctx
244 .device()
245 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
246 label: Some("Sprite Pipeline Layout"),
247 bind_group_layouts: &[&bind_group_layout],
248 push_constant_ranges: &[],
249 });
250
251 let pipeline =
253 graphics_ctx
254 .device()
255 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
256 label: Some("Sprite Pipeline"),
257 layout: Some(&pipeline_layout),
258 vertex: wgpu::VertexState {
259 module: &shader,
260 entry_point: Some("vs_main"),
261 buffers: &[wgpu::VertexBufferLayout {
262 array_stride: std::mem::size_of::<Vertex>() as u64,
263 step_mode: wgpu::VertexStepMode::Vertex,
264 attributes: &[
265 wgpu::VertexAttribute {
266 offset: 0,
267 shader_location: 0,
268 format: wgpu::VertexFormat::Float32x2,
269 },
270 wgpu::VertexAttribute {
271 offset: 8,
272 shader_location: 1,
273 format: wgpu::VertexFormat::Float32x2,
274 },
275 ],
276 }],
277 compilation_options: wgpu::PipelineCompilationOptions::default(),
278 },
279 fragment: Some(wgpu::FragmentState {
280 module: &shader,
281 entry_point: Some("fs_main"),
282 targets: &[Some(wgpu::ColorTargetState {
283 format: wgpu::TextureFormat::Bgra8UnormSrgb,
284 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
285 write_mask: wgpu::ColorWrites::ALL,
286 })],
287 compilation_options: wgpu::PipelineCompilationOptions::default(),
288 }),
289 primitive: wgpu::PrimitiveState {
290 topology: wgpu::PrimitiveTopology::TriangleList,
291 ..Default::default()
292 },
293 depth_stencil: None,
294 multisample: wgpu::MultisampleState::default(),
295 multiview: None,
296 cache: None,
297 });
298
299 let uniforms = Uniforms {
301 mvp: [
302 [1.0, 0.0, 0.0, 0.0],
303 [0.0, 1.0, 0.0, 0.0],
304 [0.0, 0.0, 1.0, 0.0],
305 [0.0, 0.0, 0.0, 1.0],
306 ],
307 };
308 let uniform_buffer =
309 graphics_ctx
310 .device()
311 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
312 label: Some("Uniform Buffer"),
313 contents: bytemuck::cast_slice(&[uniforms]),
314 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
315 });
316
317 let sampler = graphics_ctx
319 .device()
320 .create_sampler(&wgpu::SamplerDescriptor {
321 label: Some("Sprite Sampler"),
322 mag_filter: wgpu::FilterMode::Linear,
323 min_filter: wgpu::FilterMode::Linear,
324 ..Default::default()
325 });
326
327 let bind_group = graphics_ctx
329 .device()
330 .create_bind_group(&wgpu::BindGroupDescriptor {
331 label: Some("Sprite Bind Group"),
332 layout: &bind_group_layout,
333 entries: &[
334 wgpu::BindGroupEntry {
335 binding: 0,
336 resource: uniform_buffer.as_entire_binding(),
337 },
338 wgpu::BindGroupEntry {
339 binding: 1,
340 resource: wgpu::BindingResource::TextureView(sprite_sheet.view()),
341 },
342 wgpu::BindGroupEntry {
343 binding: 2,
344 resource: wgpu::BindingResource::Sampler(&sampler),
345 },
346 ],
347 });
348
349 let vertices = create_quad_vertices(0.0, 0.0, 1.0, 1.0);
351 let vertex_buffer =
352 graphics_ctx
353 .device()
354 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
355 label: Some("Vertex Buffer"),
356 contents: bytemuck::cast_slice(&vertices),
357 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
358 });
359
360 Box::new(App {
361 _context: graphics_ctx,
362 windows,
363 pipeline,
364 bind_group,
365 vertex_buffer,
366 _uniform_buffer: uniform_buffer,
367 sprite_sheet,
368 animation,
369 last_update: Instant::now(),
370 })
371 });
372}
373
374fn create_quad_vertices(u_min: f32, v_min: f32, u_max: f32, v_max: f32) -> [Vertex; 6] {
375 [
376 Vertex {
377 position: [-0.5, -0.5],
378 uv: [u_min, v_max],
379 },
380 Vertex {
381 position: [0.5, -0.5],
382 uv: [u_max, v_max],
383 },
384 Vertex {
385 position: [0.5, 0.5],
386 uv: [u_max, v_min],
387 },
388 Vertex {
389 position: [-0.5, -0.5],
390 uv: [u_min, v_max],
391 },
392 Vertex {
393 position: [0.5, 0.5],
394 uv: [u_max, v_min],
395 },
396 Vertex {
397 position: [-0.5, 0.5],
398 uv: [u_min, v_min],
399 },
400 ]
401}
402
403impl astrelis_winit::app::App for App {
404 fn update(
405 &mut self,
406 _ctx: &mut astrelis_winit::app::AppCtx,
407 _time: &astrelis_winit::FrameTime,
408 ) {
409 let now = Instant::now();
410 let dt = now.duration_since(self.last_update).as_secs_f32();
411 self.last_update = now;
412
413 if self.animation.update(dt) {
415 let frame = self.animation.current_frame();
417 let uv = self.sprite_sheet.sprite_uv(frame);
418 let vertices = create_quad_vertices(uv.u_min, uv.v_min, uv.u_max, uv.v_max);
419
420 if let Some(window) = self.windows.values().next() {
422 window.context().graphics_context().queue().write_buffer(
423 &self.vertex_buffer,
424 0,
425 bytemuck::cast_slice(&vertices),
426 );
427 }
428 }
429 }
430
431 fn render(
432 &mut self,
433 _ctx: &mut astrelis_winit::app::AppCtx,
434 window_id: WindowId,
435 events: &mut astrelis_winit::event::EventBatch,
436 ) {
437 let Some(window) = self.windows.get_mut(&window_id) else {
438 return;
439 };
440
441 events.dispatch(|event| {
443 if let astrelis_winit::event::Event::WindowResized(size) = event {
444 window.resized(*size);
445 astrelis_winit::event::HandleStatus::consumed()
446 } else {
447 astrelis_winit::event::HandleStatus::ignored()
448 }
449 });
450
451 let Some(frame) = window.begin_frame() else {
452 return; };
454 {
455 let mut pass = frame
456 .render_pass()
457 .clear_color(Color::rgb(0.1, 0.1, 0.15))
458 .label("sprite_sheet_pass")
459 .build();
460 pass.set_pipeline(&self.pipeline);
461 pass.set_bind_group(0, &self.bind_group, &[]);
462 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
463 pass.draw(0..6, 0..1);
464 }
465 }
467}