output_width(1280),
output_height(720),
top(0),
- left(0)
+ left(0),
+ border_offset_top(0.0f),
+ border_offset_left(0.0f),
+ border_offset_bottom(0.0f),
+ border_offset_right(0.0f)
{
register_vec4("border_color", (float *)&border_color);
register_int("width", &output_width);
register_int("height", &output_height);
register_float("top", &top);
register_float("left", &left);
+ register_float("border_offset_top", &border_offset_top);
+ register_float("border_offset_left", &border_offset_left);
+ register_float("border_offset_bottom", &border_offset_bottom);
+ register_float("border_offset_right", &border_offset_right);
}
string PaddingEffect::output_fragment_shader()
};
set_uniform_vec2(glsl_program_num, prefix, "scale", scale);
- // Due to roundoff errors, the test against 0.5 is seldom exact,
- // even though we test for less than and not less-than-or-equal.
- // We'd rather keep an extra border pixel in those very rare cases
- // (where the image is shifted pretty much exactly a half-pixel)
- // than losing a pixel in the common cases of integer shift.
- // Thus the 1e-3 fudge factors.
- float texcoord_min[2] = {
- float((0.5f - 1e-3) / input_width),
- float((0.5f - 1e-3) / input_height)
+ float normalized_coords_to_texels[2] = {
+ float(input_width), float(input_height)
};
- set_uniform_vec2(glsl_program_num, prefix, "texcoord_min", texcoord_min);
+ set_uniform_vec2(glsl_program_num, prefix, "normalized_coords_to_texels", normalized_coords_to_texels);
- float texcoord_max[2] = {
- float(1.0f - (0.5f - 1e-3) / input_width),
- float(1.0f - (0.5f - 1e-3) / input_height)
+ // Texels -0.5..0.5 should map to light level 0..1 (and then we
+ // clamp the rest).
+ float offset_bottomleft[2] = {
+ 0.5f - border_offset_left, 0.5f + border_offset_bottom,
};
- set_uniform_vec2(glsl_program_num, prefix, "texcoord_max", texcoord_max);
+
+ // Texels size-0.5..size+0.5 should map to light level 1..0 (and then clamp).
+ float offset_topright[2] = {
+ input_width + 0.5f + border_offset_right, input_height + 0.5f - border_offset_top,
+ };
+
+ set_uniform_vec2(glsl_program_num, prefix, "offset_bottomleft", offset_bottomleft);
+ set_uniform_vec2(glsl_program_num, prefix, "offset_topright", offset_topright);
}
// We don't change the pixels of the image itself, so the only thing that
uniform vec2 PREFIX(offset);
uniform vec2 PREFIX(scale);
-uniform vec2 PREFIX(texcoord_min);
-uniform vec2 PREFIX(texcoord_max);
+
+uniform vec2 PREFIX(normalized_coords_to_texels);
+uniform vec2 PREFIX(offset_bottomleft);
+uniform vec2 PREFIX(offset_topright);
vec4 FUNCNAME(vec2 tc) {
tc -= PREFIX(offset);
tc *= PREFIX(scale);
- if (any(lessThan(tc, PREFIX(texcoord_min))) ||
- any(greaterThan(tc, PREFIX(texcoord_max)))) {
+ vec2 tc_texels = tc * PREFIX(normalized_coords_to_texels);
+ vec2 coverage_bottomleft = clamp(tc_texels + PREFIX(offset_bottomleft), 0.0f, 1.0f);
+ vec2 coverare_topright = clamp(PREFIX(offset_topright) - tc_texels, 0.0f, 1.0f);
+ vec2 coverage_both = coverage_bottomleft * coverare_topright;
+ float coverage = coverage_both.x * coverage_both.y;
+
+ if (coverage <= 0.0f) {
+ // Short-circuit in case the underlying function is expensive to call.
return PREFIX(border_color);
+ } else {
+ return mix(PREFIX(border_color), INPUT(tc), coverage);
}
-
- return INPUT(tc);
}
// (although the latter is implemented slightly less efficiently, and you cannot both
// pad and crop in the same effect).
//
-// The source image is cut off at the texel borders (so there is no interpolation
-// outside them), and then given a user-specific color; by default, full transparent.
+// The source image is cut off at the texture border, and then given a user-specific color;
+// by default, full transparent. You can give a fractional border size (non-integral
+// "top" or "left" offset) if you wish, which will give you linear interpolation of
+// both pixel data of and the border. Furthermore, you can offset where the border falls
+// by using the "border_offset_{top,bottom,left,right}" settings; this is particularly
+// useful if you use ResampleEffect earlier in the chain for high-quality fractional-pixel
+// translation and just want PaddingEffect to get the border right.
//
// The border color is taken to be in linear gamma, sRGB, with premultiplied alpha.
// You may not change it after calling finalize(), since that could change the
// IntegralPaddingEffect is like PaddingEffect, except that "top" and "left" parameters
// are int parameters instead of float. This allows it to guarantee one-to-one sampling,
// which can speed up processing by allowing more effect passes to be collapsed.
+// border_offset_* are still allowed to be float, although you should beware that if
+// you set e.g. border_offset_top to a negative value, you will be sampling outside
+// the edge and will read data that is undefined in one-to-one-mode (could be
+// edge repeat, could be something else). With regular PaddingEffect, such samples
+// are guaranteed to be edge repeat.
#include <epoxy/gl.h>
#include <string>
int input_width, input_height;
int output_width, output_height;
float top, left;
+ float border_offset_top, border_offset_left;
+ float border_offset_bottom, border_offset_right;
};
class IntegralPaddingEffect : public PaddingEffect {
float data[4 * 1] = {
0.25f, 0.50f, 0.75f, 1.0f,
};
- // Note that the first pixel is completely blank, since the cutoff goes
- // at the immediate left of the texel.
float expected_data[5 * 2] = {
- 0.0f, 0.4375f, 0.6875f, 0.9375f, 0.0f,
+ 0.1875f, 0.4375f, 0.6875f, 0.9375f, 0.25f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
};
float out_data[5 * 2];
expect_equal(expected_data, out_data, 4, 4);
}
+TEST(PaddingEffectTest, BorderOffsetTopAndBottom) {
+ float data[2 * 2] = {
+ 1.0f, 0.5f,
+ 0.8f, 0.3f,
+ };
+ float expected_data[4 * 4] = {
+ 0.0f, 0.000f, 0.000f, 0.0f,
+ 0.0f, 0.750f, 0.375f, 0.0f,
+ 0.0f, 0.800f, 0.300f, 0.0f,
+ 0.0f, 0.200f, 0.075f, 0.0f, // Repeated pixels, 25% opacity.
+ };
+ float out_data[4 * 4];
+
+ EffectChainTester tester(NULL, 4, 4);
+
+ ImageFormat format;
+ format.color_space = COLORSPACE_sRGB;
+ format.gamma_curve = GAMMA_LINEAR;
+
+ FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
+ input->set_pixel_data(data);
+ tester.get_chain()->add_input(input);
+
+ Effect *effect = tester.get_chain()->add_effect(new PaddingEffect());
+ CHECK(effect->set_int("width", 4));
+ CHECK(effect->set_int("height", 4));
+ CHECK(effect->set_float("left", 1.0f));
+ CHECK(effect->set_float("top", 1.0f));
+ CHECK(effect->set_float("border_offset_top", 0.25f));
+ CHECK(effect->set_float("border_offset_bottom", 0.25f));
+
+ tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
+ expect_equal(expected_data, out_data, 4, 4);
+}
+
+TEST(PaddingEffectTest, BorderOffsetLeftAndRight) {
+ float data[3 * 2] = {
+ 1.0f, 0.5f, 0.6f,
+ 0.8f, 0.3f, 0.2f,
+ };
+ float expected_data[4 * 2] = {
+ 0.750f, 0.5f, 0.3f, 0.0f,
+ 0.600f, 0.3f, 0.1f, 0.0f
+ };
+ float out_data[4 * 2];
+
+ EffectChainTester tester(NULL, 4, 2);
+
+ ImageFormat format;
+ format.color_space = COLORSPACE_sRGB;
+ format.gamma_curve = GAMMA_LINEAR;
+
+ FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 3, 2);
+ input->set_pixel_data(data);
+ tester.get_chain()->add_input(input);
+
+ Effect *effect = tester.get_chain()->add_effect(new PaddingEffect());
+ CHECK(effect->set_int("width", 4));
+ CHECK(effect->set_int("height", 2));
+ CHECK(effect->set_float("left", 0.0f));
+ CHECK(effect->set_float("top", 0.0f));
+ CHECK(effect->set_float("border_offset_left", 0.25f));
+ CHECK(effect->set_float("border_offset_right", -0.5f));
+
+ tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
+ expect_equal(expected_data, out_data, 4, 2);
+}
+
} // namespace movit