Make the PaddingEffect border 1-pixel soft.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sat, 5 Sep 2015 22:57:25 +0000 (00:57 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Sat, 5 Sep 2015 22:57:25 +0000 (00:57 +0200)
Note that this is an API break; PaddingEffect now does something else
from what it used to do before when it comes to fractional offsets.
But I feel this is more useful; it allows PaddingEffect to be used
more efficiently for moving things smoothly around.

Also add a concept of border offset which moves the border around
without changing the pixels; useful if you want the subpixel placement
to be done by ResampleEffect (put the integral offset into top/left
and then move the border by the fractional amount it missed).

padding_effect.cpp
padding_effect.frag
padding_effect.h
padding_effect_test.cpp

index d8ed952..5ca976c 100644 (file)
@@ -14,13 +14,21 @@ PaddingEffect::PaddingEffect()
          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()
@@ -44,23 +52,24 @@ void PaddingEffect::set_gl_state(GLuint glsl_program_num, const string &prefix,
        };
        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 
index 4f1adc1..32ae840 100644 (file)
@@ -1,16 +1,24 @@
 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);
 }
index acd555f..16ed179 100644 (file)
@@ -5,8 +5,13 @@
 // (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>
@@ -44,6 +54,8 @@ private:
        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 {
index 88ba811..a1a36b9 100644 (file)
@@ -150,10 +150,8 @@ TEST(PaddingEffectTest, NonIntegerOffset) {
        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];
@@ -244,4 +242,72 @@ TEST(PaddingEffectTest, AlphaIsCorrectEvenWithNonLinearInputsAndOutputs) {
        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