From 82071a94aaff95d2d29d077338085a8fb27e76d1 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Sun, 6 Sep 2015 00:57:25 +0200 Subject: [PATCH] Make the PaddingEffect border 1-pixel soft. 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 | 39 +++++++++++++--------- padding_effect.frag | 20 ++++++++---- padding_effect.h | 16 +++++++-- padding_effect_test.cpp | 72 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 121 insertions(+), 26 deletions(-) diff --git a/padding_effect.cpp b/padding_effect.cpp index d8ed952..5ca976c 100644 --- a/padding_effect.cpp +++ b/padding_effect.cpp @@ -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 diff --git a/padding_effect.frag b/padding_effect.frag index 4f1adc1..32ae840 100644 --- a/padding_effect.frag +++ b/padding_effect.frag @@ -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); } diff --git a/padding_effect.h b/padding_effect.h index acd555f..16ed179 100644 --- a/padding_effect.h +++ b/padding_effect.h @@ -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 @@ -15,6 +20,11 @@ // 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 #include @@ -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 { diff --git a/padding_effect_test.cpp b/padding_effect_test.cpp index 88ba811..a1a36b9 100644 --- a/padding_effect_test.cpp +++ b/padding_effect_test.cpp @@ -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 -- 2.39.2