veloren/assets/voxygen/shaders/ui-frag.glsl
Imbris eb6d16b02c Rename draw_graphic -> prepare_graphic since it is no longer always
specifically rendering an image at the provided dimensions. Start
working on alpha premultipication which is necessary for being able to
properly sample colors on the GPU with bilinear filtering. Various
tweaks to comments and misc changes.
2023-04-08 00:28:31 -04:00

221 lines
9.4 KiB
GLSL

#version 420 core
#include <globals.glsl>
#include <constants.glsl>
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;
};
// 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 just sampling a 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.
// 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;
#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;
}
}