From 9f9f8303f4fdd9d2ec3d6f6c8dd6a3f94c79c280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaro=20Per=C3=A4maa?= <aaro.peramaa@gmail.com> Date: Fri, 7 Jul 2023 21:33:23 +0300 Subject: [PATCH] Splash --- Cargo.lock | 5 ++ Cargo.toml | 4 +- assets/sprite.wgsl | 3 + src/anim.rs | 42 +++++++++++ src/lib.rs | 76 +++++++++++-------- src/prelude.rs | 27 ++++++- src/sprite.rs | 183 +++++++++++++++++++++++++++++++++++++++++++++ src/state.rs | 44 +++++++++++ 8 files changed, 348 insertions(+), 36 deletions(-) create mode 100644 assets/sprite.wgsl create mode 100644 src/anim.rs create mode 100644 src/sprite.rs create mode 100644 src/state.rs diff --git a/Cargo.lock b/Cargo.lock index c637052..adcc1f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -530,6 +530,9 @@ name = "glam" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42218cb640844e3872cc3c153dc975229e080a6c4733b34709ef445610550226" +dependencies = [ + "bytemuck", +] [[package]] name = "glow" @@ -1356,10 +1359,12 @@ name = "quickgame" version = "0.1.0" dependencies = [ "anyhow", + "bytemuck", "console_error_panic_hook", "console_log", "env_logger", "glam", + "instant", "log", "png", "pollster", diff --git a/Cargo.toml b/Cargo.toml index 8e00a5f..d0de8ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,13 +23,15 @@ default = [] webgl = ["wgpu/webgl"] [dependencies] -glam = "0.24" +glam = { version = "0.24", features = ["bytemuck"] } wgpu = "0.16" winit = "0.28" log = "0.4" anyhow = "1" reqwest = "0.11" png = "0.17" +bytemuck = "1" +instant = "0.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pollster = "0.3" diff --git a/assets/sprite.wgsl b/assets/sprite.wgsl new file mode 100644 index 0000000..d2f610f --- /dev/null +++ b/assets/sprite.wgsl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c5f9bc276726e746c1c09b5f74d8e8a4784927646e1698aab241e2d778ee2ad +size 772 diff --git a/src/anim.rs b/src/anim.rs new file mode 100644 index 0000000..d7eba97 --- /dev/null +++ b/src/anim.rs @@ -0,0 +1,42 @@ +use crate::prelude::*; + +pub struct Animation<T> { + keyframes: Vec<T>, + begin: Instant, + duration: Duration, +} + +impl<T> Animation<T> { + pub fn new(keyframes: Vec<T>, duration: Duration) -> Self { + Self { + keyframes, + begin: Instant::now(), + duration, + } + } + + pub fn get(&self) -> T + where + T: std::ops::Mul<f32, Output = T> + std::ops::Add<Output = T> + Clone, + { + let prog = self.begin.elapsed().as_secs_f32() / self.duration.as_secs_f32(); + let scale = prog * self.keyframes.len() as f32; + let low = scale.floor() as usize; + let high = scale.ceil() as usize; + let blend = scale.fract(); + + match (self.keyframes.get(low), self.keyframes.get(high)) { + (None, None) => self.keyframes.last().unwrap().clone(), + (None, Some(end)) | (Some(end), None) => end.clone(), + (Some(a), Some(b)) => a.clone() * (1.0 - blend) + b.clone() * blend, + } + } + + pub fn complete(&self) -> bool { + self.begin.elapsed() >= self.duration + } + + pub fn reset(&mut self) { + self.begin = Instant::now(); + } +} diff --git a/src/lib.rs b/src/lib.rs index c805a5b..65dfc04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ +mod anim; mod asset; pub mod prelude; +mod sprite; +mod state; use wgpu::*; use winit::{ @@ -9,7 +12,7 @@ use winit::{ window::Window, }; -use crate::prelude::*; +use crate::{prelude::*, state::State}; pub async fn run() -> Result<()> { let event_loop = winit::event_loop::EventLoop::new(); @@ -55,7 +58,7 @@ pub async fn run() -> Result<()> { &DeviceDescriptor { label: Some("GMTK"), features: Features::empty(), - limits: Limits::downlevel_webgl2_defaults(), + limits: Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()), }, None, ) @@ -105,10 +108,7 @@ pub async fn run() -> Result<()> { view_formats: &[], }); - let fg = asset::load_png("splash_fg.png").await?; - let bg = asset::load_png("splash_bg.png").await?; - - let _ = (fg, bg); + let mut state = State::new(&queue, &device).await?; event_loop.run(move |event, _target, control_flow| { *control_flow = ControlFlow::Poll; @@ -177,35 +177,45 @@ pub async fn run() -> Result<()> { label: Some("Main encoder"), }); - enc.begin_render_pass(&RenderPassDescriptor { - label: Some("Clear"), - color_attachments: &[Some(RenderPassColorAttachment { - view: &framebuffer.create_view(&TextureViewDescriptor { - label: Some("Antialias target"), - format: view_formats.get(0).copied(), - ..Default::default() - }), - resolve_target: Some(&swapchain.texture.create_view( - &TextureViewDescriptor { - label: Some("Resolve target"), - format: view_formats.get(0).copied(), - ..Default::default() + { + let fb_view = framebuffer.create_view(&TextureViewDescriptor { + label: Some("Antialias target"), + format: view_formats.get(0).copied(), + ..Default::default() + }); + let rs_view = swapchain.texture.create_view(&TextureViewDescriptor { + label: Some("Resolve target"), + format: view_formats.get(0).copied(), + ..Default::default() + }); + let db_view = depthbuffer.create_view(&Default::default()); + + let mut pass = enc.begin_render_pass(&RenderPassDescriptor { + label: Some("Clear"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &fb_view, + resolve_target: Some(&rs_view), + ops: Operations { + load: LoadOp::Clear(Color::BLACK), + store: true, }, - )), - ops: Operations { - load: LoadOp::Clear(Color::RED), - store: true, - }, - })], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depthbuffer.create_view(&Default::default()), - depth_ops: Some(Operations { - load: LoadOp::Clear(1.0), - store: true, + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &db_view, + depth_ops: Some(Operations { + load: LoadOp::Clear(1.0), + store: true, + }), + stencil_ops: None, }), - stencil_ops: None, - }), - }); + }); + + let pipeline_fmt = view_formats.get(0).copied().unwrap_or(format); + let aspect = + window.inner_size().width as f32 / window.inner_size().height as f32; + + state.update(&queue, &device, &mut pass, pipeline_fmt, aspect); + } queue.submit([enc.finish()]); diff --git a/src/prelude.rs b/src/prelude.rs index 665f0a5..37546a4 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,3 +1,26 @@ -pub use anyhow::{Result, anyhow}; -pub use glam::{Vec2, Vec3}; +pub use crate::{ + anim::Animation, + asset::{load, load_png}, + sprite::Sprite, +}; +pub use anyhow::{anyhow, Result}; +pub use glam::{Mat4, Quat, Vec2, Vec3}; +pub use instant::{Duration, Instant}; pub use log::{debug, error, info, trace, warn}; +pub use wgpu::{ + util::*, + {Device, Queue}, +}; +pub use std::f32::consts::{FRAC_PI_2, PI}; + +pub trait Mat4Ext { + fn from_2d(position: Vec2, scale: f32, rotation: f32) -> Mat4 { + Mat4::from_scale_rotation_translation( + Vec3::splat(scale), + Quat::from_rotation_z(rotation), + Vec3::new(position.x, position.y, 0.0), + ) + } +} + +impl Mat4Ext for Mat4 {} diff --git a/src/sprite.rs b/src/sprite.rs new file mode 100644 index 0000000..9845035 --- /dev/null +++ b/src/sprite.rs @@ -0,0 +1,183 @@ +use crate::prelude::*; +use wgpu::*; + +pub struct Sprite { + layout: BindGroupLayout, + texture: Texture, + uniforms: Buffer, + bindgroup: BindGroup, + pub transforms: Mat4, +} + +impl Sprite { + pub async fn new(name: &str, queue: &Queue, device: &Device, smooth: bool) -> Result<Self> { + let img = load_png(name).await?; + + let label = format!("Sprite {name}"); + let label = Some(label.as_str()); + + let texture = device.create_texture_with_data( + queue, + &TextureDescriptor { + label, + size: Extent3d { + width: img.width, + height: img.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + &img.rgba, + ); + + let uniforms = device.create_buffer(&BufferDescriptor { + label, + size: 16 * 4, + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label, + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let filter = smooth + .then_some(FilterMode::Linear) + .unwrap_or(FilterMode::Nearest); + let sampler = device.create_sampler(&SamplerDescriptor { + label, + mag_filter: filter, + min_filter: filter, + ..Default::default() + }); + + let bindgroup = device.create_bind_group(&BindGroupDescriptor { + label, + layout: &layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: uniforms.as_entire_binding(), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::TextureView( + &texture.create_view(&Default::default()), + ), + }, + BindGroupEntry { + binding: 2, + resource: BindingResource::Sampler(&sampler), + }, + ], + }); + + let transforms = Mat4::IDENTITY; + + Ok(Self { + layout, + texture, + uniforms, + bindgroup, + transforms, + }) + } + + pub fn record<'a, 'b: 'a>( + &'b self, + queue: &Queue, + device: &Device, + pass: &mut RenderPass<'a>, + format: TextureFormat, + aspect: f32, + ) { + let mat = Mat4::from_scale(Vec3::new(1.0 / aspect, 1.0, 1.0)) * self.transforms; + + queue.write_buffer( + &self.uniforms, + 0, + bytemuck::bytes_of(&mat), + ); + + static PIPELINE: std::sync::OnceLock<RenderPipeline> = std::sync::OnceLock::new(); + let pipeline = PIPELINE.get_or_init(|| { + let module = device.create_shader_module(include_wgsl!("../assets/sprite.wgsl")); + + let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: Some("Sprite"), + bind_group_layouts: &[&self.layout], + push_constant_ranges: &[], + }); + + device.create_render_pipeline(&RenderPipelineDescriptor { + label: Some("Sprite"), + layout: Some(&layout), + vertex: VertexState { + module: &module, + entry_point: "vs_main", + buffers: &[], + }, + primitive: Default::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth24Plus, + depth_write_enabled: false, + depth_compare: CompareFunction::Always, + stencil: Default::default(), + bias: Default::default(), + }), + multisample: MultisampleState { + count: 4, + mask: 0xFF, + alpha_to_coverage_enabled: false, + }, + fragment: Some(FragmentState { + module: &module, + entry_point: "fs_main", + targets: &[Some(ColorTargetState { + format, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::all(), + })], + }), + multiview: None, + }) + }); + + pass.set_pipeline(pipeline); + pass.set_bind_group(0, &self.bindgroup, &[]); + pass.draw(0..6, 0..1); + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..f7379ff --- /dev/null +++ b/src/state.rs @@ -0,0 +1,44 @@ +use wgpu::{RenderPass, TextureFormat}; + +use crate::prelude::*; + +pub enum State { + Loading { + fg: Sprite, + bg: Sprite, + scale: Animation<f32>, + rot: Animation<f32>, + }, +} + +impl State { + pub async fn new(queue: &Queue, device: &Device) -> Result<State> { + Ok(State::Loading { + fg: Sprite::new("splash_fg.png", queue, device, true).await?, + bg: Sprite::new("splash_bg.png", queue, device, true).await?, + scale: Animation::new(vec![0.0, 0.1, 0.25], Duration::from_secs(1)), + rot: Animation::new(vec![PI, PI, 0.0], Duration::from_secs(3)), + }) + } + + pub fn update<'a, 'b: 'a>( + &'b mut self, + queue: &Queue, + device: &Device, + pass: &mut RenderPass<'a>, + format: TextureFormat, + aspect: f32, + ) { + match self { + State::Loading { fg, bg, scale, rot } => { + bg.transforms = Mat4::from_2d(Vec2::ZERO, scale.get(), rot.get()); + fg.transforms = Mat4::from_2d(Vec2::ZERO, 0.25, 0.0); + + bg.record(queue, device, pass, format, aspect); + if scale.complete() { + fg.record(queue, device, pass, format, aspect) + } + } + } + } +} -- GitLab