#version 430 core #include #include layout(location = 0) in vec2 f_uv; layout(location = 1) in vec4 f_color; 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; }; 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 just sampling 1 pixel length from the center // on each side of the nearest pixel edge. An alternative might be to // pre-compute mipmap levels that could be sampled from, although this // could interact poorly with the atlas. 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 = round(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; 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 perfectly 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)) { // 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)) { // 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. // Convert to sampled pixel coordinates. vec2 uv_pixel = f_uv * texture_size; vec4 image_color; #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 // un-premultiply alpha (linear filtering above requires alpha to be // pre-multiplied) if (image_color.a > 0.001) { image_color.rgb /= image_color.a; } tgt_color = f_color * image_color; // 2D Geometry } else if (f_mode == uint(2)) { tgt_color = f_color; } }