From e5d841e62ff1998c41387246722eb44d9c3a5d58 Mon Sep 17 00:00:00 2001 From: Imbris Date: Wed, 1 Jan 2020 22:48:11 -0500 Subject: [PATCH] Adds fullscreen and window size settings and coverage based pixel art scaling --- CHANGELOG.md | 2 + voxygen/src/hud/mod.rs | 8 ++ voxygen/src/hud/settings_window.rs | 54 ++++++++- voxygen/src/menu/char_selection/mod.rs | 2 +- voxygen/src/menu/main/mod.rs | 2 +- voxygen/src/session.rs | 12 +- voxygen/src/settings.rs | 4 + voxygen/src/ui/graphic/mod.rs | 16 ++- voxygen/src/ui/graphic/pixel_art.rs | 146 +++++++++++++++++++++++++ voxygen/src/window.rs | 34 +++++- 10 files changed, 261 insertions(+), 19 deletions(-) create mode 100644 voxygen/src/ui/graphic/pixel_art.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 92390fd6d6..15480736b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a default map, which is used to speed up starting single player. - Added a 3D renderered map, which is also used by the server to send the map to the client. +- Added fullscreen and window size to settings so that they can be persisted +- Added coverage based scaling for pixel art ### Changed diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 08d3b7798b..1bb40bdfd5 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -212,6 +212,8 @@ pub enum Event { ChangeAudioDevice(String), ChangeMaxFPS(u32), ChangeFOV(u16), + AdjustWindowSize([u16; 2]), + ToggleFullscreen, ChangeAaMode(AaMode), ChangeCloudMode(CloudMode), ChangeFluidMode(FluidMode), @@ -1758,6 +1760,12 @@ impl Hud { settings_window::Event::ChangeLanguage(language) => { events.push(Event::ChangeLanguage(language)); } + settings_window::Event::ToggleFullscreen => { + events.push(Event::ToggleFullscreen); + } + settings_window::Event::AdjustWindowSize(new_size) => { + events.push(Event::AdjustWindowSize(new_size)); + } } } } diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index 861b92c85e..bce7f38c90 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -95,6 +95,9 @@ widget_ids! { cloud_mode_list, fluid_mode_text, fluid_mode_list, + fullscreen_button, + fullscreen_label, + save_window_size_button, audio_volume_slider, audio_volume_text, sfx_volume_slider, @@ -196,6 +199,8 @@ pub enum Event { ToggleMouseYInvert(bool), AdjustViewDistance(u32), AdjustFOV(u16), + AdjustWindowSize([u16; 2]), + ToggleFullscreen, ChangeAaMode(AaMode), ChangeCloudMode(CloudMode), ChangeFluidMode(FluidMode), @@ -1211,7 +1216,7 @@ impl<'a> Widget for SettingsWindow<'a> { .right_from(state.ids.mouse_zoom_invert_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) - .graphics_for(state.ids.button_help) + .graphics_for(state.ids.mouse_zoom_invert_button) .color(TEXT_COLOR) .set(state.ids.mouse_zoom_invert_label, ui); @@ -1241,7 +1246,7 @@ impl<'a> Widget for SettingsWindow<'a> { .right_from(state.ids.mouse_y_invert_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) - .graphics_for(state.ids.button_help) + .graphics_for(state.ids.mouse_y_invert_button) .color(TEXT_COLOR) .set(state.ids.mouse_y_invert_label, ui); } @@ -1658,6 +1663,51 @@ impl<'a> Widget for SettingsWindow<'a> { { events.push(Event::ChangeFluidMode(mode_list[clicked])); } + + // Fullscreen + Text::new("Fullscreen") + .font_size(14) + .font_id(self.fonts.cyri) + .down_from(state.ids.fluid_mode_list, 125.0) + .color(TEXT_COLOR) + .set(state.ids.fullscreen_label, ui); + + let fullscreen = ToggleButton::new( + self.global_state.settings.graphics.fullscreen, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .right_from(state.ids.fullscreen_label, 10.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.fullscreen_button, ui); + + if self.global_state.settings.graphics.fullscreen != fullscreen { + events.push(Event::ToggleFullscreen); + } + + // Save current screen size + if Button::image(self.imgs.settings_button) + .w_h(31.0 * 5.0, 12.0 * 2.0) + .hover_image(self.imgs.settings_button_hover) + .press_image(self.imgs.settings_button_press) + .down_from(state.ids.fullscreen_label, 12.0) + .label("Save window size") + .label_font_size(14) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri) + .set(state.ids.save_window_size_button, ui) + .was_clicked() + { + events.push(Event::AdjustWindowSize( + self.global_state + .window + .logical_size() + .map(|e| e as u16) + .into_array(), + )); + } } // 5) Sound Tab ----------------------------------- diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 9258e32850..b1e9440b1f 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -38,7 +38,7 @@ impl PlayState for CharSelectionState { let mut current_client_state = self.client.borrow().get_client_state(); while let ClientState::Pending | ClientState::Registered = current_client_state { // Handle window events - for event in global_state.window.fetch_events() { + for event in global_state.window.fetch_events(&mut global_state.settings) { if self.char_selection_ui.handle_event(event.clone()) { continue; } diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 15df6e4da6..a5e83b54db 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -56,7 +56,7 @@ impl PlayState for MainMenuState { loop { // Handle window events. - for event in global_state.window.fetch_events() { + for event in global_state.window.fetch_events(&mut global_state.settings) { match event { Event::Close => return PlayStateResult::Shutdown, // Pass events to ui. diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index e33a377942..1cc8dfc586 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -175,7 +175,7 @@ impl PlayState for SessionState { self.scene.set_select_pos(select_pos); // Handle window events. - for event in global_state.window.fetch_events() { + for event in global_state.window.fetch_events(&mut global_state.settings) { // Pass all events to the ui first. if self.hud.handle_event(event.clone(), global_state) { continue; @@ -583,6 +583,16 @@ impl PlayState for SessionState { .unwrap(); localized_strings.log_missing_entries(); } + HudEvent::ToggleFullscreen => { + global_state + .window + .toggle_fullscreen(&mut global_state.settings); + } + HudEvent::AdjustWindowSize(new_size) => { + global_state.window.set_size(new_size.into()); + global_state.settings.graphics.window_size = new_size; + global_state.settings.save_to_file_warn(); + } } } diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 072201f376..b69fd24f84 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -195,6 +195,8 @@ pub struct GraphicsSettings { pub aa_mode: AaMode, pub cloud_mode: CloudMode, pub fluid_mode: FluidMode, + pub window_size: [u16; 2], + pub fullscreen: bool, } impl Default for GraphicsSettings { @@ -206,6 +208,8 @@ impl Default for GraphicsSettings { aa_mode: AaMode::Fxaa, cloud_mode: CloudMode::Regular, fluid_mode: FluidMode::Shiny, + window_size: [1920, 1080], + fullscreen: false, } } } diff --git a/voxygen/src/ui/graphic/mod.rs b/voxygen/src/ui/graphic/mod.rs index 138a54d52e..b825166028 100644 --- a/voxygen/src/ui/graphic/mod.rs +++ b/voxygen/src/ui/graphic/mod.rs @@ -1,3 +1,4 @@ +mod pixel_art; mod renderer; use crate::render::{Renderer, Texture}; @@ -6,6 +7,7 @@ use guillotiere::{size2, SimpleAtlasAllocator}; use hashbrown::HashMap; use image::{DynamicImage, RgbaImage}; use log::warn; +use pixel_art::resize_pixel_art; use std::sync::Arc; use vek::*; @@ -280,15 +282,11 @@ fn draw_graphic(graphic_map: &GraphicMap, graphic_id: Id, dims: Vec2) -> Op Some(Graphic::Blank) => None, // Render image at requested resolution // TODO: Use source aabr. - Some(Graphic::Image(ref image)) => Some( - image - .resize_exact( - u32::from(dims.x), - u32::from(dims.y), - image::FilterType::Nearest, - ) - .to_rgba(), - ), + Some(Graphic::Image(ref image)) => Some(resize_pixel_art( + &image.to_rgba(), + u32::from(dims.x), + u32::from(dims.y), + )), Some(Graphic::Voxel(ref vox, trans, min_samples)) => Some(renderer::draw_vox( &vox.as_ref().into(), dims, diff --git a/voxygen/src/ui/graphic/pixel_art.rs b/voxygen/src/ui/graphic/pixel_art.rs new file mode 100644 index 0000000000..53dd4112ed --- /dev/null +++ b/voxygen/src/ui/graphic/pixel_art.rs @@ -0,0 +1,146 @@ +use common::util::{linear_to_srgba, srgba_to_linear}; +/// Pixel art scaling +/// Note: The current ui is locked to the pixel grid with little animation, if we want smoothly +/// moving pixel art this should be done in the shaders +use image::RgbaImage; +use vek::*; + +const EPSILON: f32 = 0.0001; + +// Averaging colors with alpha such that when blending with the background color the same color +// will be produced as when the individual colors were blended with the background and then +// averaged +// Say we have two areas that we are combining to form a single pixel +// A1 and A2 where these are the fraction of the area of the pixel each color contributes to +// Then if the colors were opaque we would say that the final color ouput color o3 is +// E1: o3 = A1 * o1 + A2 * o2 +// where o1 and o2 are the opaque colors of the two areas +// now say the areas are actually translucent and these opaque colors are derived by blending with a +// common backgound color b +// E2: o1 = c1 * a1 + g * (1 - a1) +// E3: o2 = c2 * a2 + g * (1 - a2) +// we want to find the combined color (c3) and combined alpha (a3) such that +// E4: o3 = c3 * a3 + g * (1 - a3) +// substitution of E2 and E3 into E1 gives +// E5: o3 = A1 * (c1 * a1 + g * (1 - a1)) + A2 * (c2 * a2 + g * (1 - a2)) +// combining E4 and E5 then separting like terms into separte equations gives +// E6: c3 * a3 = A1 * c1 * a1 + A2 * c2 * a2 +// E7: g * (1 - a3) = A1 * g * (1 - a1) + A2 * g * (1 - a2) +// dropping g from E7 and solving for a3 +// E8: a3 = 1 - A1 * (1 - a1) + A2 * (1 - a2) +// we can now calculate the combined alpha value +// and E6 can then be solved for c3 +// E9: c3 = (A1 * c1 * a1 + A2 * c2 * a2) / a3 +pub fn resize_pixel_art(image: &RgbaImage, new_width: u32, new_height: u32) -> RgbaImage { + let (width, height) = image.dimensions(); + let mut new_image = RgbaImage::new(new_width, new_height); + + // Ratio of old image dimensions to new dimensions + // Also the sampling dimensions within the old image for a single pixel in the new image + let wratio = width as f32 / new_width as f32; + let hratio = height as f32 / new_height as f32; + + for x in 0..new_width { + // Calculate sampling strategy + let xsmin = x as f32 * wratio; + let xsmax = xsmin + wratio; + // Min and max pixels covered + let xminp = xsmin.floor() as u32; + let xmaxp = ((xsmax - EPSILON).ceil() as u32) + .checked_sub(1) + .unwrap_or(0); + // Fraction of first pixel to use + let first_x_frac = if xminp != xmaxp { + 1.0 - xsmin.fract() + } else { + xsmax - xsmin + }; + let last_x_frac = xsmax - xmaxp as f32; + for y in 0..new_height { + // Calculate sampling strategy + let ysmin = y as f32 * hratio; + let ysmax = ysmin + hratio; + // Min and max of pixels covered + let yminp = ysmin.floor() as u32; + let ymaxp = ((ysmax - EPSILON).ceil() as u32) + .checked_sub(1) + .unwrap_or(0); + // Fraction of first pixel to use + let first_y_frac = if yminp != ymaxp { + 1.0 - ysmin.fract() + } else { + ysmax - ysmin + }; + let last_y_frac = ysmax - ymaxp as f32; + + let mut linear_color = Rgba::new(0.0, 0.0, 0.0, wratio * hratio); + // Left column + // First pixel sample (top left assuming that is the origin) + linear_color += get_linear_with_frac(image, xminp, yminp, first_x_frac * first_y_frac); + // Left edge + for j in yminp + 1..ymaxp { + linear_color += get_linear_with_frac(image, xminp, j, first_x_frac); + } + // Bottom left corner + if yminp != ymaxp { + linear_color += + get_linear_with_frac(image, xminp, ymaxp, first_x_frac * last_y_frac); + } + // Interior columns + for i in xminp + 1..xmaxp { + // Top edge + linear_color += get_linear_with_frac(image, i, yminp, first_y_frac); + // Inner (entire pixel is encompassed by sample) + for j in yminp + 1..ymaxp { + linear_color += get_linear_with_frac(image, i, j, 1.0); + } + // Bottom edge + if yminp != ymaxp { + linear_color += get_linear_with_frac(image, xminp, ymaxp, last_y_frac); + } + } + // Right column + if xminp != xmaxp { + // Top right corner + linear_color += + get_linear_with_frac(image, xmaxp, yminp, first_y_frac * last_x_frac); + // Right edge + for j in yminp + 1..ymaxp { + linear_color += get_linear_with_frac(image, xmaxp, j, last_x_frac); + } + // Bottom right corner + if yminp != ymaxp { + linear_color += + get_linear_with_frac(image, xmaxp, ymaxp, last_x_frac * last_y_frac); + } + } + // Divide summed color by area sample covers and convert back to srgb + // I wonder if precalulating the inverse of these divs would have a significant effect + linear_color = linear_color / wratio / hratio; + linear_color = + Rgba::from_translucent(linear_color.rgb() / linear_color.a, linear_color.a); + new_image.put_pixel( + x, + y, + image::Rgba( + linear_to_srgba(linear_color) + .map(|e| (e * 255.0).round() as u8) + .into_array(), + ), + ); + } + } + new_image +} + +fn get_linear(image: &RgbaImage, x: u32, y: u32) -> Rgba { + srgba_to_linear(Rgba::::from(image.get_pixel(x, y).0).map(|e| e as f32 / 255.0)) +} + +// See comments above resize_pixel_art +fn get_linear_with_frac(image: &RgbaImage, x: u32, y: u32, frac: f32) -> Rgba { + let rgba = get_linear(image, x, y); + let adjusted_rgb = rgba.rgb() * rgba.a * frac; + let adjusted_alpha = -frac * (1.0 - rgba.a); + Rgba::from_translucent(adjusted_rgb, adjusted_alpha) +} diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index 182cf67380..db76c9caa4 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -287,9 +287,14 @@ impl Window { pub fn new(settings: &Settings) -> Result { let events_loop = glutin::EventsLoop::new(); + let size = settings.graphics.window_size; + let win_builder = glutin::WindowBuilder::new() .with_title("Veloren") - .with_dimensions(glutin::dpi::LogicalSize::new(1920.0, 1080.0)) + .with_dimensions(glutin::dpi::LogicalSize::new( + size[0] as f64, + size[1] as f64, + )) .with_maximized(true); let ctx_builder = glutin::ContextBuilder::new() @@ -413,7 +418,7 @@ impl Window { let keypress_map = HashMap::new(); - Ok(Self { + let mut this = Self { events_loop, renderer: Renderer::new( device, @@ -436,7 +441,11 @@ impl Window { keypress_map, supplement_events: vec![], focused: true, - }) + }; + + this.fullscreen(settings.graphics.fullscreen); + + Ok(this) } pub fn renderer(&self) -> &Renderer { @@ -446,7 +455,7 @@ impl Window { &mut self.renderer } - pub fn fetch_events(&mut self) -> Vec { + pub fn fetch_events(&mut self, settings: &mut Settings) -> Vec { let mut events = vec![]; events.append(&mut self.supplement_events); // Refresh ui size (used when changing playstates) @@ -596,7 +605,7 @@ impl Window { } if toggle_fullscreen { - self.fullscreen(!self.is_fullscreen()); + self.toggle_fullscreen(settings); } events @@ -618,6 +627,12 @@ impl Window { let _ = self.window.window().grab_cursor(grab); } + pub fn toggle_fullscreen(&mut self, settings: &mut Settings) { + self.fullscreen(!self.is_fullscreen()); + settings.graphics.fullscreen = self.is_fullscreen(); + settings.save_to_file_warn(); + } + pub fn is_fullscreen(&self) -> bool { self.fullscreen } @@ -646,6 +661,15 @@ impl Window { Vec2::new(w, h) } + pub fn set_size(&mut self, new_size: Vec2) { + self.window + .window() + .set_inner_size(glutin::dpi::LogicalSize::new( + new_size.x as f64, + new_size.y as f64, + )); + } + pub fn send_supplement_event(&mut self, event: Event) { self.supplement_events.push(event) }