From c57ec6aa7c5efe156a393b9d46579fd7a195a857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Mary=C5=84czak?= Date: Fri, 15 Nov 2024 21:56:24 +0100 Subject: [PATCH] Experiments with key glow (#111) --- .../src/render/glow/instance_data.rs | 36 ++++++ neothesia-core/src/render/glow/mod.rs | 105 ++++++++++++++++++ neothesia-core/src/render/glow/shader.wgsl | 46 ++++++++ .../src/render/keyboard/key_state.rs | 4 + neothesia-core/src/render/keyboard/mod.rs | 4 + neothesia-core/src/render/mod.rs | 2 + neothesia/src/scene/playing_scene/keyboard.rs | 6 +- neothesia/src/scene/playing_scene/mod.rs | 49 +++++++- 8 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 neothesia-core/src/render/glow/instance_data.rs create mode 100644 neothesia-core/src/render/glow/mod.rs create mode 100644 neothesia-core/src/render/glow/shader.wgsl diff --git a/neothesia-core/src/render/glow/instance_data.rs b/neothesia-core/src/render/glow/instance_data.rs new file mode 100644 index 00000000..374d4be4 --- /dev/null +++ b/neothesia-core/src/render/glow/instance_data.rs @@ -0,0 +1,36 @@ +use wgpu::vertex_attr_array; +use wgpu_jumpstart::wgpu; + +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Debug, Copy, Clone, Pod, Zeroable, PartialEq)] +pub struct GlowInstance { + pub position: [f32; 2], + pub size: [f32; 2], + pub color: [f32; 4], +} + +impl Default for GlowInstance { + fn default() -> Self { + Self { + position: [0.0, 0.0], + size: [0.0, 0.0], + color: [0.0, 0.0, 0.0, 1.0], + } + } +} + +impl GlowInstance { + pub fn attributes() -> [wgpu::VertexAttribute; 4] { + vertex_attr_array!(1 => Float32x2, 2 => Float32x2, 3 => Float32x4, 4 => Float32x4) + } + + pub fn layout(attributes: &[wgpu::VertexAttribute]) -> wgpu::VertexBufferLayout { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes, + } + } +} diff --git a/neothesia-core/src/render/glow/mod.rs b/neothesia-core/src/render/glow/mod.rs new file mode 100644 index 00000000..bd7d5d44 --- /dev/null +++ b/neothesia-core/src/render/glow/mod.rs @@ -0,0 +1,105 @@ +mod instance_data; +pub use instance_data::GlowInstance; + +use wgpu_jumpstart::{wgpu, Gpu, Instances, Shape, TransformUniform, Uniform}; + +pub struct GlowPipeline { + render_pipeline: wgpu::RenderPipeline, + quad: Shape, + instances: Instances, +} + +impl<'a> GlowPipeline { + pub fn new(gpu: &Gpu, transform_uniform: &Uniform) -> Self { + let shader = gpu + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("RectanglePipeline::shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( + "./shader.wgsl" + ))), + }); + + let render_pipeline_layout = + gpu.device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&transform_uniform.bind_group_layout], + push_constant_ranges: &[], + }); + + let ri_attrs = GlowInstance::attributes(); + + let target = wgpu_jumpstart::default_color_target_state(gpu.texture_format); + + let render_pipeline = gpu + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: Some(&render_pipeline_layout), + fragment: Some(wgpu_jumpstart::default_fragment(&shader, &[Some(target)])), + ..wgpu_jumpstart::default_render_pipeline(wgpu_jumpstart::default_vertex( + &shader, + &[Shape::layout(), GlowInstance::layout(&ri_attrs)], + )) + }); + + let quad = Shape::new_quad(&gpu.device); + let instances = Instances::new(&gpu.device, 100_000); + + Self { + render_pipeline, + + quad, + + instances, + } + } + + pub fn render( + &'a self, + transform_uniform: &'a Uniform, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_bind_group(0, &transform_uniform.bind_group, &[]); + + render_pass.set_vertex_buffer(0, self.quad.vertex_buffer.slice(..)); + render_pass.set_vertex_buffer(1, self.instances.buffer.slice(..)); + + render_pass.set_index_buffer(self.quad.index_buffer.slice(..), wgpu::IndexFormat::Uint16); + + render_pass.draw_indexed(0..self.quad.indices_len, 0, 0..self.instances.len()); + } + + pub fn clear(&mut self) { + self.instances.data.clear(); + } + + pub fn instances(&mut self) -> &mut Vec { + &mut self.instances.data + } + + pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { + self.instances.update(device, queue); + } + + pub fn update_instance_buffer( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + instances: Vec, + ) { + self.instances.data = instances; + self.instances.update(device, queue); + } + + pub fn with_instances_mut)>( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + cb: F, + ) { + cb(&mut self.instances.data); + self.instances.update(device, queue); + } +} diff --git a/neothesia-core/src/render/glow/shader.wgsl b/neothesia-core/src/render/glow/shader.wgsl new file mode 100644 index 00000000..c644f330 --- /dev/null +++ b/neothesia-core/src/render/glow/shader.wgsl @@ -0,0 +1,46 @@ +struct ViewUniform { + transform: mat4x4, + size: vec2, +} + +@group(0) @binding(0) +var view_uniform: ViewUniform; + +struct Vertex { + @location(0) position: vec2, +} + +struct QuadInstance { + @location(1) q_position: vec2, + @location(2) size: vec2, + @location(3) color: vec4, +} + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, + @location(1) quad_color: vec4, +} + +@vertex +fn vs_main(vertex: Vertex, quad: QuadInstance) -> VertexOutput { + var i_transform: mat4x4 = mat4x4( + vec4(quad.size.x, 0.0, 0.0, 0.0), + vec4(0.0, quad.size.y, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(quad.q_position, 0.0, 1.0) + ); + + var out: VertexOutput; + out.position = view_uniform.transform * i_transform * vec4(vertex.position, 0.0, 1.0); + out.uv = vertex.position; + out.quad_color = quad.color; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let m = distance(in.uv, vec2(0.5, 0.5)) * 2.0; + return mix(in.quad_color, vec4(0.0), m); + +} diff --git a/neothesia-core/src/render/keyboard/key_state.rs b/neothesia-core/src/render/keyboard/key_state.rs index 5cc04b36..7fb695cb 100644 --- a/neothesia-core/src/render/keyboard/key_state.rs +++ b/neothesia-core/src/render/keyboard/key_state.rs @@ -23,6 +23,10 @@ impl KeyState { } } + pub fn pressed_by_file(&self) -> Option<&Color> { + self.pressed_by_file.as_ref() + } + pub fn set_pressed_by_user(&mut self, is: bool) { self.pressed_by_user = is; } diff --git a/neothesia-core/src/render/keyboard/mod.rs b/neothesia-core/src/render/keyboard/mod.rs index 0713b381..25648802 100644 --- a/neothesia-core/src/render/keyboard/mod.rs +++ b/neothesia-core/src/render/keyboard/mod.rs @@ -53,6 +53,10 @@ impl KeyboardRenderer { &self.layout.range } + pub fn key_states(&self) -> &[KeyState] { + &self.key_states + } + pub fn key_states_mut(&mut self) -> &mut [KeyState] { &mut self.key_states } diff --git a/neothesia-core/src/render/mod.rs b/neothesia-core/src/render/mod.rs index 6b1f22c9..78193dda 100644 --- a/neothesia-core/src/render/mod.rs +++ b/neothesia-core/src/render/mod.rs @@ -1,5 +1,6 @@ mod background_animation; mod guidelines; +mod glow; mod keyboard; mod quad; mod text; @@ -7,6 +8,7 @@ mod waterfall; pub use background_animation::BgPipeline; pub use guidelines::GuidelineRenderer; +pub use glow::{GlowInstance, GlowPipeline}; pub use keyboard::{KeyState as KeyboardKeyState, KeyboardRenderer}; pub use quad::{QuadInstance, QuadPipeline}; pub use text::TextRenderer; diff --git a/neothesia/src/scene/playing_scene/keyboard.rs b/neothesia/src/scene/playing_scene/keyboard.rs index d4ac3554..a121fd2a 100644 --- a/neothesia/src/scene/playing_scene/keyboard.rs +++ b/neothesia/src/scene/playing_scene/keyboard.rs @@ -1,7 +1,7 @@ use midi_file::midly::MidiMessage; use neothesia_core::{ piano_layout, - render::{QuadPipeline, TextRenderer}, + render::{KeyboardKeyState, QuadPipeline, TextRenderer}, utils::Point, }; use piano_layout::KeyboardRange; @@ -42,6 +42,10 @@ impl Keyboard { } } + pub fn key_states(&self) -> &[KeyboardKeyState] { + self.renderer.key_states() + } + pub fn layout(&self) -> &piano_layout::KeyboardLayout { self.renderer.layout() } diff --git a/neothesia/src/scene/playing_scene/mod.rs b/neothesia/src/scene/playing_scene/mod.rs index 5e49e75a..c24a19fa 100644 --- a/neothesia/src/scene/playing_scene/mod.rs +++ b/neothesia/src/scene/playing_scene/mod.rs @@ -1,5 +1,5 @@ use midi_file::midly::MidiMessage; -use neothesia_core::render::{GuidelineRenderer, QuadPipeline}; +use neothesia_core::render::{GlowInstance, GlowPipeline, GuidelineRenderer, QuadPipeline}; use std::time::Duration; use wgpu_jumpstart::{TransformUniform, Uniform}; use winit::{ @@ -33,6 +33,10 @@ const EVENT_IGNORED: bool = false; const LAYER_BG: usize = 0; const LAYER_FG: usize = 1; +struct GlowState { + time: f32, +} + pub struct PlayingScene { keyboard: Keyboard, waterfall: WaterfallRenderer, @@ -41,6 +45,8 @@ pub struct PlayingScene { player: MidiPlayer, rewind_controller: RewindController, quad_pipeline: QuadPipeline, + glow_pipeline: GlowPipeline, + glow_states: Vec, toast_manager: ToastManager, top_bar: TopBar, @@ -88,6 +94,13 @@ impl PlayingScene { quad_pipeline.init_layer(&ctx.gpu, 50); // BG quad_pipeline.init_layer(&ctx.gpu, 150); // FG + let glow_states: Vec = keyboard + .layout() + .range + .iter() + .map(|_| GlowState { time: 0.0 }) + .collect(); + Self { keyboard, guidelines, @@ -96,11 +109,41 @@ impl PlayingScene { player, rewind_controller: RewindController::new(), quad_pipeline, + glow_pipeline: GlowPipeline::new(&ctx.gpu, &ctx.transform), + glow_states, toast_manager: ToastManager::default(), top_bar: TopBar::new(), } } + fn update_glow(&mut self) { + self.glow_pipeline.clear(); + + let key_states = self.keyboard.key_states(); + for key in self.keyboard.layout().keys.iter() { + let glow_state = &mut self.glow_states[key.id()]; + let glow_w = 150.0 + glow_state.time.sin() * 10.0; + let glow_h = 150.0 + glow_state.time.sin() * 10.0; + + let y = self.keyboard.pos().y; + if let Some(color) = key_states[key.id()].pressed_by_file() { + glow_state.time += 0.1; + let mut color = color.into_linear_rgba(); + let v = 0.2 * glow_state.time.cos().abs(); + let v = v.min(1.0); + color[0] += v; + color[1] += v; + color[2] += v; + color[3] = 0.2; + self.glow_pipeline.instances().push(GlowInstance { + position: [key.x() - glow_w / 2.0 + key.width() / 2.0, y - glow_w / 2.0], + size: [glow_w, glow_h], + color, + }); + } + } + } + #[profiling::function] fn update_midi_player(&mut self, ctx: &Context, delta: Duration) -> f32 { if self.top_bar.looper.is_active() @@ -156,7 +199,10 @@ impl Scene for PlayingScene { TopBar::update(self, ctx); + self.update_glow(); + self.quad_pipeline.prepare(&ctx.gpu.device, &ctx.gpu.queue); + self.glow_pipeline.prepare(&ctx.gpu.device, &ctx.gpu.queue); if self.player.is_finished() && !self.player.is_paused() { ctx.proxy @@ -174,6 +220,7 @@ impl Scene for PlayingScene { self.quad_pipeline.render(LAYER_BG, transform, rpass); self.waterfall.render(transform, rpass); self.quad_pipeline.render(LAYER_FG, transform, rpass); + self.glow_pipeline.render(transform, rpass); } fn window_event(&mut self, ctx: &mut Context, event: &WindowEvent) {