diff --git a/pomme-client/build.rs b/pomme-client/build.rs index 07ed8e321..005e2945a 100644 --- a/pomme-client/build.rs +++ b/pomme-client/build.rs @@ -103,7 +103,6 @@ fn main() { ("sky.frag", shaderc::ShaderKind::Fragment), ("cull.comp", shaderc::ShaderKind::Compute), ("blur.vert", shaderc::ShaderKind::Vertex), - ("blur.frag", shaderc::ShaderKind::Fragment), ("entity.vert", shaderc::ShaderKind::Vertex), ("entity.frag", shaderc::ShaderKind::Fragment), ("entity_outline.frag", shaderc::ShaderKind::Fragment), diff --git a/pomme-client/src/app/phases/connecting.rs b/pomme-client/src/app/phases/connecting.rs index e7aac853b..89cdaa293 100644 --- a/pomme-client/src/app/phases/connecting.rs +++ b/pomme-client/src/app/phases/connecting.rs @@ -3,6 +3,7 @@ use crate::app::core::AppCore; use crate::app::phases::in_game::{GameState, build_server_screens}; use crate::app::phases::{ConnectionPhase, Gfx, Panorama, draw_status}; use crate::net::connection::ConnectionHandle; +use crate::renderer::pipelines::menu_overlay::MenuElement; use crate::singleplayer::World; pub enum ConnectingUpdateResult { @@ -110,7 +111,7 @@ fn draw_server_dialog( // the same atlas the in-game text uses. core.sync_game_dynamic_atlas(game, &mut gfx.renderer, false); - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let text_events = core.input.drain_text_events(); // The connecting screen runs no client ticks, so the dialog's own // timers fall back to wall time. @@ -128,9 +129,9 @@ fn draw_server_dialog( ); let cursor = core.input.cursor_pos(); - if let Err(e) = - gfx.renderer - .render_menu(&gfx.window, panorama.scroll(), 2.0, elements, cursor, None) + if let Err(e) = gfx + .renderer + .render_menu(&gfx.window, panorama.scroll(), elements, cursor, None) { tracing::error!("Render error: {e}"); } diff --git a/pomme-client/src/app/phases/in_game.rs b/pomme-client/src/app/phases/in_game.rs index d29f8f2d3..67c5e7d2d 100644 --- a/pomme-client/src/app/phases/in_game.rs +++ b/pomme-client/src/app/phases/in_game.rs @@ -1716,6 +1716,8 @@ pub(crate) fn build_server_screens( if scroll != 0.0 && !modal_open { dialog.handle_scroll(scroll); } + // Vanilla's `DialogScreen` keeps `Screen`'s blurred background. + elements.push(MenuElement::BlurBackground); let action = dialog.build( elements, sw, @@ -2255,6 +2257,8 @@ pub fn update_game( core.audio.set_subtitles_enabled(core.menu.show_subtitles); gfx.renderer.set_vsync(core.menu.vsync); + gfx.renderer + .set_menu_blur_radius(core.menu.menu_background_blurriness); game.chat.set_options(core.menu.chat_options); // Vanilla pauseIfInactive: losing OS focus for more than half a second @@ -3213,6 +3217,9 @@ pub fn update_game( } else if game.paused && !matches!(game.pause_screen, PauseScreen::Hidden) && !dialog_open { let cursor = core.input.cursor_pos(); let clicked = core.input.left_just_pressed(); + // Vanilla `PauseScreen.extractBackground`: blurred while the menu + // shows; F3+Esc's hidden pause draws nothing. + elements.push(MenuElement::BlurBackground); pause_action = pause::build_pause_menu( &mut elements, sw, @@ -3740,6 +3747,8 @@ pub fn update_game( // F1 hides the closed-chat overlay; an open chat is a screen and renders // regardless (vanilla Hud.extractChat vs ChatScreen). + // TODO: the closed chat (and titles) build after screens, so they draw + // sharp over the menu blur; vanilla's HUD extracts them before the screen. if !game.hide_gui || game.chat.is_focused() { let command_tree = game.command_tree.clone(); // Vanilla `ChatScreen.mouseClicked` offers the click to the chat's own @@ -4270,6 +4279,8 @@ pub fn update_game( core.apply_cursor_grab(&gfx.window, Some(game)); } PauseAction::Options => { + // TODO: back from these options, the pause menu ignores Escape and + // clicks (tests/ingame/ui/menu_blur.toml stops before it). core.menu.open_options(&core.input.key_bindings); game.options_from_game = true; core.apply_cursor_grab(&gfx.window, Some(game)); diff --git a/pomme-client/src/app/phases/in_menu.rs b/pomme-client/src/app/phases/in_menu.rs index c18cdc177..39b78b8f5 100644 --- a/pomme-client/src/app/phases/in_menu.rs +++ b/pomme-client/src/app/phases/in_menu.rs @@ -132,7 +132,6 @@ pub fn update_menu( if let Err(e) = gfx.renderer.render_menu( &gfx.window, panorama.scroll(), - result.blur, result.elements, core.input.cursor_pos(), core.menu.skin_preview(), @@ -145,6 +144,8 @@ pub fn update_menu( core.sync_display_mode(&gfx.window); gfx.renderer.set_vsync(core.menu.vsync); + gfx.renderer + .set_menu_blur_radius(core.menu.menu_background_blurriness); if core.menu.rescan_packs { core.menu.rescan_packs = false; diff --git a/pomme-client/src/app/phases/mod.rs b/pomme-client/src/app/phases/mod.rs index 627bf3111..b22e2abd3 100644 --- a/pomme-client/src/app/phases/mod.rs +++ b/pomme-client/src/app/phases/mod.rs @@ -188,7 +188,7 @@ pub fn draw_status( let cy = sh / 2.0; let cursor = core.input.cursor_pos(); - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; elements.push(MenuElement::Text { x: cx, @@ -217,9 +217,9 @@ pub fn draw_status( core.input.clear_just_pressed_actions(); - if let Err(e) = - gfx.renderer - .render_menu(&gfx.window, panorama.scroll(), 2.0, elements, cursor, None) + if let Err(e) = gfx + .renderer + .render_menu(&gfx.window, panorama.scroll(), elements, cursor, None) { tracing::error!("Render error: {e}"); } diff --git a/pomme-client/src/harness/exec.rs b/pomme-client/src/harness/exec.rs index 1405e53b2..486bea71b 100644 --- a/pomme-client/src/harness/exec.rs +++ b/pomme-client/src/harness/exec.rs @@ -203,6 +203,9 @@ fn set_options(core: &mut AppCore, options: &protocol::OptionSet) -> Result, + /// Menu Background Blur, 0 (off) to 10. + pub menu_background_blur: Option, } #[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] diff --git a/pomme-client/src/harness/script.rs b/pomme-client/src/harness/script.rs index 7486080ec..2f8740c56 100644 --- a/pomme-client/src/harness/script.rs +++ b/pomme-client/src/harness/script.rs @@ -732,7 +732,8 @@ mod tests { assert_eq!( steps[19], Step::Run(Command::Option(OptionSet { - main_hand: Some(Hand::Left) + main_hand: Some(Hand::Left), + menu_background_blur: None, })) ); assert_eq!( diff --git a/pomme-client/src/renderer/mod.rs b/pomme-client/src/renderer/mod.rs index cd2c69fc0..b4f0e254c 100644 --- a/pomme-client/src/renderer/mod.rs +++ b/pomme-client/src/renderer/mod.rs @@ -31,7 +31,6 @@ use glam::dvec3; use pipelines::block_entity::BlockEntityPipeline; pub use pipelines::block_entity::BlockEntityRenderInfo; use pipelines::block_overlay::BlockOverlayPipeline; -use pipelines::blur::BlurPipeline; use pipelines::book_preview::BookPreviewPipeline; use pipelines::chunk::ChunkPipeline; use pipelines::clouds::CloudPipeline; @@ -210,7 +209,6 @@ enum RenderMode<'a> { }, MainMenu { scroll: f32, - blur: f32, elements: Vec, cursor: (f32, f32), skin: Option, @@ -251,7 +249,8 @@ pub struct Renderer { sky_pipeline: SkyPipeline, panorama_pipeline: PanoramaPipeline, menu_pipeline: MenuOverlayPipeline, - blur_pipeline: BlurPipeline, + /// The Menu Background Blur option: vanilla's `MenuBlurRadius`. + menu_blur_radius: u8, entity_outline: EntityOutline, post_effects: PostEffects, skin_preview: SkinPreviewPipeline, @@ -424,15 +423,14 @@ impl Renderer { asset_index, ); - let blur_pipeline = - BlurPipeline::new(&ctx.device, swapchain_extent.width, swapchain_extent.height); let entity_outline = EntityOutline::new(&ctx.device, swapchain_state.targets(), swapchain_extent); - let post_effects = PostEffects::new(&ctx.device, swapchain_state.targets()); + let post_effects = + PostEffects::new(&ctx.device, swapchain_state.targets(), swapchain_extent); menu_pipeline.set_blur_texture( &ctx.device, - blur_pipeline.blurred_texture(), - blur_pipeline.blurred_sampler(), + post_effects.blurred_texture(), + post_effects.blurred_sampler(), ); let entity_renderer = EntityRenderer::new( @@ -513,7 +511,7 @@ impl Renderer { sky_pipeline, panorama_pipeline, menu_pipeline, - blur_pipeline, + menu_blur_radius: crate::ui::menu::DEFAULT_MENU_BACKGROUND_BLURRINESS, entity_outline, post_effects, skin_preview, @@ -644,6 +642,59 @@ impl Renderer { /// Reopens the frame's pass after an offscreen detour, keeping what it /// drew. + pub fn set_menu_blur_radius(&mut self, radius: u8) { + self.menu_blur_radius = radius; + } + + /// Vanilla `GuiRenderer.draw`: draws `elements` up to the first blur + /// marker, then blurs the frame there (`processBlurEffect`). The blur is + /// written back over the frame for `BlurBackground`, and only kept as the + /// frosted panels' texture for `BlurBackdrop`. Returns the reopened pass, + /// the next vertex base and the elements still to draw. + fn draw_until_blur<'e>( + &mut self, + encoder: &rhi::backend::CommandEncoder, + image_index: u32, + frame: usize, + mut pass: rhi::backend::RenderPassEncoder, + elements: &'e [MenuElement], + item_atlas_uvs: &HashMap, + ) -> (rhi::backend::RenderPassEncoder, u32, &'e [MenuElement]) { + let Some(split) = elements + .iter() + .position(|e| matches!(e, MenuElement::BlurBackground | MenuElement::BlurBackdrop)) + else { + return (pass, 0, elements); + }; + let extent = self.swapchain.extent; + let vbase = self.menu_pipeline.draw_from( + &self.ctx.device, + &mut pass, + frame, + extent.width as f32, + extent.height as f32, + &elements[..split], + item_atlas_uvs, + 0, + ); + let in_place = matches!(elements[split], MenuElement::BlurBackground); + // Vanilla `extractBlurredBackground` skips a radius under 1. + if !in_place || self.menu_blur_radius > 0 { + pass.end(); + self.post_effects.blur( + &self.ctx.device, + encoder, + self.swapchain.image(image_index), + self.menu_blur_radius, + ); + pass = self.resume_frame_pass(encoder, image_index); + if in_place { + self.post_effects.finish(&mut pass); + } + } + (pass, vbase, &elements[split + 1..]) + } + fn resume_frame_pass( &self, encoder: &rhi::backend::CommandEncoder, @@ -718,14 +769,14 @@ impl Renderer { self.chunk_border_pipeline .recreate_pipeline(&self.ctx.device, targets); } - self.blur_pipeline - .resize(&self.ctx.device, self.width, self.height); self.entity_outline .resize(&self.ctx.device, self.swapchain.extent); + self.post_effects + .resize(&self.ctx.device, self.swapchain.extent); self.menu_pipeline.set_blur_texture( &self.ctx.device, - self.blur_pipeline.blurred_texture(), - self.blur_pipeline.blurred_sampler(), + self.post_effects.blurred_texture(), + self.post_effects.blurred_sampler(), ); self.swapchain_dirty = false; @@ -1120,7 +1171,6 @@ impl Renderer { &mut self, window: &Window, scroll: f32, - blur: f32, elements: Vec, cursor: (f32, f32), skin: Option, @@ -1131,7 +1181,6 @@ impl Renderer { [0.0, 0.0, 0.0, 1.0], RenderMode::MainMenu { scroll, - blur, elements, cursor, skin, @@ -1815,13 +1864,21 @@ impl Renderer { &self.ctx.device, &encoder, self.swapchain.image(image_index), - self.swapchain.extent, ); pass = self.resume_frame_pass(&encoder, image_index); self.post_effects.finish(&mut pass); } - self.menu_pipeline.draw( + let (blurred_pass, vbase, overlay) = self.draw_until_blur( + &encoder, + image_index, + frame, + pass, + overlay, + &item_atlas_uvs, + ); + pass = blurred_pass; + self.menu_pipeline.draw_from( &self.ctx.device, &mut pass, frame, @@ -1829,6 +1886,7 @@ impl Renderer { sh, overlay, &item_atlas_uvs, + vbase, ); // Each preview box gets its depth cleared and its own scissor @@ -1863,7 +1921,6 @@ impl Renderer { } RenderMode::MainMenu { scroll, - blur, elements, cursor, skin, @@ -1872,42 +1929,15 @@ impl Renderer { self.panorama_pipeline .draw(&mut pass, frame, *scroll, aspect, 0.0); - // A BlurBackdrop marker splits the elements: those before it are - // drawn into the scene so the blur pass captures them (the title - // screen behind the Friends dialog); the rest are drawn sharp. - let split = elements - .iter() - .position(|e| matches!(e, MenuElement::BlurBackdrop)); - let mut vbase = 0u32; - if let Some(i) = split { - vbase = self.menu_pipeline.draw_from( - &self.ctx.device, - &mut pass, - frame, - sw, - sh, - &elements[..i], - &item_atlas_uvs, - 0, - ); - } - - if *blur > 0.01 { - pass.end(); - - let swapchain_image = self.swapchain.image(image_index); - let iterations = ((*blur * 3.0).ceil() as u32).clamp(1, 4); - self.blur_pipeline.execute( - &self.ctx.device, - &encoder, - swapchain_image, - self.swapchain.extent.width, - self.swapchain.extent.height, - iterations, - ); - - pass = self.resume_frame_pass(&encoder, image_index); - } + let (blurred_pass, vbase, fg) = self.draw_until_blur( + &encoder, + image_index, + frame, + pass, + elements, + &item_atlas_uvs, + ); + pass = blurred_pass; if let Some(skin) = *skin { self.skin_preview.draw( @@ -1916,10 +1946,6 @@ impl Renderer { ); } - let fg = match split { - Some(i) => &elements[i + 1..], - None => &elements[..], - }; self.menu_pipeline.draw_from( &self.ctx.device, &mut pass, @@ -2313,7 +2339,7 @@ impl Drop for Renderer { self.sky_pipeline.destroy(&self.ctx.device); self.panorama_pipeline.destroy(&self.ctx.device); self.menu_pipeline.destroy(&self.ctx.device); - self.blur_pipeline.destroy(&self.ctx.device); + self.entity_outline.destroy(&self.ctx.device); self.post_effects.destroy(&self.ctx.device); self.skin_preview.destroy(&self.ctx.device); diff --git a/pomme-client/src/renderer/pipelines/blur.rs b/pomme-client/src/renderer/pipelines/blur.rs deleted file mode 100644 index 20e7ccdff..000000000 --- a/pomme-client/src/renderer/pipelines/blur.rs +++ /dev/null @@ -1,235 +0,0 @@ -use crate::renderer::rhi::TextureUse; -use crate::renderer::rhi::backend::TextureTransition; -use crate::renderer::{rhi, shader, util}; - -const FORMAT: rhi::TextureFormat = rhi::TextureFormat::Rgba8UnormSrgb; - -pub struct BlurPipeline { - texture_a: rhi::backend::Texture, - texture_b: rhi::backend::Texture, - sampler: rhi::backend::Sampler, - pipeline: rhi::backend::RenderPipeline, - pipeline_layout: rhi::backend::PipelineLayout, - texture_layout: rhi::backend::BindGroupLayout, - set_read_a: rhi::backend::BindGroup, - set_read_b: rhi::backend::BindGroup, - width: u32, - height: u32, -} - -impl BlurPipeline { - pub fn new(device: &rhi::backend::Device, width: u32, height: u32) -> Self { - let blur_w = (width / 2).max(1); - let blur_h = (height / 2).max(1); - - let texture_a = create_blur_texture(device, blur_w, blur_h, "blur_a"); - let texture_b = create_blur_texture(device, blur_w, blur_h, "blur_b"); - - let sampler = rhi::backend::create_sampler(device, &rhi::SamplerDesc::LINEAR); - - let texture_layout = rhi::backend::create_bind_group_layout(device, util::FRAGMENT_TEXTURE); - let pipeline_layout = rhi::backend::create_pipeline_layout( - device, - &[texture_layout], - Some(rhi::PushConstants { - visibility: rhi::ShaderStages::FRAGMENT, - size: 8, - }), - ); - - let pipeline = create_blur_graphics_pipeline( - device, - rhi::backend::PipelineTargets { - color: rhi::backend::texture_format(FORMAT), - depth: false, - }, - pipeline_layout, - ); - - let set_read_a = - util::create_texture_group(device, texture_layout, texture_a.handle(), sampler); - let set_read_b = - util::create_texture_group(device, texture_layout, texture_b.handle(), sampler); - - Self { - texture_a, - texture_b, - sampler, - pipeline, - pipeline_layout, - texture_layout, - set_read_a, - set_read_b, - width: blur_w, - height: blur_h, - } - } - - pub fn blurred_texture(&self) -> rhi::backend::TextureHandle { - self.texture_a.handle() - } - - pub fn blurred_sampler(&self) -> rhi::backend::Sampler { - self.sampler - } - - pub fn execute( - &self, - device: &rhi::backend::Device, - encoder: &rhi::backend::CommandEncoder, - src_image: rhi::backend::TextureHandle, - src_width: u32, - src_height: u32, - iterations: u32, - ) { - let bw = self.width; - let bh = self.height; - util::copy_frame( - encoder, - src_image, - [src_width, src_height], - self.texture_a.handle(), - [bw, bh], - ); - - let h_dir: [f32; 2] = [1.0 / bw as f32, 0.0]; - let v_dir: [f32; 2] = [0.0, 1.0 / bh as f32]; - - for _ in 0..iterations { - self.blur_pass( - device, - encoder, - self.texture_b.handle(), - self.set_read_a, - h_dir, - ); - self.blur_pass( - device, - encoder, - self.texture_a.handle(), - self.set_read_b, - v_dir, - ); - } - } - - /// Draws into `target`, leaving it for the next pass to read. - fn blur_pass( - &self, - device: &rhi::backend::Device, - encoder: &rhi::backend::CommandEncoder, - target: rhi::backend::TextureHandle, - read_set: rhi::backend::BindGroup, - dir: [f32; 2], - ) { - let desc = rhi::backend::RenderPassDesc { - color: rhi::backend::ColorAttachment { - texture: target, - format: rhi::backend::texture_format(FORMAT), - load: rhi::LoadOp::Clear([0.0; 4]), - store: rhi::StoreOp::Store, - }, - depth: None, - extent: rhi::Extent { - width: self.width, - height: self.height, - }, - }; - let mut pass = encoder.begin_render_pass(device, &desc); - pass.set_pipeline(&self.pipeline); - pass.set_bind_group(0, read_set); - pass.set_push_constants(rhi::ShaderStages::FRAGMENT, 0, bytemuck::cast_slice(&dir)); - pass.draw(3, 1, 0, 0); - pass.end(); - encoder.transition_textures(&[TextureTransition::color( - target, - TextureUse::ColorAttachment, - TextureUse::ShaderRead, - )]); - } - - pub fn resize(&mut self, device: &rhi::backend::Device, width: u32, height: u32) { - let bw = (width / 2).max(1); - let bh = (height / 2).max(1); - if bw == self.width && bh == self.height { - return; - } - - device.wait_idle(); - - self.destroy_images(device); - - self.texture_a = create_blur_texture(device, bw, bh, "blur_a"); - self.texture_b = create_blur_texture(device, bw, bh, "blur_b"); - - self.set_read_a.replace( - device, - self.texture_layout, - &util::texture_binding(self.texture_a.handle(), self.sampler), - ); - self.set_read_b.replace( - device, - self.texture_layout, - &util::texture_binding(self.texture_b.handle(), self.sampler), - ); - - self.width = bw; - self.height = bh; - } - - fn destroy_images(&mut self, device: &rhi::backend::Device) { - std::mem::take(&mut self.texture_a).destroy(device); - std::mem::take(&mut self.texture_b).destroy(device); - } - - pub fn destroy(&mut self, device: &rhi::backend::Device) { - self.destroy_images(device); - self.pipeline.destroy(device); - self.pipeline_layout.destroy(device); - self.set_read_a.destroy(device); - self.set_read_b.destroy(device); - self.texture_layout.destroy(device); - self.sampler.destroy(device); - } -} - -/// A blur target, ready to be sampled. -fn create_blur_texture( - device: &rhi::backend::Device, - w: u32, - h: u32, - name: &str, -) -> rhi::backend::Texture { - let texture = rhi::backend::Texture::new( - device, - &rhi::TextureDesc::new( - name, - w, - h, - FORMAT, - rhi::TextureUsages::RENDER_ATTACHMENT - | rhi::TextureUsages::TEXTURE_BINDING - | rhi::TextureUsages::COPY_DST, - ), - ); - util::transition_image_to_shader_read(device, texture.handle()); - texture -} - -fn create_blur_graphics_pipeline( - device: &rhi::backend::Device, - targets: rhi::backend::PipelineTargets, - layout: rhi::backend::PipelineLayout, -) -> rhi::backend::RenderPipeline { - rhi::backend::create_render_pipeline( - device, - targets, - layout, - &rhi::RenderPipelineDesc::new( - "blur", - shader::include_shader!("blur.vert"), - shader::include_shader!("blur.frag"), - &[], - ), - ) -} diff --git a/pomme-client/src/renderer/pipelines/menu_overlay.rs b/pomme-client/src/renderer/pipelines/menu_overlay.rs index c38178fb7..2a0e5450d 100644 --- a/pomme-client/src/renderer/pipelines/menu_overlay.rs +++ b/pomme-client/src/renderer/pipelines/menu_overlay.rs @@ -2184,6 +2184,9 @@ pub enum MenuElement { /// scene so the blur pass captures them; elements after are drawn sharp on /// top. Used to render the title screen blurred behind the Friends dialog. BlurBackdrop, + /// Vanilla `blurBeforeThisStratum`: elements before it are blurred in + /// place by the Menu Background Blur, and the rest drawn sharp on top. + BlurBackground, /// Full-screen vignette, multiplying the framebuffer down by `brightness`. Vignette { w: f32, diff --git a/pomme-client/src/renderer/pipelines/mod.rs b/pomme-client/src/renderer/pipelines/mod.rs index a8b8aa70d..12c3ec2c8 100644 --- a/pomme-client/src/renderer/pipelines/mod.rs +++ b/pomme-client/src/renderer/pipelines/mod.rs @@ -1,6 +1,5 @@ pub mod block_entity; pub mod block_overlay; -pub mod blur; pub mod book_preview; pub mod chunk; pub mod chunk_borders; diff --git a/pomme-client/src/renderer/pipelines/post_effect.rs b/pomme-client/src/renderer/pipelines/post_effect.rs index 6170b3518..e1a2d5da9 100644 --- a/pomme-client/src/renderer/pipelines/post_effect.rs +++ b/pomme-client/src/renderer/pipelines/post_effect.rs @@ -1,8 +1,9 @@ -//! Vanilla's spectator view effects (`post_effect/creeper.json`, -//! `spider.json`, `invert.json`), run over the frame after the glowing -//! outline, before the HUD. The frame is copied out of the sRGB swapchain into -//! `main` as vanilla's gamma-space colours, in GL's bottom-up rows, so the -//! ported shaders and their uv maths run unchanged, then written back. +//! Vanilla's post chains over the whole frame: the spectator view effects +//! (`post_effect/creeper.json`, `spider.json`, `invert.json`), run after the +//! glowing outline before the HUD, and the menu background blur +//! (`post_effect/blur.json`). The frame is copied out of the sRGB swapchain +//! into `main` as vanilla's gamma-space colours, in GL's bottom-up rows, so +//! the ported shaders and their uv maths run unchanged, then written back. use super::post; use crate::renderer::{rhi, shader, util}; @@ -37,13 +38,10 @@ enum Target { SmallBlur, } -const TARGET_NAMES: [&str; 5] = [ - "post_main", - "post_swap", - "post_temp", - "post_large_blur", - "post_small_blur", -]; +/// `minecraft:main` and every chain's `swap`, kept at the frame's size. +const FRAME_TARGETS: [&str; 2] = ["post_main", "post_swap"]; +/// `spider.json`'s own targets, after [`FRAME_TARGETS`] in [`Target`]'s order. +const SPIDER_TARGETS: [&str; 3] = ["post_temp", "post_large_blur", "post_small_blur"]; /// The (`In`, `Blur`) pairs spiderclip reads. const CLIP_INPUTS: [(Target, Target); 3] = [ @@ -117,21 +115,95 @@ const fn clip( ] } -/// The targets, made while an effect is on. -struct Targets { +/// A screen-size target per name, with its nearest and bilinear groups. +struct TargetSet { + textures: Vec, + nearest: Vec, + linear: Vec, +} + +impl TargetSet { + fn new( + device: &rhi::backend::Device, + samplers: &Samplers, + extent: rhi::Extent, + names: &[&str], + ) -> Self { + let textures: Vec<_> = names + .iter() + .map(|name| post::create_target(device, extent, name)) + .collect(); + let groups = |sampler| { + textures + .iter() + .map(|texture| samplers.group(device, texture.handle(), sampler)) + .collect() + }; + Self { + nearest: groups(samplers.nearest), + linear: groups(samplers.linear), + textures, + } + } + + fn destroy(&mut self, device: &rhi::backend::Device) { + for texture in &mut self.textures { + std::mem::take(texture).destroy(device); + } + for group in self.nearest.drain(..).chain(self.linear.drain(..)) { + group.destroy(device); + } + } +} + +/// The set `target` lives in, and its index there: [`FRAME_TARGETS`] first, +/// then [`SPIDER_TARGETS`]. +fn locate<'a>( + frame: &'a TargetSet, + spider: Option<&'a TargetSet>, + target: Target, +) -> (&'a TargetSet, usize) { + let i = target as usize; + let frame_count = FRAME_TARGETS.len(); + if i < frame_count { + (frame, i) + } else { + ( + spider.expect("made with the spider effect"), + i - frame_count, + ) + } +} + +/// The texture layout and samplers every target's groups use. +struct Samplers { + layout: rhi::backend::BindGroupLayout, + nearest: rhi::backend::Sampler, + linear: rhi::backend::Sampler, +} + +impl Samplers { + fn group( + &self, + device: &rhi::backend::Device, + texture: rhi::backend::TextureHandle, + sampler: rhi::backend::Sampler, + ) -> rhi::backend::BindGroup { + util::create_texture_group(device, self.layout, texture, sampler) + } +} + +/// The frame's copy and [`FRAME_TARGETS`], at the frame's size. +struct FrameTargets { extent: rhi::Extent, /// The swapchain's copy, sRGB so sampling decodes it. frame: rhi::backend::Texture, - textures: [rhi::backend::Texture; 5], frame_group: rhi::backend::BindGroup, - nearest: [rhi::backend::BindGroup; 5], - linear: [rhi::backend::BindGroup; 5], - /// By [`CLIP_INPUTS`]. - clip: [rhi::backend::BindGroup; 3], + targets: TargetSet, } -impl Targets { - fn new(device: &rhi::backend::Device, effects: &PostEffects, extent: rhi::Extent) -> Self { +impl FrameTargets { + fn new(device: &rhi::backend::Device, samplers: &Samplers, extent: rhi::Extent) -> Self { let frame = rhi::backend::Texture::new( device, &rhi::TextureDesc::new( @@ -142,46 +214,33 @@ impl Targets { rhi::TextureUsages::COPY_DST | rhi::TextureUsages::TEXTURE_BINDING, ), ); - let textures = TARGET_NAMES.map(|name| post::create_target(device, extent, name)); - let group = |texture: &rhi::backend::Texture, sampler| { - util::create_texture_group(device, effects.texture_layout, texture.handle(), sampler) - }; - let clip = CLIP_INPUTS.map(|(input, blur)| { - let [texture, sampler] = - util::texture_binding(textures[input as usize].handle(), effects.nearest); - let [blur_texture, blur_sampler] = - util::texture_binding(textures[blur as usize].handle(), effects.nearest); - device.create_bind_group( - effects.clip_layout, - &[texture, sampler, (2, blur_texture.1), (3, blur_sampler.1)], - ) - }); Self { extent, - frame_group: group(&frame, effects.nearest), - nearest: std::array::from_fn(|i| group(&textures[i], effects.nearest)), - linear: std::array::from_fn(|i| group(&textures[i], effects.linear)), - clip, + frame_group: samplers.group(device, frame.handle(), samplers.nearest), frame, - textures, + targets: TargetSet::new(device, samplers, extent, &FRAME_TARGETS), } } - fn handle(&self, target: Target) -> rhi::backend::TextureHandle { - self.textures[target as usize].handle() + fn destroy(&mut self, device: &rhi::backend::Device) { + std::mem::take(&mut self.frame).destroy(device); + self.frame_group.destroy(device); + self.targets.destroy(device); } +} - fn destroy(self, device: &rhi::backend::Device) { - self.frame.destroy(device); - for texture in self.textures { - texture.destroy(device); - } - for group in [self.frame_group] - .into_iter() - .chain(self.nearest) - .chain(self.linear) - .chain(self.clip) - { +/// [`SPIDER_TARGETS`] and spiderclip's (`In`, `Blur`) groups, made while the +/// spider effect runs. +struct SpiderTargets { + targets: TargetSet, + /// By [`CLIP_INPUTS`]. + clip: [rhi::backend::BindGroup; 3], +} + +impl SpiderTargets { + fn destroy(&mut self, device: &rhi::backend::Device) { + self.targets.destroy(device); + for group in self.clip { group.destroy(device); } } @@ -189,10 +248,9 @@ impl Targets { pub struct PostEffects { effect: Option, - targets: Option, - nearest: rhi::backend::Sampler, - linear: rhi::backend::Sampler, - texture_layout: rhi::backend::BindGroupLayout, + samplers: Samplers, + frame: FrameTargets, + spider: Option, clip_layout: rhi::backend::BindGroupLayout, pipeline_layout: rhi::backend::PipelineLayout, clip_pipeline_layout: rhi::backend::PipelineLayout, @@ -210,15 +268,20 @@ impl PostEffects { pub fn new( device: &rhi::backend::Device, frame_targets: rhi::backend::PipelineTargets, + extent: rhi::Extent, ) -> Self { - let texture_layout = rhi::backend::create_bind_group_layout(device, util::FRAGMENT_TEXTURE); + let samplers = Samplers { + layout: rhi::backend::create_bind_group_layout(device, util::FRAGMENT_TEXTURE), + nearest: rhi::backend::create_sampler(device, &rhi::SamplerDesc::NEAREST), + linear: rhi::backend::create_sampler(device, &rhi::SamplerDesc::LINEAR), + }; let clip_layout = rhi::backend::create_bind_group_layout( device, &[util::FRAGMENT_TEXTURE, util::FRAGMENT_TEXTURE].concat(), ); let pipeline_layout = rhi::backend::create_pipeline_layout( device, - &[texture_layout], + &[samplers.layout], Some(post::PUSH_CONSTANTS), ); let clip_pipeline_layout = rhi::backend::create_pipeline_layout( @@ -239,9 +302,9 @@ impl PostEffects { }; Self { effect: None, - targets: None, - nearest: rhi::backend::create_sampler(device, &rhi::SamplerDesc::NEAREST), - linear: rhi::backend::create_sampler(device, &rhi::SamplerDesc::LINEAR), + frame: FrameTargets::new(device, &samplers, extent), + spider: None, + samplers, decode: pipeline("post decode", shader::include_shader!("post_gamma.frag")), encode: create_encode_pipeline(device, frame_targets, pipeline_layout), invert: pipeline("post invert", shader::include_shader!("post_invert.frag")), @@ -264,7 +327,6 @@ impl PostEffects { shader::include_shader!("post_spiderclip.frag"), None, ), - texture_layout, clip_layout, pipeline_layout, clip_pipeline_layout, @@ -275,55 +337,143 @@ impl PostEffects { self.effect } - /// Sets the effect to run, freeing the targets once none is. + /// Sets the effect to run, freeing the spider's targets once it stops. pub fn set(&mut self, device: &rhi::backend::Device, effect: Option) { self.effect = effect; - if effect.is_none() - && let Some(targets) = self.targets.take() + if effect != Some(PostEffect::Spider) + && let Some(mut spider) = self.spider.take() { device.wait_idle(); - targets.destroy(device); + spider.destroy(device); + } + } + + /// Remakes the targets for a new frame size. + pub fn resize(&mut self, device: &rhi::backend::Device, extent: rhi::Extent) { + if extent == self.frame.extent { + return; + } + device.wait_idle(); + if let Some(mut spider) = self.spider.take() { + spider.destroy(device); + } + let mut stale = std::mem::replace( + &mut self.frame, + FrameTargets::new(device, &self.samplers, extent), + ); + stale.destroy(device); + } + + /// `main`, gamma-space in bottom-up rows, as the last blur left it. + pub fn blurred_texture(&self) -> rhi::backend::TextureHandle { + self.handle(Target::Main) + } + + pub fn blurred_sampler(&self) -> rhi::backend::Sampler { + self.samplers.linear + } + + fn locate(&self, target: Target) -> (&TargetSet, usize) { + locate( + &self.frame.targets, + self.spider.as_ref().map(|spider| &spider.targets), + target, + ) + } + + fn handle(&self, target: Target) -> rhi::backend::TextureHandle { + let (set, i) = self.locate(target); + set.textures[i].handle() + } + + /// `target`'s group with the nearest or bilinear sampler. + fn group(&self, target: Target, linear: bool) -> rhi::backend::BindGroup { + let (set, i) = self.locate(target); + if linear { + set.linear[i] + } else { + set.nearest[i] } } - /// Copies `frame` (the swapchain image, between passes) into `main` and - /// runs the effect's chain over it; [`Self::finish`] writes it back. + fn make_spider_targets(&self, device: &rhi::backend::Device) -> SpiderTargets { + let targets = TargetSet::new(device, &self.samplers, self.frame.extent, &SPIDER_TARGETS); + let handle = |target| { + let (set, i) = locate(&self.frame.targets, Some(&targets), target); + set.textures[i].handle() + }; + let clip = CLIP_INPUTS.map(|(input, blur)| { + let [texture, sampler] = util::texture_binding(handle(input), self.samplers.nearest); + let [blur_texture, blur_sampler] = + util::texture_binding(handle(blur), self.samplers.nearest); + device.create_bind_group( + self.clip_layout, + &[texture, sampler, (2, blur_texture.1), (3, blur_sampler.1)], + ) + }); + SpiderTargets { targets, clip } + } + + /// One full-screen pass of `pipeline` reading `inputs` into `target`. + fn pass( + &self, + device: &rhi::backend::Device, + encoder: &rhi::backend::CommandEncoder, + target: Target, + pipeline: &rhi::backend::RenderPipeline, + inputs: rhi::backend::BindGroup, + uniforms: &[f32], + ) { + post::run( + device, + encoder, + self.handle(target), + self.frame.extent, + pipeline, + inputs, + uniforms, + ); + } + + /// Copies `frame` (the swapchain image, between passes) into `main` as + /// gamma-space colours. + fn capture( + &self, + device: &rhi::backend::Device, + encoder: &rhi::backend::CommandEncoder, + frame: rhi::backend::TextureHandle, + ) { + let size = [self.frame.extent.width, self.frame.extent.height]; + util::copy_frame(encoder, frame, size, self.frame.frame.handle(), size); + self.pass( + device, + encoder, + Target::Main, + &self.decode, + self.frame.frame_group, + &[1.0 / 2.2], + ); + } + + /// Runs the effect's chain over `frame`; [`Self::finish`] writes it back. pub fn apply( &mut self, device: &rhi::backend::Device, encoder: &rhi::backend::CommandEncoder, frame: rhi::backend::TextureHandle, - extent: rhi::Extent, ) { let Some(effect) = self.effect else { return; }; - if self.targets.as_ref().is_none_or(|t| t.extent != extent) { - if let Some(stale) = self.targets.take() { - device.wait_idle(); - stale.destroy(device); - } - self.targets = Some(Targets::new(device, self, extent)); + if effect == PostEffect::Spider && self.spider.is_none() { + self.spider = Some(self.make_spider_targets(device)); } - let t = self.targets.as_ref().expect("made above"); - - let size = [extent.width, extent.height]; - util::copy_frame(encoder, frame, size, t.frame.handle(), size); + self.capture(device, encoder, frame); - let pass = |target: Target, pipeline, inputs, uniforms: &[f32]| { - post::run( - device, - encoder, - t.handle(target), - extent, - pipeline, - inputs, - uniforms, - ); + let pass = |target, pipeline, inputs, uniforms: &[f32]| { + self.pass(device, encoder, target, pipeline, inputs, uniforms); }; - let nearest = |target: Target| t.nearest[target as usize]; - let linear = |target: Target| t.linear[target as usize]; - pass(Target::Main, &self.decode, t.frame_group, &[1.0 / 2.2]); + let nearest = |target| self.group(target, false); match effect { PostEffect::Invert => { pass(Target::Swap, &self.invert, nearest(Target::Main), &[0.8]); @@ -351,21 +501,11 @@ impl PostEffects { } PostEffect::Spider => { for (radius, blurred) in [(15.0, Target::LargeBlur), (7.0, Target::SmallBlur)] { - pass( - Target::Temp, - &self.box_blur, - linear(Target::Main), - &[1.0, 0.0, radius], - ); - pass( - blurred, - &self.box_blur, - linear(Target::Temp), - &[0.0, 1.0, radius], - ); + self.box_blur(device, encoder, Target::Main, Target::Temp, blurred, radius); } + let clip = &self.spider.as_ref().expect("made above").clip; for (inputs, output, uniforms) in SPIDER_CLIPS { - pass(output, &self.spiderclip, t.clip[inputs], &uniforms); + pass(output, &self.spiderclip, clip[inputs], &uniforms); } pass( Target::Main, @@ -377,11 +517,58 @@ impl PostEffects { } } + /// Vanilla `processBlurEffect` (`post_effect/blur.json`): three rounds of + /// box blur at `radius` over `frame`, left in `main`. A radius of 0 only + /// captures the frame. + pub fn blur( + &self, + device: &rhi::backend::Device, + encoder: &rhi::backend::CommandEncoder, + frame: rhi::backend::TextureHandle, + radius: u8, + ) { + self.capture(device, encoder, frame); + if radius == 0 { + return; + } + for _ in 0..3 { + self.box_blur( + device, + encoder, + Target::Main, + Target::Swap, + Target::Main, + f32::from(radius), + ); + } + } + + /// A bilinear `box_blur` of `radius` from `from`, horizontally into + /// `through` and then vertically into `to`. + fn box_blur( + &self, + device: &rhi::backend::Device, + encoder: &rhi::backend::CommandEncoder, + from: Target, + through: Target, + to: Target, + radius: f32, + ) { + for (input, output, direction) in [(from, through, [1.0, 0.0]), (through, to, [0.0, 1.0])] { + self.pass( + device, + encoder, + output, + &self.box_blur, + self.group(input, true), + &[direction[0], direction[1], radius], + ); + } + } + /// Writes `main` back over the frame, in the reopened frame pass. pub fn finish(&self, pass: &mut rhi::backend::RenderPassEncoder) { - if let Some(t) = &self.targets { - post::draw(pass, &self.encode, t.nearest[Target::Main as usize], &[2.2]); - } + post::draw(pass, &self.encode, self.group(Target::Main, false), &[2.2]); } /// Rebuilds the write-back against a new frame format. @@ -395,9 +582,10 @@ impl PostEffects { } pub fn destroy(&mut self, device: &rhi::backend::Device) { - if let Some(targets) = self.targets.take() { - targets.destroy(device); + if let Some(mut spider) = self.spider.take() { + spider.destroy(device); } + self.frame.destroy(device); for pipeline in [ self.decode, self.encode, @@ -412,10 +600,10 @@ impl PostEffects { } self.pipeline_layout.destroy(device); self.clip_pipeline_layout.destroy(device); - self.texture_layout.destroy(device); + self.samplers.layout.destroy(device); self.clip_layout.destroy(device); - self.nearest.destroy(device); - self.linear.destroy(device); + self.samplers.nearest.destroy(device); + self.samplers.linear.destroy(device); } } diff --git a/pomme-client/src/renderer/shaders/blur.frag b/pomme-client/src/renderer/shaders/blur.frag deleted file mode 100644 index 0b590b0b7..000000000 --- a/pomme-client/src/renderer/shaders/blur.frag +++ /dev/null @@ -1,23 +0,0 @@ -#version 450 - -layout(set = 0, binding = 0) uniform texture2D src_tex; -layout(set = 0, binding = 1) uniform sampler src_tex_sampler; - -layout(push_constant) uniform PushConstants { - vec2 direction; -}; - -layout(location = 0) in vec2 v_uv; -layout(location = 0) out vec4 out_color; - -void main() { - float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); - - vec4 result = texture(sampler2D(src_tex, src_tex_sampler), v_uv) * weights[0]; - for (int i = 1; i < 5; i++) { - vec2 offset = direction * float(i); - result += texture(sampler2D(src_tex, src_tex_sampler), v_uv + offset) * weights[i]; - result += texture(sampler2D(src_tex, src_tex_sampler), v_uv - offset) * weights[i]; - } - out_color = result; -} diff --git a/pomme-client/src/renderer/shaders/menu_overlay.frag b/pomme-client/src/renderer/shaders/menu_overlay.frag index 7be2d6e53..69a6e58bd 100644 --- a/pomme-client/src/renderer/shaders/menu_overlay.frag +++ b/pomme-client/src/renderer/shaders/menu_overlay.frag @@ -87,8 +87,12 @@ void main() { float d = sdf_rounded_rect(local, half_s, v_corner_radius); float alpha = 1.0 - smoothstep(-1.0, 1.0, d); + // The blur lives in vanilla's `main`: bottom-up rows of gamma-space + // colours. vec2 screen_uv = gl_FragCoord.xy / vec2(screen_w, screen_h); - vec4 blurred = texture(sampler2D(blur_tex, blur_tex_sampler), screen_uv); + vec2 main_uv = vec2(screen_uv.x, 1.0 - screen_uv.y); + vec4 blurred = texture(sampler2D(blur_tex, blur_tex_sampler), main_uv); + blurred.rgb = pow(blurred.rgb, vec3(2.2)); vec3 tinted = blurred.rgb * v_color.rgb; float a = v_color.a * alpha; diff --git a/pomme-client/src/ui/menu/account.rs b/pomme-client/src/ui/menu/account.rs index f46deb944..980f53dc4 100644 --- a/pomme-client/src/ui/menu/account.rs +++ b/pomme-client/src/ui/menu/account.rs @@ -92,7 +92,7 @@ impl MainMenu { let leave = if gated { "Quit" } else { "Back" }; if input.escape && !gated { self.close_account(key_bindings); - return empty_result(2.0); + return empty_result(); } if matches!(*self.sign_in.read(), SignIn::Done(_)) { let SignIn::Done(account) = std::mem::take(&mut *self.sign_in.write()) else { @@ -100,7 +100,7 @@ impl MainMenu { }; self.sign_in_task = None; self.set_screen(Screen::Main, key_bindings); - let mut result = empty_result(2.0); + let mut result = empty_result(); result.action = MenuAction::Account(Some(account)); return result; } @@ -156,7 +156,7 @@ impl MainMenu { let column_h = (fs + spacing) * lines.len() as f32 + code_h + 2.0 * spacing + btn_h; let mut y = (screen_h - column_h) / 2.0; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; for line in lines { elements.push(MenuElement::Text { @@ -229,7 +229,6 @@ impl MainMenu { elements, action, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: (input.clicked && any_hovered) || ctx.fired, } } diff --git a/pomme-client/src/ui/menu/bedrock_screen.rs b/pomme-client/src/ui/menu/bedrock_screen.rs index 05cdb912d..6366f3128 100644 --- a/pomme-client/src/ui/menu/bedrock_screen.rs +++ b/pomme-client/src/ui/menu/bedrock_screen.rs @@ -734,7 +734,6 @@ impl MainMenu { elements, action, cursor_pointer: any_hovered, - blur: 0.0, clicked_button: any_clicked, } } diff --git a/pomme-client/src/ui/menu/credits.rs b/pomme-client/src/ui/menu/credits.rs index 0b11230ab..6aa986885 100644 --- a/pomme-client/src/ui/menu/credits.rs +++ b/pomme-client/src/ui/menu/credits.rs @@ -163,14 +163,14 @@ impl MainMenu { let back = self.settings_back.clone_screen(); if input.escape { self.set_screen(back, key_bindings); - return empty_result(2.0); + return empty_result(); } let gs = self.gui_scale(sw, sh); let btn_h = common::BTN_H * gs; let cx = sw / 2.0; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; let chrome = push_screen_chrome(&mut elements, sw, sh, gs, "Credits and Attribution"); @@ -232,7 +232,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: ctx.fired, } } @@ -259,7 +258,7 @@ impl MainMenu { let lines = credits(); if input.escape || lines.is_empty() { self.set_screen(Screen::OptionsCredits, key_bindings); - return empty_result(2.0); + return empty_result(); } let gs = self.gui_scale(sw, sh); @@ -284,10 +283,10 @@ impl MainMenu { let total_scroll_length = lines.len() as f32 * LINE_H; if self.credits_scroll > total_scroll_length + 2.0 * sh_units + 24.0 { self.set_screen(Screen::OptionsCredits, key_bindings); - return empty_result(2.0); + return empty_result(); } - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; // `WinScreen.extractMenuBackground` scrolls the tiling at half speed. // `TiledImage` has no UV offset, so shift the quad by one tile instead. @@ -362,7 +361,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: false, - blur: 2.0, clicked_button: false, } } diff --git a/pomme-client/src/ui/menu/friends_screen.rs b/pomme-client/src/ui/menu/friends_screen.rs index d4f9f795e..bbbf8736d 100644 --- a/pomme-client/src/ui/menu/friends_screen.rs +++ b/pomme-client/src/ui/menu/friends_screen.rs @@ -61,11 +61,11 @@ impl MainMenu { // Esc: close the popup, else blur a field, else leave the screen. if input.escape { if self.pending_remove.take().is_some() { - return empty_result(2.0); + return empty_result(); } if self.focused_field.take().is_none() { self.set_screen(Screen::Main, key_bindings); - return empty_result(2.0); + return empty_result(); } } // Click outside the panel closes (unless a popup is capturing input). @@ -77,7 +77,7 @@ impl MainMenu { ) { self.set_screen(Screen::Main, key_bindings); - return empty_result(2.0); + return empty_result(); } if input.f5 { self.refresh_friends_now(); @@ -294,7 +294,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: false, - blur: 1.0, clicked_button: false, } } diff --git a/pomme-client/src/ui/menu/helpers.rs b/pomme-client/src/ui/menu/helpers.rs index 5f5d24441..baacf39e7 100644 --- a/pomme-client/src/ui/menu/helpers.rs +++ b/pomme-client/src/ui/menu/helpers.rs @@ -1,12 +1,12 @@ use super::*; use crate::ui::text::TextSpan; -pub(super) fn empty_result(blur: f32) -> MainMenuResult { +/// A screen with nothing on it yet: its blurred background only. +pub(super) fn empty_result() -> MainMenuResult { MainMenuResult { - elements: Vec::new(), + elements: vec![MenuElement::BlurBackground], action: MenuAction::None, cursor_pointer: false, - blur, clicked_button: false, } } @@ -1433,7 +1433,7 @@ impl MainMenu { yes: &str, ) -> (MainMenuResult, Option) { if input.escape { - return (empty_result(2.0), Some(false)); + return (empty_result(), Some(false)); } let gs = self.gui_scale(screen_w, screen_h); @@ -1450,7 +1450,7 @@ impl MainMenu { let column_h = (fs + spacing) * 2.0 + row_pad + btn_h; let mut y = (screen_h - column_h) / 2.0; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; for text in [question, warning] { elements.push(MenuElement::Text { @@ -1504,7 +1504,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: (clicked && any_hovered) || ctx.fired, }, choice, diff --git a/pomme-client/src/ui/menu/main_screen.rs b/pomme-client/src/ui/menu/main_screen.rs index e6ef0f24d..dcf2b4a94 100644 --- a/pomme-client/src/ui/menu/main_screen.rs +++ b/pomme-client/src/ui/menu/main_screen.rs @@ -39,7 +39,7 @@ impl MainMenu { let cursor = input.cursor; let clicked = input.clicked; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackdrop]; let mut action = MenuAction::None; let mut any_hovered = false; let mut any_clicked = false; @@ -478,7 +478,6 @@ impl MainMenu { elements, action, cursor_pointer: any_hovered, - blur: 1.0, clicked_button: any_clicked, } } diff --git a/pomme-client/src/ui/menu/mod.rs b/pomme-client/src/ui/menu/mod.rs index 8b4f32a17..f64a8e5ef 100644 --- a/pomme-client/src/ui/menu/mod.rs +++ b/pomme-client/src/ui/menu/mod.rs @@ -53,6 +53,8 @@ struct Settings { invert_mouse_y: bool, #[serde(default = "default_true")] view_bobbing: bool, + #[serde(default = "default_menu_background_blurriness")] + menu_background_blurriness: u8, #[serde(default)] show_subtitles: bool, #[serde(default = "default_true")] @@ -143,6 +145,14 @@ fn default_damage_tilt_strength() -> f32 { 1.0 } +/// Vanilla `menuBackgroundBlurriness`: 0 (off) to 10, default 5. +const MAX_MENU_BACKGROUND_BLURRINESS: u8 = 10; +pub const DEFAULT_MENU_BACKGROUND_BLURRINESS: u8 = 5; + +fn default_menu_background_blurriness() -> u8 { + DEFAULT_MENU_BACKGROUND_BLURRINESS +} + /// Vanilla's `OptionInstance.UnitDouble` range, for sliders an out-of-range /// options.json would otherwise feed straight into a render or input curve. fn unit_range(value: f32) -> f32 { @@ -201,6 +211,7 @@ impl Default for Settings { invert_mouse_x: false, invert_mouse_y: false, view_bobbing: true, + menu_background_blurriness: default_menu_background_blurriness(), show_subtitles: false, show_autosave_indicator: true, vignette: true, @@ -352,7 +363,6 @@ pub struct MainMenuResult { // TODO: vanilla `AbstractWidget.handleCursor` wants NOT_ALLOWED over a // hovered inactive widget; one bool only says pointer or arrow. pub cursor_pointer: bool, - pub blur: f32, pub clicked_button: bool, } @@ -698,6 +708,8 @@ pub struct MainMenu { pub invert_mouse_x: bool, pub invert_mouse_y: bool, pub view_bobbing: bool, + /// The Menu Background Blur radius (vanilla `MenuBlurRadius`), 0 for off. + pub menu_background_blurriness: u8, pub show_subtitles: bool, pub show_autosave_indicator: bool, pub vignette: bool, @@ -853,6 +865,9 @@ impl MainMenu { invert_mouse_x: settings.invert_mouse_x, invert_mouse_y: settings.invert_mouse_y, view_bobbing: settings.view_bobbing, + menu_background_blurriness: settings + .menu_background_blurriness + .min(MAX_MENU_BACKGROUND_BLURRINESS), show_subtitles: settings.show_subtitles, show_autosave_indicator: settings.show_autosave_indicator, vignette: settings.vignette, @@ -1096,6 +1111,7 @@ impl MainMenu { invert_mouse_x: self.invert_mouse_x, invert_mouse_y: self.invert_mouse_y, view_bobbing: self.view_bobbing, + menu_background_blurriness: self.menu_background_blurriness, show_subtitles: self.show_subtitles, show_autosave_indicator: self.show_autosave_indicator, vignette: self.vignette, @@ -1157,6 +1173,11 @@ impl MainMenu { } } + pub fn set_menu_background_blurriness(&mut self, radius: u8) { + self.menu_background_blurriness = radius.min(MAX_MENU_BACKGROUND_BLURRINESS); + self.settings_dirty = true; + } + pub fn set_main_hand_right(&mut self, right: bool, key_bindings: &KeyBindings) { self.skin_main_hand_right = right; self.save_settings(key_bindings); @@ -1636,6 +1657,25 @@ mod tests { assert_eq!(chunk_detail, 4); } + #[test] + fn menu_background_blur_defaults_to_five_and_clamps_to_ten() { + let mut legacy = serde_json::to_value(Settings::default()).unwrap(); + legacy + .as_object_mut() + .unwrap() + .remove("menu_background_blurriness"); + let legacy: Settings = serde_json::from_value(legacy).unwrap(); + assert_eq!(legacy.menu_background_blurriness, 5); + + let mut key_bindings = KeyBindings::default(); + let mut menu = test_menu("menu-blur-clamp", &mut key_bindings); + menu.set_menu_background_blurriness(42); + assert_eq!( + menu.menu_background_blurriness, + MAX_MENU_BACKGROUND_BLURRINESS + ); + } + #[test] fn damage_tilt_defaults_to_full_strength_and_clamps_to_unit_range() { let mut legacy = serde_json::to_value(Settings::default()).unwrap(); diff --git a/pomme-client/src/ui/menu/options.rs b/pomme-client/src/ui/menu/options.rs index cf239cc8a..e7248077e 100644 --- a/pomme-client/src/ui/menu/options.rs +++ b/pomme-client/src/ui/menu/options.rs @@ -49,6 +49,16 @@ const TEXT_BACKGROUND_OPACITY: &str = "Text Background Opacity:"; const CHAT_SCALE: &str = "Chat Text Size:"; const CHAT_LINE_SPACING: &str = "Line Spacing:"; const CHAT_DELAY: &str = "Chat Delay:"; +const MENU_BACKGROUND_BLUR: &str = "Menu Background Blur:"; + +/// Vanilla `genericValueOrOffLabel`. +fn menu_background_blur_label(radius: u8) -> String { + if radius == 0 { + format!("{MENU_BACKGROUND_BLUR} OFF") + } else { + format!("{MENU_BACKGROUND_BLUR} {radius}") + } +} const CHAT_WIDTH: &str = "Width:"; const CHAT_HEIGHT_FOCUSED: &str = "Focused Height:"; const CHAT_HEIGHT_UNFOCUSED: &str = "Unfocused Height:"; @@ -182,6 +192,15 @@ impl MainMenu { } } + /// The Menu Background Blur slider's label and position. + fn menu_background_blur_slider(&self) -> (String, f32) { + let radius = self.menu_background_blurriness; + ( + menu_background_blur_label(radius), + f32::from(radius) / f32::from(MAX_MENU_BACKGROUND_BLURRINESS), + ) + } + fn show_subtitles_label(&self) -> &'static str { if self.show_subtitles { "Show Subtitles: ON" @@ -201,6 +220,7 @@ impl MainMenu { "Max Framerate:" => 25.0, "FOV:" => 80.0, CHAT_DELAY => CHAT_DELAY_MAX_TENTHS as f32, + MENU_BACKGROUND_BLUR => f32::from(MAX_MENU_BACKGROUND_BLURRINESS), _ => return None, }) } @@ -272,6 +292,7 @@ impl MainMenu { } else { "Entity Shadows: OFF" }; + let (blur_label, blur_frac) = self.menu_background_blur_slider(); let rows: Vec = vec![ OptRow::Header("Display"), OptRow::Big("Fullscreen Resolution: Current"), @@ -288,7 +309,7 @@ impl MainMenu { OptRow::PairLeft(&cd), OptRow::Pair("Particles: All", "Mipmap Levels: 4"), OptRow::Pair(entity_shadows_label, "Entity Distance: 100%"), - OptRow::Pair("Menu Background Blur: 50%", "Cloud Range: 128"), + OptRow::Pair(&blur_label, "Cloud Range: 128"), OptRow::Pair("Cutout Leaves: Fancy", "Improved Transparency: OFF"), OptRow::Pair("Texture Filtering: None", "Max Anisotropy: 1"), OptRow::PairLeft("Weather Radius: 10"), @@ -305,6 +326,7 @@ impl MainMenu { ("Chunk Detail:", cd_frac), ("Simulation Distance:", sd_frac), ("Max Framerate:", mf_frac), + (MENU_BACKGROUND_BLUR, blur_frac), ]; let disabled = &[ "Fullscreen Resolution:", @@ -320,7 +342,6 @@ impl MainMenu { "Particles:", "Mipmap Levels:", "Entity Distance:", - "Menu Background Blur:", "Cloud Range:", "Cutout Leaves:", "Improved Transparency:", @@ -472,7 +493,7 @@ impl MainMenu { if input.escape { self.set_screen(Screen::OptionsControls, key_bindings); - return empty_result(1.0); + return empty_result(); } let gs = self.gui_scale(sw, sh); @@ -480,7 +501,7 @@ impl MainMenu { let btn_h = BTN_H * gs; let mut any_hovered = false; let mut any_clicked = false; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; self.focus_advance(input); let ctx = &mut self.make_focus_ctx(input); @@ -711,7 +732,7 @@ impl MainMenu { true, ) { self.set_screen(Screen::OptionsControls, key_bindings); - let mut result = empty_result(1.0); + let mut result = empty_result(); result.clicked_button = true; return result; } @@ -720,7 +741,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: any_clicked, } } @@ -867,9 +887,10 @@ impl MainMenu { let chat_opacity = chat_opacity_label(o.opacity); let spacing = percent_label(CHAT_LINE_SPACING, o.line_spacing.into()); let delay = chat_delay_label(o.delay_secs); + let (blur_label, blur_frac) = self.menu_background_blur_slider(); let rows: Vec = vec![ OptRow::Pair("Narrator: OFF", self.show_subtitles_label()), - OptRow::Pair("High Contrast: OFF", "Menu Background Blur: 50%"), + OptRow::Pair("High Contrast: OFF", &blur_label), OptRow::Pair(&bg_opacity, "Background for Chat Only: OFF"), OptRow::Pair(&chat_opacity, &spacing), OptRow::Pair(&delay, "Notification Time: 10.0s"), @@ -889,11 +910,11 @@ impl MainMenu { (CHAT_DELAY, chat_delay_slider(o.delay_secs)), ("FOV Effects:", self.fov_effect_scale), ("Damage Tilt:", self.damage_tilt_strength), + (MENU_BACKGROUND_BLUR, blur_frac), ]; let disabled = &[ "Narrator:", "High Contrast:", - "Menu Background Blur:", "Background for Chat Only:", "Notification Time:", "Distortion Effects:", @@ -1159,7 +1180,7 @@ impl MainMenu { ) -> MainMenuResult { if input.escape { self.set_screen(back.clone_screen(), key_bindings); - return empty_result(2.0); + return empty_result(); } let gs = self.gui_scale(sw, sh); @@ -1176,7 +1197,7 @@ impl MainMenu { let cursor = input.cursor; let clicked = input.clicked; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; let mut any_clicked = false; @@ -1525,6 +1546,9 @@ impl MainMenu { } "Max Framerate:" => self.max_framerate = 10 + 10 * (v * span).round() as u32, "FOV:" => self.fov = (30.0 + v * span).round() as u32, + MENU_BACKGROUND_BLUR => { + self.set_menu_background_blurriness((v * span).round() as u8) + } "FOV Effects:" => self.fov_effect_scale = v, "Damage Tilt:" => self.damage_tilt_strength = v, "Sensitivity:" => self.sensitivity = v, @@ -1619,7 +1643,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: any_clicked, } } @@ -1637,7 +1660,7 @@ impl MainMenu { if input.escape { self.pack_search.clear(); self.set_screen(Screen::Options, key_bindings); - return empty_result(2.0); + return empty_result(); } self.cycle_fields(input, 1); @@ -1650,7 +1673,7 @@ impl MainMenu { let cursor = input.cursor; let clicked = input.clicked; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; common::push_overlay(&mut elements, sw, sh, 0.4); @@ -1918,7 +1941,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: false, } } @@ -1936,13 +1958,13 @@ impl MainMenu { ) -> MainMenuResult { if input.escape { self.set_screen(back.clone_screen(), key_bindings); - return empty_result(2.0); + return empty_result(); } let gs = self.gui_scale(sw, sh); let cx = sw / 2.0; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; let chrome = push_screen_chrome(&mut elements, sw, sh, gs, title); @@ -2001,7 +2023,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: ctx.fired, } } @@ -2022,6 +2043,13 @@ mod tests { assert!(option_enabled("Graphics Backend: Default", disabled)); } + #[test] + fn menu_background_blur_label_is_generic_value_or_off() { + assert_eq!(menu_background_blur_label(0), "Menu Background Blur: OFF"); + assert_eq!(menu_background_blur_label(5), "Menu Background Blur: 5"); + assert_eq!(menu_background_blur_label(10), "Menu Background Blur: 10"); + } + #[test] fn chat_option_labels_match_vanilla() { let o = crate::ui::chat::ChatOptions::default(); diff --git a/pomme-client/src/ui/menu/servers.rs b/pomme-client/src/ui/menu/servers.rs index f10b4ed0e..0f7f967d3 100644 --- a/pomme-client/src/ui/menu/servers.rs +++ b/pomme-client/src/ui/menu/servers.rs @@ -27,7 +27,7 @@ impl MainMenu { let list_bottom = screen_h - footer_h; let list_h = list_bottom - list_top; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut action = MenuAction::None; let mut any_hovered = false; @@ -37,10 +37,9 @@ impl MainMenu { if input.escape { self.set_screen(Screen::Main, key_bindings); return MainMenuResult { - elements: Vec::new(), + elements: vec![MenuElement::BlurBackground], action: MenuAction::None, cursor_pointer: false, - blur: 1.0, clicked_button: false, }; } @@ -504,7 +503,6 @@ impl MainMenu { elements, action, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: (input.clicked && any_hovered) || ctx.fired, } } @@ -518,7 +516,7 @@ impl MainMenu { key_bindings: &KeyBindings, ) -> MainMenuResult { let Screen::ConfirmDelete(idx) = self.screen else { - return empty_result(2.0); + return empty_result(); }; let warning = self .server_list @@ -564,12 +562,12 @@ impl MainMenu { if input.escape { self.set_screen(Screen::ServerList, key_bindings); - return empty_result(2.0); + return empty_result(); } self.cycle_fields(input, 1); - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut action = MenuAction::None; let mut any_hovered = false; @@ -672,7 +670,6 @@ impl MainMenu { elements, action, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: input.clicked && any_hovered, } } @@ -696,12 +693,12 @@ impl MainMenu { if input.escape { self.set_screen(Screen::ServerList, key_bindings); - return empty_result(2.0); + return empty_result(); } self.cycle_fields(input, 2); - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; let cx = screen_w / 2.0; @@ -839,7 +836,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: input.clicked && any_hovered, } } @@ -1050,7 +1046,7 @@ impl MainMenu { ) -> MainMenuResult { let reason = match &self.screen { Screen::Disconnected(r) => r.clone(), - _ => return empty_result(2.0), + _ => return empty_result(), }; let gs = self.gui_scale(screen_w, screen_h); @@ -1070,7 +1066,7 @@ impl MainMenu { let top = ((screen_h - total_h) / 2.0 / gs).floor() * gs; let cx = screen_w / 2.0; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; let mut text = |text: String, y: f32| { elements.push(MenuElement::Text { @@ -1114,7 +1110,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: (input.clicked && any_hovered) || ctx.fired, } } diff --git a/pomme-client/src/ui/menu/title_screen.rs b/pomme-client/src/ui/menu/title_screen.rs index 6ad26f9f9..b9f08777c 100644 --- a/pomme-client/src/ui/menu/title_screen.rs +++ b/pomme-client/src/ui/menu/title_screen.rs @@ -335,7 +335,6 @@ impl MainMenu { action, cursor_pointer: any_hovered, // `TitleScreen.extractBackground` is empty: just the panorama. - blur: 0.0, clicked_button: any_clicked, } } diff --git a/pomme-client/src/ui/menu/worlds.rs b/pomme-client/src/ui/menu/worlds.rs index aa852f571..548bd2ed6 100644 --- a/pomme-client/src/ui/menu/worlds.rs +++ b/pomme-client/src/ui/menu/worlds.rs @@ -70,14 +70,14 @@ impl MainMenu { } if input.escape { self.set_screen(Screen::Main, key_bindings); - return empty_result(2.0); + return empty_result(); } let list_top = header_h; let list_bottom = screen_h - footer_h; let list_h = list_bottom - list_top; - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; let mut action = MenuAction::None; let playable = crate::singleplayer::AVAILABLE; @@ -397,7 +397,6 @@ impl MainMenu { elements, action, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: (clicked && any_hovered) || ctx.fired, } } @@ -617,14 +616,14 @@ impl MainMenu { if input.escape { self.leave_create_world(key_bindings); - return empty_result(2.0); + return empty_result(); } if self.create.field_target().is_some() { self.cycle_fields(input, 1); } - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; let inert = |elements: &mut Vec, any_hovered: &mut bool, @@ -920,7 +919,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: clicked && any_hovered, } } @@ -1042,7 +1040,7 @@ impl MainMenu { key_bindings: &KeyBindings, ) -> MainMenuResult { let Some(folder) = self.screen_folder() else { - return empty_result(2.0); + return empty_result(); }; let gs = self.gui_scale(screen_w, screen_h); @@ -1057,12 +1055,12 @@ impl MainMenu { if input.escape { self.set_screen(Screen::WorldList, key_bindings); - return empty_result(2.0); + return empty_result(); } self.cycle_fields(input, 1); - let mut elements = Vec::new(); + let mut elements = vec![MenuElement::BlurBackground]; let mut any_hovered = false; let cx = screen_w / 2.0; @@ -1202,7 +1200,6 @@ impl MainMenu { elements, action: MenuAction::None, cursor_pointer: any_hovered, - blur: 2.0, clicked_button: clicked && any_hovered, } } @@ -1216,7 +1213,7 @@ impl MainMenu { key_bindings: &KeyBindings, ) -> MainMenuResult { let Some(folder) = self.screen_folder() else { - return empty_result(2.0); + return empty_result(); }; let warning = format!( "'{}' will be lost forever! (A long time!)", diff --git a/tests/ingame/ui/menu_blur.toml b/tests/ingame/ui/menu_blur.toml new file mode 100644 index 000000000..75b2217a4 --- /dev/null +++ b/tests/ingame/ui/menu_blur.toml @@ -0,0 +1,89 @@ +# The pause menu blurs the world and HUD behind it by the Menu Background +# Blur option: the default 5, nothing at 0, more at 10. + +[[step]] +wait_ms = 2000 + +[[step]] +chat = "/time set 6000" + +[[step]] +look_at = [8.0, -61.0, 8.0] + +[[step]] +wait_ms = 11000 + +[[step]] +screenshot = "unpaused" + +[[step]] +press = { action = "key.menu" } + +[[step]] +expect = { menu = "pause" } + +[[step]] +wait_ms = 300 + +[[step]] +screenshot = "blur_5" + +[[step]] +press = { action = "key.menu" } + +[[step]] +option = { menu_background_blur = 0 } + +[[step]] +press = { action = "key.menu" } + +[[step]] +expect = { menu = "pause" } + +[[step]] +wait_ms = 300 + +[[step]] +screenshot = "blur_0" + +[[step]] +press = { action = "key.menu" } + +[[step]] +option = { menu_background_blur = 10 } + +[[step]] +press = { action = "key.menu" } + +[[step]] +expect = { menu = "pause" } + +[[step]] +wait_ms = 300 + +[[step]] +screenshot = "blur_10" + +# Options opened from the pause menu blurs too, at the default again. The +# script ends here: back from Options, the pause menu ignores input (a +# separate bug), and the client closes with the script. +[[step]] +option = { menu_background_blur = 5 } + +[[step]] +cursor = [161, 151] + +[[step]] +mouse_down = "left" + +[[step]] +wait_ms = 100 + +[[step]] +mouse_up = "left" + +[[step]] +wait_ms = 300 + +[[step]] +screenshot = "options"