diff --git a/assets/voxygen/element/ui/generic/frames/banner_bot.png b/assets/voxygen/element/ui/generic/frames/banner_bot.png index 1e06228b78..695b03d727 100644 --- a/assets/voxygen/element/ui/generic/frames/banner_bot.png +++ b/assets/voxygen/element/ui/generic/frames/banner_bot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f83239c77d84ba225425a4ba36e439d105df12deef189a36897ce548a70ae0ee -size 110 +oid sha256:bec5e310f6793255dc77eb04ba2ffaa9786d288108576503cbac8a133d4347ae +size 149 diff --git a/assets/voxygen/element/ui/generic/frames/esc_menu.png b/assets/voxygen/element/ui/generic/frames/esc_menu.png index 0c6a4e0faa..ad36939de5 100644 --- a/assets/voxygen/element/ui/generic/frames/esc_menu.png +++ b/assets/voxygen/element/ui/generic/frames/esc_menu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7adc6dea422a19eb8153c0f7a5c28095f69c9f099c816c9905a473c210fc6a87 -size 113 +oid sha256:06de968f79c35f9258467eaa726e973c5b2bf0ebed74f04a205f6d7ad871975e +size 239 diff --git a/assets/voxygen/shaders/ui-frag.glsl b/assets/voxygen/shaders/ui-frag.glsl index 549f60bfb3..c1f7721580 100644 --- a/assets/voxygen/shaders/ui-frag.glsl +++ b/assets/voxygen/shaders/ui-frag.glsl @@ -1,31 +1,215 @@ #version 420 core #include +#include layout(location = 0) in vec2 f_uv; layout(location = 1) in vec4 f_color; -layout(location = 2) flat in uint f_mode; +layout(location = 2) flat in vec2 f_scale; +layout(location = 3) flat in uint f_mode; layout (std140, set = 1, binding = 0) uniform u_locals { vec4 w_pos; }; +// TODO: swap with u_locals because that may change more frequently? layout(set = 2, binding = 0) uniform texture2D t_tex; layout(set = 2, binding = 1) uniform sampler s_tex; +layout (std140, set = 2, binding = 2) +uniform tex_locals { + uvec2 texture_size; +}; layout(location = 0) out vec4 tgt_color; +// Adjusts the provided uv value to account for coverage of pixels from the +// sampled texture by the current fragment when upscaling. +// +// * `pos` - Position in the sampled texture in pixel coordinates. This is +// where the center of the current fragment lies on the sampled texture. +// * `scale` - Scaling of pixels from the sampled texture to the render target. +// This is the amount of fragments that each pixel from the sampled texture +// covers. +float upscale_adjust(float pos, float scale) { + // To retain crisp borders of upscaled pixel art, images are upscaled + // following the algorithm outlined here: + // + // https://csantosbh.wordpress.com/2014/01/25/manual-texture-filtering-for-pixelated-games-in-webgl/ + // + // `min(x * scale, 0.5) + max((x - 1.0) * scale, 0.0)` + // + float frac = fract(pos); + // Right of nearest pixel in the sampled texture. + float base = floor(pos); + // This will be 0.5 when the current fragment lies entirely inside a pixel + // in the sampled texture. + float adjustment = min(frac * scale, 0.5) + max((frac - 1.0) * scale + 0.5, 0.0); + return base + adjustment; +} + +// Computes info needed for downscaling using two samples in a single +// dimension. This info includes the two position to sample at (called +// `offsets` even though they aren't actually offsets from the supplied +// position) and the `weights` to multiply each of those samples by before +// combining them. +// +// See `upscale_adjust` for semantics of `pos` and `scale` parameters. +// +// Ouput via `weights` and `offsets` parameters. +void downscale_params(float pos, float scale, out vec2 weights, out vec2 offsets) { + // For `scale` 0.33333..1.0 we round to the nearest pixel edge and split + // there. We compute the length of each side. Then the sampling point is + // computed as this distance from the split point via this formula where + // `l` is the length of that side of split: + // + // `1.5 - (1.0 / max(l, 1.0))` + // + // For `scale` ..0.3333 the current fragment can potentially span more than + // 4 pixels (within a single dimension) in the sampled texture. So we can't + // perfectly compute the contribution of each covered pixel in the sampled + // texture with only 2 samples (along each dimension). Thus, we fallback to + // an imperfect technique of of just sampling a 1 pixel length from the + // center on each side. An alternative would be to pre-compute mipmap + // levels that could be sampled from. + if (scale > (1.0 / 3.0)) { + // Width of the fragment in terms of pixels in the sampled texture. + float width = 1.0 / scale; + // Right side of the fragment in the sampled texture. + float right = pos - width / 2.0; + float split = round(pos); + float right_len = split - right; + float left_len = width - right_len; + float right_sample_offset = 1.5 - (1.0 / max(right_len, 1.0)); + float left_sample_offset = 1.5 - (1.0 / max(left_len, 1.0)); + offsets = vec2(split) + vec2(-right_sample_offset, left_sample_offset); + weights = vec2(right_len, left_len) / width; + } else { + offsets = pos + vec2(-1.0, 1.0); + // We split in the middle so weights for both sides are the same. + weights = vec2(0.5); + } +} + +// 1 sample +vec4 upscale_xy(vec2 uv_pixel, vec2 scale) { + // When slowly panning something (e.g. the map), a very small amount of + // wobbling is still observable (not as much as nearest sampling). It + // is possible to eliminate this by making the edges slightly blurry by + // lowering the scale a bit here. However, this does make edges little + // less crisp and can cause bleeding in from other images packed into + // the atlas in the current setup. + vec2 adjusted = vec2(upscale_adjust(uv_pixel.x, scale.x), upscale_adjust(uv_pixel.y, scale.y)); + // Convert back to 0.0..1.0 by dividing by texture size. + vec2 uv = adjusted / texture_size; + return textureLod(sampler2D(t_tex, s_tex), uv, 0); +} + +// 2 samples +vec4 upscale_x_downscale_y(vec2 uv_pixel, vec2 scale) { + float x_adjusted = upscale_adjust(uv_pixel.x, scale.x); + vec2 weights, offsets; + downscale_params(uv_pixel.y, scale.y, weights, offsets); + vec2 uv0 = vec2(x_adjusted, offsets[0]) / texture_size; + vec2 uv1 = vec2(x_adjusted, offsets[1]) / texture_size; + vec4 s0 = textureLod(sampler2D(t_tex, s_tex), uv0, 0); + vec4 s1 = textureLod(sampler2D(t_tex, s_tex), uv1, 0); + return s0 * weights[0] + s1 * weights[1]; +} + +// 2 samples +vec4 downscale_x_upscale_y(vec2 uv_pixel, vec2 scale) { + float y_adjusted = upscale_adjust(uv_pixel.y, scale.y); + vec2 weights, offsets; + downscale_params(uv_pixel.x, scale.x, weights, offsets); + vec2 uv0 = vec2(offsets[0], y_adjusted) / texture_size; + vec2 uv1 = vec2(offsets[1], y_adjusted) / texture_size; + vec4 s0 = textureLod(sampler2D(t_tex, s_tex), uv0, 0); + vec4 s1 = textureLod(sampler2D(t_tex, s_tex), uv1, 0); + return s0 * weights[0] + s1 * weights[1]; +} + +// 4 samples +vec4 downscale_xy(vec2 uv_pixel, vec2 scale) { + vec2 weights_x, offsets_x, weights_y, offsets_y; + downscale_params(uv_pixel.x, scale.x, weights_x, offsets_x); + downscale_params(uv_pixel.y, scale.y, weights_y, offsets_y); + vec2 uv0 = vec2(offsets_x[0], offsets_y[0]) / texture_size; + vec2 uv1 = vec2(offsets_x[1], offsets_y[0]) / texture_size; + vec2 uv2 = vec2(offsets_x[0], offsets_y[1]) / texture_size; + vec2 uv3 = vec2(offsets_x[1], offsets_y[1]) / texture_size; + uv0 = uv_pixel / texture_size; + uv1 = uv_pixel / texture_size; + uv2 = uv_pixel / texture_size; + uv3 = uv_pixel / texture_size; + vec4 s0 = textureLod(sampler2D(t_tex, s_tex), uv0, 0); + vec4 s1 = textureLod(sampler2D(t_tex, s_tex), uv1, 0); + vec4 s2 = textureLod(sampler2D(t_tex, s_tex), uv2, 0); + vec4 s3 = textureLod(sampler2D(t_tex, s_tex), uv3, 0); + vec4 s01 = s0 * weights_x[0] + s1 * weights_x[1]; + vec4 s23 = s2 * weights_x[0] + s3 * weights_x[1]; + // Useful to visualize things below the limit where downscaling is supposed + // to be accurate. + /*if (scale.x < (1.0 / 3.0)) { + return vec4(1, 0, 0, 1); + }*/ + return s01 * weights_y[0] + s23 * weights_y[1]; +} + void main() { // Text if (f_mode == uint(0)) { - tgt_color = f_color * vec4(1.0, 1.0, 1.0, textureLod(sampler2D(t_tex, s_tex), f_uv, 0).a); + // NOTE: This now uses linear filter since all `Texture::new_dynamic` + // was changed to this by default. Glyphs are usually rasterized to be + // pretty close to the target size (so the filter change may have no + // effect), but there are thresholds within which the same rasterized + // glyph will be re-used. I wasn't able to observe any differences. + vec2 uv = f_uv; + #ifdef EXPERIMENTAL_UINEARESTSCALING + uv = (floor(uv * texture_size) + 0.5) / texture_size; + #endif + tgt_color = f_color * vec4(1.0, 1.0, 1.0, textureLod(sampler2D(t_tex, s_tex), uv, 0).a); // Image // HACK: bit 0 is set for both ordinary and north-facing images. } else if ((f_mode & uint(1)) == uint(1)) { - tgt_color = f_color * textureLod(sampler2D(t_tex, s_tex), f_uv, 0); + // NOTE: We don't have to account for bleeding over the border of an image + // due to how the ui currently handles rendering images. Currently, any + // edges of an image being rendered that don't line up with a pixel are + // snapped to a pixel, so we will never render any pixels containing an + // image that lie partly outside that image (and thus the sampling here + // will never try to sample outside an image). So we don't have to + // worry about bleeding in the atlas and/or what the border behavior + // should be. + + // TODO: benchmark before and after by viewing zoomed out map in the UI + // (at particular ui scale and map zoom out, far enough to trigger downscaling) + // (seems like ~10% increase if any) + + // Convert to sampled pixel coordinates. + vec2 uv_pixel = f_uv * texture_size; + vec4 image_color = vec4(1.0, 0, 0, 1); + #ifdef EXPERIMENTAL_UINEARESTSCALING + vec2 uv = (floor(uv_pixel) + 0.5) / texture_size; + image_color = textureLod(sampler2D(t_tex, s_tex), uv, 0); + #else + if (f_scale.x >= 1.0) { + if (f_scale.y >= 1.0) { + image_color = upscale_xy(uv_pixel, f_scale); + } else { + image_color = upscale_x_downscale_y(uv_pixel, f_scale); + } + } else { + if (f_scale.y >= 1.0) { + image_color = downscale_x_upscale_y(uv_pixel, f_scale); + } else { + image_color = downscale_xy(uv_pixel, f_scale); + } + } + #endif + + tgt_color = f_color * image_color; // 2D Geometry } else if (f_mode == uint(2)) { tgt_color = f_color; diff --git a/assets/voxygen/shaders/ui-vert.glsl b/assets/voxygen/shaders/ui-vert.glsl index 7c25f01fd3..0990656757 100644 --- a/assets/voxygen/shaders/ui-vert.glsl +++ b/assets/voxygen/shaders/ui-vert.glsl @@ -6,7 +6,8 @@ layout(location = 0) in vec2 v_pos; layout(location = 1) in vec2 v_uv; layout(location = 2) in vec4 v_color; layout(location = 3) in vec2 v_center; -layout(location = 4) in uint v_mode; +layout(location = 4) in vec2 v_scale; +layout(location = 5) in uint v_mode; layout (std140, set = 1, binding = 0) uniform u_locals { @@ -17,10 +18,15 @@ layout(set = 2, binding = 0) uniform texture2D t_tex; layout(set = 2, binding = 1) uniform sampler s_tex; +layout (std140, set = 2, binding = 2) +uniform tex_locals { + uvec2 texture_size; +}; layout(location = 0) out vec2 f_uv; layout(location = 1) out vec4 f_color; -layout(location = 2) flat out uint f_mode; +layout(location = 2) flat out vec2 f_scale; +layout(location = 3) flat out uint f_mode; void main() { f_color = v_color; @@ -39,7 +45,7 @@ void main() { gl_Position = vec4(v_pos, 0.5, 1.0); vec2 look_at_dir = normalize(vec2(-view_mat[0][2], -view_mat[1][2])); // TODO: Consider cleaning up matrix to something more efficient (e.g. a mat3). - vec2 aspect_ratio = textureSize(sampler2D(t_tex, s_tex), 0).yx; + vec2 aspect_ratio = texture_size.yx; mat2 look_at = mat2(look_at_dir.y, look_at_dir.x, -look_at_dir.x, look_at_dir.y); vec2 v_centered = (v_uv - v_center) / aspect_ratio; vec2 v_rotated = look_at * v_centered; @@ -60,5 +66,6 @@ void main() { gl_Position = vec4(v_pos, 0.5, 1.0); } + f_scale = v_scale; f_mode = v_mode; } diff --git a/voxygen/src/render/mod.rs b/voxygen/src/render/mod.rs index 76494cb8b2..601200128f 100644 --- a/voxygen/src/render/mod.rs +++ b/voxygen/src/render/mod.rs @@ -536,4 +536,7 @@ pub enum ExperimentalShader { SmearReflections, /// Apply the point shadows from cheap shadows on top of shadow mapping. PointShadowsWithShadowMapping, + /// Make the UI uses nearest neighbor filtering for scaling images instead + /// of trying to filter based on the coverage of the sampled pixels. + UiNearestScaling, } diff --git a/voxygen/src/render/pipelines/ui.rs b/voxygen/src/render/pipelines/ui.rs index 8181af9e10..a741020103 100644 --- a/voxygen/src/render/pipelines/ui.rs +++ b/voxygen/src/render/pipelines/ui.rs @@ -10,12 +10,17 @@ pub struct Vertex { uv: [f32; 2], color: [f32; 4], center: [f32; 2], + // Used calculating where to sample scaled images. + scale: [f32; 2], mode: u32, } impl Vertex { fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { - const ATTRIBUTES: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4, 3 => Float32x2, 4 => Uint32]; + const ATTRIBUTES: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![ + 0 => Float32x2, 1 => Float32x2, 2 => Float32x4, + 3 => Float32x2, 4 => Float32x2, 5 => Uint32, + ]; wgpu::VertexBufferLayout { array_stride: Self::STRIDE, step_mode: wgpu::InputStepMode::Vertex, @@ -47,6 +52,20 @@ impl Default for Locals { fn default() -> Self { Self { pos: [0.0; 4] } } } +#[repr(C)] +#[derive(Copy, Clone, Debug, Zeroable, Pod)] +pub struct TexLocals { + texture_size: [u32; 2], +} + +impl From> for TexLocals { + fn from(texture_size: Vec2) -> Self { + Self { + texture_size: texture_size.into_array(), + } + } +} + /// Draw text from the text cache texture `tex` in the fragment shader. pub const MODE_TEXT: u32 = 0; /// Draw an image from the texture at `tex` in the fragment shader. @@ -64,22 +83,44 @@ pub const MODE_IMAGE_SOURCE_NORTH: u32 = 3; /// FIXME: Make more principled. pub const MODE_IMAGE_TARGET_NORTH: u32 = 5; +#[derive(Clone, Copy)] pub enum Mode { Text, - Image, + Image { + scale: Vec2, + }, Geometry, - ImageSourceNorth, - ImageTargetNorth, + /// Draw an image from the texture at `tex` in the fragment shader, with the + /// source rectangle rotated to face north (TODO: detail on what "north" + /// means here). + ImageSourceNorth { + scale: Vec2, + }, + /// Draw an image from the texture at `tex` in the fragment shader, with the + /// target rectangle rotated to face north. (TODO: detail on what "target" + /// means) + ImageTargetNorth { + scale: Vec2, + }, } impl Mode { fn value(self) -> u32 { match self { Mode::Text => MODE_TEXT, - Mode::Image => MODE_IMAGE, + Mode::Image { .. } => MODE_IMAGE, Mode::Geometry => MODE_GEOMETRY, - Mode::ImageSourceNorth => MODE_IMAGE_SOURCE_NORTH, - Mode::ImageTargetNorth => MODE_IMAGE_TARGET_NORTH, + Mode::ImageSourceNorth { .. } => MODE_IMAGE_SOURCE_NORTH, + Mode::ImageTargetNorth { .. } => MODE_IMAGE_TARGET_NORTH, + } + } + + /// Gets the scaling of the displayed image compared to the source. + fn scale(self) -> Vec2 { + match self { + Mode::ImageSourceNorth { scale } | Mode::ImageTargetNorth { scale } => scale, + Mode::Image { scale } => scale, + Mode::Text | Mode::Geometry => Vec2::one(), } } } @@ -137,6 +178,17 @@ impl UiLayout { }, count: None, }, + // tex_locals + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ], }), } @@ -158,7 +210,12 @@ impl UiLayout { } } - pub fn bind_texture(&self, device: &wgpu::Device, texture: &Texture) -> TextureBindGroup { + pub fn bind_texture( + &self, + device: &wgpu::Device, + texture: &Texture, + tex_locals: Consts, + ) -> TextureBindGroup { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &self.texture, @@ -171,6 +228,10 @@ impl UiLayout { binding: 1, resource: wgpu::BindingResource::Sampler(&texture.sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: tex_locals.buf().as_entire_binding(), + }, ], }); @@ -268,17 +329,19 @@ pub fn create_quad_vert_gradient( let top_color = top_color.into_array(); let bottom_color = bottom_color.into_array(); - let center = if let Mode::ImageSourceNorth = mode { + let center = if let Mode::ImageSourceNorth { .. } = mode { uv_rect.center().into_array() } else { rect.center().into_array() }; + let scale = mode.scale().into_array(); let mode_val = mode.value(); let v = |pos, uv, color| Vertex { pos, uv, center, color, + scale, mode: mode_val, }; let aabr_to_lbrt = |aabr: Aabr| (aabr.min.x, aabr.min.y, aabr.max.x, aabr.max.y); @@ -315,12 +378,14 @@ pub fn create_tri( mode: Mode, ) -> Tri { let center = [0.0, 0.0]; + let scale = mode.scale().into_array(); let mode_val = mode.value(); let v = |pos, uv| Vertex { pos, uv, center, color: color.into_array(), + scale, mode: mode_val, }; Tri::new( diff --git a/voxygen/src/render/renderer/binding.rs b/voxygen/src/render/renderer/binding.rs index 6deffb0f28..a97c0be179 100644 --- a/voxygen/src/render/renderer/binding.rs +++ b/voxygen/src/render/renderer/binding.rs @@ -47,8 +47,12 @@ impl Renderer { self.layouts.ui.bind_locals(&self.device, locals) } - pub fn ui_bind_texture(&self, texture: &Texture) -> ui::TextureBindGroup { - self.layouts.ui.bind_texture(&self.device, texture) + pub fn ui_bind_texture(&mut self, texture: &Texture) -> ui::TextureBindGroup { + let tex_locals = ui::TexLocals::from(texture.get_dimensions().xy()); + let tex_locals_consts = self.create_consts(&[tex_locals]); + self.layouts + .ui + .bind_texture(&self.device, texture, tex_locals_consts) } pub fn create_figure_bound_locals( diff --git a/voxygen/src/render/texture.rs b/voxygen/src/render/texture.rs index 39db02e2ba..fe07cb1921 100644 --- a/voxygen/src/render/texture.rs +++ b/voxygen/src/render/texture.rs @@ -136,8 +136,8 @@ impl Texture { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }; diff --git a/voxygen/src/ui/graphic/mod.rs b/voxygen/src/ui/graphic/mod.rs index 5bf35e870b..859c81b084 100644 --- a/voxygen/src/ui/graphic/mod.rs +++ b/voxygen/src/ui/graphic/mod.rs @@ -61,7 +61,6 @@ pub struct Id(u32); #[derive(PartialEq, Eq, Hash, Copy, Clone)] pub struct TexId(usize); -type Parameters = (Id, Vec2); // TODO replace with slab/slotmap type GraphicMap = HashMap; @@ -93,7 +92,7 @@ impl CachedDetails { fn info( &self, atlases: &[(SimpleAtlasAllocator, usize)], - dims: Vec2, + textures: &Slab<(Texture, UiTextureBindGroup)>, ) -> (usize, bool, Aabr) { match *self { CachedDetails::Atlas { @@ -105,14 +104,16 @@ impl CachedDetails { (index, valid, Aabr { min: Vec2::zero(), // Note texture should always match the cached dimensions - max: dims, + // TODO: Justify cast here? + max: textures[index].0.get_dimensions().xy().map(|e| e as u16), }) }, CachedDetails::Immutable { index } => { (index, true, Aabr { min: Vec2::zero(), // Note texture should always match the cached dimensions - max: dims, + // TODO: Justify cast here? + max: textures[index].0.get_dimensions().xy().map(|e| e as u16), }) }, } @@ -147,7 +148,7 @@ pub struct GraphicCache { atlases: Vec<(SimpleAtlasAllocator, usize)>, textures: Slab<(Texture, UiTextureBindGroup)>, // Stores the location of graphics rendered at a particular resolution and cached on the cpu - cache_map: HashMap, + cache_map: HashMap, keyed_jobs: KeyedJobs<(Id, Vec2), Option<(RgbaImage, Option>)>>, } @@ -237,17 +238,23 @@ impl GraphicCache { renderer: &mut Renderer, pool: Option<&SlowJobPool>, graphic_id: Id, + // TODO: if we aren't resizing here we can upload image earlier... (as long as this doesn't + // lead to uploading too much unused stuff). (cache_res name invalid) dims: Vec2, source: Aabr, rotation: Rotation, - ) -> Option<(Aabr, TexId)> { + ) -> Option<((Aabr, Vec2), TexId)> { let dims = match rotation { + // The image is placed into the atlas with no rotation, so we need to swap the + // dimensions here to get the resolution that the image will be displayed at but + // re-oriented into the "upright" space that the image is stored in and sampled from + // (this can be bit confusing initially / hard to explain). Rotation::Cw90 | Rotation::Cw270 => Vec2::new(dims.y, dims.x), Rotation::None | Rotation::Cw180 => dims, Rotation::SourceNorth => dims, Rotation::TargetNorth => dims, }; - let key = (graphic_id, dims); + let key = graphic_id; // Rotate aabr according to requested rotation. let rotated_aabr = |Aabr { min, max }| match rotation { @@ -264,7 +271,7 @@ impl GraphicCache { }; // Scale aabr according to provided source rectangle. let scaled_aabr = |aabr: Aabr<_>| { - let size: Vec2<_> = aabr.size().into(); + let size: Vec2 = aabr.size().into(); Aabr { min: size.mul_add(source.min, aabr.min), max: size.mul_add(source.max, aabr.min), @@ -272,7 +279,19 @@ impl GraphicCache { }; // Apply all transformations. // TODO: Verify rotation is being applied correctly. - let transformed_aabr = |aabr| rotated_aabr(scaled_aabr(aabr)); + let transformed_aabr = |aabr| { + let scaled = scaled_aabr(aabr); + // Calculate how many displayed pixels there are for each pixel in the source + // image. We need this to calculate where to sample in the shader to + // retain crisp pixel borders when scaling the image. + // TODO: A bit hacky inserting this here, just to get things working initially + let scale = dims.map2( + Vec2::from(scaled.size()), + |screen_pixels, sample_pixels: f64| screen_pixels as f32 / sample_pixels as f32, + ); + let transformed = rotated_aabr(scaled); + (transformed, scale) + }; let Self { textures, @@ -285,7 +304,7 @@ impl GraphicCache { let details = match cache_map.entry(key) { Entry::Occupied(details) => { let details = details.get(); - let (idx, valid, aabr) = details.info(atlases, dims); + let (idx, valid, aabr) = details.info(atlases, textures); // Check if the cached version has been invalidated by replacing the underlying // graphic @@ -312,28 +331,33 @@ impl GraphicCache { let (image, border_color) = draw_graphic(graphic_map, graphic_id, dims, &mut self.keyed_jobs, pool)?; + // TODO: justify cast? + let image_dims = Vec2::::from(image.dimensions()).map(|e| e as u16); + // Upload let atlas_size = atlas_size(renderer); - // Allocate space on the gpu - // Check size of graphic - // Graphics over a particular size are sent to their own textures + // Allocate space on the gpu. + // + // Graphics with a border color. let location = if let Some(border_color) = border_color { // Create a new immutable texture. let texture = create_image(renderer, image, border_color); // NOTE: All mutations happen only after the upload succeeds! let index = textures.insert(texture); CachedDetails::Immutable { index } + // Graphics over a particular size compared to the atlas size are sent + // to their own textures. Here we check for ones under that + // size. } else if atlas_size - .map2(dims, |a, d| a as f32 * ATLAS_CUTOFF_FRAC >= d as f32) + .map2(image_dims, |a, d| a as f32 * ATLAS_CUTOFF_FRAC >= d as f32) .reduce_and() { // Fit into an atlas let mut loc = None; for (atlas_idx, &mut (ref mut atlas, texture_idx)) in atlases.iter_mut().enumerate() { - let dims = dims.map(|e| e.max(1)); - if let Some(rectangle) = atlas.allocate(size2(i32::from(dims.x), i32::from(dims.y))) - { + let clamped_dims = image_dims.map(|e| i32::from(e.max(1))); + if let Some(rectangle) = atlas.allocate(size2(clamped_dims.x, clamped_dims.y)) { let aabr = aabr_from_alloc_rect(rectangle); loc = Some(CachedDetails::Atlas { atlas_idx, @@ -350,9 +374,9 @@ impl GraphicCache { // Create a new atlas None => { let (mut atlas, texture) = create_atlas_texture(renderer); - let dims = dims.map(|e| e.max(1)); + let clamped_dims = image_dims.map(|e| i32::from(e.max(1))); let aabr = atlas - .allocate(size2(i32::from(dims.x), i32::from(dims.y))) + .allocate(size2(clamped_dims.x, clamped_dims.y)) .map(aabr_from_alloc_rect) .unwrap(); // NOTE: All mutations happen only after the texture creation succeeds! @@ -370,7 +394,7 @@ impl GraphicCache { } else { // Create a texture just for this let texture = { - let tex = renderer.create_dynamic_texture(dims.map(|e| e as u32)); + let tex = renderer.create_dynamic_texture(image_dims.map(u32::from)); let bind = renderer.ui_bind_texture(&tex); (tex, bind) }; @@ -381,7 +405,7 @@ impl GraphicCache { Aabr { min: Vec2::zero(), // Note texture should always match the cached dimensions - max: dims, + max: image_dims, }, &textures[index].0, &image, @@ -390,7 +414,7 @@ impl GraphicCache { }; // Extract information from cache entry. - let (idx, _, aabr) = location.info(atlases, dims); + let (idx, _, aabr) = location.info(atlases, textures); // Insert into cached map details.insert(location); @@ -419,14 +443,17 @@ fn draw_graphic( // Render image at requested resolution // TODO: Use source aabr. Graphic::Image(ref image, border_color) => Some(( - resize_pixel_art( + /*resize_pixel_art( &image.to_rgba8(), u32::from(dims.x), u32::from(dims.y), - ), + ),*/ + image.to_rgba8(), border_color, )), Graphic::Voxel(ref segment, trans, sample_strat) => { + // TODO: how to decide on dimensions to render voxel models to? + // (with resizing being done in shaders) Some((renderer::draw_vox(segment, dims, trans, sample_strat), None)) }, Graphic::Blank => None, @@ -498,12 +525,12 @@ fn create_image( let tex = renderer .create_texture( &DynamicImage::ImageRgba8(image), - None, - //TODO: either use the desktop only border color or just emulate this + Some(wgpu::FilterMode::Linear), + // TODO: either use the desktop only border color or just emulate this // Some(border_color.into_array().into()), Some(wgpu::AddressMode::ClampToBorder), ) - .expect("create_texture only panics is non ImageRbga8 is passed"); + .expect("create_texture only panics if non ImageRbga8 is passed"); let bind = renderer.ui_bind_texture(&tex); (tex, bind) diff --git a/voxygen/src/ui/ice/renderer/mod.rs b/voxygen/src/ui/ice/renderer/mod.rs index b3b415741e..eb7ea61237 100644 --- a/voxygen/src/ui/ice/renderer/mod.rs +++ b/voxygen/src/ui/ice/renderer/mod.rs @@ -533,7 +533,7 @@ impl IcedRenderer { } // Cache graphic at particular resolution. - let (uv_aabr, tex_id) = match graphic_cache.cache_res( + let (uv_aabr, scale, tex_id) = match graphic_cache.cache_res( renderer, pool, graphic_id, @@ -543,7 +543,7 @@ impl IcedRenderer { rotation, ) { // TODO: get dims from graphic_cache (or have it return floats directly) - Some((aabr, tex_id)) => { + Some(((aabr, scale), tex_id)) => { let cache_dims = graphic_cache .get_tex(tex_id) .0 @@ -552,7 +552,7 @@ impl IcedRenderer { .map(|e| e as f32); let min = Vec2::new(aabr.min.x as f32, aabr.max.y as f32) / cache_dims; let max = Vec2::new(aabr.max.x as f32, aabr.min.y as f32) / cache_dims; - (Aabr { min, max }, tex_id) + (Aabr { min, max }, scale, tex_id) }, None => return, }; @@ -562,7 +562,9 @@ impl IcedRenderer { self.switch_state(State::Image(tex_id)); self.mesh - .push_quad(create_ui_quad(gl_aabr, uv_aabr, color, UiMode::Image)); + .push_quad(create_ui_quad(gl_aabr, uv_aabr, color, UiMode::Image { + scale, + })); }, Primitive::Gradient { bounds, diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index 535ec2e3ad..c48726c025 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -854,7 +854,7 @@ impl Ui { srgba_to_linear(color.unwrap_or(conrod_core::color::WHITE).to_fsa().into()); // Cache graphic at particular resolution. - let (uv_aabr, tex_id) = match graphic_cache.cache_res( + let (uv_aabr, scale, tex_id) = match graphic_cache.cache_res( renderer, pool, *graphic_id, @@ -863,7 +863,7 @@ impl Ui { *rotation, ) { // TODO: get dims from graphic_cache (or have it return floats directly) - Some((aabr, tex_id)) => { + Some(((aabr, scale), tex_id)) => { let cache_dims = graphic_cache .get_tex(tex_id) .0 @@ -872,7 +872,7 @@ impl Ui { .map(|e| e as f32); let min = Vec2::new(aabr.min.x as f32, aabr.max.y as f32) / cache_dims; let max = Vec2::new(aabr.max.x as f32, aabr.min.y as f32) / cache_dims; - (Aabr { min, max }, tex_id) + (Aabr { min, max }, scale, tex_id) }, None => continue, }; @@ -897,10 +897,10 @@ impl Ui { mesh.push_quad(create_ui_quad(gl_aabr, uv_aabr, color, match *rotation { Rotation::None | Rotation::Cw90 | Rotation::Cw180 | Rotation::Cw270 => { - UiMode::Image + UiMode::Image { scale } }, - Rotation::SourceNorth => UiMode::ImageSourceNorth, - Rotation::TargetNorth => UiMode::ImageTargetNorth, + Rotation::SourceNorth => UiMode::ImageSourceNorth { scale }, + Rotation::TargetNorth => UiMode::ImageTargetNorth { scale }, })); }, PrimitiveKind::Text { .. } => {