├── add.frag ├── blue.frag ├── multiply_effect.frag ├── autogen.sh ├── highlight_cutoff_effect.frag ├── alpha_multiplication_effect.frag ├── mipmap_needing_effect.frag ├── header.130.frag ├── header.150.frag ├── identity.frag ├── mirror_effect.frag ├── color.130.frag ├── color.150.frag ├── sandbox_effect.frag ├── header.300es.frag ├── invert_effect.frag ├── texture1d.130.frag ├── texture1d.150.frag ├── white_balance_effect.frag ├── texture1d.300es.frag ├── identity.comp ├── overlay_matte_effect.frag ├── saturation_effect.frag ├── downscale2x.frag ├── downscale2x.comp ├── alpha_division_effect.cpp ├── colorspace_conversion_effect.frag ├── alpha_division_effect.frag ├── mirror_effect.cpp ├── alpha_multiplication_effect.cpp ├── complex_modulate_effect.frag ├── defs.h ├── multiply_effect.cpp ├── saturation_effect.cpp ├── version.h ├── mix_effect.cpp ├── vs-color.130.vert ├── vs-color.150.vert ├── movit.pc.in ├── widgets.h ├── overlay_effect.cpp ├── flat_input.frag ├── resize_effect.cpp ├── vs.vert ├── sandbox_effect.cpp ├── alpha_division_effect.h ├── vs.130.vert ├── vs.150.vert ├── mirror.comp ├── header.comp ├── vs.300es.vert ├── alpha_multiplication_effect.h ├── vignette_effect.frag ├── lift_gamma_gain_effect.frag ├── blur_effect.frag ├── mirror_effect.h ├── d65.h ├── multiply_effect.h ├── gamma_expansion_effect.frag ├── alpha_multiplication_effect_test.cpp ├── padding_effect.frag ├── ycbcr_422interleaved_input.frag ├── luma_mix_effect.cpp ├── sandbox_effect.h ├── saturation_effect.h ├── overlay_effect.frag ├── fft_pass_effect.frag ├── ycbcr_conversion_effect.frag ├── mix_effect.h ├── gamma_compression_effect.frag ├── slice_effect.frag ├── white_balance_effect.h ├── footer.comp ├── vignette_effect.h ├── alpha_division_effect_test.cpp ├── lift_gamma_gain_effect.cpp ├── unsharp_mask_effect.cpp ├── resize_effect.h ├── .gitignore ├── configure.ac ├── input.h ├── ycbcr_input.frag ├── colorspace_conversion_effect.h ├── unsharp_mask_effect.h ├── luma_mix_effect.h ├── diffusion_effect.cpp ├── vignette_effect.cpp ├── gamma_compression_effect.h ├── overlay_effect.h ├── deconvolution_sharpen_effect.frag ├── gamma_expansion_effect.h ├── ycbcr_conversion_effect.h ├── dither_effect.frag ├── glow_effect.cpp ├── image_format.h ├── effect_util.h ├── mix_effect.frag ├── gtest_sdl_main.cpp ├── glow_effect.h ├── luma_mix_effect.frag ├── slice_effect.h ├── lift_gamma_gain_effect.h ├── complex_modulate_effect.cpp ├── dither_effect_test.cpp ├── diffusion_effect.h ├── footer.frag ├── saturation_effect_test.cpp ├── diffusion_effect_test.cpp ├── resample_effect.frag ├── complex_modulate_effect.h ├── ycbcr_conversion_effect.cpp ├── fft_input.h ├── fp16_test.cpp ├── deconvolution_sharpen_effect.h ├── unsharp_mask_effect_test.cpp ├── slice_effect.cpp ├── vignette_effect_test.cpp ├── effect_util.cpp ├── complex_modulate_effect_test.cpp ├── padding_effect.h ├── init.h ├── overlay_effect_test.cpp ├── blur_effect.h ├── white_balance_effect_test.cpp ├── dither_effect.h ├── ycbcr.h ├── glow_effect_test.cpp ├── fft_input.cpp ├── fp16.h ├── dither_effect.cpp ├── blur_effect_test.cpp ├── luma_mix_effect_test.cpp ├── mix_effect_test.cpp ├── lift_gamma_gain_effect_test.cpp ├── ycbcr_422interleaved_input.cpp └── ycbcr.cpp /add.frag: -------------------------------------------------------------------------------- 1 | vec4 FUNCNAME(vec2 tc) { 2 | return INPUT1(tc) + INPUT2(tc); 3 | } 4 | -------------------------------------------------------------------------------- /blue.frag: -------------------------------------------------------------------------------- 1 | vec4 FUNCNAME(vec2 tc) { 2 | return vec4(0.0, 0.0, 1.0, 1.0); 3 | } 4 | -------------------------------------------------------------------------------- /multiply_effect.frag: -------------------------------------------------------------------------------- 1 | vec4 FUNCNAME(vec2 tc) { 2 | return INPUT(tc) * PREFIX(factor); 3 | } 4 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | aclocal 3 | libtoolize --install --copy 4 | autoconf 5 | ./configure "$@" 6 | -------------------------------------------------------------------------------- /highlight_cutoff_effect.frag: -------------------------------------------------------------------------------- 1 | vec4 FUNCNAME(vec2 tc) { 2 | return max(INPUT(tc) - vec4(PREFIX(cutoff)), 0.0); 3 | } 4 | -------------------------------------------------------------------------------- /alpha_multiplication_effect.frag: -------------------------------------------------------------------------------- 1 | vec4 FUNCNAME(vec2 tc) { 2 | vec4 x = INPUT(tc); 3 | x.rgb *= x.aaa; 4 | return x; 5 | } 6 | -------------------------------------------------------------------------------- /mipmap_needing_effect.frag: -------------------------------------------------------------------------------- 1 | // Used only for testing. 2 | vec4 FUNCNAME(vec2 tc) 3 | { 4 | return INPUT(tc * vec2(4.0)); 5 | } 6 | -------------------------------------------------------------------------------- /header.130.frag: -------------------------------------------------------------------------------- 1 | #version 130 2 | 3 | in vec2 tc; 4 | 5 | vec4 tex2D(sampler2D s, vec2 coord) 6 | { 7 | return texture(s, coord); 8 | } 9 | -------------------------------------------------------------------------------- /header.150.frag: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | in vec2 tc; 4 | 5 | vec4 tex2D(sampler2D s, vec2 coord) 6 | { 7 | return texture(s, coord); 8 | } 9 | -------------------------------------------------------------------------------- /identity.frag: -------------------------------------------------------------------------------- 1 | // Identity transformation (sometimes useful to do nothing). 2 | vec4 FUNCNAME(vec2 tc) 3 | { 4 | return INPUT(tc); 5 | } 6 | -------------------------------------------------------------------------------- /mirror_effect.frag: -------------------------------------------------------------------------------- 1 | // Mirrors the image horizontally. 2 | vec4 FUNCNAME(vec2 tc) 3 | { 4 | tc.x = 1.0 - tc.x; 5 | return INPUT(tc); 6 | } 7 | -------------------------------------------------------------------------------- /color.130.frag: -------------------------------------------------------------------------------- 1 | #version 130 2 | 3 | in vec2 tc; 4 | in vec4 frag_color; 5 | 6 | out vec4 FragColor; 7 | 8 | void main() 9 | { 10 | FragColor = frag_color; 11 | } 12 | -------------------------------------------------------------------------------- /color.150.frag: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | in vec2 tc; 4 | in vec4 frag_color; 5 | 6 | out vec4 FragColor; 7 | 8 | void main() 9 | { 10 | FragColor = frag_color; 11 | } 12 | -------------------------------------------------------------------------------- /sandbox_effect.frag: -------------------------------------------------------------------------------- 1 | vec4 FUNCNAME(vec2 tc) { 2 | // Your code goes here, obviously. 3 | // You can use PREFIX(parm) to access the parameter you gave in. 4 | return INPUT(tc); 5 | } 6 | -------------------------------------------------------------------------------- /header.300es.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | in vec2 tc; 6 | 7 | vec4 tex2D(sampler2D s, vec2 coord) 8 | { 9 | return texture(s, coord); 10 | } 11 | -------------------------------------------------------------------------------- /invert_effect.frag: -------------------------------------------------------------------------------- 1 | // Used only during testing. Inverts its input. 2 | vec4 FUNCNAME(vec2 tc) 3 | { 4 | vec4 rgba = INPUT(tc); 5 | rgba.rgb = vec3(1.0) - rgba.rgb; 6 | return rgba; 7 | } 8 | -------------------------------------------------------------------------------- /texture1d.130.frag: -------------------------------------------------------------------------------- 1 | #version 130 2 | 3 | uniform sampler2D tex; 4 | in vec2 tc; 5 | 6 | out vec4 FragColor; 7 | 8 | void main() 9 | { 10 | FragColor = texture(tex, tc); // Second component is irrelevant. 11 | } 12 | -------------------------------------------------------------------------------- /texture1d.150.frag: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform sampler2D tex; 4 | in vec2 tc; 5 | 6 | out vec4 FragColor; 7 | 8 | void main() 9 | { 10 | FragColor = texture(tex, tc); // Second component is irrelevant. 11 | } 12 | -------------------------------------------------------------------------------- /white_balance_effect.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform mat3 PREFIX(correction_matrix); 3 | 4 | vec4 FUNCNAME(vec2 tc) { 5 | vec4 ret = INPUT(tc); 6 | ret.rgb = PREFIX(correction_matrix) * ret.rgb; 7 | return ret; 8 | } 9 | -------------------------------------------------------------------------------- /texture1d.300es.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | uniform sampler2D tex; 6 | in vec2 tc; 7 | 8 | out vec4 FragColor; 9 | 10 | void main() 11 | { 12 | FragColor = texture(tex, tc); // Second component is irrelevant. 13 | } 14 | -------------------------------------------------------------------------------- /identity.comp: -------------------------------------------------------------------------------- 1 | // Identity compute shader (sometimes useful to do nothing). 2 | 3 | layout(local_size_x = 1) in; 4 | 5 | void FUNCNAME() 6 | { 7 | vec4 val = INPUT(NORMALIZE_TEXTURE_COORDS(gl_GlobalInvocationID.xy)); 8 | OUTPUT(gl_GlobalInvocationID.xy, val); 9 | } 10 | -------------------------------------------------------------------------------- /overlay_matte_effect.frag: -------------------------------------------------------------------------------- 1 | vec4 FUNCNAME(vec2 tc) { 2 | vec4 orig = INPUT1(tc); 3 | vec4 blurred = INPUT2(tc); 4 | float luminance = clamp(dot(orig.rgb, vec3(0.2126, 0.7152, 0.0722)), 0.0, 1.0); 5 | return mix(orig, blurred, luminance * vec4(PREFIX(blurred_mix_amount))); 6 | } 7 | -------------------------------------------------------------------------------- /saturation_effect.frag: -------------------------------------------------------------------------------- 1 | // Saturate/desaturate (in linear space). 2 | 3 | vec4 FUNCNAME(vec2 tc) { 4 | vec4 x = INPUT(tc); 5 | 6 | float luminance = dot(x.rgb, vec3(0.2126, 0.7152, 0.0722)); 7 | x.rgb = mix(vec3(luminance), x.rgb, PREFIX(saturation)); 8 | 9 | return x; 10 | } 11 | -------------------------------------------------------------------------------- /downscale2x.frag: -------------------------------------------------------------------------------- 1 | // Used only for testing. 2 | 3 | // Implicit uniforms: 4 | // uniform vec2 PREFIX(offset); 5 | 6 | vec4 FUNCNAME(vec2 tc) 7 | { 8 | return INPUT(tc * 2.0 + PREFIX(offset)); 9 | // vec2 z = tc * 2.0 + PREFIX(offset); 10 | // return vec4(z.y, 0.0f, 0.0f, 1.0f); 11 | } 12 | -------------------------------------------------------------------------------- /downscale2x.comp: -------------------------------------------------------------------------------- 1 | // Used for testing only. 2 | 3 | layout(local_size_x = 1) in; 4 | 5 | void FUNCNAME() 6 | { 7 | ivec2 tc = ivec2(gl_GlobalInvocationID.xy); 8 | vec2 coord = NORMALIZE_TEXTURE_COORDS(vec2(tc.x, tc.y)); 9 | OUTPUT(tc, INPUT(vec2(coord.x - 0.125, coord.y + 0.25))); 10 | } 11 | -------------------------------------------------------------------------------- /alpha_division_effect.cpp: -------------------------------------------------------------------------------- 1 | #include "alpha_division_effect.h" 2 | #include "util.h" 3 | 4 | using namespace std; 5 | 6 | namespace movit { 7 | 8 | string AlphaDivisionEffect::output_fragment_shader() 9 | { 10 | return read_file("alpha_division_effect.frag"); 11 | } 12 | 13 | } // namespace 14 | -------------------------------------------------------------------------------- /colorspace_conversion_effect.frag: -------------------------------------------------------------------------------- 1 | // Colorspace conversion (needs to be done in linear space). 2 | // The matrix is computed on the host and baked into the shader at compile time. 3 | 4 | vec4 FUNCNAME(vec2 tc) { 5 | vec4 x = INPUT(tc); 6 | x.rgb = PREFIX(conversion_matrix) * x.rgb; 7 | return x; 8 | } 9 | -------------------------------------------------------------------------------- /alpha_division_effect.frag: -------------------------------------------------------------------------------- 1 | // Note: Division by zero will give inf or nan, whose conversion to 2 | // integer types is implementation-defined. However, anything is fine for 3 | // alpha=0, since that's undefined anyway. 4 | vec4 FUNCNAME(vec2 tc) { 5 | vec4 x = INPUT(tc); 6 | x.rgb /= x.aaa; 7 | return x; 8 | } 9 | -------------------------------------------------------------------------------- /mirror_effect.cpp: -------------------------------------------------------------------------------- 1 | #include "mirror_effect.h" 2 | #include "util.h" 3 | 4 | using namespace std; 5 | 6 | namespace movit { 7 | 8 | MirrorEffect::MirrorEffect() 9 | { 10 | } 11 | 12 | string MirrorEffect::output_fragment_shader() 13 | { 14 | return read_file("mirror_effect.frag"); 15 | } 16 | 17 | } // namespace movit 18 | -------------------------------------------------------------------------------- /alpha_multiplication_effect.cpp: -------------------------------------------------------------------------------- 1 | #include "alpha_multiplication_effect.h" 2 | #include "util.h" 3 | 4 | using namespace std; 5 | 6 | namespace movit { 7 | 8 | string AlphaMultiplicationEffect::output_fragment_shader() 9 | { 10 | return read_file("alpha_multiplication_effect.frag"); 11 | } 12 | 13 | } // namespace movit 14 | -------------------------------------------------------------------------------- /complex_modulate_effect.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform vec2 PREFIX(num_repeats); 3 | 4 | vec4 FUNCNAME(vec2 tc) { 5 | vec4 pixel = INPUT1(tc); 6 | vec2 pattern = INPUT2(tc * PREFIX(num_repeats)).xy; 7 | 8 | // Complex multiplication between each of (pixel.xy, pixel.zw) and pattern.xy. 9 | return pattern.x * pixel + pattern.y * vec4(-pixel.y, pixel.x, -pixel.w, pixel.z); 10 | } 11 | -------------------------------------------------------------------------------- /defs.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_DEFS_H 2 | #define _MOVIT_DEFS_H 3 | 4 | // Utility macros that are useful from other header files. 5 | 6 | #ifdef __GNUC__ 7 | #define MUST_CHECK_RESULT __attribute__((warn_unused_result)) 8 | #define DOES_NOT_RETURN __attribute__((noreturn)) 9 | #else 10 | #define MUST_CHECK_RESULT 11 | #define DOES_NOT_RETURN 12 | #endif 13 | 14 | #endif // !defined(_MOVIT_DEFS_H) 15 | -------------------------------------------------------------------------------- /multiply_effect.cpp: -------------------------------------------------------------------------------- 1 | #include "multiply_effect.h" 2 | #include "util.h" 3 | 4 | using namespace std; 5 | 6 | namespace movit { 7 | 8 | MultiplyEffect::MultiplyEffect() 9 | : factor(1.0f, 1.0f, 1.0f, 1.0f) 10 | { 11 | register_vec4("factor", (float *)&factor); 12 | } 13 | 14 | string MultiplyEffect::output_fragment_shader() 15 | { 16 | return read_file("multiply_effect.frag"); 17 | } 18 | 19 | } // namespace movit 20 | -------------------------------------------------------------------------------- /saturation_effect.cpp: -------------------------------------------------------------------------------- 1 | #include "saturation_effect.h" 2 | #include "util.h" 3 | 4 | using namespace std; 5 | 6 | namespace movit { 7 | 8 | SaturationEffect::SaturationEffect() 9 | : saturation(1.0f) 10 | { 11 | register_float("saturation", &saturation); 12 | } 13 | 14 | string SaturationEffect::output_fragment_shader() 15 | { 16 | return read_file("saturation_effect.frag"); 17 | } 18 | 19 | } // namespace movit 20 | -------------------------------------------------------------------------------- /version.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_VERSION_H 2 | #define _MOVIT_VERSION_H 1 3 | 4 | // A number that will increase every time the visible user API 5 | // changes, even within git versions. There is no specific version 6 | // documentation outside the regular changelogs, though. 7 | // The shotcut fork uses a different version number using a base 8 | // of 1000. 9 | 10 | #define MOVIT_VERSION 1037 11 | 12 | #endif // !defined(_MOVIT_VERSION_H) 13 | -------------------------------------------------------------------------------- /mix_effect.cpp: -------------------------------------------------------------------------------- 1 | #include "mix_effect.h" 2 | #include "util.h" 3 | 4 | using namespace std; 5 | 6 | namespace movit { 7 | 8 | MixEffect::MixEffect() 9 | : strength_first(0.5f), strength_second(0.5f) 10 | { 11 | register_float("strength_first", &strength_first); 12 | register_float("strength_second", &strength_second); 13 | } 14 | 15 | string MixEffect::output_fragment_shader() 16 | { 17 | return read_file("mix_effect.frag"); 18 | } 19 | 20 | } // namespace movit 21 | -------------------------------------------------------------------------------- /vs-color.130.vert: -------------------------------------------------------------------------------- 1 | #version 130 2 | 3 | in vec2 position; 4 | in vec4 color; 5 | out vec4 frag_color; 6 | 7 | void main() 8 | { 9 | // The result of glOrtho(0.0, 1.0, 0.0, 1.0, 0.0, 1.0) is: 10 | // 11 | // 2.000 0.000 0.000 -1.000 12 | // 0.000 2.000 0.000 -1.000 13 | // 0.000 0.000 -2.000 -1.000 14 | // 0.000 0.000 0.000 1.000 15 | gl_Position = vec4(2.0 * position.x - 1.0, 2.0 * position.y - 1.0, -1.0, 1.0); 16 | frag_color = color; 17 | } 18 | -------------------------------------------------------------------------------- /vs-color.150.vert: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | in vec2 position; 4 | in vec4 color; 5 | out vec4 frag_color; 6 | 7 | void main() 8 | { 9 | // The result of glOrtho(0.0, 1.0, 0.0, 1.0, 0.0, 1.0) is: 10 | // 11 | // 2.000 0.000 0.000 -1.000 12 | // 0.000 2.000 0.000 -1.000 13 | // 0.000 0.000 -2.000 -1.000 14 | // 0.000 0.000 0.000 1.000 15 | gl_Position = vec4(2.0 * position.x - 1.0, 2.0 * position.y - 1.0, -1.0, 1.0); 16 | frag_color = color; 17 | } 18 | -------------------------------------------------------------------------------- /movit.pc.in: -------------------------------------------------------------------------------- 1 | # Movit pkg-config source file. 2 | 3 | prefix=@prefix@ 4 | exec_prefix=@exec_prefix@ 5 | libdir=@libdir@ 6 | includedir=@includedir@ 7 | datarootdir=@datarootdir@ 8 | datadir=@datadir@ 9 | shaderdir=@datadir@/movit 10 | 11 | Name: movit 12 | Description: Movit is a library for high-quality, high-performance video filters. 13 | Version: git 14 | Requires: epoxy eigen3 15 | Requires.private: fftw3 16 | Conflicts: 17 | Libs: -lmovit 18 | Cflags: -I${includedir}/movit 19 | -------------------------------------------------------------------------------- /widgets.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_WIDGETS_H 2 | #define _MOVIT_WIDGETS_H 1 3 | 4 | // Some simple UI widgets for test use. 5 | 6 | namespace movit { 7 | 8 | void draw_hsv_wheel(float y, float rad, float theta, float value); 9 | void draw_saturation_bar(float y, float saturation); 10 | void init_hsv_resources(); 11 | void cleanup_hsv_resources(); 12 | void read_colorwheel(float xf, float yf, float *rad, float *theta, float *value); 13 | 14 | } // namespace movit 15 | 16 | #endif // !defined(_MOVIT_WIDGETS_H) 17 | -------------------------------------------------------------------------------- /overlay_effect.cpp: -------------------------------------------------------------------------------- 1 | #include "overlay_effect.h" 2 | #include "util.h" 3 | 4 | using namespace std; 5 | 6 | namespace movit { 7 | 8 | OverlayEffect::OverlayEffect() 9 | : swap_inputs(false) 10 | { 11 | register_int("swap_inputs", (int *)&swap_inputs); 12 | } 13 | 14 | string OverlayEffect::output_fragment_shader() 15 | { 16 | char buf[256]; 17 | snprintf(buf, sizeof(buf), "#define SWAP_INPUTS %d\n", swap_inputs); 18 | return buf + read_file("overlay_effect.frag"); 19 | } 20 | 21 | } // namespace movit 22 | -------------------------------------------------------------------------------- /flat_input.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform sampler2D PREFIX(tex); 3 | 4 | vec4 FUNCNAME(vec2 tc) { 5 | // OpenGL's origin is bottom-left, but most graphics software assumes 6 | // a top-left origin. Thus, for inputs that come from the user, 7 | // we flip the y coordinate. 8 | tc.y = 1.0 - tc.y; 9 | 10 | vec4 pixel = tex2D(PREFIX(tex), tc); 11 | 12 | // These two are #defined to 0 or 1 in flat_input.cpp. 13 | #if FIXUP_SWAP_RB 14 | pixel.rb = pixel.br; 15 | #endif 16 | #if FIXUP_RED_TO_GRAYSCALE 17 | pixel.gb = pixel.rr; 18 | #endif 19 | return pixel; 20 | } 21 | 22 | #undef FIXUP_SWAP_RB 23 | #undef FIXUP_RED_TO_GRAYSCALE 24 | -------------------------------------------------------------------------------- /resize_effect.cpp: -------------------------------------------------------------------------------- 1 | #include "resize_effect.h" 2 | #include "util.h" 3 | 4 | using namespace std; 5 | 6 | namespace movit { 7 | 8 | ResizeEffect::ResizeEffect() 9 | : width(1280), height(720) 10 | { 11 | register_int("width", &width); 12 | register_int("height", &height); 13 | } 14 | 15 | string ResizeEffect::output_fragment_shader() 16 | { 17 | return read_file("identity.frag"); 18 | } 19 | 20 | void ResizeEffect::get_output_size(unsigned *width, unsigned *height, unsigned *virtual_width, unsigned *virtual_height) const 21 | { 22 | *virtual_width = *width = this->width; 23 | *virtual_height = *height = this->height; 24 | } 25 | 26 | } // namespace movit 27 | -------------------------------------------------------------------------------- /vs.vert: -------------------------------------------------------------------------------- 1 | attribute vec2 position; 2 | attribute vec2 texcoord; 3 | varying vec2 tc; 4 | 5 | // Will be overridden by compile_glsl_program() if needed. 6 | // (It cannot just be prepended, as #version must be before everything.) 7 | #define FLIP_ORIGIN 0 8 | 9 | void main() 10 | { 11 | // The result of glOrtho(0.0, 1.0, 0.0, 1.0, 0.0, 1.0) is: 12 | // 13 | // 2.000 0.000 0.000 -1.000 14 | // 0.000 2.000 0.000 -1.000 15 | // 0.000 0.000 -2.000 -1.000 16 | // 0.000 0.000 0.000 1.000 17 | gl_Position = vec4(2.0 * position.x - 1.0, 2.0 * position.y - 1.0, -1.0, 1.0); 18 | tc = texcoord; 19 | #if FLIP_ORIGIN 20 | tc.y = 1.0f - tc.y; 21 | #endif 22 | } 23 | -------------------------------------------------------------------------------- /sandbox_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "sandbox_effect.h" 4 | #include "util.h" 5 | 6 | using namespace std; 7 | 8 | namespace movit { 9 | 10 | SandboxEffect::SandboxEffect() 11 | : parm(0.0f) 12 | { 13 | register_float("parm", &parm); 14 | } 15 | 16 | string SandboxEffect::output_fragment_shader() 17 | { 18 | return read_file("sandbox_effect.frag"); 19 | } 20 | 21 | void SandboxEffect::set_gl_state(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) 22 | { 23 | Effect::set_gl_state(glsl_program_num, prefix, sampler_num); 24 | 25 | // Any OpenGL state you might want to set, goes here. 26 | } 27 | 28 | } // namespace movit 29 | -------------------------------------------------------------------------------- /alpha_division_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_ALPHA_DIVISION_EFFECT_H 2 | #define _MOVIT_ALPHA_DIVISION_EFFECT_H 1 3 | 4 | // Convert postmultiplied alpha to premultiplied alpha, simply by dividing. 5 | 6 | #include 7 | 8 | #include "effect.h" 9 | 10 | namespace movit { 11 | 12 | class AlphaDivisionEffect : public Effect { 13 | public: 14 | AlphaDivisionEffect() {} 15 | std::string effect_type_id() const override { return "AlphaDivisionEffect"; } 16 | std::string output_fragment_shader() override; 17 | bool strong_one_to_one_sampling() const override { return true; } 18 | }; 19 | 20 | } // namespace movit 21 | 22 | #endif // !defined(_MOVIT_ALPHA_DIVISION_EFFECT_H) 23 | -------------------------------------------------------------------------------- /vs.130.vert: -------------------------------------------------------------------------------- 1 | #version 130 2 | 3 | in vec2 position; 4 | in vec2 texcoord; 5 | out vec2 tc; 6 | 7 | // Will be overridden by compile_glsl_program() if needed. 8 | // (It cannot just be prepended, as #version must be before everything.) 9 | #define FLIP_ORIGIN 0 10 | 11 | void main() 12 | { 13 | // The result of glOrtho(0.0, 1.0, 0.0, 1.0, 0.0, 1.0) is: 14 | // 15 | // 2.000 0.000 0.000 -1.000 16 | // 0.000 2.000 0.000 -1.000 17 | // 0.000 0.000 -2.000 -1.000 18 | // 0.000 0.000 0.000 1.000 19 | gl_Position = vec4(2.0 * position.x - 1.0, 2.0 * position.y - 1.0, -1.0, 1.0); 20 | tc = texcoord; 21 | #if FLIP_ORIGIN 22 | tc.y = 1.0f - tc.y; 23 | #endif 24 | } 25 | -------------------------------------------------------------------------------- /vs.150.vert: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | in vec2 position; 4 | in vec2 texcoord; 5 | out vec2 tc; 6 | 7 | // Will be overridden by compile_glsl_program() if needed. 8 | // (It cannot just be prepended, as #version must be before everything.) 9 | #define FLIP_ORIGIN 0 10 | 11 | void main() 12 | { 13 | // The result of glOrtho(0.0, 1.0, 0.0, 1.0, 0.0, 1.0) is: 14 | // 15 | // 2.000 0.000 0.000 -1.000 16 | // 0.000 2.000 0.000 -1.000 17 | // 0.000 0.000 -2.000 -1.000 18 | // 0.000 0.000 0.000 1.000 19 | gl_Position = vec4(2.0 * position.x - 1.0, 2.0 * position.y - 1.0, -1.0, 1.0); 20 | tc = texcoord; 21 | #if FLIP_ORIGIN 22 | tc.y = 1.0f - tc.y; 23 | #endif 24 | } 25 | -------------------------------------------------------------------------------- /mirror.comp: -------------------------------------------------------------------------------- 1 | // A compute shader to mirror the inputs, in 2x2 blocks. For testing only. 2 | 3 | layout(local_size_x = 1) in; 4 | 5 | void FUNCNAME() 6 | { 7 | ivec2 tc = ivec2(gl_GlobalInvocationID.xy) * ivec2(2, 2); 8 | int offs = int(gl_NumWorkGroups.x) * 2 - 1; 9 | 10 | OUTPUT(ivec2(offs - tc.x, tc.y), INPUT(NORMALIZE_TEXTURE_COORDS(vec2(tc.x, tc.y)))); 11 | OUTPUT(ivec2(offs - tc.x - 1, tc.y), INPUT(NORMALIZE_TEXTURE_COORDS(vec2(tc.x + 1, tc.y)))); 12 | OUTPUT(ivec2(offs - tc.x, tc.y + 1), INPUT(NORMALIZE_TEXTURE_COORDS(vec2(tc.x, tc.y + 1)))); 13 | OUTPUT(ivec2(offs - tc.x - 1, tc.y + 1), INPUT(NORMALIZE_TEXTURE_COORDS(vec2(tc.x + 1, tc.y + 1)))); 14 | } 15 | -------------------------------------------------------------------------------- /header.comp: -------------------------------------------------------------------------------- 1 | #version 150 2 | #extension GL_ARB_compute_shader : enable 3 | #extension GL_ARB_shader_image_load_store : enable 4 | #extension GL_ARB_shader_image_size : enable 5 | 6 | // The texture the compute shader is writing to. 7 | uniform restrict writeonly image2D tex_outbuf; 8 | 9 | // Defined in footer.comp. 10 | vec4 tex2D(sampler2D s, vec2 coord); 11 | void cs_output(uvec2 coord, vec4 val); 12 | void cs_output(ivec2 coord, vec4 val); 13 | 14 | // Used if there are any steps used to postprocess compute shader output. 15 | // Initialized due to https://bugs.freedesktop.org/show_bug.cgi?id=103895. 16 | vec4 CS_OUTPUT_VAL = vec4(0.0); 17 | 18 | #define OUTPUT(tc, val) cs_output(tc, val) 19 | -------------------------------------------------------------------------------- /vs.300es.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | in vec2 position; 6 | in vec2 texcoord; 7 | out vec2 tc; 8 | 9 | // Will be overridden by compile_glsl_program() if needed. 10 | // (It cannot just be prepended, as #version must be before everything.) 11 | #define FLIP_ORIGIN 0 12 | 13 | void main() 14 | { 15 | // The result of glOrtho(0.0, 1.0, 0.0, 1.0, 0.0, 1.0) is: 16 | // 17 | // 2.000 0.000 0.000 -1.000 18 | // 0.000 2.000 0.000 -1.000 19 | // 0.000 0.000 -2.000 -1.000 20 | // 0.000 0.000 0.000 1.000 21 | gl_Position = vec4(2.0 * position.x - 1.0, 2.0 * position.y - 1.0, -1.0, 1.0); 22 | tc = texcoord; 23 | #if FLIP_ORIGIN 24 | tc.y = 1.0f - tc.y; 25 | #endif 26 | } 27 | -------------------------------------------------------------------------------- /alpha_multiplication_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_ALPHA_MULTIPLICATION_EFFECT_H 2 | #define _MOVIT_ALPHA_MULTIPLICATION_EFFECT_H 1 3 | 4 | // Convert postmultiplied alpha to premultiplied alpha, simply by multiplying. 5 | 6 | #include 7 | 8 | #include "effect.h" 9 | 10 | namespace movit { 11 | 12 | class AlphaMultiplicationEffect : public Effect { 13 | public: 14 | AlphaMultiplicationEffect() {} 15 | std::string effect_type_id() const override { return "AlphaMultiplicationEffect"; } 16 | std::string output_fragment_shader() override; 17 | bool strong_one_to_one_sampling() const override { return true; } 18 | }; 19 | 20 | } // namespace movit 21 | 22 | #endif // !defined(_MOVIT_ALPHA_MULTIPLICATION_EFFECT_H) 23 | -------------------------------------------------------------------------------- /vignette_effect.frag: -------------------------------------------------------------------------------- 1 | // A simple, circular vignette, with a cos² falloff. 2 | 3 | // Implicit uniforms: 4 | // uniform float PREFIX(pihalf_div_radius); 5 | // 6 | // uniform vec2 PREFIX(aspect_correction); 7 | // uniform vec2 PREFIX(flipped_center); 8 | 9 | vec4 FUNCNAME(vec2 tc) { 10 | vec4 x = INPUT(tc); 11 | 12 | const float pihalf = 0.5 * 3.14159265358979324; 13 | 14 | vec2 normalized_pos = (tc - PREFIX(flipped_center)) * PREFIX(aspect_correction); 15 | float dist = (length(normalized_pos) - PREFIX(inner_radius)) * PREFIX(pihalf_div_radius); 16 | float linear_falloff = clamp(dist, 0.0, pihalf); 17 | float falloff = cos(linear_falloff) * cos(linear_falloff); 18 | x.rgb *= vec3(falloff); 19 | 20 | return x; 21 | } 22 | -------------------------------------------------------------------------------- /lift_gamma_gain_effect.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // 3 | // These are calculated in the host code to save some arithmetic. 4 | // uniform vec3 PREFIX(gain_pow_inv_gamma); // gain^(1/gamma). 5 | // uniform vec3 PREFIX(inv_gamma_22); // 2.2 / gamma. 6 | 7 | vec4 FUNCNAME(vec2 tc) { 8 | vec4 x = INPUT(tc); 9 | 10 | x.rgb /= x.aaa; 11 | 12 | // pow() of negative numbers is undefined, so clip out-of-gamut values. 13 | x.rgb = max(x.rgb, 0.0); 14 | 15 | x.rgb = pow(x.rgb, vec3(1.0/2.2)); 16 | x.rgb += PREFIX(lift) * (vec3(1) - x.rgb); 17 | 18 | // Clip out-of-gamut values again. 19 | x.rgb = max(x.rgb, 0.0); 20 | 21 | x.rgb = pow(x.rgb, PREFIX(inv_gamma_22)); 22 | x.rgb *= PREFIX(gain_pow_inv_gamma); 23 | x.rgb *= x.aaa; 24 | 25 | return x; 26 | } 27 | -------------------------------------------------------------------------------- /blur_effect.frag: -------------------------------------------------------------------------------- 1 | // A simple un.directional blur. 2 | // DIRECTION_VERTICAL will be #defined to 1 if we are doing a vertical blur, 3 | // 0 otherwise. 4 | 5 | // Implicit uniforms: 6 | // uniform vec2 PREFIX(samples)[NUM_TAPS / 2 + 1]; 7 | 8 | vec4 FUNCNAME(vec2 tc) { 9 | vec4 sum = vec4(PREFIX(samples)[0].y) * INPUT(tc); 10 | for (int i = 1; i < NUM_TAPS / 2 + 1; ++i) { 11 | vec2 sample = PREFIX(samples)[i]; 12 | vec2 sample1_tc = tc, sample2_tc = tc; 13 | #if DIRECTION_VERTICAL 14 | sample1_tc.y -= sample.x; 15 | sample2_tc.y += sample.x; 16 | #else 17 | sample1_tc.x -= sample.x; 18 | sample2_tc.x += sample.x; 19 | #endif 20 | sum += vec4(sample.y) * (INPUT(sample1_tc) + INPUT(sample2_tc)); 21 | } 22 | return sum; 23 | } 24 | 25 | #undef DIRECTION_VERTICAL 26 | -------------------------------------------------------------------------------- /mirror_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_MIRROR_EFFECT_H 2 | #define _MOVIT_MIRROR_EFFECT_H 1 3 | 4 | // A simple horizontal mirroring. 5 | 6 | #include 7 | 8 | #include "effect.h" 9 | 10 | namespace movit { 11 | 12 | class MirrorEffect : public Effect { 13 | public: 14 | MirrorEffect(); 15 | std::string effect_type_id() const override { return "MirrorEffect"; } 16 | std::string output_fragment_shader() override; 17 | 18 | bool needs_linear_light() const override { return false; } 19 | bool needs_srgb_primaries() const override { return false; } 20 | AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; } 21 | bool one_to_one_sampling() const override { return true; } 22 | }; 23 | 24 | } // namespace movit 25 | 26 | #endif // !defined(_MOVIT_MIRROR_EFFECT_H) 27 | -------------------------------------------------------------------------------- /d65.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_D65_H 2 | #define _MOVIT_D65_H 1 3 | 4 | namespace movit { 5 | 6 | // The D65 illuminant, which is the standard white point (ie. what you should get 7 | // for R=G=B=1) for almost all video color spaces in common use. It has a color 8 | // temperature roughly around 6500 K, which is sort of bluish; it is intended to 9 | // simulate average daylight conditions. 10 | // 11 | // The definition (in xyz space) is given, for instance, in both Rec. 601 and 709. 12 | static const double d65_x = 0.3127, d65_y = 0.3290, d65_z = 1.0 - d65_x - d65_y; 13 | 14 | // XYZ coordinates of D65, normalized so that Y=1. 15 | static const double d65_X = d65_x / d65_y; 16 | static const double d65_Y = 1.0; 17 | static const double d65_Z = d65_z / d65_y; 18 | 19 | } // namespace movit 20 | 21 | #endif // !defined(_MOVIT_D65_H) 22 | 23 | -------------------------------------------------------------------------------- /multiply_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_MULTIPLY_EFFECT_H 2 | #define _MOVIT_MULTIPLY_EFFECT_H 1 3 | 4 | // An effect that multiplies every pixel by a constant (separate for each of 5 | // R, G, B, A). A common use would be to reduce the alpha of an overlay before 6 | // sending it through OverlayEffect, e.g. with R=G=B=A=0.3 to get 30% alpha 7 | // (remember, alpha is premultiplied). 8 | 9 | #include 10 | #include 11 | 12 | #include "effect.h" 13 | 14 | namespace movit { 15 | 16 | class MultiplyEffect : public Effect { 17 | public: 18 | MultiplyEffect(); 19 | std::string effect_type_id() const override { return "MultiplyEffect"; } 20 | std::string output_fragment_shader() override; 21 | bool strong_one_to_one_sampling() const override { return true; } 22 | 23 | private: 24 | RGBATuple factor; 25 | }; 26 | 27 | } // namespace movit 28 | 29 | #endif // !defined(_MOVIT_MULTIPLY_EFFECT_H) 30 | -------------------------------------------------------------------------------- /gamma_expansion_effect.frag: -------------------------------------------------------------------------------- 1 | // Expand gamma curve. 2 | 3 | // Implicit uniforms: 4 | // uniform float PREFIX(linear_scale); 5 | // uniform float PREFIX(c)[5]; 6 | // uniform float PREFIX(clog)[5]; 7 | // uniform float PREFIX(beta); 8 | // uniform float PREFIX(lambda); 9 | 10 | vec4 FUNCNAME(vec2 tc) { 11 | vec4 x = INPUT(tc); 12 | 13 | vec3 a = x.rgb * PREFIX(linear_scale); 14 | 15 | // Fourth-order polynomial approximation to pow(). See the .cpp file for details. 16 | vec3 b = PREFIX(c[0]) + (PREFIX(c[1]) + (PREFIX(c[2]) + (PREFIX(c[3]) + PREFIX(c[4]) * x.rgb) * x.rgb) * x.rgb) * x.rgb; 17 | vec3 c = PREFIX(clog)[0] + (PREFIX(clog)[1] + (PREFIX(clog)[2] + (PREFIX(clog)[3] + PREFIX(clog)[4] * x.rgb) * x.rgb) * x.rgb) * x.rgb; 18 | 19 | vec3 f = vec3(greaterThan(x.rgb, vec3(PREFIX(beta)))); 20 | vec3 g = vec3(greaterThan(x.rgb, vec3(PREFIX(lambda)))); 21 | x = vec4(mix(mix(a, b, f), c, g), x.a); 22 | 23 | return x; 24 | } 25 | -------------------------------------------------------------------------------- /alpha_multiplication_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for AlphaMultiplicationEffect. 2 | 3 | #include 4 | 5 | #include "effect_chain.h" 6 | #include "gtest/gtest.h" 7 | #include "image_format.h" 8 | #include "test_util.h" 9 | 10 | namespace movit { 11 | 12 | TEST(AlphaMultiplicationEffectTest, SimpleTest) { 13 | const int size = 3; 14 | float data[4 * size] = { 15 | 1.0f, 0.2f, 0.2f, 0.0f, 16 | 0.2f, 1.0f, 0.2f, 0.5f, 17 | 0.2f, 0.2f, 1.0f, 1.0f, 18 | }; 19 | float expected_data[4 * size] = { 20 | 0.0f, 0.0f, 0.0f, 0.0f, 21 | 0.1f, 0.5f, 0.1f, 0.5f, 22 | 0.2f, 0.2f, 1.0f, 1.0f, 23 | }; 24 | float out_data[4 * size]; 25 | EffectChainTester tester(data, 1, size, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 26 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED); 27 | 28 | expect_equal(expected_data, out_data, 4, size); 29 | } 30 | 31 | } // namespace movit 32 | -------------------------------------------------------------------------------- /padding_effect.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform vec2 PREFIX(offset); 3 | // uniform vec2 PREFIX(scale); 4 | // 5 | // uniform vec2 PREFIX(normalized_coords_to_texels); 6 | // uniform vec2 PREFIX(offset_bottomleft); 7 | // uniform vec2 PREFIX(offset_topright); 8 | 9 | vec4 FUNCNAME(vec2 tc) { 10 | tc -= PREFIX(offset); 11 | tc *= PREFIX(scale); 12 | 13 | vec2 tc_texels = tc * PREFIX(normalized_coords_to_texels); 14 | vec2 coverage_bottomleft = clamp(tc_texels + PREFIX(offset_bottomleft), 0.0f, 1.0f); 15 | vec2 coverage_topright = clamp(PREFIX(offset_topright) - tc_texels, 0.0f, 1.0f); 16 | vec2 coverage_both = coverage_bottomleft * coverage_topright; 17 | float coverage = coverage_both.x * coverage_both.y; 18 | 19 | if (coverage <= 0.0f) { 20 | // Short-circuit in case the underlying function is expensive to call. 21 | return PREFIX(border_color); 22 | } else { 23 | return mix(PREFIX(border_color), INPUT(tc), coverage); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ycbcr_422interleaved_input.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform sampler2D PREFIX(tex_y); 3 | // uniform sampler2D PREFIX(tex_cbcr); 4 | 5 | vec4 FUNCNAME(vec2 tc) { 6 | // OpenGL's origin is bottom-left, but most graphics software assumes 7 | // a top-left origin. Thus, for inputs that come from the user, 8 | // we flip the y coordinate. 9 | tc.y = 1.0 - tc.y; 10 | 11 | vec3 ycbcr; 12 | ycbcr.x = tex2D(PREFIX(tex_y), tc).y; 13 | #if CB_CR_OFFSETS_EQUAL 14 | vec2 tc_cbcr = tc; 15 | tc_cbcr.x += PREFIX(cb_offset_x); 16 | ycbcr.yz = tex2D(PREFIX(tex_cbcr), tc_cbcr).xz; 17 | #else 18 | vec2 tc_cb = tc; 19 | tc_cb.x += PREFIX(cb_offset_x); 20 | ycbcr.y = tex2D(PREFIX(tex_cbcr), tc_cb).x; 21 | 22 | vec2 tc_cr = tc; 23 | tc_cr.x += PREFIX(cr_offset_x); 24 | ycbcr.z = tex2D(PREFIX(tex_cbcr), tc_cr).z; 25 | #endif 26 | 27 | ycbcr -= PREFIX(offset); 28 | 29 | vec4 rgba; 30 | rgba.rgb = PREFIX(inv_ycbcr_matrix) * ycbcr; 31 | rgba.a = 1.0; 32 | return rgba; 33 | } 34 | -------------------------------------------------------------------------------- /luma_mix_effect.cpp: -------------------------------------------------------------------------------- 1 | #include "luma_mix_effect.h" 2 | #include "effect_util.h" 3 | #include "util.h" 4 | 5 | using namespace std; 6 | 7 | namespace movit { 8 | 9 | LumaMixEffect::LumaMixEffect() 10 | : transition_width(1.0f), progress(0.5f), inverse(0) 11 | { 12 | register_float("transition_width", &transition_width); 13 | register_float("progress", &progress); 14 | register_int("inverse", &inverse); 15 | register_uniform_bool("bool_inverse", &uniform_inverse); 16 | register_uniform_float("progress_mul_w_plus_one", &uniform_progress_mul_w_plus_one); 17 | } 18 | 19 | string LumaMixEffect::output_fragment_shader() 20 | { 21 | return read_file("luma_mix_effect.frag"); 22 | } 23 | 24 | void LumaMixEffect::set_gl_state(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) 25 | { 26 | Effect::set_gl_state(glsl_program_num, prefix, sampler_num); 27 | uniform_progress_mul_w_plus_one = progress * (transition_width + 1.0); 28 | uniform_inverse = inverse; 29 | } 30 | 31 | } // namespace movit 32 | -------------------------------------------------------------------------------- /sandbox_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_SANDBOX_EFFECT_H 2 | #define _MOVIT_SANDBOX_EFFECT_H 1 3 | 4 | // This effect, by default, does nothing. 5 | // 6 | // But imagine all the cool things you can make it do! Thus, the SandboxEffect 7 | // is intended to be a sandbox for you to have a place to write your test or 8 | // throwaway code. When you're happy, you can do a bit of search and replace 9 | // to give it a proper name and its own place in the build system. 10 | 11 | #include 12 | #include 13 | 14 | #include "effect.h" 15 | 16 | namespace movit { 17 | 18 | class SandboxEffect : public Effect { 19 | public: 20 | SandboxEffect(); 21 | std::string effect_type_id() const override { return "SandboxEffect"; } 22 | std::string output_fragment_shader() override; 23 | 24 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 25 | 26 | private: 27 | float parm; 28 | }; 29 | 30 | } // namespace movit 31 | 32 | #endif // !defined(_MOVIT_SANDBOX_EFFECT_H) 33 | -------------------------------------------------------------------------------- /saturation_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_SATURATION_EFFECT_H 2 | #define _MOVIT_SATURATION_EFFECT_H 1 3 | 4 | // A simple desaturation/saturation effect. We use the Rec. 709 5 | // definition of luminance (in linear light, of course) and linearly 6 | // interpolate between that (saturation=0) and the original signal 7 | // (saturation=1). Extrapolating that curve further (ie., saturation > 1) 8 | // gives us increased saturation if so desired. 9 | 10 | #include 11 | 12 | #include "effect.h" 13 | 14 | namespace movit { 15 | 16 | class SaturationEffect : public Effect { 17 | public: 18 | SaturationEffect(); 19 | std::string effect_type_id() const override { return "SaturationEffect"; } 20 | AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; } 21 | bool strong_one_to_one_sampling() const override { return true; } 22 | std::string output_fragment_shader() override; 23 | 24 | private: 25 | float saturation; 26 | }; 27 | 28 | } // namespace movit 29 | 30 | #endif // !defined(_MOVIT_SATURATION_EFFECT_H) 31 | -------------------------------------------------------------------------------- /overlay_effect.frag: -------------------------------------------------------------------------------- 1 | // It's actually (but surprisingly) not correct to do a mix() here; 2 | // it would be if we had postmultiplied alpha and didn't have to worry 3 | // about alpha in the bottom layer, but given that we use premultiplied 4 | // alpha all over, top shouldn't actually be multiplied by anything. 5 | // 6 | // These formulas come from Wikipedia: 7 | // 8 | // http://en.wikipedia.org/wiki/Alpha_compositing 9 | // 10 | // We use the associative version given. However, note that since we want 11 | // _output_ to be premultiplied, C_o from Wikipedia is not what we want, 12 | // but rather c_o (which is not explicitly given, but obviously is just 13 | // C_o without the division by alpha_o). 14 | 15 | vec4 FUNCNAME(vec2 tc) { 16 | // SWAP_INPUTS will be #defined to 1 if we want to swap the two inputs, 17 | #if SWAP_INPUTS 18 | vec4 bottom = INPUT2(tc); 19 | vec4 top = INPUT1(tc); 20 | #else 21 | vec4 bottom = INPUT1(tc); 22 | vec4 top = INPUT2(tc); 23 | #endif 24 | return top + (1.0 - top.a) * bottom; 25 | } 26 | 27 | #undef SWAP_INPUTS 28 | -------------------------------------------------------------------------------- /fft_pass_effect.frag: -------------------------------------------------------------------------------- 1 | // DIRECTION_VERTICAL will be #defined to 1 if we are doing a vertical FFT, 2 | // and 0 otherwise. 3 | 4 | // Implicit uniforms: 5 | // uniform float PREFIX(num_repeats); 6 | // uniform sampler2D PREFIX(support_tex); 7 | 8 | vec4 FUNCNAME(vec2 tc) { 9 | #if DIRECTION_VERTICAL 10 | vec4 support = tex2D(PREFIX(support_tex), vec2(tc.y * PREFIX(num_repeats), 0.0)); 11 | vec4 c1 = INPUT(vec2(tc.x, tc.y + support.x)); 12 | vec4 c2 = INPUT(vec2(tc.x, tc.y + support.y)); 13 | #else 14 | vec4 support = tex2D(PREFIX(support_tex), vec2(tc.x * PREFIX(num_repeats), 0.0)); 15 | vec4 c1 = INPUT(vec2(tc.x + support.x, tc.y)); 16 | vec4 c2 = INPUT(vec2(tc.x + support.y, tc.y)); 17 | #endif 18 | // Two complex additions and multiplications in parallel; essentially 19 | // 20 | // result.xy = c1.xy + twiddle * c2.xy 21 | // result.zw = c1.zw + twiddle * c2.zw 22 | // 23 | // where * is complex multiplication. 24 | return c1 + support.z * c2 + support.w * vec4(-c2.y, c2.x, -c2.w, c2.z); 25 | } 26 | 27 | #undef DIRECTION_VERTICAL 28 | -------------------------------------------------------------------------------- /ycbcr_conversion_effect.frag: -------------------------------------------------------------------------------- 1 | // See footer.frag for details about this if statement. 2 | #ifndef YCBCR_ALSO_OUTPUT_RGBA 3 | #define YCBCR_ALSO_OUTPUT_RGBA 0 4 | #endif 5 | 6 | #if YCBCR_ALSO_OUTPUT_RGBA 7 | vec4[2] FUNCNAME(vec2 tc) { 8 | #else 9 | vec4 FUNCNAME(vec2 tc) { 10 | #endif 11 | vec4 rgba = INPUT(tc); 12 | vec4 ycbcr_a; 13 | 14 | ycbcr_a.rgb = PREFIX(ycbcr_matrix) * rgba.rgb + PREFIX(offset); 15 | 16 | if (PREFIX(clamp_range)) { 17 | // If we use limited-range Y'CbCr, the card's usual 0–255 clamping 18 | // won't be enough, so we need to clamp ourselves here. 19 | // 20 | // We clamp before dither, which is a bit unfortunate, since 21 | // it means dither can take us out of the clamped range again. 22 | // However, since DitherEffect never adds enough dither to change 23 | // the quantized levels, we will be fine in practice. 24 | ycbcr_a.rgb = clamp(ycbcr_a.rgb, PREFIX(ycbcr_min), PREFIX(ycbcr_max)); 25 | } 26 | 27 | ycbcr_a.a = rgba.a; 28 | 29 | #if YCBCR_ALSO_OUTPUT_RGBA 30 | return vec4[2](ycbcr_a, rgba); 31 | #else 32 | return ycbcr_a; 33 | #endif 34 | } 35 | -------------------------------------------------------------------------------- /mix_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_MIX_EFFECT_H 2 | #define _MOVIT_MIX_EFFECT_H 1 3 | 4 | // Combine two images: a*x + b*y. If you set a within [0,1] and b=1-a, 5 | // you will get a fade; if not, you may get surprising results (consider alpha). 6 | 7 | #include 8 | 9 | #include "effect.h" 10 | 11 | namespace movit { 12 | 13 | class MixEffect : public Effect { 14 | public: 15 | MixEffect(); 16 | std::string effect_type_id() const override { return "MixEffect"; } 17 | std::string output_fragment_shader() override; 18 | 19 | bool needs_srgb_primaries() const override { return false; } 20 | unsigned num_inputs() const override { return 2; } 21 | bool strong_one_to_one_sampling() const override { return true; } 22 | 23 | // TODO: In the common case where a+b=1, it would be useful to be able to set 24 | // alpha_handling() to INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK. However, right now 25 | // we have no way of knowing that at instantiation time. 26 | 27 | private: 28 | float strength_first, strength_second; 29 | }; 30 | 31 | } // namespace movit 32 | 33 | #endif // !defined(_MOVIT_MIX_EFFECT_H) 34 | -------------------------------------------------------------------------------- /gamma_compression_effect.frag: -------------------------------------------------------------------------------- 1 | // Compress gamma curve. 2 | 3 | // Implicit uniforms: 4 | // uniform float PREFIX(linear_scale); 5 | // uniform float PREFIX(c)[5]; 6 | // uniform float PREFIX(clog)[5]; 7 | // uniform float PREFIX(beta); 8 | // uniform float PREFIX(lambda); 9 | 10 | vec4 FUNCNAME(vec2 tc) { 11 | vec4 x = INPUT(tc); 12 | 13 | // We could reasonably get values outside (0.0, 1.0), but the formulas below 14 | // are not valid outside that range, so clamp before we do anything else. 15 | x.rgb = clamp(x.rgb, 0.0, 1.0); 16 | 17 | vec3 a = x.rgb * PREFIX(linear_scale); 18 | 19 | // Fourth-order polynomial approximation to pow(). See the .cpp file for details. 20 | vec3 s = sqrt(x.rgb); 21 | vec3 b = PREFIX(c)[0] + (PREFIX(c)[1] + (PREFIX(c)[2] + (PREFIX(c)[3] + PREFIX(c)[4] * s) * s) * s) * s; 22 | vec3 c = PREFIX(clog)[0] + (PREFIX(clog)[1] + (PREFIX(clog)[2] + (PREFIX(clog)[3] + PREFIX(clog)[4] * s) * s) * s) * s; 23 | 24 | vec3 f = vec3(greaterThan(x.rgb, vec3(PREFIX(beta)))); 25 | vec3 g = vec3(greaterThan(x.rgb, vec3(PREFIX(lambda)))); 26 | x = vec4(mix(mix(a, b, f), c, g), x.a); 27 | 28 | return x; 29 | } 30 | -------------------------------------------------------------------------------- /slice_effect.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform float PREFIX(output_coord_to_slice_num); 3 | // uniform float PREFIX(slice_num_to_input_coord); 4 | // uniform float PREFIX(slice_offset_to_input_coord); 5 | // uniform float PREFIX(normalized_offset); 6 | 7 | vec4 FUNCNAME(vec2 tc) { 8 | // DIRECTION_VERTICAL will be #defined to 1 if we are expanding vertically, 9 | // and 0 otherwise. 10 | #if DIRECTION_VERTICAL 11 | float sliced_coord = 1.0 - tc.y; 12 | #else 13 | float sliced_coord = tc.x; 14 | #endif 15 | 16 | // Find out which slice we are in, and a 0..1 coordinate for the offset within that slice. 17 | float slice_num = floor(sliced_coord * PREFIX(output_coord_to_slice_num)); 18 | float slice_offset = fract(sliced_coord * PREFIX(output_coord_to_slice_num)); 19 | 20 | // Find out where this slice begins in the input data, and then offset from that. 21 | float input_coord = slice_num * PREFIX(slice_num_to_input_coord) + slice_offset * PREFIX(slice_offset_to_input_coord) + PREFIX(normalized_offset); 22 | 23 | #if DIRECTION_VERTICAL 24 | return INPUT(vec2(tc.x, 1.0 - input_coord)); 25 | #else 26 | return INPUT(vec2(input_coord, tc.y)); 27 | #endif 28 | } 29 | 30 | #undef DIRECTION_VERTICAL 31 | -------------------------------------------------------------------------------- /white_balance_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_WHITE_BALANCE_EFFECT_H 2 | #define _MOVIT_WHITE_BALANCE_EFFECT_H 1 3 | 4 | // Color correction in LMS color space. 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "effect.h" 11 | 12 | namespace movit { 13 | 14 | class WhiteBalanceEffect : public Effect { 15 | public: 16 | WhiteBalanceEffect(); 17 | std::string effect_type_id() const override { return "WhiteBalanceEffect"; } 18 | AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; } 19 | bool strong_one_to_one_sampling() const override { return true; } 20 | std::string output_fragment_shader() override; 21 | 22 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 23 | 24 | private: 25 | // The neutral color, in linear sRGB. 26 | RGBTriplet neutral_color; 27 | 28 | // Output color temperature (in Kelvins). 29 | // Choosing 6500 will lead to no color cast (ie., the neutral color becomes perfectly gray). 30 | float output_color_temperature; 31 | 32 | Eigen::Matrix3d uniform_correction_matrix; 33 | }; 34 | 35 | } // namespace movit 36 | 37 | #endif // !defined(_MOVIT_WHITE_BALANCE_EFFECT_H) 38 | -------------------------------------------------------------------------------- /footer.comp: -------------------------------------------------------------------------------- 1 | // GLSL is pickier than the C++ preprocessor in if-testing for undefined 2 | // tokens; do some fixups here to keep it happy. 3 | 4 | #ifndef SQUARE_ROOT_TRANSFORMATION 5 | #define SQUARE_ROOT_TRANSFORMATION 0 6 | #endif 7 | 8 | #ifndef FLIP_ORIGIN 9 | #define FLIP_ORIGIN 0 10 | #endif 11 | 12 | void main() 13 | { 14 | INPUT(); 15 | } 16 | 17 | vec4 tex2D(sampler2D s, vec2 coord) 18 | { 19 | return texture(s, coord); 20 | } 21 | 22 | void cs_output(uvec2 coord, vec4 val) 23 | { 24 | cs_output(ivec2(coord), val); 25 | } 26 | 27 | void cs_output(ivec2 coord, vec4 val) 28 | { 29 | // Run the value through any postprocessing steps we might have. 30 | // Note that we need to give in the actual coordinates, since the 31 | // effect could have multiple (non-compute) inputs, and would also 32 | // be allowed to make effects based on the texture coordinate alone. 33 | CS_OUTPUT_VAL = val; 34 | val = CS_POSTPROC(NORMALIZE_TEXTURE_COORDS(coord)); 35 | 36 | #if SQUARE_ROOT_TRANSFORMATION 37 | // Make sure we don't give negative values to sqrt. 38 | val.rgb = sqrt(max(val.rgb, 0.0)); 39 | #endif 40 | 41 | #if FLIP_ORIGIN 42 | coord.y = imageSize(tex_outbuf).y - coord.y - 1; 43 | #endif 44 | 45 | imageStore(tex_outbuf, coord, val); 46 | } 47 | -------------------------------------------------------------------------------- /vignette_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_VIGNETTE_EFFECT_H 2 | #define _MOVIT_VIGNETTE_EFFECT_H 1 3 | 4 | // A circular vignette, falling off as cos² of the distance from the center 5 | // (the classic formula for approximating a real lens). 6 | 7 | #include 8 | #include 9 | 10 | #include "effect.h" 11 | 12 | namespace movit { 13 | 14 | class VignetteEffect : public Effect { 15 | public: 16 | VignetteEffect(); 17 | std::string effect_type_id() const override { return "VignetteEffect"; } 18 | std::string output_fragment_shader() override; 19 | 20 | bool needs_srgb_primaries() const override { return false; } 21 | AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; } 22 | bool strong_one_to_one_sampling() const override { return true; } 23 | 24 | void inform_input_size(unsigned input_num, unsigned width, unsigned height) override; 25 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 26 | 27 | private: 28 | Point2D center; 29 | Point2D uniform_aspect_correction, uniform_flipped_center; 30 | float radius, inner_radius; 31 | float uniform_pihalf_div_radius; 32 | }; 33 | 34 | } // namespace movit 35 | 36 | #endif // !defined(_MOVIT_VIGNETTE_EFFECT_H) 37 | -------------------------------------------------------------------------------- /alpha_division_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for AlphaDivisionEffect. 2 | 3 | #include 4 | #include "gtest/gtest.h" 5 | #include "image_format.h" 6 | #include "test_util.h" 7 | 8 | namespace movit { 9 | 10 | TEST(AlphaDivisionEffectTest, SimpleTest) { 11 | const int size = 2; 12 | float data[4 * size] = { 13 | 0.1f, 0.5f, 0.1f, 0.5f, 14 | 0.2f, 0.2f, 1.0f, 1.0f, 15 | }; 16 | float expected_data[4 * size] = { 17 | 0.2f, 1.0f, 0.2f, 0.5f, 18 | 0.2f, 0.2f, 1.0f, 1.0f, 19 | }; 20 | float out_data[4 * size]; 21 | EffectChainTester tester(data, 1, size, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 22 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 23 | 24 | expect_equal(expected_data, out_data, 4, size); 25 | } 26 | 27 | TEST(AlphaDivisionEffectTest, ZeroAlphaIsPreserved) { 28 | const int size = 2; 29 | float data[4 * size] = { 30 | 0.1f, 0.5f, 0.1f, 0.0f, 31 | 0.0f, 0.0f, 0.0f, 0.0f, 32 | }; 33 | float out_data[4 * size]; 34 | EffectChainTester tester(data, 1, size, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 35 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 36 | 37 | EXPECT_EQ(0.0f, out_data[3]); 38 | EXPECT_EQ(0.0f, out_data[7]); 39 | } 40 | 41 | } // namespace movit 42 | -------------------------------------------------------------------------------- /lift_gamma_gain_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "effect_util.h" 5 | #include "lift_gamma_gain_effect.h" 6 | #include "util.h" 7 | 8 | using namespace std; 9 | 10 | namespace movit { 11 | 12 | LiftGammaGainEffect::LiftGammaGainEffect() 13 | : lift(0.0f, 0.0f, 0.0f), 14 | gamma(1.0f, 1.0f, 1.0f), 15 | gain(1.0f, 1.0f, 1.0f) 16 | { 17 | register_vec3("lift", (float *)&lift); 18 | register_vec3("gamma", (float *)&gamma); 19 | register_vec3("gain", (float *)&gain); 20 | register_uniform_vec3("gain_pow_inv_gamma", (float *)&uniform_gain_pow_inv_gamma); 21 | register_uniform_vec3("inv_gamma_22", (float *)&uniform_inv_gamma22); 22 | } 23 | 24 | string LiftGammaGainEffect::output_fragment_shader() 25 | { 26 | return read_file("lift_gamma_gain_effect.frag"); 27 | } 28 | 29 | void LiftGammaGainEffect::set_gl_state(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) 30 | { 31 | Effect::set_gl_state(glsl_program_num, prefix, sampler_num); 32 | 33 | uniform_gain_pow_inv_gamma = RGBTriplet( 34 | pow(gain.r, 1.0f / gamma.r), 35 | pow(gain.g, 1.0f / gamma.g), 36 | pow(gain.b, 1.0f / gamma.b)); 37 | 38 | uniform_inv_gamma22 = RGBTriplet( 39 | 2.2f / gamma.r, 40 | 2.2f / gamma.g, 41 | 2.2f / gamma.b); 42 | } 43 | 44 | } // namespace movit 45 | -------------------------------------------------------------------------------- /unsharp_mask_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "blur_effect.h" 5 | #include "effect_chain.h" 6 | #include "mix_effect.h" 7 | #include "unsharp_mask_effect.h" 8 | #include "util.h" 9 | 10 | using namespace std; 11 | 12 | namespace movit { 13 | 14 | UnsharpMaskEffect::UnsharpMaskEffect() 15 | : blur(new BlurEffect), 16 | mix(new MixEffect) 17 | { 18 | CHECK(mix->set_float("strength_first", 1.0f)); 19 | CHECK(mix->set_float("strength_second", -0.3f)); 20 | } 21 | 22 | void UnsharpMaskEffect::rewrite_graph(EffectChain *graph, Node *self) 23 | { 24 | assert(self->incoming_links.size() == 1); 25 | Node *input = self->incoming_links[0]; 26 | 27 | Node *blur_node = graph->add_node(blur); 28 | Node *mix_node = graph->add_node(mix); 29 | graph->replace_receiver(self, mix_node); 30 | graph->connect_nodes(input, blur_node); 31 | graph->connect_nodes(blur_node, mix_node); 32 | graph->replace_sender(self, mix_node); 33 | 34 | self->disabled = true; 35 | } 36 | 37 | bool UnsharpMaskEffect::set_float(const string &key, float value) { 38 | if (key == "amount") { 39 | bool ok = mix->set_float("strength_first", 1.0f + value); 40 | return ok && mix->set_float("strength_second", -value); 41 | } 42 | return blur->set_float(key, value); 43 | } 44 | 45 | } // namespace movit 46 | -------------------------------------------------------------------------------- /resize_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_RESIZE_EFFECT_H 2 | #define _MOVIT_RESIZE_EFFECT_H 1 3 | 4 | // An effect that simply resizes the picture to a given output size 5 | // (set by the two integer parameters "width" and "height"). 6 | // Mostly useful as part of other algorithms. 7 | 8 | #include 9 | 10 | #include "effect.h" 11 | 12 | namespace movit { 13 | 14 | class ResizeEffect : public Effect { 15 | public: 16 | ResizeEffect(); 17 | std::string effect_type_id() const override { return "ResizeEffect"; } 18 | std::string output_fragment_shader() override; 19 | 20 | // We want processing done pre-filtering and mipmapped, 21 | // in case we need to scale down a lot. 22 | bool needs_texture_bounce() const override { return true; } 23 | MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; } 24 | AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; } 25 | 26 | bool changes_output_size() const override { return true; } 27 | bool sets_virtual_output_size() const override { return false; } 28 | void get_output_size(unsigned *width, unsigned *height, unsigned *virtual_width, unsigned *virtual_height) const override; 29 | 30 | private: 31 | int width, height; 32 | }; 33 | 34 | } // namespace movit 35 | 36 | #endif // !defined(_MOVIT_RESIZE_EFFECT_H) 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.lo 3 | *.la 4 | *.ld 5 | *.a 6 | *.d 7 | *.jpg 8 | *.png 9 | *.gcda 10 | *.gcno 11 | perf.data 12 | *.dot 13 | demo 14 | effect_chain_test 15 | compute_shader_test 16 | gamma_compression_effect_test 17 | gamma_expansion_effect_test 18 | alpha_multiplication_effect_test 19 | alpha_division_effect_test 20 | colorspace_conversion_effect_test 21 | mix_effect_test 22 | overlay_effect_test 23 | saturation_effect_test 24 | deconvolution_sharpen_effect_test 25 | blur_effect_test 26 | unsharp_mask_effect_test 27 | diffusion_effect_test 28 | white_balance_effect_test 29 | lift_gamma_gain_effect_test 30 | resample_effect_test 31 | dither_effect_test 32 | glow_effect_test 33 | padding_effect_test 34 | flat_input_test 35 | ycbcr_input_test 36 | ycbcr_422interleaved_input_test 37 | complex_modulate_effect_test 38 | fft_pass_effect_test 39 | fft_convolution_effect_test 40 | fp16_test 41 | luma_mix_effect_test 42 | slice_effect_test 43 | vignette_effect_test 44 | ycbcr_conversion_effect_test 45 | deinterlace_effect_test 46 | chain-*.frag 47 | chain-*.comp 48 | movit.info 49 | coverage/ 50 | aclocal.m4 51 | autogen.sh 52 | autom4te.cache/ 53 | config.h.in 54 | config.log 55 | config.status 56 | .libs 57 | config.guess 58 | config.sub 59 | install-sh 60 | libtool 61 | ltmain.sh 62 | m4 63 | configure 64 | Makefile 65 | movit.pc 66 | .qtc_clangd/ 67 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_CONFIG_MACRO_DIR([m4]) 2 | AC_INIT(movit, git) 3 | LT_INIT 4 | PKG_PROG_PKG_CONFIG 5 | 6 | AC_CONFIG_SRCDIR(effect.cpp) 7 | AC_CONFIG_AUX_DIR(.) 8 | 9 | AC_PROG_CC 10 | AC_PROG_CXX 11 | PKG_CHECK_MODULES([Eigen3], [eigen3]) 12 | PKG_CHECK_MODULES([epoxy], [epoxy]) 13 | PKG_CHECK_MODULES([FFTW3], [fftw3]) 14 | 15 | CXXFLAGS="$CXXFLAGS -std=gnu++14" 16 | 17 | # Needed for unit tests and the demo app. 18 | with_demo_app=yes 19 | PKG_CHECK_MODULES([SDL2], [sdl2]) 20 | 21 | # This is only needed for the demo app. 22 | PKG_CHECK_MODULES([SDL2_image], [SDL2_image], [], [with_demo_app=no; AC_MSG_WARN([SDL2_image not found, demo program will not be built])]) 23 | PKG_CHECK_MODULES([libpng], [libpng], [], [with_demo_app=no; AC_MSG_WARN([libpng not found, demo program will not be built])]) 24 | 25 | # This is only needed for microbenchmarks, so optional. 26 | PKG_CHECK_MODULES([benchmark], [benchmark], [with_benchmark=yes], [with_benchmark=no; AC_MSG_WARN([Google microbenchmark framework not found, microbenchmarks will not be built])]) 27 | 28 | AC_SUBST([with_demo_app]) 29 | AC_SUBST([with_benchmark]) 30 | 31 | with_coverage=no 32 | AC_ARG_ENABLE([coverage], [ --enable-coverage build with information needed to compute test coverage], [with_coverage=yes]) 33 | AC_SUBST([with_coverage]) 34 | 35 | AC_CONFIG_FILES([Makefile movit.pc]) 36 | AC_OUTPUT 37 | -------------------------------------------------------------------------------- /input.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_INPUT_H 2 | #define _MOVIT_INPUT_H 1 3 | 4 | #include 5 | 6 | #include "effect.h" 7 | #include "image_format.h" 8 | 9 | namespace movit { 10 | 11 | // An input is a degenerate case of an effect; it represents the picture data 12 | // that comes from the user. As such, it has zero “inputs” itself. 13 | // 14 | // An input is, like any other effect, required to be able to output a GLSL 15 | // fragment giving a RGBA value (although that GLSL fragment will have zero 16 | // inputs itself), and set the required OpenGL state on set_gl_state(), 17 | // including possibly uploading the texture if so required. 18 | class Input : public Effect { 19 | public: 20 | unsigned num_inputs() const override { return 0; } 21 | 22 | // Whether this input can deliver linear gamma directly if it's 23 | // asked to. (If so, set the parameter “output_linear_gamma” 24 | // to activate it.) 25 | virtual bool can_output_linear_gamma() const = 0; 26 | 27 | // Whether this input can supply mipmaps if asked to (by setting 28 | // the "needs_mipmaps" integer parameter set to 1). 29 | virtual bool can_supply_mipmaps() const { return true; } 30 | 31 | virtual unsigned get_width() const = 0; 32 | virtual unsigned get_height() const = 0; 33 | virtual Colorspace get_color_space() const = 0; 34 | virtual GammaCurve get_gamma_curve() const = 0; 35 | }; 36 | 37 | } // namespace movit 38 | 39 | #endif // !defined(_MOVIT_INPUT_H) 40 | -------------------------------------------------------------------------------- /ycbcr_input.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform sampler2D PREFIX(tex_y); 3 | // uniform sampler2D PREFIX(tex_cbcr); // If CB_CR_SAME_TEXTURE. 4 | // uniform sampler2D PREFIX(tex_cb); // If not CB_CR_SAME_TEXTURE. 5 | // uniform sampler2D PREFIX(tex_cr); // If not CB_CR_SAME_TEXTURE. 6 | // uniform mat3 PREFIX(ycbcr_matrix); 7 | // uniform vec3 PREFIX(offset); 8 | // uniform vec2 PREFIX(cb_offset); 9 | // uniform vec2 PREFIX(cr_offset); 10 | 11 | vec4 FUNCNAME(vec2 tc) { 12 | // OpenGL's origin is bottom-left, but most graphics software assumes 13 | // a top-left origin. Thus, for inputs that come from the user, 14 | // we flip the y coordinate. 15 | tc.y = 1.0 - tc.y; 16 | 17 | vec3 ycbcr; 18 | #if Y_CB_CR_SAME_TEXTURE 19 | ycbcr = tex2D(PREFIX(tex_y), tc).xyz; 20 | #else 21 | ycbcr.x = tex2D(PREFIX(tex_y), tc).x; 22 | #if CB_CR_SAME_TEXTURE 23 | #if CB_CR_OFFSETS_EQUAL 24 | ycbcr.yz = tex2D(PREFIX(tex_cbcr), tc + PREFIX(cb_offset)).xy; 25 | #else 26 | ycbcr.y = tex2D(PREFIX(tex_cbcr), tc + PREFIX(cb_offset)).x; 27 | ycbcr.z = tex2D(PREFIX(tex_cbcr), tc + PREFIX(cr_offset)).x; 28 | #endif 29 | #else 30 | ycbcr.y = tex2D(PREFIX(tex_cb), tc + PREFIX(cb_offset)).x; 31 | ycbcr.z = tex2D(PREFIX(tex_cr), tc + PREFIX(cr_offset)).x; 32 | #endif 33 | #endif 34 | 35 | ycbcr -= PREFIX(offset); 36 | 37 | vec4 rgba; 38 | rgba.rgb = PREFIX(inv_ycbcr_matrix) * ycbcr; 39 | rgba.a = 1.0; 40 | return rgba; 41 | } 42 | -------------------------------------------------------------------------------- /colorspace_conversion_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_COLORSPACE_CONVERSION_EFFECT_H 2 | #define _MOVIT_COLORSPACE_CONVERSION_EFFECT_H 1 3 | 4 | // An effect to convert between different color spaces. 5 | // Can convert freely between sRGB/Rec. 709 and the two different Rec. 601 6 | // color spaces (which thankfully have the same white point). 7 | // 8 | // We don't do any fancy gamut mapping or similar; colors that are out-of-gamut 9 | // will simply stay out-of-gamut, and probably clip in the output stage. 10 | 11 | #include 12 | #include 13 | 14 | #include "effect.h" 15 | #include "image_format.h" 16 | 17 | namespace movit { 18 | 19 | class ColorspaceConversionEffect : public Effect { 20 | private: 21 | // Should not be instantiated by end users. 22 | ColorspaceConversionEffect(); 23 | friend class EffectChain; 24 | 25 | public: 26 | std::string effect_type_id() const override { return "ColorspaceConversionEffect"; } 27 | std::string output_fragment_shader() override; 28 | 29 | bool needs_srgb_primaries() const override { return false; } 30 | AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; } 31 | bool strong_one_to_one_sampling() const override { return true; } 32 | 33 | // Get a conversion matrix from the given color space to XYZ. 34 | static Eigen::Matrix3d get_xyz_matrix(Colorspace space); 35 | 36 | private: 37 | Colorspace source_space, destination_space; 38 | }; 39 | 40 | } // namespace movit 41 | 42 | #endif // !defined(_MOVIT_COLORSPACE_CONVERSION_EFFECT_H) 43 | -------------------------------------------------------------------------------- /unsharp_mask_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_UNSHARP_MASK_EFFECT_H 2 | #define _MOVIT_UNSHARP_MASK_EFFECT_H 1 3 | 4 | // Unsharp mask is probably the most popular way of doing sharpening today, 5 | // although it does not always deliver the best results (it is very prone 6 | // to haloing). It simply consists of removing a blurred copy of the image from 7 | // itself (multiplied by some strength factor). In this aspect, it's similar to 8 | // glow, except by subtracting instead of adding. 9 | // 10 | // See DeconvolutionSharpenEffect for a different, possibly better 11 | // sharpening algorithm. 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | #include "effect.h" 18 | 19 | namespace movit { 20 | 21 | class BlurEffect; 22 | class EffectChain; 23 | class MixEffect; 24 | class Node; 25 | 26 | class UnsharpMaskEffect : public Effect { 27 | public: 28 | UnsharpMaskEffect(); 29 | std::string effect_type_id() const override { return "UnsharpMaskEffect"; } 30 | 31 | bool needs_srgb_primaries() const override { return false; } 32 | 33 | void rewrite_graph(EffectChain *graph, Node *self) override; 34 | bool set_float(const std::string &key, float value) override; 35 | 36 | std::string output_fragment_shader() override { 37 | assert(false); 38 | } 39 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override { 40 | assert(false); 41 | } 42 | 43 | private: 44 | BlurEffect *blur; 45 | MixEffect *mix; 46 | }; 47 | 48 | } // namespace movit 49 | 50 | #endif // !defined(_MOVIT_UNSHARP_MASK_EFFECT_H) 51 | -------------------------------------------------------------------------------- /luma_mix_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_LUMA_MIX_EFFECT_H 2 | #define _MOVIT_LUMA_MIX_EFFECT_H 1 3 | 4 | // Fade between two images based on a third monochrome one; lighter pixels 5 | // will be faded before darker pixels (unless the inverse flag is set, 6 | // in which case darker pixels will be faded before lighter pixels). 7 | // This allows a wide range of different video wipes implemented using 8 | // a single effect. 9 | // 10 | // Note that despite the name, the third input's _red_ channel is what's used 11 | // for transitions; there is no luma calculation done. If you need that, 12 | // put a SaturationEffect in front to desaturate (which calculates luma). 13 | 14 | #include 15 | 16 | #include "effect.h" 17 | 18 | namespace movit { 19 | 20 | class LumaMixEffect : public Effect { 21 | public: 22 | LumaMixEffect(); 23 | std::string effect_type_id() const override { return "LumaMixEffect"; } 24 | std::string output_fragment_shader() override; 25 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 26 | 27 | bool needs_srgb_primaries() const override { return false; } 28 | unsigned num_inputs() const override { return 3; } 29 | bool strong_one_to_one_sampling() const override { return true; } 30 | AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; } 31 | 32 | private: 33 | float transition_width, progress; 34 | int inverse; // 0 or 1. 35 | bool uniform_inverse; 36 | float uniform_progress_mul_w_plus_one; 37 | }; 38 | 39 | } // namespace movit 40 | 41 | #endif // !defined(_MOVIT_MIX_EFFECT_H) 42 | -------------------------------------------------------------------------------- /diffusion_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "blur_effect.h" 5 | #include "diffusion_effect.h" 6 | #include "effect_chain.h" 7 | #include "util.h" 8 | 9 | using namespace std; 10 | 11 | namespace movit { 12 | 13 | DiffusionEffect::DiffusionEffect() 14 | : blur(new BlurEffect), 15 | overlay_matte(new OverlayMatteEffect), 16 | owns_overlay_matte(true) 17 | { 18 | } 19 | 20 | DiffusionEffect::~DiffusionEffect() 21 | { 22 | if (owns_overlay_matte) { 23 | delete overlay_matte; 24 | } 25 | } 26 | 27 | void DiffusionEffect::rewrite_graph(EffectChain *graph, Node *self) 28 | { 29 | assert(self->incoming_links.size() == 1); 30 | Node *input = self->incoming_links[0]; 31 | 32 | Node *blur_node = graph->add_node(blur); 33 | Node *overlay_matte_node = graph->add_node(overlay_matte); 34 | owns_overlay_matte = false; 35 | graph->replace_receiver(self, overlay_matte_node); 36 | graph->connect_nodes(input, blur_node); 37 | graph->connect_nodes(blur_node, overlay_matte_node); 38 | graph->replace_sender(self, overlay_matte_node); 39 | 40 | self->disabled = true; 41 | } 42 | 43 | bool DiffusionEffect::set_float(const string &key, float value) { 44 | if (key == "blurred_mix_amount") { 45 | return overlay_matte->set_float(key, value); 46 | } 47 | return blur->set_float(key, value); 48 | } 49 | 50 | OverlayMatteEffect::OverlayMatteEffect() 51 | : blurred_mix_amount(0.3f) 52 | { 53 | register_float("blurred_mix_amount", &blurred_mix_amount); 54 | } 55 | 56 | string OverlayMatteEffect::output_fragment_shader() 57 | { 58 | return read_file("overlay_matte_effect.frag"); 59 | } 60 | 61 | } // namespace movit 62 | -------------------------------------------------------------------------------- /vignette_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "effect_util.h" 6 | #include "util.h" 7 | #include "vignette_effect.h" 8 | 9 | using namespace std; 10 | 11 | namespace movit { 12 | 13 | VignetteEffect::VignetteEffect() 14 | : center(0.5f, 0.5f), 15 | uniform_aspect_correction(1.0f, 1.0f), 16 | uniform_flipped_center(0.5f, 0.5f), 17 | radius(0.3f), 18 | inner_radius(0.3f) 19 | { 20 | register_vec2("center", (float *)¢er); 21 | register_float("radius", (float *)&radius); 22 | register_float("inner_radius", (float *)&inner_radius); 23 | register_uniform_float("pihalf_div_radius", &uniform_pihalf_div_radius); 24 | register_uniform_vec2("aspect_correction", (float *)&uniform_aspect_correction); 25 | register_uniform_vec2("flipped_center", (float *)&uniform_flipped_center); 26 | } 27 | 28 | string VignetteEffect::output_fragment_shader() 29 | { 30 | return read_file("vignette_effect.frag"); 31 | } 32 | 33 | void VignetteEffect::inform_input_size(unsigned input_num, unsigned width, unsigned height) { 34 | assert(input_num == 0); 35 | if (width >= height) { 36 | uniform_aspect_correction = Point2D(float(width) / float(height), 1.0f); 37 | } else { 38 | uniform_aspect_correction = Point2D(1.0f, float(height) / float(width)); 39 | } 40 | } 41 | 42 | void VignetteEffect::set_gl_state(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) 43 | { 44 | Effect::set_gl_state(glsl_program_num, prefix, sampler_num); 45 | 46 | uniform_pihalf_div_radius = 0.5 * M_PI / radius; 47 | uniform_flipped_center = Point2D(center.x, 1.0f - center.y); 48 | } 49 | 50 | } // namespace movit 51 | -------------------------------------------------------------------------------- /gamma_compression_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_GAMMA_COMPRESSION_EFFECT_H 2 | #define _MOVIT_GAMMA_COMPRESSION_EFFECT_H 1 3 | 4 | // An effect to convert linear light to the given gamma curve, 5 | // typically inserted by the framework automatically at the end 6 | // of the processing chain. 7 | // 8 | // Currently supports sRGB, Rec. 601/709 and Rec. 2020 (10- and 12-bit). 9 | // Note that Movit's internal formats generally do not have enough accuracy 10 | // for 12-bit input or output. 11 | 12 | #include 13 | #include 14 | 15 | #include "effect.h" 16 | #include "image_format.h" 17 | 18 | namespace movit { 19 | 20 | class GammaCompressionEffect : public Effect { 21 | private: 22 | // Should not be instantiated by end users. 23 | GammaCompressionEffect(); 24 | friend class EffectChain; 25 | 26 | public: 27 | std::string effect_type_id() const override { return "GammaCompressionEffect"; } 28 | std::string output_fragment_shader() override; 29 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 30 | 31 | bool needs_srgb_primaries() const override { return false; } 32 | bool strong_one_to_one_sampling() const override { return true; } 33 | 34 | // Actually needs postmultiplied input as well as outputting it. 35 | // EffectChain will take care of that. 36 | AlphaHandling alpha_handling() const override { return OUTPUT_POSTMULTIPLIED_ALPHA; } 37 | 38 | private: 39 | GammaCurve destination_curve; 40 | float uniform_linear_scale, uniform_c[5], uniform_clog[5], uniform_beta, uniform_lambda; 41 | }; 42 | 43 | } // namespace movit 44 | 45 | #endif // !defined(_MOVIT_GAMMA_COMPRESSION_EFFECT_H) 46 | -------------------------------------------------------------------------------- /overlay_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_OVERLAY_EFFECT_H 2 | #define _MOVIT_OVERLAY_EFFECT_H 1 3 | 4 | // Put one image on top of another, using alpha where appropriate. 5 | // (If both images are the same aspect and the top image has alpha=1.0 6 | // for all pixels, you will not see anything of the one on the bottom.) 7 | // 8 | // This is the “over” operation from Porter-Duff blending, also used 9 | // when merging layers in e.g. GIMP or Photoshop. 10 | // 11 | // The first input is the bottom, and the second is the top. 12 | 13 | #include 14 | 15 | #include "effect.h" 16 | 17 | namespace movit { 18 | 19 | class OverlayEffect : public Effect { 20 | public: 21 | OverlayEffect(); 22 | std::string effect_type_id() const override { return "OverlayEffect"; } 23 | std::string output_fragment_shader() override; 24 | 25 | bool needs_srgb_primaries() const override { return false; } 26 | unsigned num_inputs() const override { return 2; } 27 | bool strong_one_to_one_sampling() const override { return true; } 28 | 29 | // Actually, if _either_ image has blank alpha, our output will have 30 | // blank alpha, too (this only tells the framework that having _both_ 31 | // images with blank alpha would result in blank alpha). 32 | // However, understanding that would require changes 33 | // to EffectChain, so postpone that optimization for later. 34 | AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; } 35 | 36 | private: 37 | // If true, overlays input1 on top of input2 instead of vice versa. 38 | // Must be set before finalize. 39 | bool swap_inputs; 40 | }; 41 | 42 | } // namespace movit 43 | 44 | #endif // !defined(_MOVIT_OVERLAY_EFFECT_H) 45 | -------------------------------------------------------------------------------- /deconvolution_sharpen_effect.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform vec4 PREFIX(samples)[(R + 1) * (R + 1)]; 3 | 4 | vec4 FUNCNAME(vec2 tc) { 5 | // The full matrix has five different symmetry cases, that look like this: 6 | // 7 | // D D D C D D D 8 | // D D D C D D D 9 | // D D D C D D D 10 | // B B B A B B B 11 | // D D D C D D D 12 | // D D D C D D D 13 | // D D D C D D D 14 | // 15 | // We only store the lower-right part of the matrix: 16 | // 17 | // A B B B 18 | // C D D D 19 | // C D D D 20 | // C D D D 21 | 22 | // Case A: Top-left sample has no symmetry. 23 | vec4 sum = PREFIX(samples)[0].z * INPUT(tc); 24 | 25 | // Case B: Uppermost samples have left/right symmetry. 26 | for (int x = 1; x <= R; ++x) { 27 | vec4 sample = PREFIX(samples)[x]; 28 | sum += sample.z * (INPUT(tc - sample.xy) + INPUT(tc + sample.xy)); 29 | } 30 | 31 | // Case C: Leftmost samples have top/bottom symmetry. 32 | for (int y = 1; y <= R; ++y) { 33 | vec4 sample = PREFIX(samples)[y * (R + 1)]; 34 | sum += sample.z * (INPUT(tc - sample.xy) + INPUT(tc + sample.xy)); 35 | } 36 | 37 | // Case D: All other samples have four-way symmetry. 38 | // (Actually we have eight-way, but since we are using normalized 39 | // coordinates, we can't just flip x and y.) 40 | for (int y = 1; y <= R; ++y) { 41 | for (int x = 1; x <= R; ++x) { 42 | vec4 sample = PREFIX(samples)[y * (R + 1) + x]; 43 | vec2 mirror_sample = vec2(sample.x, -sample.y); 44 | 45 | vec4 local_sum = INPUT(tc - sample.xy) + INPUT(tc + sample.xy); 46 | local_sum += INPUT(tc - mirror_sample.xy) + INPUT(tc + mirror_sample.xy); 47 | sum += sample.z * local_sum; 48 | } 49 | } 50 | 51 | return sum; 52 | } 53 | 54 | #undef R 55 | -------------------------------------------------------------------------------- /gamma_expansion_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_GAMMA_EXPANSION_EFFECT_H 2 | #define _MOVIT_GAMMA_EXPANSION_EFFECT_H 1 3 | 4 | // An effect to convert the given gamma curve into linear light, 5 | // typically inserted by the framework automatically at the beginning 6 | // of the processing chain. 7 | // 8 | // Currently supports sRGB, Rec. 601/709 and Rec. 2020 (10- and 12-bit). 9 | // Note that Movit's internal formats generally do not have enough accuracy 10 | // for 12-bit input or output. 11 | 12 | #include 13 | #include 14 | 15 | #include "effect.h" 16 | #include "image_format.h" 17 | 18 | namespace movit { 19 | 20 | class GammaExpansionEffect : public Effect { 21 | private: 22 | // Should not be instantiated by end users. 23 | GammaExpansionEffect(); 24 | friend class EffectChain; 25 | 26 | public: 27 | std::string effect_type_id() const override { return "GammaExpansionEffect"; } 28 | std::string output_fragment_shader() override; 29 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 30 | 31 | bool needs_linear_light() const override { return false; } 32 | bool needs_srgb_primaries() const override { return false; } 33 | bool strong_one_to_one_sampling() const override { return true; } 34 | 35 | // Actually processes its input in a nonlinear fashion, 36 | // but does not touch alpha, and we are a special case anyway. 37 | AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; } 38 | 39 | private: 40 | GammaCurve source_curve; 41 | float uniform_linear_scale, uniform_c[5], uniform_clog[5], uniform_beta, uniform_lambda; 42 | }; 43 | 44 | } // namespace movit 45 | 46 | #endif // !defined(_MOVIT_GAMMA_EXPANSION_EFFECT_H) 47 | -------------------------------------------------------------------------------- /ycbcr_conversion_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_YCBCR_CONVERSION_EFFECT_H 2 | #define _MOVIT_YCBCR_CONVERSION_EFFECT_H 1 3 | 4 | // Converts from R'G'B' to Y'CbCr; that is, more or less the opposite of YCbCrInput, 5 | // except that it keeps the data as 4:4:4 chunked Y'CbCr; you'll need to subsample 6 | // and/or convert to planar somehow else. 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "effect.h" 13 | #include "ycbcr.h" 14 | 15 | namespace movit { 16 | 17 | class YCbCrConversionEffect : public Effect { 18 | private: 19 | // Should not be instantiated by end users; 20 | // call EffectChain::add_ycbcr_output() instead. 21 | YCbCrConversionEffect(const YCbCrFormat &ycbcr_format, GLenum type); 22 | friend class EffectChain; 23 | 24 | public: 25 | std::string effect_type_id() const override { return "YCbCrConversionEffect"; } 26 | std::string output_fragment_shader() override; 27 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 28 | AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; } 29 | bool strong_one_to_one_sampling() const override { return true; } 30 | 31 | // Should not be called by end users; call 32 | // EffectChain::change_ycbcr_output_format() instead. 33 | void change_output_format(const YCbCrFormat &ycbcr_format) { 34 | this->ycbcr_format = ycbcr_format; 35 | } 36 | 37 | private: 38 | YCbCrFormat ycbcr_format; 39 | GLenum type; 40 | 41 | Eigen::Matrix3d uniform_ycbcr_matrix; 42 | float uniform_offset[3]; 43 | bool uniform_clamp_range; 44 | float uniform_ycbcr_min[3], uniform_ycbcr_max[3]; 45 | }; 46 | 47 | } // namespace movit 48 | 49 | #endif // !defined(_MOVIT_YCBCR_CONVERSION_EFFECT_H) 50 | -------------------------------------------------------------------------------- /dither_effect.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform sampler2D PREFIX(dither_tex); 3 | // uniform vec2 PREFIX(tc_scale); 4 | // uniform float PREFIX(round_fac), PREFIX(inv_round_fac); 5 | 6 | // See footer.frag for details about this if statement. 7 | #ifndef YCBCR_ALSO_OUTPUT_RGBA 8 | #define YCBCR_ALSO_OUTPUT_RGBA 0 9 | #endif 10 | 11 | #if YCBCR_ALSO_OUTPUT_RGBA 12 | 13 | // There are two values to dither; otherwise, exactly the same as the algorithm below 14 | // (so comments are not duplicated). 15 | 16 | vec4[2] FUNCNAME(vec2 tc) { 17 | vec4[2] result = INPUT(tc); 18 | float d = tex2D(PREFIX(dither_tex), tc * PREFIX(tc_scale)).x; 19 | result[0].rgb += vec3(d); 20 | result[1].rgb += vec3(d); 21 | 22 | #if NEED_EXPLICIT_ROUND 23 | result[0] = round(result[0] * vec4(PREFIX(round_fac))) * vec4(PREFIX(inv_round_fac)); 24 | result[1] = round(result[1] * vec4(PREFIX(round_fac))) * vec4(PREFIX(inv_round_fac)); 25 | #endif 26 | 27 | return result; 28 | } 29 | 30 | #else 31 | 32 | vec4 FUNCNAME(vec2 tc) { 33 | vec4 result = INPUT(tc); 34 | float d = tex2D(PREFIX(dither_tex), tc * PREFIX(tc_scale)).x; 35 | 36 | // Don't dither alpha; the case of alpha=255 (1.0) is very important to us, 37 | // and if there's any inaccuracy earlier in the chain so that it becomes e.g. 38 | // 254.8, it's better to just get it rounded off than to dither and have it 39 | // possibly get down to 254. This is not the case for the color components. 40 | result.rgb += vec3(d); 41 | 42 | // NEED_EXPLICIT_ROUND will be #defined to 1 if the GPU has inaccurate 43 | // fp32 -> int8 framebuffer rounding, and 0 otherwise. 44 | #if NEED_EXPLICIT_ROUND 45 | result = round(result * vec4(PREFIX(round_fac))) * vec4(PREFIX(inv_round_fac)); 46 | #endif 47 | 48 | return result; 49 | } 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /glow_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "blur_effect.h" 5 | #include "effect_chain.h" 6 | #include "glow_effect.h" 7 | #include "mix_effect.h" 8 | #include "util.h" 9 | 10 | using namespace std; 11 | 12 | namespace movit { 13 | 14 | GlowEffect::GlowEffect() 15 | : blur(new BlurEffect), 16 | cutoff(new HighlightCutoffEffect), 17 | mix(new MixEffect) 18 | { 19 | CHECK(blur->set_float("radius", 20.0f)); 20 | CHECK(mix->set_float("strength_first", 1.0f)); 21 | CHECK(mix->set_float("strength_second", 1.0f)); 22 | CHECK(cutoff->set_float("cutoff", 0.2f)); 23 | } 24 | 25 | void GlowEffect::rewrite_graph(EffectChain *graph, Node *self) 26 | { 27 | assert(self->incoming_links.size() == 1); 28 | Node *input = self->incoming_links[0]; 29 | 30 | Node *blur_node = graph->add_node(blur); 31 | Node *mix_node = graph->add_node(mix); 32 | Node *cutoff_node = graph->add_node(cutoff); 33 | graph->replace_receiver(self, mix_node); 34 | graph->connect_nodes(input, cutoff_node); 35 | graph->connect_nodes(cutoff_node, blur_node); 36 | graph->connect_nodes(blur_node, mix_node); 37 | graph->replace_sender(self, mix_node); 38 | 39 | self->disabled = true; 40 | } 41 | 42 | bool GlowEffect::set_float(const string &key, float value) { 43 | if (key == "blurred_mix_amount") { 44 | return mix->set_float("strength_second", value); 45 | } 46 | if (key == "highlight_cutoff") { 47 | return cutoff->set_float("cutoff", value); 48 | } 49 | return blur->set_float(key, value); 50 | } 51 | 52 | HighlightCutoffEffect::HighlightCutoffEffect() 53 | : cutoff(0.0f) 54 | { 55 | register_float("cutoff", &cutoff); 56 | } 57 | 58 | string HighlightCutoffEffect::output_fragment_shader() 59 | { 60 | return read_file("highlight_cutoff_effect.frag"); 61 | } 62 | 63 | } // namespace movit 64 | -------------------------------------------------------------------------------- /image_format.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_IMAGE_FORMAT_H 2 | #define _MOVIT_IMAGE_FORMAT_H 1 3 | 4 | // Note: Movit's internal processing formats do not have enough 5 | // accuracy to support 12-bit input, so if you want to use Rec. 2020, 6 | // you should probably stick to 10-bit, or accept somewhat reduced 7 | // accuracy for 12-bit. Input depths above 8 bits are also generally 8 | // less tested. 9 | // 10 | // We also only support “conventional non-constant luminance” for Rec. 2020, 11 | // where Y' is derived from R'G'B' instead of RGB, since this is the same 12 | // system as used in Rec. 601 and 709. 13 | 14 | namespace movit { 15 | 16 | enum MovitPixelFormat { 17 | FORMAT_RGB, 18 | FORMAT_RGBA_PREMULTIPLIED_ALPHA, 19 | FORMAT_RGBA_POSTMULTIPLIED_ALPHA, 20 | FORMAT_BGR, 21 | FORMAT_BGRA_PREMULTIPLIED_ALPHA, 22 | FORMAT_BGRA_POSTMULTIPLIED_ALPHA, 23 | FORMAT_GRAYSCALE, 24 | FORMAT_RG, 25 | FORMAT_R 26 | }; 27 | 28 | enum Colorspace { 29 | COLORSPACE_INVALID = -1, // For internal use. 30 | COLORSPACE_REC_709 = 0, 31 | COLORSPACE_REC_601_525 = 1, 32 | COLORSPACE_REC_601_625 = 2, 33 | COLORSPACE_XYZ = 3, // Mostly useful for testing and debugging. 34 | COLORSPACE_REC_2020 = 4, 35 | COLORSPACE_sRGB = 5, // Used to be same as COLORSPACE_REC_709. 36 | }; 37 | 38 | enum GammaCurve { 39 | GAMMA_INVALID = -1, // For internal use. 40 | GAMMA_LINEAR = 0, 41 | GAMMA_sRGB = 1, 42 | GAMMA_REC_601 = 2, 43 | GAMMA_REC_709 = 2, // Same as Rec. 601. 44 | GAMMA_REC_2020_10_BIT = 2, // Same as Rec. 601. 45 | GAMMA_REC_2020_12_BIT = 3, 46 | GAMMA_HLG = 4, 47 | }; 48 | 49 | enum YCbCrLumaCoefficients { 50 | YCBCR_REC_601 = 0, 51 | YCBCR_REC_709 = 1, 52 | YCBCR_REC_2020 = 2, 53 | }; 54 | 55 | struct ImageFormat { 56 | Colorspace color_space; 57 | GammaCurve gamma_curve; 58 | }; 59 | 60 | } // namespace movit 61 | 62 | #endif // !defined(_MOVIT_IMAGE_FORMAT_H) 63 | -------------------------------------------------------------------------------- /effect_util.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_EFFECT_UTIL_H 2 | #define _MOVIT_EFFECT_UTIL_H 1 3 | 4 | // Utilities that are often useful for implementing Effect instances, 5 | // but don't need to be included from effect.h. 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "util.h" 16 | 17 | namespace movit { 18 | 19 | class EffectChain; 20 | class Node; 21 | 22 | // Convenience functions that deal with prepending the prefix. 23 | // Note that using EffectChain::register_uniform_*() is more efficient 24 | // than calling these from set_gl_state(). 25 | GLint get_uniform_location(GLuint glsl_program_num, const std::string &prefix, const std::string &key); 26 | void set_uniform_int(GLuint glsl_program_num, const std::string &prefix, const std::string &key, int value); 27 | void set_uniform_float(GLuint glsl_program_num, const std::string &prefix, const std::string &key, float value); 28 | void set_uniform_vec2(GLuint glsl_program_num, const std::string &prefix, const std::string &key, const float *values); 29 | void set_uniform_vec3(GLuint glsl_program_num, const std::string &prefix, const std::string &key, const float *values); 30 | void set_uniform_vec4(GLuint glsl_program_num, const std::string &prefix, const std::string &key, const float *values); 31 | void set_uniform_vec2_array(GLuint glsl_program_num, const std::string &prefix, const std::string &key, const float *values, size_t num_values); 32 | void set_uniform_vec4_array(GLuint glsl_program_num, const std::string &prefix, const std::string &key, const float *values, size_t num_values); 33 | void set_uniform_mat3(GLuint glsl_program_num, const std::string &prefix, const std::string &key, const Eigen::Matrix3d &matrix); 34 | 35 | } // namespace movit 36 | 37 | #endif // !defined(_MOVIT_EFFECT_UTIL_H) 38 | -------------------------------------------------------------------------------- /mix_effect.frag: -------------------------------------------------------------------------------- 1 | vec4 FUNCNAME(vec2 tc) { 2 | vec4 first = INPUT1(tc); 3 | vec4 second = INPUT2(tc); 4 | vec4 result = vec4(PREFIX(strength_first)) * first + vec4(PREFIX(strength_second)) * second; 5 | 6 | // Clamping alpha at some stage, either here or in AlphaDivisionEffect, 7 | // is actually very important for some use cases. Consider, for instance, 8 | // the case where we have additive blending (strength_first = strength_second = 1), 9 | // and add two 50% gray 100% opaque (0.5, 0.5, 0.5, 1.0) pixels. Without 10 | // alpha clamping, we'd get (1.0, 1.0, 1.0, 2.0), which would then in 11 | // conversion to postmultiplied be divided back to (0.5, 0.5, 0.5)! 12 | // Clamping alpha to 1.0 fixes the problem, and we get the expected result 13 | // of (1.0, 1.0, 1.0). Similarly, adding (0.5, 0.5, 0.5, 0.5) to itself 14 | // yields (1.0, 1.0, 1.0, 1.0) (100% white 100% opaque), which makes sense. 15 | // 16 | // The classic way of doing additive blending with premultiplied alpha 17 | // is to give the additive component alpha=0, but this also doesn't make 18 | // sense in a world where we could end up postmultiplied; just consider 19 | // the case where we have first=(0, 0, 0, 0) (ie., completely transparent) 20 | // and second=(0.5, 0.5, 0.5, 0.5) (ie., white at 50% opacity). 21 | // Zeroing out the alpha of second would yield (0.5, 0.5, 0.5, 0.0), 22 | // which has undefined RGB values in postmultiplied storage; certainly 23 | // e.g. (0, 0, 0, 0) would not be an expected output. Also, it would 24 | // break the expectation that A+B = B+A. 25 | // 26 | // Note that we do _not_ clamp RGB, since it might be useful to have 27 | // out-of-gamut colors. We could choose to do the alpha clamping in 28 | // AlphaDivisionEffect instead, though; I haven't thought a lot about 29 | // if that would be better or not. 30 | result.a = clamp(result.a, 0.0, 1.0); 31 | 32 | return result; 33 | } 34 | -------------------------------------------------------------------------------- /gtest_sdl_main.cpp: -------------------------------------------------------------------------------- 1 | #define GTEST_HAS_EXCEPTIONS 0 2 | 3 | #include 4 | #include 5 | #include 6 | #ifdef HAVE_BENCHMARK 7 | #include 8 | #endif 9 | #include 10 | #include 11 | 12 | #include "gtest/gtest.h" 13 | 14 | int main(int argc, char **argv) { 15 | // Set up an OpenGL context using SDL. 16 | if (SDL_Init(SDL_INIT_VIDEO) == -1) { 17 | fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); 18 | exit(1); 19 | } 20 | SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0); 21 | SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0); 22 | SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); 23 | 24 | // Use a core context, because Mesa only allows certain OpenGL versions in core. 25 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); 26 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); 27 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); 28 | 29 | // See also init.cpp for how to enable debugging. 30 | // SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); 31 | 32 | SDL_Window *window = SDL_CreateWindow("OpenGL window for unit test", 33 | SDL_WINDOWPOS_UNDEFINED, 34 | SDL_WINDOWPOS_UNDEFINED, 35 | 32, 32, 36 | SDL_WINDOW_OPENGL); 37 | SDL_GLContext context = SDL_GL_CreateContext(window); 38 | assert(context != nullptr); 39 | 40 | int err; 41 | if (argc >= 2 && strcmp(argv[1], "--benchmark") == 0) { 42 | #ifdef HAVE_BENCHMARK 43 | --argc; 44 | ::benchmark::Initialize(&argc, argv + 1); 45 | if (::benchmark::ReportUnrecognizedArguments(argc, argv)) return 1; 46 | ::benchmark::RunSpecifiedBenchmarks(); 47 | err = 0; 48 | #else 49 | fprintf(stderr, "No support for microbenchmarks compiled in.\n"); 50 | err = 1; 51 | #endif 52 | } else { 53 | testing::InitGoogleTest(&argc, argv); 54 | err = RUN_ALL_TESTS(); 55 | } 56 | SDL_Quit(); 57 | return err; 58 | } 59 | -------------------------------------------------------------------------------- /glow_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_GLOW_EFFECT_H 2 | #define _MOVIT_GLOW_EFFECT_H 1 3 | 4 | // Glow: Cut out the highlights of the image (everything above a certain threshold), 5 | // blur them, and overlay them onto the original image. 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "effect.h" 12 | 13 | namespace movit { 14 | 15 | class BlurEffect; 16 | class EffectChain; 17 | class HighlightCutoffEffect; 18 | class MixEffect; 19 | class Node; 20 | 21 | class GlowEffect : public Effect { 22 | public: 23 | GlowEffect(); 24 | std::string effect_type_id() const override { return "GlowEffect"; } 25 | 26 | bool needs_srgb_primaries() const override { return false; } 27 | 28 | void rewrite_graph(EffectChain *graph, Node *self) override; 29 | bool set_float(const std::string &key, float value) override; 30 | 31 | std::string output_fragment_shader() override { 32 | assert(false); 33 | } 34 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override { 35 | assert(false); 36 | } 37 | 38 | private: 39 | BlurEffect *blur; 40 | HighlightCutoffEffect *cutoff; 41 | MixEffect *mix; 42 | }; 43 | 44 | // An effect that cuts out only the highlights of an image; 45 | // anything at the cutoff or below is set to 0.0, and then all other pixels 46 | // get the cutoff subtracted. Used only as part of GlowEffect. 47 | 48 | class HighlightCutoffEffect : public Effect { 49 | public: 50 | HighlightCutoffEffect(); 51 | std::string effect_type_id() const override { return "HighlightCutoffEffect"; } 52 | std::string output_fragment_shader() override; 53 | 54 | AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; } 55 | bool strong_one_to_one_sampling() const override { return true; } 56 | 57 | private: 58 | float cutoff; 59 | }; 60 | 61 | } // namespace movit 62 | 63 | #endif // !defined(_MOVIT_GLOW_EFFECT_H) 64 | -------------------------------------------------------------------------------- /luma_mix_effect.frag: -------------------------------------------------------------------------------- 1 | // Implicit uniforms: 2 | // uniform float PREFIX(progress_mul_w_plus_one); 3 | // uniform bool PREFIX(bool_inverse); 4 | 5 | vec4 FUNCNAME(vec2 tc) { 6 | vec4 first = INPUT1(tc); 7 | vec4 second = INPUT2(tc); 8 | 9 | // We treat the luma as going from 0 to w, where w is the transition width 10 | // (wider means the boundary between transitioned and non-transitioned 11 | // will be harder, while w=0 is essentially just a straight fade). 12 | // We need to map this 0..w range in the luma image to a (clamped) 0..1 13 | // range for how far this pixel has come in a fade. At the very 14 | // beginning, we can visualize it like this, where every pixel is in 15 | // the state 0.0 (100% first image, 0% second image): 16 | // 17 | // 0 w 18 | // luma: |---------------------| 19 | // mix: |----| 20 | // 0 1 21 | // 22 | // Then as we progress, eventually the luma range should move to the right 23 | // so that more pixels start moving towards higher mix value: 24 | // 25 | // 0 w 26 | // luma: |---------------------| 27 | // mix: |----| 28 | // 0 1 29 | // 30 | // and at the very end, all pixels should be in the state 1.0 (0% first image, 31 | // 100% second image): 32 | // 33 | // 0 w 34 | // luma: |---------------------| 35 | // mix: |----| 36 | // 0 1 37 | // 38 | // So clearly, it should move (w+1) units to the right, and apart from that 39 | // just stay a simple mapping. 40 | float w = PREFIX(transition_width); 41 | float luma = INPUT3(tc).x; 42 | if (PREFIX(bool_inverse)) { 43 | luma = 1.0 - luma; 44 | } 45 | float m = clamp((luma * w - w) + PREFIX(progress_mul_w_plus_one), 0.0, 1.0); 46 | 47 | return mix(first, second, m); 48 | } 49 | -------------------------------------------------------------------------------- /slice_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_SLICE_EFFECT_H 2 | #define _MOVIT_SLICE_EFFECT_H 1 3 | 4 | // SliceEffect takes an image, cuts it into (potentially overlapping) slices, 5 | // and puts those slices back together again consecutively. It is primarily 6 | // useful in an overlap-discard setting, where it can do both the overlap and 7 | // discard roles, where one does convolutions by means of many small FFTs, but 8 | // could also work as a (relatively boring) video effect on its own. 9 | // 10 | // Note that vertical slices happen from the top, consistent with the rest of 11 | // Movit. 12 | 13 | #include 14 | #include 15 | 16 | #include "effect.h" 17 | 18 | namespace movit { 19 | 20 | class SliceEffect : public Effect { 21 | public: 22 | SliceEffect(); 23 | std::string effect_type_id() const override { return "SliceEffect"; } 24 | std::string output_fragment_shader() override; 25 | bool needs_texture_bounce() const override { return true; } 26 | bool changes_output_size() const override { return true; } 27 | bool sets_virtual_output_size() const override { return false; } 28 | void inform_input_size(unsigned input_num, unsigned width, unsigned height) override; 29 | void get_output_size(unsigned *width, unsigned *height, 30 | unsigned *virtual_width, unsigned *virtual_height) const override; 31 | 32 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 33 | void inform_added(EffectChain *chain) override { this->chain = chain; } 34 | 35 | enum Direction { HORIZONTAL = 0, VERTICAL = 1 }; 36 | 37 | private: 38 | EffectChain *chain; 39 | int input_width, input_height; 40 | int input_slice_size, output_slice_size; 41 | int offset; 42 | Direction direction; 43 | 44 | float uniform_output_coord_to_slice_num, uniform_slice_num_to_input_coord; 45 | float uniform_slice_offset_to_input_coord, uniform_offset; 46 | }; 47 | 48 | } // namespace movit 49 | 50 | #endif // !defined(_MOVIT_SLICE_EFFECT_H) 51 | -------------------------------------------------------------------------------- /lift_gamma_gain_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_LIFT_GAMMA_GAIN_EFFECT_H 2 | #define _MOVIT_LIFT_GAMMA_GAIN_EFFECT_H 1 3 | 4 | // A simple lift/gamma/gain effect, used for color grading. 5 | // 6 | // Very roughly speaking, lift=shadows, gamma=midtones and gain=highlights, 7 | // although all parameters affect the entire curve. Mathematically speaking, 8 | // it is a bit unusual to look at gamma as a color, but it works pretty well 9 | // in practice. 10 | // 11 | // The classic formula is: output = (gain * (x + lift * (1-x)))^(1/gamma). 12 | // 13 | // The lift is actually a case where we actually would _not_ want linear light; 14 | // since black by definition becomes equal to the lift color, we want lift to 15 | // be pretty close to black, but in linear light that means lift affects the 16 | // rest of the curve relatively little. Thus, we actually convert to gamma 2.2 17 | // before lift, and then back again afterwards. (Gain and gamma are, 18 | // up to constants, commutative with the de-gamma operation.) 19 | // 20 | // Also, gamma is a case where we would not want premultiplied alpha. 21 | // Thus, we have to divide away alpha first, and then re-multiply it back later. 22 | 23 | #include 24 | #include 25 | 26 | #include "effect.h" 27 | 28 | namespace movit { 29 | 30 | class LiftGammaGainEffect : public Effect { 31 | public: 32 | LiftGammaGainEffect(); 33 | std::string effect_type_id() const override { return "LiftGammaGainEffect"; } 34 | AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; } 35 | bool strong_one_to_one_sampling() const override { return true; } 36 | std::string output_fragment_shader() override; 37 | 38 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 39 | 40 | private: 41 | RGBTriplet lift, gamma, gain; 42 | RGBTriplet uniform_gain_pow_inv_gamma, uniform_inv_gamma22; 43 | }; 44 | 45 | } // namespace movit 46 | 47 | #endif // !defined(_MOVIT_LIFT_GAMMA_GAIN_EFFECT_H) 48 | -------------------------------------------------------------------------------- /complex_modulate_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "complex_modulate_effect.h" 4 | #include "effect_chain.h" 5 | #include "effect_util.h" 6 | #include "util.h" 7 | 8 | using namespace std; 9 | 10 | namespace movit { 11 | 12 | ComplexModulateEffect::ComplexModulateEffect() 13 | : num_repeats_x(1), num_repeats_y(1) 14 | { 15 | register_int("num_repeats_x", &num_repeats_x); 16 | register_int("num_repeats_y", &num_repeats_y); 17 | register_vec2("num_repeats", uniform_num_repeats); 18 | } 19 | 20 | string ComplexModulateEffect::output_fragment_shader() 21 | { 22 | return read_file("complex_modulate_effect.frag"); 23 | } 24 | 25 | void ComplexModulateEffect::set_gl_state(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) 26 | { 27 | Effect::set_gl_state(glsl_program_num, prefix, sampler_num); 28 | 29 | uniform_num_repeats[0] = float(num_repeats_x); 30 | uniform_num_repeats[1] = float(num_repeats_y); 31 | 32 | // Set the secondary input to repeat (and nearest while we're at it). 33 | Node *self = chain->find_node_for_effect(this); 34 | glActiveTexture(chain->get_input_sampler(self, 1)); 35 | check_error(); 36 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 37 | check_error(); 38 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 39 | check_error(); 40 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 41 | check_error(); 42 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 43 | check_error(); 44 | } 45 | 46 | void ComplexModulateEffect::inform_input_size(unsigned input_num, unsigned width, unsigned height) 47 | { 48 | if (input_num == 0) { 49 | primary_input_width = width; 50 | primary_input_height = height; 51 | } 52 | } 53 | 54 | void ComplexModulateEffect::get_output_size(unsigned *width, unsigned *height, 55 | unsigned *virtual_width, unsigned *virtual_height) const 56 | { 57 | *width = *virtual_width = primary_input_width; 58 | *height = *virtual_height = primary_input_height; 59 | } 60 | 61 | } // namespace movit 62 | -------------------------------------------------------------------------------- /dither_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for DitherEffect. 2 | // 3 | // Note: Dithering of multiple outputs is tested (somewhat weakly) 4 | // in YCbCrConversionEffectTest. 5 | 6 | #include 7 | #include 8 | 9 | #include "effect_chain.h" 10 | #include "gtest/gtest.h" 11 | #include "image_format.h" 12 | #include "test_util.h" 13 | #include "util.h" 14 | 15 | namespace movit { 16 | 17 | TEST(DitherEffectTest, NoDitherOnExactValues) { 18 | const int size = 4; 19 | 20 | float data[size * size] = { 21 | 0.0, 1.0, 0.0, 1.0, 22 | 0.0, 1.0, 1.0, 0.0, 23 | 0.0, 0.2, 1.0, 0.2, 24 | 0.0, 0.0, 0.0, 0.0, 25 | }; 26 | unsigned char expected_data[size * size] = { 27 | 0, 255, 0, 255, 28 | 0, 255, 255, 0, 29 | 0, 51, 255, 51, 30 | 0, 0, 0, 0, 31 | }; 32 | unsigned char out_data[size * size]; 33 | 34 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8); 35 | check_error(); 36 | tester.get_chain()->set_dither_bits(8); 37 | check_error(); 38 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 39 | check_error(); 40 | 41 | expect_equal(expected_data, out_data, size, size); 42 | } 43 | 44 | TEST(DitherEffectTest, SinusoidBelowOneLevelComesThrough) { 45 | const float frequency = 0.3f * M_PI; 46 | const unsigned size = 2048; 47 | const float amplitude = 0.25f / 255.0f; // 6 dB below what can be represented without dithering. 48 | 49 | float data[size]; 50 | for (unsigned i = 0; i < size; ++i) { 51 | data[i] = 0.2 + amplitude * sin(i * frequency); 52 | } 53 | unsigned char out_data[size]; 54 | 55 | EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8); 56 | tester.get_chain()->set_dither_bits(8); 57 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 58 | 59 | // Measure how strong the given sinusoid is in the output. 60 | float sum = 0.0f; 61 | for (unsigned i = 0; i < size; ++i) { 62 | sum += 2.0 * (int(out_data[i]) - 0.2*255.0) * sin(i * frequency); 63 | } 64 | 65 | EXPECT_NEAR(amplitude, sum / (size * 255.0f), 1.1e-5); 66 | } 67 | 68 | } // namespace movit 69 | -------------------------------------------------------------------------------- /diffusion_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_DIFFUSION_EFFECT_H 2 | #define _MOVIT_DIFFUSION_EFFECT_H 1 3 | 4 | // There are many different effects that go under the name of "diffusion", 5 | // seemingly all of the inspired by the effect you get when you put a 6 | // diffusion filter in front of your camera lens. The effect most people 7 | // want is a general flattening/smoothing of the light, and reduction of 8 | // fine detail (most notably, blemishes in people's skin), without ruining 9 | // edges, which a regular blur would do. 10 | // 11 | // We do a relatively simple version, sometimes known as "white diffusion", 12 | // where we first blur the picture, and then overlay it on the original 13 | // using the original as a matte. 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include "effect.h" 20 | 21 | namespace movit { 22 | 23 | class BlurEffect; 24 | class EffectChain; 25 | class Node; 26 | class OverlayMatteEffect; 27 | 28 | class DiffusionEffect : public Effect { 29 | public: 30 | DiffusionEffect(); 31 | ~DiffusionEffect(); 32 | std::string effect_type_id() const override { return "DiffusionEffect"; } 33 | 34 | void rewrite_graph(EffectChain *graph, Node *self) override; 35 | bool set_float(const std::string &key, float value) override; 36 | 37 | std::string output_fragment_shader() override { 38 | assert(false); 39 | } 40 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override { 41 | assert(false); 42 | } 43 | 44 | private: 45 | BlurEffect *blur; 46 | OverlayMatteEffect *overlay_matte; 47 | bool owns_overlay_matte; 48 | }; 49 | 50 | // Used internally by DiffusionEffect; combines the blurred and the original 51 | // version using the original as a matte. 52 | class OverlayMatteEffect : public Effect { 53 | public: 54 | OverlayMatteEffect(); 55 | std::string effect_type_id() const override { return "OverlayMatteEffect"; } 56 | std::string output_fragment_shader() override; 57 | AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; } 58 | 59 | unsigned num_inputs() const override { return 2; } 60 | bool strong_one_to_one_sampling() const override { return true; } 61 | 62 | private: 63 | float blurred_mix_amount; 64 | }; 65 | 66 | } // namespace movit 67 | 68 | #endif // !defined(_MOVIT_DIFFUSION_EFFECT_H) 69 | -------------------------------------------------------------------------------- /footer.frag: -------------------------------------------------------------------------------- 1 | // GLSL is pickier than the C++ preprocessor in if-testing for undefined 2 | // tokens; do some fixups here to keep it happy. 3 | 4 | #ifndef YCBCR_OUTPUT_PLANAR 5 | #define YCBCR_OUTPUT_PLANAR 0 6 | #endif 7 | 8 | #ifndef YCBCR_OUTPUT_SPLIT_Y_AND_CBCR 9 | #define YCBCR_OUTPUT_SPLIT_Y_AND_CBCR 0 10 | #endif 11 | 12 | #ifndef SECOND_YCBCR_OUTPUT_PLANAR 13 | #define SECOND_YCBCR_OUTPUT_PLANAR 0 14 | #endif 15 | 16 | #ifndef SECOND_YCBCR_OUTPUT_SPLIT_Y_AND_CBCR 17 | #define SECOND_YCBCR_OUTPUT_SPLIT_Y_AND_CBCR 0 18 | #endif 19 | 20 | #ifndef SECOND_YCBCR_OUTPUT_INTERLEAVED 21 | #define SECOND_YCBCR_OUTPUT_INTERLEAVED 0 22 | #endif 23 | 24 | #ifndef YCBCR_ALSO_OUTPUT_RGBA 25 | #define YCBCR_ALSO_OUTPUT_RGBA 0 26 | #endif 27 | 28 | #ifndef SQUARE_ROOT_TRANSFORMATION 29 | #define SQUARE_ROOT_TRANSFORMATION 0 30 | #endif 31 | 32 | #if YCBCR_OUTPUT_PLANAR 33 | out vec4 Y, Cb, Cr; 34 | #elif YCBCR_OUTPUT_SPLIT_Y_AND_CBCR 35 | out vec4 Y, Chroma; 36 | #else 37 | out vec4 FragColor; // Y'CbCr or RGBA. 38 | #endif 39 | 40 | #if SECOND_YCBCR_OUTPUT_PLANAR 41 | out vec4 Y2, Cb2, Cr2; 42 | #elif SECOND_YCBCR_OUTPUT_SPLIT_Y_AND_CBCR 43 | out vec4 Y2, Chroma2; 44 | #elif SECOND_YCBCR_OUTPUT_INTERLEAVED 45 | out vec4 YCbCr2; 46 | #endif 47 | 48 | #if YCBCR_ALSO_OUTPUT_RGBA 49 | out vec4 RGBA; 50 | #endif 51 | 52 | void main() 53 | { 54 | #if YCBCR_ALSO_OUTPUT_RGBA 55 | vec4 color[2] = INPUT(tc); 56 | vec4 color0 = color[0]; 57 | vec4 color1 = color[1]; 58 | #else 59 | vec4 color0 = INPUT(tc); 60 | #endif 61 | 62 | #if SQUARE_ROOT_TRANSFORMATION 63 | // Make sure we don't give negative values to sqrt. 64 | color0.rgb = sqrt(max(color0.rgb, 0.0)); 65 | #endif 66 | 67 | #if YCBCR_OUTPUT_PLANAR 68 | Y = color0.rrra; 69 | Cb = color0.ggga; 70 | Cr = color0.bbba; 71 | #elif YCBCR_OUTPUT_SPLIT_Y_AND_CBCR 72 | Y = color0.rrra; 73 | Chroma = color0.gbba; 74 | #else 75 | FragColor = color0; 76 | #endif 77 | 78 | // Exactly the same, just with other outputs. 79 | // (GLSL does not allow arrays of outputs.) 80 | #if SECOND_YCBCR_OUTPUT_PLANAR 81 | Y2 = color0.rrra; 82 | Cb2 = color0.ggga; 83 | Cr2 = color0.bbba; 84 | #elif SECOND_YCBCR_OUTPUT_SPLIT_Y_AND_CBCR 85 | Y2 = color0.rrra; 86 | Chroma2 = color0.gbba; 87 | #elif SECOND_YCBCR_OUTPUT_INTERLEAVED 88 | YCbCr2 = color0; 89 | #endif 90 | 91 | #if YCBCR_ALSO_OUTPUT_RGBA 92 | RGBA = color1; 93 | #endif 94 | } 95 | -------------------------------------------------------------------------------- /saturation_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for SaturationEffect. 2 | 3 | #include 4 | 5 | #include "effect_chain.h" 6 | #include "gtest/gtest.h" 7 | #include "image_format.h" 8 | #include "saturation_effect.h" 9 | #include "test_util.h" 10 | 11 | namespace movit { 12 | 13 | TEST(SaturationEffectTest, SaturationOneIsPassThrough) { 14 | float data[] = { 15 | 1.0f, 0.5f, 0.75f, 0.6f, 16 | }; 17 | float out_data[4]; 18 | EffectChainTester tester(data, 1, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 19 | Effect *saturation_effect = tester.get_chain()->add_effect(new SaturationEffect()); 20 | ASSERT_TRUE(saturation_effect->set_float("saturation", 1.0f)); 21 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 22 | 23 | expect_equal(data, out_data, 1, 1); 24 | } 25 | 26 | TEST(SaturationEffectTest, SaturationZeroRemovesColorButPreservesAlpha) { 27 | float data[] = { 28 | 0.0f, 0.0f, 0.0f, 1.0f, 29 | 0.5f, 0.5f, 0.5f, 0.3f, 30 | 1.0f, 0.0f, 0.0f, 1.0f, 31 | 0.0f, 1.0f, 0.0f, 0.7f, 32 | 0.0f, 0.0f, 1.0f, 1.0f, 33 | }; 34 | float expected_data[] = { 35 | 0.0f, 0.0f, 0.0f, 1.0f, 36 | 0.5f, 0.5f, 0.5f, 0.3f, 37 | 0.2126f, 0.2126f, 0.2126f, 1.0f, 38 | 0.7152f, 0.7152f, 0.7152f, 0.7f, 39 | 0.0722f, 0.0722f, 0.0722f, 1.0f, 40 | }; 41 | 42 | float out_data[5 * 4]; 43 | EffectChainTester tester(data, 5, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 44 | Effect *saturation_effect = tester.get_chain()->add_effect(new SaturationEffect()); 45 | ASSERT_TRUE(saturation_effect->set_float("saturation", 0.0f)); 46 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 47 | 48 | expect_equal(expected_data, out_data, 4, 5); 49 | } 50 | 51 | TEST(SaturationEffectTest, DoubleSaturation) { 52 | float data[] = { 53 | 0.0f, 0.0f, 0.0f, 1.0f, 54 | 0.5f, 0.5f, 0.5f, 0.3f, 55 | 0.3f, 0.1f, 0.1f, 1.0f, 56 | }; 57 | float expected_data[] = { 58 | 0.0f, 0.0f, 0.0f, 1.0f, 59 | 0.5f, 0.5f, 0.5f, 0.3f, 60 | 0.4570f, 0.0575f, 0.0575f, 1.0f, 61 | }; 62 | 63 | float out_data[3 * 4]; 64 | EffectChainTester tester(data, 3, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 65 | Effect *saturation_effect = tester.get_chain()->add_effect(new SaturationEffect()); 66 | ASSERT_TRUE(saturation_effect->set_float("saturation", 2.0f)); 67 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 68 | 69 | expect_equal(expected_data, out_data, 4, 3); 70 | } 71 | 72 | } // namespace movit 73 | -------------------------------------------------------------------------------- /diffusion_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for DiffusionEffect. 2 | 3 | #include 4 | 5 | #include "diffusion_effect.h" 6 | #include "effect_chain.h" 7 | #include "gtest/gtest.h" 8 | #include "image_format.h" 9 | #include "test_util.h" 10 | 11 | namespace movit { 12 | 13 | TEST(DiffusionEffectTest, IdentityTransformDoesNothing) { 14 | const int size = 4; 15 | 16 | float data[size * size] = { 17 | 0.0, 1.0, 0.0, 1.0, 18 | 0.0, 1.0, 1.0, 0.0, 19 | 0.0, 0.5, 1.0, 0.5, 20 | 0.0, 0.0, 0.0, 0.0, 21 | }; 22 | float out_data[size * size]; 23 | 24 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 25 | Effect *diffusion_effect = tester.get_chain()->add_effect(new DiffusionEffect()); 26 | ASSERT_TRUE(diffusion_effect->set_float("radius", 0.0f)); 27 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 28 | 29 | expect_equal(data, out_data, size, size); 30 | } 31 | 32 | TEST(DiffusionEffectTest, FlattensOutWhitePyramid) { 33 | const int size = 9; 34 | 35 | float data[size * size] = { 36 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 37 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 38 | 0.0, 0.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 39 | 0.0, 0.0, 0.5, 0.7, 0.7, 0.7, 0.5, 0.0, 0.0, 40 | 0.0, 0.0, 0.5, 0.7, 1.0, 0.7, 0.5, 0.0, 0.0, 41 | 0.0, 0.0, 0.5, 0.7, 0.7, 0.7, 0.5, 0.0, 0.0, 42 | 0.0, 0.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 43 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 44 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 45 | }; 46 | float expected_data[size * size] = { 47 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 48 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 49 | 0.0, 0.0, 0.4, 0.4, 0.4, 0.4, 0.4, 0.0, 0.0, 50 | 0.0, 0.0, 0.4, 0.5, 0.5, 0.5, 0.4, 0.0, 0.0, 51 | 0.0, 0.0, 0.4, 0.5, 0.6, 0.5, 0.4, 0.0, 0.0, 52 | 0.0, 0.0, 0.4, 0.5, 0.5, 0.5, 0.4, 0.0, 0.0, 53 | 0.0, 0.0, 0.4, 0.4, 0.4, 0.4, 0.4, 0.0, 0.0, 54 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 55 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 56 | }; 57 | float out_data[size * size]; 58 | 59 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 60 | Effect *diffusion_effect = tester.get_chain()->add_effect(new DiffusionEffect()); 61 | ASSERT_TRUE(diffusion_effect->set_float("radius", 2.0f)); 62 | ASSERT_TRUE(diffusion_effect->set_float("blurred_mix_amount", 0.7f)); 63 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 64 | 65 | expect_equal(expected_data, out_data, size, size, 0.05f, 0.002); 66 | } 67 | 68 | } // namespace movit 69 | -------------------------------------------------------------------------------- /resample_effect.frag: -------------------------------------------------------------------------------- 1 | // DIRECTION_VERTICAL will be #defined to 1 if we are scaling vertically, 2 | // and 0 otherwise. 3 | 4 | // Implicit uniforms: 5 | // uniform sampler2D PREFIX(sample_tex); 6 | // uniform int PREFIX(num_samples); 7 | // uniform float PREFIX(num_loops); 8 | // uniform float PREFIX(sample_x_scale); 9 | // uniform float PREFIX(sample_x_offset); 10 | // uniform float PREFIX(slice_height); 11 | 12 | // We put the fractional part of the offset (-0.5 to 0.5 pixels) in the weights 13 | // because we have to (otherwise they'd do nothing). However, the support texture 14 | // has limited numerical precision; we'd need as much of it as we can for 15 | // getting the subpixel sampling right, and adding a large constant to each value 16 | // will reduce the precision further. Thus, the non-fractional part of the offset 17 | // is sent in through a uniform that we simply add in. (It should be said that 18 | // for high values of (dst_size/num_loop), we're pretty much hosed anyway wrt. 19 | // this accuracy.) 20 | // 21 | // Unfortunately, we cannot just do it at the beginning of the shader, 22 | // since the texcoord value is used to index into the support texture, 23 | // and if zoom != 1, the support texture will not wrap properly, causing 24 | // us to read the wrong texels. (Also remember that whole_pixel_offset is 25 | // measured in _input_ pixels and tc is in _output_ pixels, although we could 26 | // compensate for that.) However, the shader should be mostly bandwidth bound 27 | // and not ALU bound, so an extra add per sample shouldn't be too hopeless. 28 | // 29 | // Implicitly declared: 30 | // uniform float PREFIX(whole_pixel_offset); 31 | 32 | // Sample a single weight. First fetch information about where to sample 33 | // and the weight from sample_tex, and then read the pixel itself. 34 | vec4 PREFIX(do_sample)(vec2 tc, int i) 35 | { 36 | vec2 sample_tc; 37 | sample_tc.x = float(i) * PREFIX(sample_x_scale) + PREFIX(sample_x_offset); 38 | #if DIRECTION_VERTICAL 39 | sample_tc.y = tc.y * PREFIX(num_loops); 40 | #else 41 | sample_tc.y = tc.x * PREFIX(num_loops); 42 | #endif 43 | vec2 sample = tex2D(PREFIX(sample_tex), sample_tc).rg; 44 | 45 | #if DIRECTION_VERTICAL 46 | tc.y = sample.g + (floor(sample_tc.y) * PREFIX(slice_height) + PREFIX(whole_pixel_offset)); 47 | #else 48 | tc.x = sample.g + (floor(sample_tc.y) * PREFIX(slice_height) + PREFIX(whole_pixel_offset)); 49 | #endif 50 | return vec4(sample.r) * INPUT(tc); 51 | } 52 | 53 | vec4 FUNCNAME(vec2 tc) { 54 | vec4 sum = PREFIX(do_sample)(tc, 0); 55 | for (int i = 1; i < PREFIX(num_samples); ++i) { 56 | sum += PREFIX(do_sample)(tc, i); 57 | } 58 | return sum; 59 | } 60 | 61 | #undef DIRECTION_VERTICAL 62 | -------------------------------------------------------------------------------- /complex_modulate_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_COMPLEX_MODULATE_EFFECT_H 2 | #define _MOVIT_COMPLEX_MODULATE_EFFECT_H 1 3 | 4 | // An effect that treats each pixel as two complex numbers (xy and zw), 5 | // and multiplies it with some other complex number (xy and xy, so the 6 | // same in both cases). The latter can be repeated both horizontally and 7 | // vertically if desired. 8 | // 9 | // The typical use is to implement convolution by way of FFT; since 10 | // FFT(A ⊙ B) = FFT(A) * FFT(B), you can FFT both inputs (where B 11 | // would often even be a constant, so you'd only need to do FFT once), 12 | // multiply them together and then IFFT the result to get a convolution. 13 | // 14 | // It is in a sense “wrong” to do this directly on pixels, since the color 15 | // channels are independent and real-valued (ie., not complex numbers), but 16 | // since convolution is a linear operation, it's unproblematic to treat R + Gi 17 | // as a single complex number and B + Ai and another one; barring numerical 18 | // errors, there should be no leakage between the channels as long as you're 19 | // convolving with a real quantity. (There are more sophisticated ways of doing 20 | // two real FFTs with a single complex one, but we won't need them, as we 21 | // don't care about the actual FFT result, just that the convolution property 22 | // holds.) 23 | 24 | #include 25 | #include 26 | 27 | #include "effect.h" 28 | 29 | namespace movit { 30 | 31 | class EffectChain; 32 | 33 | class ComplexModulateEffect : public Effect { 34 | public: 35 | ComplexModulateEffect(); 36 | std::string effect_type_id() const override { return "ComplexModulateEffect"; } 37 | std::string output_fragment_shader() override; 38 | 39 | // Technically we only need texture bounce for the second input 40 | // (to be allowed to mess with its sampler state), but there's 41 | // no way of expressing that currently. 42 | bool needs_texture_bounce() const override { return true; } 43 | bool changes_output_size() const override { return true; } 44 | bool sets_virtual_output_size() const override { return false; } 45 | 46 | void inform_input_size(unsigned input_num, unsigned width, unsigned height) override; 47 | void get_output_size(unsigned *width, unsigned *height, 48 | unsigned *virtual_width, unsigned *virtual_height) const override; 49 | unsigned num_inputs() const override { return 2; } 50 | void inform_added(EffectChain *chain) override { this->chain = chain; } 51 | 52 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 53 | 54 | private: 55 | EffectChain *chain; 56 | int primary_input_width, primary_input_height; 57 | int num_repeats_x, num_repeats_y; 58 | float uniform_num_repeats[2]; 59 | }; 60 | 61 | } // namespace movit 62 | 63 | #endif // !defined(_MOVIT_COMPLEX_MODULATE_EFFECT_H) 64 | -------------------------------------------------------------------------------- /ycbcr_conversion_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "ycbcr_conversion_effect.h" 9 | #include "effect_util.h" 10 | #include "util.h" 11 | #include "ycbcr.h" 12 | 13 | using namespace std; 14 | using namespace Eigen; 15 | 16 | namespace movit { 17 | 18 | YCbCrConversionEffect::YCbCrConversionEffect(const YCbCrFormat &ycbcr_format, GLenum type) 19 | : ycbcr_format(ycbcr_format), type(type) 20 | { 21 | register_uniform_mat3("ycbcr_matrix", &uniform_ycbcr_matrix); 22 | register_uniform_vec3("offset", uniform_offset); 23 | register_uniform_bool("clamp_range", &uniform_clamp_range); 24 | 25 | // Only used when clamp_range is true. 26 | register_uniform_vec3("ycbcr_min", uniform_ycbcr_min); 27 | register_uniform_vec3("ycbcr_max", uniform_ycbcr_max); 28 | } 29 | 30 | string YCbCrConversionEffect::output_fragment_shader() 31 | { 32 | return read_file("ycbcr_conversion_effect.frag"); 33 | } 34 | 35 | void YCbCrConversionEffect::set_gl_state(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) 36 | { 37 | Effect::set_gl_state(glsl_program_num, prefix, sampler_num); 38 | 39 | Matrix3d ycbcr_to_rgb; 40 | double scale_factor; 41 | compute_ycbcr_matrix(ycbcr_format, uniform_offset, &ycbcr_to_rgb, type, &scale_factor); 42 | 43 | uniform_ycbcr_matrix = ycbcr_to_rgb.inverse(); 44 | 45 | if (ycbcr_format.full_range) { 46 | // The card will clamp for us later. 47 | uniform_clamp_range = false; 48 | } else { 49 | uniform_clamp_range = true; 50 | 51 | if (ycbcr_format.num_levels == 0 || ycbcr_format.num_levels == 256) { // 8-bit. 52 | // These limits come from BT.601 page 8, or BT.709, page 5. 53 | uniform_ycbcr_min[0] = 16.0 / 255.0; 54 | uniform_ycbcr_min[1] = 16.0 / 255.0; 55 | uniform_ycbcr_min[2] = 16.0 / 255.0; 56 | uniform_ycbcr_max[0] = 235.0 / 255.0; 57 | uniform_ycbcr_max[1] = 240.0 / 255.0; 58 | uniform_ycbcr_max[2] = 240.0 / 255.0; 59 | } else if (ycbcr_format.num_levels == 1024) { // 10-bit. 60 | // BT.709, page 5, or BT.2020, page 6. 61 | uniform_ycbcr_min[0] = 64.0 / 1023.0; 62 | uniform_ycbcr_min[1] = 64.0 / 1023.0; 63 | uniform_ycbcr_min[2] = 64.0 / 1023.0; 64 | uniform_ycbcr_max[0] = 940.0 / 1023.0; 65 | uniform_ycbcr_max[1] = 960.0 / 1023.0; 66 | uniform_ycbcr_max[2] = 960.0 / 1023.0; 67 | } else if (ycbcr_format.num_levels == 4096) { // 12-bit. 68 | // BT.2020, page 6. 69 | uniform_ycbcr_min[0] = 256.0 / 4095.0; 70 | uniform_ycbcr_min[1] = 256.0 / 4095.0; 71 | uniform_ycbcr_min[2] = 256.0 / 4095.0; 72 | uniform_ycbcr_max[0] = 3760.0 / 4095.0; 73 | uniform_ycbcr_max[1] = 3840.0 / 4095.0; 74 | uniform_ycbcr_max[2] = 3840.0 / 4095.0; 75 | } else { 76 | assert(false); 77 | } 78 | uniform_ycbcr_min[0] /= scale_factor; 79 | uniform_ycbcr_min[1] /= scale_factor; 80 | uniform_ycbcr_min[2] /= scale_factor; 81 | } 82 | } 83 | 84 | } // namespace movit 85 | -------------------------------------------------------------------------------- /fft_input.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_FFT_INPUT_H 2 | #define _MOVIT_FFT_INPUT_H 1 3 | 4 | // FFTInput is used by FFTConvolutionEffect to send in the FFTed version of a 5 | // mostly static, one-channel data set, typically the convolution kernel 6 | // with some zero padding. 7 | // 8 | // Since the kernel is typically small and unlikely to change often, 9 | // it will be faster to FFT it once on the CPU (using the excellent FFTW3 10 | // library) and keep it in a texture, rather than FFT-ing it over and over on 11 | // the GPU. (We do not currently support caching Movit intermediates between 12 | // frames.) As an extra bonus, we can then do it in double precision and round 13 | // precisely to fp16 afterwards. 14 | // 15 | // This class is tested as part of by FFTConvolutionEffectTest. 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "effect.h" 22 | #include "effect_chain.h" 23 | #include "image_format.h" 24 | #include "input.h" 25 | 26 | namespace movit { 27 | 28 | class ResourcePool; 29 | 30 | class FFTInput : public Input { 31 | public: 32 | FFTInput(unsigned width, unsigned height); 33 | ~FFTInput(); 34 | 35 | std::string effect_type_id() const override { return "FFTInput"; } 36 | std::string output_fragment_shader() override; 37 | 38 | // FFTs the data and uploads the texture if it has changed since last time. 39 | void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num) override; 40 | 41 | unsigned get_width() const override { return fft_width; } 42 | unsigned get_height() const override { return fft_height; } 43 | 44 | // Strictly speaking, FFT data doesn't have any colorspace or gamma; 45 | // these values are the Movit standards for “do nothing”. 46 | Colorspace get_color_space() const override { return COLORSPACE_sRGB; } 47 | GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; } 48 | AlphaHandling alpha_handling() const override { return INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA; } 49 | bool is_single_texture() const override { return true; } 50 | bool can_output_linear_gamma() const override { return true; } 51 | bool can_supply_mipmaps() const override { return false; } 52 | 53 | // Tells the input where to fetch the actual pixel data. Note that if you change 54 | // this data, you must either call set_pixel_data() again (using the same pointer 55 | // is fine), or invalidate_pixel_data(). Otherwise, the FFT won't be recalculated, 56 | // and the texture won't be re-uploaded on subsequent frames. 57 | void set_pixel_data(const float *pixel_data) 58 | { 59 | this->pixel_data = pixel_data; 60 | invalidate_pixel_data(); 61 | } 62 | 63 | void invalidate_pixel_data(); 64 | 65 | void inform_added(EffectChain *chain) override 66 | { 67 | resource_pool = chain->get_resource_pool(); 68 | } 69 | 70 | bool set_int(const std::string& key, int value) override; 71 | 72 | private: 73 | GLuint texture_num; 74 | int fft_width, fft_height; 75 | unsigned convolve_width, convolve_height; 76 | const float *pixel_data; 77 | ResourcePool *resource_pool; 78 | GLint uniform_tex; 79 | }; 80 | 81 | } // namespace movit 82 | 83 | #endif // !defined(_MOVIT_FFT_INPUT_H) 84 | -------------------------------------------------------------------------------- /fp16_test.cpp: -------------------------------------------------------------------------------- 1 | #include "fp16.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace movit { 7 | namespace { 8 | 9 | fp16_int_t make_fp16(unsigned short x) 10 | { 11 | fp16_int_t ret; 12 | ret.val = x; 13 | return ret; 14 | } 15 | 16 | } // namespace 17 | 18 | TEST(FP16Test, Simple) { 19 | EXPECT_EQ(0x0000, fp32_to_fp16(0.0).val); 20 | EXPECT_DOUBLE_EQ(0.0, fp16_to_fp32(make_fp16(0x0000))); 21 | 22 | EXPECT_EQ(0x3c00, fp32_to_fp16(1.0).val); 23 | EXPECT_DOUBLE_EQ(1.0, fp16_to_fp32(make_fp16(0x3c00))); 24 | 25 | EXPECT_EQ(0x3555, fp32_to_fp16(1.0 / 3.0).val); 26 | EXPECT_DOUBLE_EQ(0.333251953125, fp16_to_fp32(make_fp16(0x3555))); 27 | } 28 | 29 | TEST(FP16Test, RoundToNearestEven) { 30 | ASSERT_DOUBLE_EQ(1.0, fp16_to_fp32(make_fp16(0x3c00))); 31 | 32 | double x0 = fp16_to_fp32(make_fp16(0x3c00)); 33 | double x1 = fp16_to_fp32(make_fp16(0x3c01)); 34 | double x2 = fp16_to_fp32(make_fp16(0x3c02)); 35 | double x3 = fp16_to_fp32(make_fp16(0x3c03)); 36 | double x4 = fp16_to_fp32(make_fp16(0x3c04)); 37 | 38 | EXPECT_EQ(0x3c00, fp32_to_fp16(0.5 * (x0 + x1)).val); 39 | EXPECT_EQ(0x3c02, fp32_to_fp16(0.5 * (x1 + x2)).val); 40 | EXPECT_EQ(0x3c02, fp32_to_fp16(0.5 * (x2 + x3)).val); 41 | EXPECT_EQ(0x3c04, fp32_to_fp16(0.5 * (x3 + x4)).val); 42 | } 43 | 44 | union fp64 { 45 | double f; 46 | unsigned long long ll; 47 | }; 48 | 49 | #ifdef __F16C__ 50 | union fp32 { 51 | float f; 52 | unsigned int u; 53 | }; 54 | #endif 55 | 56 | TEST(FP16Test, NaN) { 57 | // Ignore the sign bit. 58 | EXPECT_EQ(0x7e00, fp32_to_fp16(0.0 / 0.0).val & 0x7fff); 59 | EXPECT_TRUE(std::isnan(fp16_to_fp32(make_fp16(0xfe00)))); 60 | 61 | fp32 borderline_inf; 62 | borderline_inf.u = 0x7f800000ull; 63 | fp32 borderline_nan; 64 | borderline_nan.u = 0x7f800001ull; 65 | 66 | ASSERT_FALSE(std::isfinite(borderline_inf.f)); 67 | ASSERT_FALSE(std::isnan(borderline_inf.f)); 68 | 69 | ASSERT_FALSE(std::isfinite(borderline_nan.f)); 70 | ASSERT_TRUE(std::isnan(borderline_nan.f)); 71 | 72 | double borderline_inf_roundtrip = fp16_to_fp32(fp32_to_fp16(borderline_inf.f)); 73 | double borderline_nan_roundtrip = fp16_to_fp32(fp32_to_fp16(borderline_nan.f)); 74 | 75 | EXPECT_FALSE(std::isfinite(borderline_inf_roundtrip)); 76 | EXPECT_FALSE(std::isnan(borderline_inf_roundtrip)); 77 | 78 | EXPECT_FALSE(std::isfinite(borderline_nan_roundtrip)); 79 | EXPECT_TRUE(std::isnan(borderline_nan_roundtrip)); 80 | } 81 | 82 | TEST(FP16Test, Denormals) { 83 | const double smallest_fp16_denormal = 5.9604644775390625e-08; 84 | EXPECT_EQ(0x0001, fp32_to_fp16(smallest_fp16_denormal).val); 85 | EXPECT_EQ(0x0000, fp32_to_fp16(0.5 * smallest_fp16_denormal).val); // Round-to-even. 86 | EXPECT_EQ(0x0001, fp32_to_fp16(0.51 * smallest_fp16_denormal).val); 87 | EXPECT_EQ(0x0002, fp32_to_fp16(1.5 * smallest_fp16_denormal).val); 88 | 89 | const double smallest_fp16_non_denormal = 6.103515625e-05; 90 | EXPECT_EQ(0x0400, fp32_to_fp16(smallest_fp16_non_denormal).val); 91 | EXPECT_EQ(0x0400, fp32_to_fp16(smallest_fp16_non_denormal - 0.5 * smallest_fp16_denormal).val); // Round-to-even. 92 | EXPECT_EQ(0x03ff, fp32_to_fp16(smallest_fp16_non_denormal - smallest_fp16_denormal).val); 93 | } 94 | 95 | } // namespace movit 96 | -------------------------------------------------------------------------------- /deconvolution_sharpen_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_DECONVOLUTION_SHARPEN_EFFECT_H 2 | #define _MOVIT_DECONVOLUTION_SHARPEN_EFFECT_H 1 3 | 4 | // DeconvolutionSharpenEffect is an effect that sharpens by way of deconvolution 5 | // (i.e., trying to reverse the blur kernel, as opposed to just boosting high 6 | // frequencies), more specifically by FIR Wiener filters. It is the same 7 | // algorithm as used by the (now largely abandoned) Refocus plug-in for GIMP, 8 | // and I suspect the same as in Photoshop's “Smart Sharpen” filter. 9 | // The implementation is, however, distinct from either. 10 | // 11 | // The effect gives generally better results than unsharp masking, but can be very 12 | // GPU intensive, and requires a fair bit of tweaking to get good results without 13 | // ringing and/or excessive noise. It should be mentioned that for the larger 14 | // convolutions (e.g. R approaching 10), we should probably move to FFT-based 15 | // convolution algorithms, especially as Mesa's shader compiler starts having 16 | // problems compiling our shader. 17 | // 18 | // We follow the same book as Refocus was implemented from, namely 19 | // 20 | // Jain, Anil K.: “Fundamentals of Digital Image Processing”, Prentice Hall, 1988. 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | #include "effect.h" 27 | 28 | namespace movit { 29 | 30 | class DeconvolutionSharpenEffect : public Effect { 31 | public: 32 | DeconvolutionSharpenEffect(); 33 | virtual ~DeconvolutionSharpenEffect(); 34 | std::string effect_type_id() const override { return "DeconvolutionSharpenEffect"; } 35 | std::string output_fragment_shader() override; 36 | 37 | // Samples a lot of times from its input. 38 | bool needs_texture_bounce() const override { return true; } 39 | 40 | void inform_input_size(unsigned input_num, unsigned width, unsigned height) override 41 | { 42 | this->width = width; 43 | this->height = height; 44 | } 45 | 46 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 47 | AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; } 48 | 49 | private: 50 | // Input size. 51 | unsigned width, height; 52 | 53 | // The maximum radius of the (de)convolution kernel. 54 | // Note that since this extends both ways, and we also have a center element, 55 | // the actual convolution matrix will be (2R + 1) x (2R + 1). 56 | // 57 | // Must match the definition in the shader, and as such, cannot be set once 58 | // the chain has been finalized. 59 | int R; 60 | 61 | // The parameters. Typical OK values are circle_radius = 2, gaussian_radius = 0 62 | // (ie., blur is assumed to be a 2px circle), correlation = 0.95, and noise = 0.01. 63 | // Note that once the radius starts going too far past R, you will get nonsensical results. 64 | float circle_radius, gaussian_radius, correlation, noise; 65 | 66 | // The deconvolution kernel, and the parameters last time we did an update. 67 | Eigen::MatrixXf g; 68 | int last_R; 69 | float last_circle_radius, last_gaussian_radius, last_correlation, last_noise; 70 | 71 | float *uniform_samples; 72 | 73 | void update_deconvolution_kernel(); 74 | }; 75 | 76 | } // namespace movit 77 | 78 | #endif // !defined(_MOVIT_DECONVOLUTION_SHARPEN_EFFECT_H) 79 | -------------------------------------------------------------------------------- /unsharp_mask_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for UnsharpMaskEffect. 2 | 3 | #include 4 | #include 5 | 6 | #include "effect_chain.h" 7 | #include "gtest/gtest.h" 8 | #include "image_format.h" 9 | #include "test_util.h" 10 | #include "unsharp_mask_effect.h" 11 | 12 | namespace movit { 13 | 14 | TEST(UnsharpMaskEffectTest, NoAmountDoesNothing) { 15 | const int size = 4; 16 | 17 | float data[size * size] = { 18 | 0.0, 1.0, 0.0, 1.0, 19 | 0.0, 1.0, 1.0, 0.0, 20 | 0.0, 0.5, 1.0, 0.5, 21 | 0.0, 0.0, 0.0, 0.0, 22 | }; 23 | float out_data[size * size]; 24 | 25 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 26 | Effect *unsharp_mask_effect = tester.get_chain()->add_effect(new UnsharpMaskEffect()); 27 | ASSERT_TRUE(unsharp_mask_effect->set_float("radius", 2.0f)); 28 | ASSERT_TRUE(unsharp_mask_effect->set_float("amount", 0.0f)); 29 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 30 | 31 | expect_equal(data, out_data, size, size); 32 | } 33 | 34 | TEST(UnsharpMaskEffectTest, UnblursGaussianBlur) { 35 | const int size = 13; 36 | const float sigma = 0.5f; 37 | 38 | float data[size * size], out_data[size * size]; 39 | float expected_data[] = { // One single dot in the middle. 40 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 41 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 42 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 43 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 44 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 45 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 46 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 47 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 48 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 49 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 51 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 52 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 53 | }; 54 | 55 | // Create a Gaussian input. (Note that our blur is not Gaussian.) 56 | for (int y = 0; y < size; ++y) { 57 | for (int x = 0; x < size; ++x) { 58 | float z = hypot(x - 6, y - 6); 59 | data[y * size + x] = exp(-z*z / (2.0 * sigma * sigma)) / (2.0 * M_PI * sigma * sigma); 60 | } 61 | } 62 | 63 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 64 | Effect *unsharp_mask_effect = tester.get_chain()->add_effect(new UnsharpMaskEffect()); 65 | ASSERT_TRUE(unsharp_mask_effect->set_float("radius", sigma)); 66 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 67 | 68 | // Check the center sample separately; it's bound to be the sample with the largest 69 | // single error, and we know we can't get it perfect anyway. 70 | int center = size / 2; 71 | EXPECT_GT(out_data[center * size + center], 0.45f); 72 | out_data[center * size + center] = 1.0f; 73 | 74 | // Add some leeway for the rest; unsharp masking is not expected to be extremely good. 75 | expect_equal(expected_data, out_data, size, size, 0.1, 0.001); 76 | } 77 | 78 | } // namespace movit 79 | -------------------------------------------------------------------------------- /slice_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "effect_chain.h" 4 | #include "slice_effect.h" 5 | #include "effect_util.h" 6 | #include "util.h" 7 | 8 | using namespace std; 9 | 10 | namespace movit { 11 | 12 | SliceEffect::SliceEffect() 13 | : input_slice_size(1), 14 | output_slice_size(1), 15 | offset(0), 16 | direction(VERTICAL) 17 | { 18 | register_int("input_slice_size", &input_slice_size); 19 | register_int("output_slice_size", &output_slice_size); 20 | register_int("offset", &offset); 21 | register_int("direction", (int *)&direction); 22 | register_uniform_float("output_coord_to_slice_num", &uniform_output_coord_to_slice_num); 23 | register_uniform_float("slice_num_to_input_coord", &uniform_slice_num_to_input_coord); 24 | register_uniform_float("slice_offset_to_input_coord", &uniform_slice_offset_to_input_coord); 25 | register_uniform_float("normalized_offset", &uniform_offset); 26 | } 27 | 28 | string SliceEffect::output_fragment_shader() 29 | { 30 | char buf[256]; 31 | sprintf(buf, "#define DIRECTION_VERTICAL %d\n", (direction == VERTICAL)); 32 | return buf + read_file("slice_effect.frag"); 33 | } 34 | 35 | void SliceEffect::inform_input_size(unsigned input_num, unsigned width, unsigned height) 36 | { 37 | assert(input_num == 0); 38 | input_width = width; 39 | input_height = height; 40 | } 41 | 42 | void SliceEffect::get_output_size(unsigned *width, unsigned *height, 43 | unsigned *virtual_width, unsigned *virtual_height) const 44 | { 45 | if (direction == HORIZONTAL) { 46 | *width = div_round_up(input_width, input_slice_size) * output_slice_size; 47 | *height = input_height; 48 | } else { 49 | *width = input_width; 50 | *height = div_round_up(input_height, input_slice_size) * output_slice_size; 51 | } 52 | *virtual_width = *width; 53 | *virtual_height = *height; 54 | } 55 | 56 | void SliceEffect::set_gl_state(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) 57 | { 58 | Effect::set_gl_state(glsl_program_num, prefix, sampler_num); 59 | 60 | unsigned output_width, output_height; 61 | get_output_size(&output_width, &output_height, &output_width, &output_height); 62 | 63 | if (direction == HORIZONTAL) { 64 | uniform_output_coord_to_slice_num = float(output_width) / float(output_slice_size); 65 | uniform_slice_num_to_input_coord = float(input_slice_size) / float(input_width); 66 | uniform_slice_offset_to_input_coord = float(output_slice_size) / float(input_width); 67 | uniform_offset = float(offset) / float(input_width); 68 | } else { 69 | uniform_output_coord_to_slice_num = float(output_height) / float(output_slice_size); 70 | uniform_slice_num_to_input_coord = float(input_slice_size) / float(input_height); 71 | uniform_slice_offset_to_input_coord = float(output_slice_size) / float(input_height); 72 | uniform_offset = float(offset) / float(input_height); 73 | } 74 | 75 | // Normalized coordinates could potentially cause blurring of the image. 76 | // It isn't critical, but still good practice. 77 | Node *self = chain->find_node_for_effect(this); 78 | glActiveTexture(chain->get_input_sampler(self, 0)); 79 | check_error(); 80 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 81 | check_error(); 82 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 83 | check_error(); 84 | } 85 | 86 | } // namespace movit 87 | -------------------------------------------------------------------------------- /vignette_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for VignetteEffect. 2 | 3 | #include 4 | #include 5 | 6 | #include "effect_chain.h" 7 | #include "gtest/gtest.h" 8 | #include "image_format.h" 9 | #include "test_util.h" 10 | #include "vignette_effect.h" 11 | 12 | namespace movit { 13 | 14 | TEST(VignetteEffectTest, HugeInnerRadiusDoesNothing) { 15 | const int size = 4; 16 | 17 | float data[size * size] = { 18 | 0.0, 1.0, 0.0, 1.0, 19 | 0.0, 1.0, 1.0, 0.0, 20 | 0.0, 0.5, 1.0, 0.5, 21 | 0.0, 0.0, 0.0, 0.0, 22 | }; 23 | float out_data[size * size]; 24 | 25 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 26 | Effect *vignette_effect = tester.get_chain()->add_effect(new VignetteEffect()); 27 | ASSERT_TRUE(vignette_effect->set_float("inner_radius", 10.0f)); 28 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 29 | 30 | expect_equal(data, out_data, size, size); 31 | } 32 | 33 | TEST(VignetteEffectTest, HardCircle) { 34 | const int size = 16; 35 | 36 | float data[size * size], out_data[size * size], expected_data[size * size]; 37 | for (int y = 0; y < size; ++y) { 38 | for (int x = 0; x < size; ++x) { 39 | data[y * size + x] = 1.0f; 40 | } 41 | } 42 | for (int y = 0; y < size; ++y) { 43 | const float yf = (y + 0.5f) / size; 44 | for (int x = 0; x < size; ++x) { 45 | const float xf = (x + 0.5f) / size; 46 | if (hypot(xf - 0.5, yf - 0.5) < 0.3) { 47 | expected_data[y * size + x] = 1.0f; 48 | } else { 49 | expected_data[y * size + x] = 0.0f; 50 | } 51 | } 52 | } 53 | 54 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 55 | Effect *vignette_effect = tester.get_chain()->add_effect(new VignetteEffect()); 56 | ASSERT_TRUE(vignette_effect->set_float("radius", 0.0f)); 57 | ASSERT_TRUE(vignette_effect->set_float("inner_radius", 0.3f)); 58 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 59 | 60 | expect_equal(expected_data, out_data, size, size); 61 | } 62 | 63 | TEST(VignetteEffectTest, BurstFromUpperLeftCorner) { 64 | const int width = 16, height = 24; 65 | float radius = 0.5f; 66 | 67 | float data[width * height], out_data[width * height], expected_data[width * height]; 68 | for (int y = 0; y < height; ++y) { 69 | for (int x = 0; x < width; ++x) { 70 | data[y * width + x] = 1.0f; 71 | } 72 | } 73 | for (int y = 0; y < height; ++y) { 74 | const float yf = (y + 0.5f) / width; // Note: Division by width. 75 | for (int x = 0; x < width; ++x) { 76 | const float xf = (x + 0.5f) / width; 77 | const float d = hypot(xf, yf) / radius; 78 | if (d >= 1.0f) { 79 | expected_data[y * width + x] = 0.0f; 80 | } else { 81 | expected_data[y * width + x] = cos(d * 0.5 * M_PI) * cos(d * 0.5 * M_PI); 82 | } 83 | } 84 | } 85 | 86 | EffectChainTester tester(data, width, height, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 87 | Effect *vignette_effect = tester.get_chain()->add_effect(new VignetteEffect()); 88 | float center[] = { 0.0f, 0.0f }; 89 | ASSERT_TRUE(vignette_effect->set_vec2("center", center)); 90 | ASSERT_TRUE(vignette_effect->set_float("radius", radius)); 91 | ASSERT_TRUE(vignette_effect->set_float("inner_radius", 0.0f)); 92 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 93 | 94 | expect_equal(expected_data, out_data, width, height); 95 | } 96 | 97 | } // namespace movit 98 | -------------------------------------------------------------------------------- /effect_util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "util.h" 6 | 7 | using namespace std; 8 | 9 | namespace movit { 10 | 11 | GLint get_uniform_location(GLuint glsl_program_num, const string &prefix, const string &key) 12 | { 13 | string name = prefix + "_" + key; 14 | return glGetUniformLocation(glsl_program_num, name.c_str()); 15 | } 16 | 17 | void set_uniform_int(GLuint glsl_program_num, const string &prefix, const string &key, int value) 18 | { 19 | GLint location = get_uniform_location(glsl_program_num, prefix, key); 20 | if (location == -1) { 21 | return; 22 | } 23 | check_error(); 24 | glUniform1i(location, value); 25 | check_error(); 26 | } 27 | 28 | void set_uniform_float(GLuint glsl_program_num, const string &prefix, const string &key, float value) 29 | { 30 | GLint location = get_uniform_location(glsl_program_num, prefix, key); 31 | if (location == -1) { 32 | return; 33 | } 34 | check_error(); 35 | glUniform1f(location, value); 36 | check_error(); 37 | } 38 | 39 | void set_uniform_vec2(GLuint glsl_program_num, const string &prefix, const string &key, const float *values) 40 | { 41 | GLint location = get_uniform_location(glsl_program_num, prefix, key); 42 | if (location == -1) { 43 | return; 44 | } 45 | check_error(); 46 | glUniform2fv(location, 1, values); 47 | check_error(); 48 | } 49 | 50 | void set_uniform_vec3(GLuint glsl_program_num, const string &prefix, const string &key, const float *values) 51 | { 52 | GLint location = get_uniform_location(glsl_program_num, prefix, key); 53 | if (location == -1) { 54 | return; 55 | } 56 | check_error(); 57 | glUniform3fv(location, 1, values); 58 | check_error(); 59 | } 60 | 61 | void set_uniform_vec4(GLuint glsl_program_num, const string &prefix, const string &key, const float *values) 62 | { 63 | GLint location = get_uniform_location(glsl_program_num, prefix, key); 64 | if (location == -1) { 65 | return; 66 | } 67 | check_error(); 68 | glUniform4fv(location, 1, values); 69 | check_error(); 70 | } 71 | 72 | void set_uniform_vec2_array(GLuint glsl_program_num, const string &prefix, const string &key, const float *values, size_t num_values) 73 | { 74 | GLint location = get_uniform_location(glsl_program_num, prefix, key); 75 | if (location == -1) { 76 | return; 77 | } 78 | check_error(); 79 | glUniform2fv(location, num_values, values); 80 | check_error(); 81 | } 82 | 83 | void set_uniform_vec4_array(GLuint glsl_program_num, const string &prefix, const string &key, const float *values, size_t num_values) 84 | { 85 | GLint location = get_uniform_location(glsl_program_num, prefix, key); 86 | if (location == -1) { 87 | return; 88 | } 89 | check_error(); 90 | glUniform4fv(location, num_values, values); 91 | check_error(); 92 | } 93 | 94 | void set_uniform_mat3(GLuint glsl_program_num, const string &prefix, const string &key, const Eigen::Matrix3d& matrix) 95 | { 96 | GLint location = get_uniform_location(glsl_program_num, prefix, key); 97 | if (location == -1) { 98 | return; 99 | } 100 | check_error(); 101 | 102 | // Convert to float (GLSL has no double matrices). 103 | float matrixf[9]; 104 | for (unsigned y = 0; y < 3; ++y) { 105 | for (unsigned x = 0; x < 3; ++x) { 106 | matrixf[y + x * 3] = matrix(y, x); 107 | } 108 | } 109 | 110 | glUniformMatrix3fv(location, 1, GL_FALSE, matrixf); 111 | check_error(); 112 | } 113 | 114 | } // namespace movit 115 | -------------------------------------------------------------------------------- /complex_modulate_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for ComplexModulateEffect. 2 | 3 | #include 4 | 5 | #include "effect_chain.h" 6 | #include "gtest/gtest.h" 7 | #include "complex_modulate_effect.h" 8 | #include "image_format.h" 9 | #include "input.h" 10 | #include "test_util.h" 11 | 12 | namespace movit { 13 | 14 | TEST(ComplexModulateEffectTest, Identity) { 15 | const int size = 3; 16 | float data_a[size * 4] = { 17 | 0.0f, 0.1f, 0.2f, 0.1f, 18 | 0.4f, 0.3f, 0.8f, 2.0f, 19 | 0.5f, 0.2f, 0.1f, 0.0f, 20 | }; 21 | float data_b[size * 2] = { 22 | 1.0f, 0.0f, 23 | 1.0f, 0.0f, 24 | 1.0f, 0.0f, 25 | }; 26 | float out_data[size * 4]; 27 | 28 | EffectChainTester tester(data_a, 1, size, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 29 | Effect *input1 = tester.get_chain()->last_added_effect(); 30 | Effect *input2 = tester.add_input(data_b, FORMAT_RG, COLORSPACE_sRGB, GAMMA_LINEAR); 31 | 32 | tester.get_chain()->add_effect(new ComplexModulateEffect(), input1, input2); 33 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED); 34 | 35 | expect_equal(data_a, out_data, 4, size); 36 | } 37 | 38 | TEST(ComplexModulateEffectTest, ComplexMultiplication) { 39 | const int size = 2; 40 | float data_a[size * 4] = { 41 | 0.0f, 0.1f, 0.2f, 0.1f, 42 | 0.4f, 0.3f, 0.8f, 2.0f, 43 | }; 44 | float data_b[size * 2] = { 45 | 0.0f, 1.0f, 46 | 0.5f, -0.8f, 47 | }; 48 | float expected_data[size * 4] = { 49 | -0.1f, 0.0f, -0.1f, 0.2f, 50 | 0.44f, -0.17f, 2.0f, 0.36f, 51 | }; 52 | float out_data[size * 4]; 53 | 54 | EffectChainTester tester(data_a, 1, size, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 55 | Effect *input1 = tester.get_chain()->last_added_effect(); 56 | Effect *input2 = tester.add_input(data_b, FORMAT_RG, COLORSPACE_sRGB, GAMMA_LINEAR); 57 | 58 | tester.get_chain()->add_effect(new ComplexModulateEffect(), input1, input2); 59 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED); 60 | 61 | expect_equal(expected_data, out_data, 4, size); 62 | } 63 | 64 | TEST(ComplexModulateEffectTest, Repeat) { 65 | const int size = 2, repeats = 3; 66 | float data_a[size * repeats * 4] = { 67 | 0.0f, 0.1f, 0.2f, 0.3f, 68 | 1.0f, 1.1f, 1.2f, 1.3f, 69 | 2.0f, 2.1f, 2.2f, 2.3f, 70 | 3.0f, 3.1f, 3.2f, 3.3f, 71 | 4.0f, 4.1f, 4.2f, 4.3f, 72 | 5.0f, 5.1f, 5.2f, 5.3f, 73 | }; 74 | float data_b[size * 2] = { 75 | 1.0f, 0.0f, 76 | 0.0f, -1.0f, 77 | }; 78 | float expected_data[size * repeats * 4] = { 79 | 0.0f, 0.1f, 0.2f, 0.3f, 80 | 1.1f, -1.0f, 1.3f, -1.2f, 81 | 2.0f, 2.1f, 2.2f, 2.3f, 82 | 3.1f, -3.0f, 3.3f, -3.2f, 83 | 4.0f, 4.1f, 4.2f, 4.3f, 84 | 5.1f, -5.0f, 5.3f, -5.2f, 85 | }; 86 | float out_data[size * repeats * 4]; 87 | 88 | EffectChainTester tester(data_a, 1, repeats * size, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 89 | Effect *input1 = tester.get_chain()->last_added_effect(); 90 | Effect *input2 = tester.add_input(data_b, FORMAT_RG, COLORSPACE_sRGB, GAMMA_LINEAR, 1, size); 91 | 92 | Effect *effect = tester.get_chain()->add_effect(new ComplexModulateEffect(), input1, input2); 93 | ASSERT_TRUE(effect->set_int("num_repeats_y", repeats)); 94 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED); 95 | 96 | expect_equal(expected_data, out_data, 4, size * repeats); 97 | } 98 | 99 | } // namespace movit 100 | -------------------------------------------------------------------------------- /padding_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_PADDING_EFFECT_H 2 | #define _MOVIT_PADDING_EFFECT_H 1 3 | 4 | // Takes an image and pads it to fit a larger image, or crops it to fit a smaller one 5 | // (although the latter is implemented slightly less efficiently, and you cannot both 6 | // pad and crop in the same effect). 7 | // 8 | // The source image is cut off at the texture border, and then given a user-specific color; 9 | // by default, full transparent. You can give a fractional border size (non-integral 10 | // "top" or "left" offset) if you wish, which will give you linear interpolation of 11 | // both pixel data of and the border. Furthermore, you can offset where the border falls 12 | // by using the "border_offset_{top,bottom,left,right}" settings; this is particularly 13 | // useful if you use ResampleEffect earlier in the chain for high-quality fractional-pixel 14 | // translation and just want PaddingEffect to get the border right. 15 | // 16 | // The border color is taken to be in linear gamma, sRGB, with premultiplied alpha. 17 | // You may not change it after calling finalize(), since that could change the 18 | // graph (need_linear_light() etc. depend on the border color you choose). 19 | // 20 | // IntegralPaddingEffect is like PaddingEffect, except that "top" and "left" parameters 21 | // are int parameters instead of float. This allows it to guarantee one-to-one sampling, 22 | // which can speed up processing by allowing more effect passes to be collapsed. 23 | // border_offset_* are still allowed to be float, although you should beware that if 24 | // you set e.g. border_offset_top to a negative value, you will be sampling outside 25 | // the edge and will read data that is undefined in one-to-one-mode (could be 26 | // edge repeat, could be something else). With regular PaddingEffect, such samples 27 | // are guaranteed to be edge repeat. 28 | 29 | #include 30 | #include 31 | 32 | #include "effect.h" 33 | 34 | namespace movit { 35 | 36 | class PaddingEffect : public Effect { 37 | public: 38 | PaddingEffect(); 39 | std::string effect_type_id() const override { return "PaddingEffect"; } 40 | std::string output_fragment_shader() override; 41 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 42 | 43 | bool needs_linear_light() const override; 44 | bool needs_srgb_primaries() const override; 45 | AlphaHandling alpha_handling() const override; 46 | 47 | bool changes_output_size() const override { return true; } 48 | bool sets_virtual_output_size() const override { return false; } 49 | void get_output_size(unsigned *width, unsigned *height, unsigned *virtual_width, unsigned *virtual_height) const override; 50 | void inform_input_size(unsigned input_num, unsigned width, unsigned height) override; 51 | 52 | private: 53 | RGBATuple border_color; 54 | int input_width, input_height; 55 | int output_width, output_height; 56 | float top, left; 57 | float border_offset_top, border_offset_left; 58 | float border_offset_bottom, border_offset_right; 59 | float uniform_offset[2], uniform_scale[2]; 60 | float uniform_normalized_coords_to_texels[2]; 61 | float uniform_offset_bottomleft[2], uniform_offset_topright[2]; 62 | }; 63 | 64 | class IntegralPaddingEffect : public PaddingEffect { 65 | public: 66 | IntegralPaddingEffect(); 67 | std::string effect_type_id() const override { return "IntegralPaddingEffect"; } 68 | bool one_to_one_sampling() const override { return true; } 69 | bool set_int(const std::string&, int value) override; 70 | bool set_float(const std::string &key, float value) override; 71 | }; 72 | 73 | } // namespace movit 74 | 75 | #endif // !defined(_MOVIT_PADDING_EFFECT_H) 76 | -------------------------------------------------------------------------------- /init.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_INIT_H 2 | #define _MOVIT_INIT_H 3 | 4 | #include "defs.h" 5 | #include 6 | 7 | namespace movit { 8 | 9 | enum MovitDebugLevel { 10 | MOVIT_DEBUG_OFF = 0, 11 | MOVIT_DEBUG_ON = 1, 12 | }; 13 | 14 | // Initialize the library; in particular, will query the GPU for information 15 | // that is needed by various components. For instance, it verifies that 16 | // we have all the OpenGL extensions we need. Returns true if initialization 17 | // succeeded. 18 | // 19 | // The first parameter gives which directory to read .frag files from. 20 | // This is a temporary hack until we add something more solid. 21 | // 22 | // The second parameter specifies whether debugging is on or off. 23 | // If it is on, Movit will write intermediate graphs and the final 24 | // generated shaders to the current directory. 25 | // 26 | // If you call init_movit() twice with different parameters, 27 | // only the first will count, and the second will always return true. 28 | bool init_movit(const std::string& data_directory, MovitDebugLevel debug_level) MUST_CHECK_RESULT; 29 | 30 | // GPU features. These are not intended for end-user use. 31 | 32 | // Whether init_movit() has been called. 33 | extern bool movit_initialized; 34 | 35 | // The current debug level. 36 | extern MovitDebugLevel movit_debug_level; 37 | 38 | // An estimate on the smallest values the linear texture interpolation 39 | // of the GPU can distinguish between, i.e., for a GPU with N-bit 40 | // texture subpixel precision, this value will be 2^-N. 41 | // 42 | // From reading the little specs that exist and through practical tests, 43 | // the broad picture seems to be that Intel cards have 6-bit precision, 44 | // nVidia cards have 8-bit, and Radeon cards have 6-bit before R6xx 45 | // (at least when not using trilinear sampling), but can reach 46 | // 8-bit precision on R6xx or newer in some (unspecified) cases. 47 | // 48 | // We currently don't bother to test for more than 1024 levels. 49 | extern float movit_texel_subpixel_precision; 50 | 51 | // Some GPUs use very inaccurate fixed-function circuits for rounding 52 | // floating-point values to 8-bit outputs, leading to absurdities like 53 | // the roundoff point between 128 and 129 being 128.62 instead of 128.5. 54 | // We test, for every integer, x+0.48 and x+0.52 and check that they 55 | // round the right way (giving some leeway, but not a lot); the number 56 | // of errors are stored here. 57 | // 58 | // If this value is above 0, we will round off explicitly at the very end 59 | // of the shader. Note the following limitations: 60 | // 61 | // - The measurement is done on linear 8-bit, not any sRGB format, 62 | // 10-bit output, or the likes. 63 | // - This only covers the final pass; intermediates are not covered 64 | // (only relevant if you use e.g. GL_SRGB8 intermediates). 65 | extern int movit_num_wrongly_rounded; 66 | 67 | // Whether the OpenGL driver (or GPU) in use supports GL_ARB_timer_query. 68 | extern bool movit_timer_queries_supported; 69 | 70 | // Whether the OpenGL driver (or GPU) in use supports compute shaders. 71 | // Note that certain OpenGL implementations might only allow this in core mode. 72 | extern bool movit_compute_shaders_supported; 73 | 74 | // What shader model we are compiling for. This only affects the choice 75 | // of a few files (like header.frag); most of the shaders are the same. 76 | enum MovitShaderModel { 77 | MOVIT_GLSL_110, // No longer in use, but kept until next ABI break in order not to change the enums. 78 | MOVIT_GLSL_130, 79 | MOVIT_ESSL_300, 80 | MOVIT_GLSL_150, 81 | }; 82 | extern MovitShaderModel movit_shader_model; 83 | 84 | } // namespace movit 85 | 86 | #endif // !defined(_MOVIT_INIT_H) 87 | -------------------------------------------------------------------------------- /overlay_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for OverlayEffect. 2 | 3 | #include 4 | 5 | #include "effect_chain.h" 6 | #include "gtest/gtest.h" 7 | #include "image_format.h" 8 | #include "input.h" 9 | #include "overlay_effect.h" 10 | #include "test_util.h" 11 | #include "util.h" 12 | 13 | namespace movit { 14 | 15 | TEST(OverlayEffectTest, TopDominatesBottomWhenNoAlpha) { 16 | for (int swap_inputs = 0; swap_inputs < 2; ++swap_inputs) { // false, true. 17 | float data_a[] = { 18 | 0.0f, 0.25f, 19 | 0.75f, 1.0f, 20 | }; 21 | float data_b[] = { 22 | 1.0f, 0.5f, 23 | 0.75f, 0.6f, 24 | }; 25 | float out_data[4]; 26 | EffectChainTester tester(data_a, 2, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 27 | Effect *input1 = tester.get_chain()->last_added_effect(); 28 | Effect *input2 = tester.add_input(data_b, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 29 | 30 | OverlayEffect *effect = new OverlayEffect(); 31 | CHECK(effect->set_int("swap_inputs", swap_inputs)); 32 | tester.get_chain()->add_effect(effect, input1, input2); 33 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 34 | 35 | if (swap_inputs) { 36 | expect_equal(data_a, out_data, 2, 2); 37 | } else { 38 | expect_equal(data_b, out_data, 2, 2); 39 | } 40 | } 41 | } 42 | 43 | TEST(OverlayEffectTest, BottomDominatesTopWhenTopIsTransparent) { 44 | float data_a[] = { 45 | 1.0f, 0.0f, 0.0f, 0.5f, 46 | }; 47 | float data_b[] = { 48 | 0.5f, 0.5f, 0.5f, 0.0f, 49 | }; 50 | float out_data[4]; 51 | EffectChainTester tester(data_a, 1, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 52 | Effect *input1 = tester.get_chain()->last_added_effect(); 53 | Effect *input2 = tester.add_input(data_b, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 54 | 55 | tester.get_chain()->add_effect(new OverlayEffect(), input1, input2); 56 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 57 | 58 | expect_equal(data_a, out_data, 4, 1); 59 | } 60 | 61 | TEST(OverlayEffectTest, ZeroAlphaRemainsZeroAlpha) { 62 | float data_a[] = { 63 | 0.0f, 0.25f, 0.5f, 0.0f 64 | }; 65 | float data_b[] = { 66 | 1.0f, 1.0f, 1.0f, 0.0f 67 | }; 68 | float expected_data[] = { 69 | 0.0f, 0.0f, 0.0f, 0.0f 70 | }; 71 | float out_data[4]; 72 | EffectChainTester tester(data_a, 1, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 73 | Effect *input1 = tester.get_chain()->last_added_effect(); 74 | Effect *input2 = tester.add_input(data_b, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 75 | 76 | tester.get_chain()->add_effect(new OverlayEffect(), input1, input2); 77 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 78 | 79 | EXPECT_FLOAT_EQ(0.0f, expected_data[3]); 80 | } 81 | 82 | // This is tested against what Photoshop does: (255,0,128, 0.25) over (128,255,0, 0.5) 83 | // becomes (179,153,51, 0.63). (Actually we fudge 0.63 to 0.625, because that's 84 | // what it should be.) 85 | TEST(OverlayEffectTest, PhotoshopReferenceTest) { 86 | float data_a[] = { 87 | 128.0f/255.0f, 1.0f, 0.0f, 0.5f 88 | }; 89 | float data_b[] = { 90 | 1.0f, 0.0f, 128.0f/255.0f, 0.25f 91 | }; 92 | float expected_data[] = { 93 | 179.0f/255.0f, 153.0f/255.0f, 51.0f/255.0f, 0.625f 94 | }; 95 | float out_data[4]; 96 | EffectChainTester tester(data_a, 1, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 97 | Effect *input1 = tester.get_chain()->last_added_effect(); 98 | Effect *input2 = tester.add_input(data_b, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 99 | 100 | tester.get_chain()->add_effect(new OverlayEffect(), input1, input2); 101 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 102 | 103 | expect_equal(expected_data, out_data, 4, 1); 104 | } 105 | 106 | } // namespace movit 107 | -------------------------------------------------------------------------------- /blur_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_BLUR_EFFECT_H 2 | #define _MOVIT_BLUR_EFFECT_H 1 3 | 4 | // A separable 2D blur implemented by a combination of mipmap filtering 5 | // and convolution (essentially giving a convolution with a piecewise linear 6 | // approximation to the true impulse response). 7 | // 8 | // Works in two passes; first horizontal, then vertical (BlurEffect, 9 | // which is what the user is intended to use, instantiates two copies of 10 | // SingleBlurPassEffect behind the scenes). 11 | // 12 | // The recommended number of taps is the default (16). Fewer will be faster 13 | // but uglier; a tradeoff that might be worth it as part of more complicated 14 | // effects. This can be set only before finalization, and must be an 15 | // even number. 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "effect.h" 23 | 24 | namespace movit { 25 | 26 | class EffectChain; 27 | class Node; 28 | class SingleBlurPassEffect; 29 | 30 | class BlurEffect : public Effect { 31 | public: 32 | BlurEffect(); 33 | 34 | std::string effect_type_id() const override { return "BlurEffect"; } 35 | 36 | void inform_input_size(unsigned input_num, unsigned width, unsigned height) override; 37 | 38 | std::string output_fragment_shader() override { 39 | assert(false); 40 | } 41 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override { 42 | assert(false); 43 | } 44 | 45 | void rewrite_graph(EffectChain *graph, Node *self) override; 46 | bool set_float(const std::string &key, float value) override; 47 | bool set_int(const std::string &key, int value) override; 48 | 49 | private: 50 | void update_radius(); 51 | 52 | int num_taps; 53 | float radius; 54 | SingleBlurPassEffect *hpass, *vpass; 55 | unsigned input_width, input_height; 56 | }; 57 | 58 | class SingleBlurPassEffect : public Effect { 59 | public: 60 | // If parent is non-nullptr, calls to inform_input_size will be forwarded 61 | // so that it can make reasonable decisions for both blur passes. 62 | SingleBlurPassEffect(BlurEffect *parent); 63 | virtual ~SingleBlurPassEffect(); 64 | std::string effect_type_id() const override { return "SingleBlurPassEffect"; } 65 | 66 | std::string output_fragment_shader() override; 67 | 68 | // We want this for the same reason as ResizeEffect; we could end up scaling 69 | // down quite a lot. 70 | bool needs_texture_bounce() const override { return true; } 71 | MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; } 72 | bool needs_srgb_primaries() const override { return false; } 73 | AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; } 74 | 75 | void inform_input_size(unsigned input_num, unsigned width, unsigned height) override { 76 | if (parent != nullptr) { 77 | parent->inform_input_size(input_num, width, height); 78 | } 79 | } 80 | bool changes_output_size() const override { return true; } 81 | bool sets_virtual_output_size() const override { return true; } 82 | bool one_to_one_sampling() const override { return false; } // Can sample outside the border. 83 | 84 | void get_output_size(unsigned *width, unsigned *height, unsigned *virtual_width, unsigned *virtual_height) const override { 85 | *width = this->width; 86 | *height = this->height; 87 | *virtual_width = this->virtual_width; 88 | *virtual_height = this->virtual_height; 89 | } 90 | 91 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 92 | void clear_gl_state() override; 93 | 94 | enum Direction { HORIZONTAL = 0, VERTICAL = 1 }; 95 | 96 | private: 97 | BlurEffect *parent; 98 | int num_taps; 99 | float radius; 100 | Direction direction; 101 | int width, height, virtual_width, virtual_height; 102 | float *uniform_samples; 103 | }; 104 | 105 | } // namespace movit 106 | 107 | #endif // !defined(_MOVIT_BLUR_EFFECT_H) 108 | -------------------------------------------------------------------------------- /white_balance_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for WhiteBalanceEffect. 2 | 3 | #include 4 | 5 | #include "effect_chain.h" 6 | #include "gtest/gtest.h" 7 | #include "image_format.h" 8 | #include "test_util.h" 9 | #include "white_balance_effect.h" 10 | 11 | namespace movit { 12 | 13 | TEST(WhiteBalanceEffectTest, GrayNeutralDoesNothing) { 14 | float data[] = { 15 | 0.0f, 0.0f, 0.0f, 1.0f, 16 | 0.5f, 0.5f, 0.5f, 0.3f, 17 | 1.0f, 0.0f, 0.0f, 1.0f, 18 | 0.0f, 1.0f, 0.0f, 0.7f, 19 | 0.0f, 0.0f, 1.0f, 1.0f, 20 | }; 21 | float neutral[] = { 22 | 0.5f, 0.5f, 0.5f 23 | }; 24 | 25 | float out_data[5 * 4]; 26 | EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 27 | Effect *white_balance_effect = tester.get_chain()->add_effect(new WhiteBalanceEffect()); 28 | ASSERT_TRUE(white_balance_effect->set_vec3("neutral_color", neutral)); 29 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 30 | 31 | expect_equal(data, out_data, 4, 5); 32 | } 33 | 34 | TEST(WhiteBalanceEffectTest, SettingReddishNeutralColorNeutralizesReddishColor) { 35 | float data[] = { 36 | 0.0f, 0.0f, 0.0f, 1.0f, 37 | 0.6f, 0.5f, 0.5f, 1.0f, 38 | 0.5f, 0.5f, 0.5f, 1.0f, 39 | }; 40 | float neutral[] = { 41 | 0.6f, 0.5f, 0.5f 42 | }; 43 | 44 | float out_data[3 * 4]; 45 | EffectChainTester tester(data, 1, 3, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 46 | Effect *white_balance_effect = tester.get_chain()->add_effect(new WhiteBalanceEffect()); 47 | ASSERT_TRUE(white_balance_effect->set_vec3("neutral_color", neutral)); 48 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 49 | 50 | // Black should stay black. 51 | EXPECT_FLOAT_EQ(0.0f, out_data[4 * 0 + 0]); 52 | EXPECT_FLOAT_EQ(0.0f, out_data[4 * 0 + 1]); 53 | EXPECT_FLOAT_EQ(0.0f, out_data[4 * 0 + 2]); 54 | EXPECT_FLOAT_EQ(1.0f, out_data[4 * 0 + 3]); 55 | 56 | // The neutral color should now have R=G=B. 57 | EXPECT_NEAR(out_data[4 * 1 + 0], out_data[4 * 1 + 1], 0.001); 58 | EXPECT_NEAR(out_data[4 * 1 + 0], out_data[4 * 1 + 2], 0.001); 59 | EXPECT_FLOAT_EQ(1.0f, out_data[4 * 1 + 3]); 60 | 61 | // It should also have kept its luminance. 62 | float old_luminance = 0.2126 * data[4 * 1 + 0] + 63 | 0.7152 * data[4 * 1 + 1] + 64 | 0.0722 * data[4 * 1 + 2]; 65 | float new_luminance = 0.2126 * out_data[4 * 1 + 0] + 66 | 0.7152 * out_data[4 * 1 + 1] + 67 | 0.0722 * out_data[4 * 1 + 2]; 68 | EXPECT_NEAR(old_luminance, new_luminance, 0.001); 69 | 70 | // Finally, the old gray should now have significantly less red than green and blue. 71 | EXPECT_GT(out_data[4 * 2 + 1] - out_data[4 * 2 + 0], 0.05); 72 | EXPECT_GT(out_data[4 * 2 + 2] - out_data[4 * 2 + 0], 0.05); 73 | } 74 | 75 | TEST(WhiteBalanceEffectTest, HigherColorTemperatureIncreasesBlue) { 76 | float data[] = { 77 | 0.0f, 0.0f, 0.0f, 1.0f, 78 | 0.5f, 0.5f, 0.5f, 1.0f, 79 | }; 80 | 81 | float out_data[2 * 4]; 82 | EffectChainTester tester(data, 1, 2, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 83 | Effect *white_balance_effect = tester.get_chain()->add_effect(new WhiteBalanceEffect()); 84 | ASSERT_TRUE(white_balance_effect->set_float("output_color_temperature", 10000.0f)); 85 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 86 | 87 | // Black should stay black. 88 | EXPECT_FLOAT_EQ(0.0f, out_data[4 * 0 + 0]); 89 | EXPECT_FLOAT_EQ(0.0f, out_data[4 * 0 + 1]); 90 | EXPECT_FLOAT_EQ(0.0f, out_data[4 * 0 + 2]); 91 | EXPECT_FLOAT_EQ(1.0f, out_data[4 * 0 + 3]); 92 | 93 | // The neutral color should have kept its luminance. 94 | float old_luminance = 0.2126 * data[4 * 1 + 0] + 95 | 0.7152 * data[4 * 1 + 1] + 96 | 0.0722 * data[4 * 1 + 2]; 97 | float new_luminance = 0.2126 * out_data[4 * 1 + 0] + 98 | 0.7152 * out_data[4 * 1 + 1] + 99 | 0.0722 * out_data[4 * 1 + 2]; 100 | EXPECT_NEAR(old_luminance, new_luminance, 0.001); 101 | 102 | // It should also have significantly more blue then green, 103 | // and significantly less red than green. 104 | EXPECT_GT(out_data[4 * 1 + 2] - out_data[4 * 1 + 1], 0.05); 105 | EXPECT_GT(out_data[4 * 1 + 1] - out_data[4 * 1 + 0], 0.05); 106 | } 107 | 108 | } // namespace movit 109 | -------------------------------------------------------------------------------- /dither_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_DITHER_EFFECT_H 2 | #define _MOVIT_DITHER_EFFECT_H 1 3 | 4 | // Implements simple rectangular-PDF dither. 5 | // 6 | // Although all of our processing internally is in floating-point (a mix of 16- 7 | // and 32-bit), eventually most pipelines will end up downconverting to a fixed-point 8 | // format, typically 8-bits unsigned integer (GL_RGBA8). 9 | // 10 | // The hardware will typically do proper rounding for us, so that we minimize 11 | // quantization noise, but for some applications, if you look closely, you can still 12 | // see some banding; 8 bits is not really all that much (and if we didn't have the 13 | // perceptual gamma curve, it would be a lot worse). 14 | // 15 | // The standard solution to this is dithering; in short, to add a small random component 16 | // to each pixel before quantization. This increases the overall noise floor slightly, 17 | // but allows us to represent frequency components with an amplitude lower than 1/256. 18 | // 19 | // My standard reference on dither is: 20 | // 21 | // Cameron Nicklaus Christou: “Optimal Dither and Noise Shaping in Image Processing” 22 | // http://uwspace.uwaterloo.ca/bitstream/10012/3867/1/thesis.pdf 23 | // 24 | // However, we need to make two significant deviations from the recommendations it makes. 25 | // First of all, it recommends using a triangular-PDF (TPDF) dither (which can be synthesized 26 | // effectively by adding two uniformly distributed random numbers) instead of rectangular-PDF 27 | // (RPDF; using one uniformly distributed random number), in order to make the second moment 28 | // of the error signal independent from the original image. However, since the recommended 29 | // TPDF must be twice as wide as the RPDF, it means it can go to +/- 1, which means that 30 | // some of the time, it will add enough noise to change a pixel just by itself. Given that 31 | // a very common use case for us is converting 8-bit -> 8-bit (ie., no bit reduction at all), 32 | // it would seem like a more important goal to have no noise in that situation than to 33 | // improve the dither further. 34 | // 35 | // Second, the thesis recommends noise shaping (also known as error diffusion in the image 36 | // processing world). This is, however, very hard to implement properly on a GPU, since it 37 | // almost by definition feeds the value of output pixels into the neighboring input pixels. 38 | // Maybe one could make a version that implemented the noise shapers by way of FIR filters 39 | // instead of IIR like this, but it would seem a lot of work for very subtle gain. 40 | // 41 | // We keep the dither noise fixed as long as the output resolution doesn't change; 42 | // this ensures we don't upset video codecs too much. (One could also dither in time, 43 | // like many LCD monitors do, but it starts to get very hairy, again, for limited gains.) 44 | // The dither is also deterministic across runs. 45 | 46 | #include 47 | #include 48 | 49 | #include "effect.h" 50 | 51 | namespace movit { 52 | 53 | class DitherEffect : public Effect { 54 | private: 55 | // Should not be instantiated by end users; 56 | // call EffectChain::set_dither_bits() instead. 57 | DitherEffect(); 58 | friend class EffectChain; 59 | 60 | public: 61 | ~DitherEffect(); 62 | std::string effect_type_id() const override { return "DitherEffect"; } 63 | std::string output_fragment_shader() override; 64 | 65 | // Note that if we did error diffusion, we'd actually want to diffuse the 66 | // premultiplied error. However, we need to do dithering in the same 67 | // space as quantization, whether that be pre- or postmultiply. 68 | AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; } 69 | bool strong_one_to_one_sampling() const override { return true; } 70 | 71 | void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) override; 72 | 73 | private: 74 | void update_texture(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num); 75 | 76 | int width, height, num_bits; 77 | int last_width, last_height, last_num_bits; 78 | int texture_width, texture_height; 79 | 80 | GLuint texnum; 81 | float uniform_round_fac, uniform_inv_round_fac; 82 | float uniform_tc_scale[2]; 83 | GLint uniform_dither_tex; 84 | }; 85 | 86 | } // namespace movit 87 | 88 | #endif // !defined(_MOVIT_DITHER_EFFECT_H) 89 | -------------------------------------------------------------------------------- /ycbcr.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_YCBCR_H 2 | #define _MOVIT_YCBCR_H 1 3 | 4 | // Shared utility functions between YCbCrInput, YCbCr422InterleavedInput 5 | // and YCbCrConversionEffect. 6 | // 7 | // Conversion from integer to floating-point representation in case of 8 | // Y'CbCr is seemingly tricky: 9 | // 10 | // BT.601 page 8 has a table that says that for luma, black is at 16.00_d and 11 | // white is at 235.00_d. _d seemingly means “on a floating-point scale from 0 12 | // to 255.75”, see §2.4. The .75 is because BT.601 wants to support 10-bit, 13 | // but all values are scaled for 8-bit since that's the most common; it is 14 | // specified that conversion from 8-bit to 10-bit is done by inserting two 15 | // binary zeroes at the end (not repeating bits as one would often do 16 | // otherwise). It would seem that BT.601 lives in a world where the idealized 17 | // range is really [0,256), not [0,255]. 18 | // 19 | // However, GPUs (and by extension Movit) don't work this way. For them, 20 | // typically 1.0 maps to the largest possible representable value in the 21 | // framebuffer, ie., the range [0.0,1.0] maps to [0,255] for 8-bit 22 | // and to [0,1023] (or [0_d,255.75_d] in BT.601 parlance) for 10-bit. 23 | // 24 | // BT.709 (page 5) seems to agree with BT.601; it specifies range 16–235 for 25 | // 8-bit luma, and 64–940 for 10-bit luma. This would indicate, for a GPU, 26 | // that that for 8-bit mode, the range would be 16/255 to 235/255 27 | // (0.06275 to 0.92157), while for 10-bit, it should be 64/1023 to 940/1023 28 | // (0.06256 to 0.91887). There's no good compromise here; if you select 8-bit 29 | // range, 10-bit goes out of range (white gets to 942), while if you select 30 | // 10-bit range, 8-bit gets only to 234, making true white impossible. 31 | // 32 | // Thus, you will need to specify the actual precision of the Y'CbCr source 33 | // (or destination); the num_levels field is the right place. Most people 34 | // will want to simply set this to 256, as 8-bit Y'CbCr is the most common, 35 | // but the right value will naturally depend on your input. 36 | // 37 | // We could use unsigned formats (e.g. GL_R8UI), which in a sense would 38 | // solve all of this, but then we'd lose filtering. 39 | 40 | #include "image_format.h" 41 | 42 | #include 43 | #include 44 | 45 | namespace movit { 46 | 47 | struct YCbCrFormat { 48 | // Which formula for Y' to use. 49 | YCbCrLumaCoefficients luma_coefficients; 50 | 51 | // If true, assume Y'CbCr coefficients are full-range, ie. go from 0 to 255 52 | // instead of the limited 220/225 steps in classic MPEG. For instance, 53 | // JPEG uses the Rec. 601 luma coefficients, but full range. 54 | bool full_range; 55 | 56 | // Set to 2^n for n-bit Y'CbCr (e.g. 256 for 8-bit Y'CbCr). 57 | // See file-level comment. 58 | int num_levels; 59 | 60 | // Sampling factors for chroma components. For no subsampling (4:4:4), 61 | // set both to 1. 62 | unsigned chroma_subsampling_x, chroma_subsampling_y; 63 | 64 | // Positioning of the chroma samples. MPEG-1 and JPEG is (0.5, 0.5); 65 | // MPEG-2 and newer typically are (0.0, 0.5). 66 | float cb_x_position, cb_y_position; 67 | float cr_x_position, cr_y_position; 68 | }; 69 | 70 | // Convert texel sampling offset for the given chroma channel, given that 71 | // chroma position is (0..1), we are downsampling this chroma channel 72 | // by a factor of and the texture we are sampling from 73 | // is pixels wide/high. 74 | float compute_chroma_offset(float pos, unsigned subsampling_factor, unsigned resolution); 75 | 76 | // Given , compute the values needed to turn Y'CbCr into R'G'B'; 77 | // first subtract the returned offset, then left-multiply the returned matrix 78 | // (the scaling is already folded into it). 79 | // 80 | // is the data type you're rendering from; normally, it would should match 81 | // , but for the special case of 10- and 12-bit Y'CbCr, 82 | // we support storing it in 16-bit formats, which incurs extra scaling factors. 83 | // You can get that scaling factor in if you want. 84 | void compute_ycbcr_matrix(YCbCrFormat ycbcr_format, float *offset, Eigen::Matrix3d *ycbcr_to_rgb, 85 | GLenum type = GL_UNSIGNED_BYTE, double *scale_factor = nullptr); 86 | 87 | } // namespace movit 88 | 89 | #endif // !defined(_MOVIT_YCBCR_INPUT_H) 90 | -------------------------------------------------------------------------------- /glow_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for GlowEffect. 2 | 3 | #include 4 | #include 5 | 6 | #include "effect_chain.h" 7 | #include "glow_effect.h" 8 | #include "gtest/gtest.h" 9 | #include "image_format.h" 10 | #include "test_util.h" 11 | 12 | namespace movit { 13 | 14 | TEST(GlowEffectTest, NoAmountDoesNothing) { 15 | const int size = 4; 16 | 17 | float data[size * size] = { 18 | 0.0, 1.0, 0.0, 1.0, 19 | 0.0, 1.0, 1.0, 0.0, 20 | 0.0, 0.5, 1.0, 0.5, 21 | 0.0, 0.0, 0.0, 0.0, 22 | }; 23 | float out_data[size * size]; 24 | 25 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 26 | Effect *glow_effect = tester.get_chain()->add_effect(new GlowEffect()); 27 | ASSERT_TRUE(glow_effect->set_float("radius", 2.0f)); 28 | ASSERT_TRUE(glow_effect->set_float("blurred_mix_amount", 0.0f)); 29 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 30 | 31 | expect_equal(data, out_data, size, size); 32 | } 33 | 34 | TEST(GlowEffectTest, SingleDot) { 35 | const int size = 13; 36 | const float sigma = 0.5f; 37 | const float amount = 0.2f; 38 | 39 | float data[] = { // One single dot in the middle. 40 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 41 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 42 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 43 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 44 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 45 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 46 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 47 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 48 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 49 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 51 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 52 | 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 53 | }; 54 | float expected_data[size * size], out_data[size * size]; 55 | 56 | // The output should be equal to the input, plus approximately a logistic blob. 57 | // From http://en.wikipedia.org/wiki/Logistic_distribution#Alternative_parameterization. 58 | const float c1 = M_PI / (sigma * 4 * sqrt(3.0f)); 59 | const float c2 = M_PI / (sigma * 2.0 * sqrt(3.0f)); 60 | 61 | for (int y = 0; y < size; ++y) { 62 | for (int x = 0; x < size; ++x) { 63 | float xd = c2 * (x - 6); 64 | float yd = c2 * (y - 6); 65 | expected_data[y * size + x] = data[y * size + x] + 66 | (amount * c1 * c1) / (cosh(xd) * cosh(xd) * cosh(yd) * cosh(yd)); 67 | } 68 | } 69 | 70 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 71 | Effect *glow_effect = tester.get_chain()->add_effect(new GlowEffect()); 72 | ASSERT_TRUE(glow_effect->set_float("radius", sigma)); 73 | ASSERT_TRUE(glow_effect->set_float("blurred_mix_amount", amount)); 74 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 75 | 76 | expect_equal(expected_data, out_data, size, size, 0.1f, 1e-3); 77 | } 78 | 79 | TEST(GlowEffectTest, GlowsOntoZeroAlpha) { 80 | const int size = 7; 81 | const float sigma = 1.0f; 82 | const float amount = 1.0f; 83 | 84 | float data[4 * size] = { 85 | 0.0, 0.0, 0.0, 0.0, 86 | 0.0, 0.0, 0.0, 0.0, 87 | 0.0, 0.0, 0.0, 0.0, 88 | 0.0, 1.0, 0.0, 0.5, 89 | 0.0, 0.0, 0.0, 0.0, 90 | 0.0, 0.0, 0.0, 0.0, 91 | 0.0, 0.0, 0.0, 0.0, 92 | }; 93 | float expected_data[4 * size] = { 94 | 0.0, 1.0, 0.0, 0.002, 95 | 0.0, 1.0, 0.0, 0.014, 96 | 0.0, 1.0, 0.0, 0.065, 97 | 0.0, 1.0, 0.0, 0.635, 98 | 0.0, 1.0, 0.0, 0.065, 99 | 0.0, 1.0, 0.0, 0.014, 100 | 0.0, 1.0, 0.0, 0.002, 101 | }; 102 | 103 | float out_data[4 * size]; 104 | 105 | EffectChainTester tester(data, 1, size, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 106 | Effect *glow_effect = tester.get_chain()->add_effect(new GlowEffect()); 107 | ASSERT_TRUE(glow_effect->set_float("radius", sigma)); 108 | ASSERT_TRUE(glow_effect->set_float("blurred_mix_amount", amount)); 109 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 110 | 111 | expect_equal(expected_data, out_data, 4, size); 112 | } 113 | 114 | } // namespace movit 115 | -------------------------------------------------------------------------------- /fft_input.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "effect_util.h" 7 | #include "fp16.h" 8 | #include "fft_input.h" 9 | #include "resource_pool.h" 10 | #include "util.h" 11 | 12 | using namespace std; 13 | 14 | namespace movit { 15 | 16 | FFTInput::FFTInput(unsigned width, unsigned height) 17 | : texture_num(0), 18 | fft_width(width), 19 | fft_height(height), 20 | convolve_width(width), 21 | convolve_height(height), 22 | pixel_data(nullptr) 23 | { 24 | register_int("fft_width", &fft_width); 25 | register_int("fft_height", &fft_height); 26 | register_uniform_sampler2d("tex", &uniform_tex); 27 | } 28 | 29 | FFTInput::~FFTInput() 30 | { 31 | if (texture_num != 0) { 32 | resource_pool->release_2d_texture(texture_num); 33 | } 34 | } 35 | 36 | void FFTInput::set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num) 37 | { 38 | glActiveTexture(GL_TEXTURE0 + *sampler_num); 39 | check_error(); 40 | 41 | if (texture_num == 0) { 42 | assert(pixel_data != nullptr); 43 | 44 | // Do the FFT. Our FFTs should typically be small enough and 45 | // the data changed often enough that FFTW_ESTIMATE should be 46 | // quite OK. Otherwise, we'd need to worry about caching these 47 | // plans (possibly including FFTW wisdom). 48 | fftw_complex *in = (fftw_complex *)fftw_malloc(sizeof(fftw_complex) * fft_width * fft_height); 49 | fftw_complex *out = (fftw_complex *)fftw_malloc(sizeof(fftw_complex) * fft_width * fft_height); 50 | fftw_plan p = fftw_plan_dft_2d(fft_height, fft_width, in, out, FFTW_FORWARD, FFTW_ESTIMATE); 51 | 52 | // Zero pad. 53 | for (int i = 0; i < fft_height * fft_width; ++i) { 54 | in[i][0] = 0.0; 55 | in[i][1] = 0.0; 56 | } 57 | for (unsigned y = 0; y < convolve_height; ++y) { 58 | for (unsigned x = 0; x < convolve_width; ++x) { 59 | int i = y * fft_width + x; 60 | in[i][0] = pixel_data[y * convolve_width + x]; 61 | in[i][1] = 0.0; 62 | } 63 | } 64 | 65 | fftw_execute(p); 66 | 67 | // Convert to fp16. 68 | fp16_int_t *kernel = new fp16_int_t[fft_width * fft_height * 2]; 69 | for (int i = 0; i < fft_width * fft_height; ++i) { 70 | kernel[i * 2 + 0] = fp32_to_fp16(out[i][0]); 71 | kernel[i * 2 + 1] = fp32_to_fp16(out[i][1]); 72 | } 73 | 74 | // (Re-)upload the texture. 75 | texture_num = resource_pool->create_2d_texture(GL_RG16F, fft_width, fft_height); 76 | glBindTexture(GL_TEXTURE_2D, texture_num); 77 | check_error(); 78 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 79 | check_error(); 80 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 81 | check_error(); 82 | glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 83 | check_error(); 84 | glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, fft_width, fft_height, GL_RG, GL_HALF_FLOAT, kernel); 85 | check_error(); 86 | glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); 87 | check_error(); 88 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 89 | check_error(); 90 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 91 | check_error(); 92 | 93 | fftw_free(in); 94 | fftw_free(out); 95 | delete[] kernel; 96 | } else { 97 | glBindTexture(GL_TEXTURE_2D, texture_num); 98 | check_error(); 99 | } 100 | 101 | // Bind it to a sampler. 102 | uniform_tex = *sampler_num; 103 | ++*sampler_num; 104 | } 105 | 106 | string FFTInput::output_fragment_shader() 107 | { 108 | return string("#define FIXUP_SWAP_RB 0\n#define FIXUP_RED_TO_GRAYSCALE 0\n") + 109 | read_file("flat_input.frag"); 110 | } 111 | 112 | void FFTInput::invalidate_pixel_data() 113 | { 114 | if (texture_num != 0) { 115 | resource_pool->release_2d_texture(texture_num); 116 | texture_num = 0; 117 | } 118 | } 119 | 120 | bool FFTInput::set_int(const std::string& key, int value) 121 | { 122 | if (key == "needs_mipmaps") { 123 | // We cannot supply mipmaps; it would not make any sense for FFT data. 124 | return (value == 0); 125 | } 126 | if (key == "fft_width") { 127 | if (value < int(convolve_width)) { 128 | return false; 129 | } 130 | invalidate_pixel_data(); 131 | } 132 | if (key == "fft_height") { 133 | if (value < int(convolve_height)) { 134 | return false; 135 | } 136 | invalidate_pixel_data(); 137 | } 138 | return Effect::set_int(key, value); 139 | } 140 | 141 | } // namespace movit 142 | -------------------------------------------------------------------------------- /fp16.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOVIT_FP16_H 2 | #define _MOVIT_FP16_H 1 3 | 4 | #ifdef __F16C__ 5 | #include 6 | #endif 7 | 8 | // Code for converting to and from fp16 (from fp64), without any particular 9 | // machine support, with proper IEEE round-to-even behavior (and correct 10 | // handling of NaNs and infinities). This is needed because some OpenGL 11 | // drivers don't properly round off when asked to convert data themselves. 12 | // 13 | // These routines are originally written by Fabian Giesen, and released by 14 | // him into the public domain; 15 | // see https://fgiesen.wordpress.com/2012/03/28/half-to-float-done-quic/. 16 | // They are quite fast, and can be vectorized if need be; of course, using 17 | // the f16c instructions (see below) will be faster still. 18 | 19 | namespace movit { 20 | 21 | // structs instead of ints, so that they are not implicitly convertible. 22 | struct fp32_int_t { 23 | unsigned int val; 24 | }; 25 | struct fp16_int_t { 26 | unsigned short val; 27 | }; 28 | 29 | #ifdef __F16C__ 30 | 31 | // Use the f16c instructions from Haswell if available (and we know that they 32 | // are at compile time). 33 | static inline float fp16_to_fp32(fp16_int_t x) 34 | { 35 | return _cvtsh_ss(x.val); 36 | } 37 | 38 | static inline fp16_int_t fp32_to_fp16(float x) 39 | { 40 | fp16_int_t ret; 41 | ret.val = _cvtss_sh(x, 0); 42 | return ret; 43 | } 44 | 45 | #else 46 | 47 | union fp32 { 48 | float f; 49 | unsigned int u; 50 | }; 51 | 52 | static inline float fp16_to_fp32(fp16_int_t h) 53 | { 54 | fp32 magic; magic.u = 113 << 23; 55 | unsigned int shifted_exp = 0x7c00 << 13; // exponent mask after shift 56 | fp32 o; 57 | 58 | // mantissa+exponent 59 | unsigned int shifted = (h.val & 0x7fff) << 13; 60 | unsigned int exponent = shifted & shifted_exp; 61 | 62 | // exponent cases 63 | o.u = shifted; 64 | if (exponent == 0) { // Zero / Denormal 65 | o.u += magic.u; 66 | o.f -= magic.f; 67 | } else if (exponent == shifted_exp) { // Inf/NaN 68 | o.u += (255 - 31) << 23; 69 | } else { 70 | o.u += (127 - 15) << 23; 71 | } 72 | 73 | o.u |= (h.val & 0x8000) << 16; // copy sign bit 74 | return o.f; 75 | } 76 | 77 | static inline fp16_int_t fp32_to_fp16(float x) 78 | { 79 | fp32 f; f.f = x; 80 | unsigned int f32infty = 255 << 23; 81 | unsigned int f16max = (127 + 16) << 23; 82 | fp32 denorm_magic; denorm_magic.u = ((127 - 15) + (23 - 10) + 1) << 23; 83 | unsigned int sign_mask = 0x80000000u; 84 | fp16_int_t o = { 0 }; 85 | 86 | unsigned int sign = f.u & sign_mask; 87 | f.u ^= sign; 88 | 89 | // NOTE all the integer compares in this function can be safely 90 | // compiled into signed compares since all operands are below 91 | // 0x80000000. Important if you want fast straight SSE2 code 92 | // (since there's no unsigned PCMPGTD). 93 | 94 | if (f.u >= f16max) { // result is Inf or NaN (all exponent bits set) 95 | o.val = (f.u > f32infty) ? 0x7e00 : 0x7c00; // NaN->qNaN and Inf->Inf 96 | } else { // (De)normalized number or zero 97 | if (f.u < (113 << 23)) { // resulting FP16 is subnormal or zero 98 | // use a magic value to align our 10 mantissa bits at the bottom of 99 | // the float. as long as FP addition is round-to-nearest-even this 100 | // just works. 101 | f.f += denorm_magic.f; 102 | 103 | // and one integer subtract of the bias later, we have our final float! 104 | o.val = f.u - denorm_magic.u; 105 | } else { 106 | unsigned int mant_odd = (f.u >> 13) & 1; // resulting mantissa is odd 107 | 108 | // update exponent, rounding bias part 1 109 | f.u += (unsigned(15 - 127) << 23) + 0xfff; 110 | // rounding bias part 2 111 | f.u += mant_odd; 112 | // take the bits! 113 | o.val = f.u >> 13; 114 | } 115 | } 116 | 117 | o.val |= sign >> 16; 118 | return o; 119 | } 120 | 121 | #endif 122 | 123 | // Overloads for use in templates. 124 | static inline float to_fp32(double x) { return x; } 125 | static inline float to_fp32(float x) { return x; } 126 | static inline float to_fp32(fp16_int_t x) { return fp16_to_fp32(x); } 127 | 128 | template inline T from_fp32(float x); 129 | template<> inline double from_fp32(float x) { return x; } 130 | template<> inline float from_fp32(float x) { return x; } 131 | template<> inline fp16_int_t from_fp32(float x) { return fp32_to_fp16(x); } 132 | 133 | template 134 | inline To convert_float(From x) { return from_fp32(to_fp32(x)); } 135 | 136 | template 137 | inline Same convert_float(Same x) { return x; } 138 | 139 | } // namespace movit 140 | 141 | #endif // _MOVIT_FP16_H 142 | -------------------------------------------------------------------------------- /dither_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "dither_effect.h" 7 | #include "effect_util.h" 8 | #include "init.h" 9 | #include "util.h" 10 | 11 | using namespace std; 12 | 13 | namespace movit { 14 | 15 | namespace { 16 | 17 | // A simple LCG (linear congruental generator) random generator. 18 | // We implement our own so we can be deterministic from frame to frame 19 | // and run to run; we don't have special needs for speed or quality, 20 | // as long as the period is reasonably long. The output is in range 21 | // [0, 2^31>. 22 | // 23 | // This comes from http://en.wikipedia.org/wiki/Linear_congruential_generator. 24 | unsigned lcg_rand(unsigned x) 25 | { 26 | return (x * 1103515245U + 12345U) & ((1U << 31) - 1); 27 | } 28 | 29 | } // namespace 30 | 31 | DitherEffect::DitherEffect() 32 | : width(1280), height(720), num_bits(8), 33 | last_width(-1), last_height(-1), last_num_bits(-1) 34 | { 35 | register_int("output_width", &width); 36 | register_int("output_height", &height); 37 | register_int("num_bits", &num_bits); 38 | register_uniform_float("round_fac", &uniform_round_fac); 39 | register_uniform_float("inv_round_fac", &uniform_inv_round_fac); 40 | register_uniform_vec2("tc_scale", uniform_tc_scale); 41 | register_uniform_sampler2d("dither_tex", &uniform_dither_tex); 42 | 43 | glGenTextures(1, &texnum); 44 | } 45 | 46 | DitherEffect::~DitherEffect() 47 | { 48 | glDeleteTextures(1, &texnum); 49 | } 50 | 51 | string DitherEffect::output_fragment_shader() 52 | { 53 | char buf[256]; 54 | sprintf(buf, "#define NEED_EXPLICIT_ROUND %d\n", (movit_num_wrongly_rounded > 0)); 55 | return buf + read_file("dither_effect.frag"); 56 | } 57 | 58 | void DitherEffect::update_texture(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) 59 | { 60 | float *dither_noise = new float[width * height]; 61 | float dither_double_amplitude = 1.0f / (1 << num_bits); 62 | 63 | // We don't need a strictly nonrepeating dither; reducing the resolution 64 | // to max 128x128 saves a lot of texture bandwidth, without causing any 65 | // noticeable harm to the dither's performance. 66 | texture_width = min(width, 128); 67 | texture_height = min(height, 128); 68 | 69 | // Using the resolution as a seed gives us a consistent dither from frame to frame. 70 | // It also gives a different dither for e.g. different aspect ratios, which _feels_ 71 | // good, but probably shouldn't matter. 72 | unsigned seed = (width << 16) ^ height; 73 | for (int i = 0; i < texture_width * texture_height; ++i) { 74 | seed = lcg_rand(seed); 75 | float normalized_rand = seed * (1.0f / (1U << 31)) - 0.5; // [-0.5, 0.5> 76 | dither_noise[i] = dither_double_amplitude * normalized_rand; 77 | } 78 | 79 | glActiveTexture(GL_TEXTURE0 + *sampler_num); 80 | check_error(); 81 | glBindTexture(GL_TEXTURE_2D, texnum); 82 | check_error(); 83 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 84 | check_error(); 85 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 86 | check_error(); 87 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 88 | check_error(); 89 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 90 | check_error(); 91 | glTexImage2D(GL_TEXTURE_2D, 0, GL_R16F, texture_width, texture_height, 0, GL_RED, GL_FLOAT, dither_noise); 92 | check_error(); 93 | 94 | delete[] dither_noise; 95 | } 96 | 97 | void DitherEffect::set_gl_state(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) 98 | { 99 | Effect::set_gl_state(glsl_program_num, prefix, sampler_num); 100 | 101 | assert(width > 0); 102 | assert(height > 0); 103 | assert(num_bits > 0); 104 | 105 | if (width != last_width || height != last_height || num_bits != last_num_bits) { 106 | update_texture(glsl_program_num, prefix, sampler_num); 107 | last_width = width; 108 | last_height = height; 109 | last_num_bits = num_bits; 110 | } 111 | 112 | glActiveTexture(GL_TEXTURE0 + *sampler_num); 113 | check_error(); 114 | glBindTexture(GL_TEXTURE_2D, texnum); 115 | check_error(); 116 | 117 | uniform_dither_tex = *sampler_num; 118 | ++*sampler_num; 119 | 120 | // In theory, we should adjust for the texel centers that have moved here as well, 121 | // but since we use GL_NEAREST and we don't really care a lot what texel we sample, 122 | // we don't have to worry about it. 123 | uniform_tc_scale[0] = float(width) / float(texture_width); 124 | uniform_tc_scale[1] = float(height) / float(texture_height); 125 | 126 | // Used if the shader needs to do explicit rounding. 127 | int round_fac = (1 << num_bits) - 1; 128 | uniform_round_fac = round_fac; 129 | uniform_inv_round_fac = 1.0f / round_fac; 130 | } 131 | 132 | } // namespace movit 133 | -------------------------------------------------------------------------------- /blur_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for BlurEffect. 2 | #include 3 | #include 4 | #include 5 | 6 | #include "blur_effect.h" 7 | #include "effect_chain.h" 8 | #include "gtest/gtest.h" 9 | #include "image_format.h" 10 | #include "test_util.h" 11 | 12 | namespace movit { 13 | 14 | TEST(BlurEffectTest, IdentityTransformDoesNothing) { 15 | const int size = 4; 16 | 17 | float data[size * size] = { 18 | 0.0, 1.0, 0.0, 1.0, 19 | 0.0, 1.0, 1.0, 0.0, 20 | 0.0, 0.5, 1.0, 0.5, 21 | 0.0, 0.0, 0.0, 0.0, 22 | }; 23 | float out_data[size * size]; 24 | 25 | for (int num_taps = 2; num_taps < 20; num_taps += 2) { 26 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 27 | Effect *blur_effect = tester.get_chain()->add_effect(new BlurEffect()); 28 | ASSERT_TRUE(blur_effect->set_float("radius", 0.0f)); 29 | ASSERT_TRUE(blur_effect->set_int("num_taps", num_taps)); 30 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 31 | 32 | expect_equal(data, out_data, size, size); 33 | } 34 | } 35 | 36 | namespace { 37 | 38 | void add_blurred_point(float *out, int size, int x0, int y0, float strength, float sigma) 39 | { 40 | // From http://en.wikipedia.org/wiki/Logistic_distribution#Alternative_parameterization. 41 | const float c1 = M_PI / (sigma * 4 * sqrt(3.0f)); 42 | const float c2 = M_PI / (sigma * 2.0 * sqrt(3.0f)); 43 | 44 | for (int y = 0; y < size; ++y) { 45 | for (int x = 0; x < size; ++x) { 46 | float xd = c2 * (x - x0); 47 | float yd = c2 * (y - y0); 48 | out[y * size + x] += (strength * c1 * c1) / (cosh(xd) * cosh(xd) * cosh(yd) * cosh(yd)); 49 | } 50 | } 51 | } 52 | 53 | } // namespace 54 | 55 | TEST(BlurEffectTest, BlurTwoDotsSmallRadius) { 56 | const float sigma = 3.0f; 57 | const int size = 32; 58 | const int x1 = 8; 59 | const int y1 = 8; 60 | const int x2 = 20; 61 | const int y2 = 10; 62 | 63 | float data[size * size], out_data[size * size], expected_data[size * size]; 64 | memset(data, 0, sizeof(data)); 65 | memset(expected_data, 0, sizeof(expected_data)); 66 | 67 | data[y1 * size + x1] = 1.0f; 68 | data[y2 * size + x2] = 1.0f; 69 | 70 | add_blurred_point(expected_data, size, x1, y1, 1.0f, sigma); 71 | add_blurred_point(expected_data, size, x2, y2, 1.0f, sigma); 72 | 73 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 74 | Effect *blur_effect = tester.get_chain()->add_effect(new BlurEffect()); 75 | ASSERT_TRUE(blur_effect->set_float("radius", sigma)); 76 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 77 | 78 | // Set the limits a bit tighter than usual, since there is so little energy in here. 79 | expect_equal(expected_data, out_data, size, size, 1e-3, 1e-5); 80 | } 81 | 82 | TEST(BlurEffectTest, BlurTwoDotsLargeRadius) { 83 | const float sigma = 20.0f; // Large enough that we will begin scaling. 84 | const int size = 256; 85 | const int x1 = 64; 86 | const int y1 = 64; 87 | const int x2 = 160; 88 | const int y2 = 120; 89 | 90 | static float data[size * size], out_data[size * size], expected_data[size * size]; 91 | memset(data, 0, sizeof(data)); 92 | memset(expected_data, 0, sizeof(expected_data)); 93 | 94 | data[y1 * size + x1] = 128.0f; 95 | data[y2 * size + x2] = 128.0f; 96 | 97 | add_blurred_point(expected_data, size, x1, y1, 128.0f, sigma); 98 | add_blurred_point(expected_data, size, x2, y2, 128.0f, sigma); 99 | 100 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 101 | Effect *blur_effect = tester.get_chain()->add_effect(new BlurEffect()); 102 | ASSERT_TRUE(blur_effect->set_float("radius", sigma)); 103 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 104 | 105 | expect_equal(expected_data, out_data, size, size, 0.1f, 1e-3); 106 | } 107 | 108 | TEST(BlurEffectTest, BlurTwoDotsSmallRadiusFewerTaps) { 109 | const float sigma = 3.0f; 110 | const int size = 32; 111 | const int x1 = 8; 112 | const int y1 = 8; 113 | const int x2 = 20; 114 | const int y2 = 10; 115 | 116 | float data[size * size], out_data[size * size], expected_data[size * size]; 117 | memset(data, 0, sizeof(data)); 118 | memset(expected_data, 0, sizeof(expected_data)); 119 | 120 | data[y1 * size + x1] = 1.0f; 121 | data[y2 * size + x2] = 1.0f; 122 | 123 | add_blurred_point(expected_data, size, x1, y1, 1.0f, sigma); 124 | add_blurred_point(expected_data, size, x2, y2, 1.0f, sigma); 125 | 126 | EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 127 | Effect *blur_effect = tester.get_chain()->add_effect(new BlurEffect()); 128 | ASSERT_TRUE(blur_effect->set_float("radius", sigma)); 129 | ASSERT_TRUE(blur_effect->set_int("num_taps", 10)); 130 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 131 | 132 | // Set the limits a bit tighter than usual, since there is so little energy in here. 133 | expect_equal(expected_data, out_data, size, size, 1e-3, 1e-5); 134 | } 135 | 136 | } // namespace movit 137 | -------------------------------------------------------------------------------- /luma_mix_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for LumaMixEffect. 2 | 3 | #include 4 | 5 | #include "effect_chain.h" 6 | #include "gtest/gtest.h" 7 | #include "image_format.h" 8 | #include "input.h" 9 | #include "luma_mix_effect.h" 10 | #include "test_util.h" 11 | 12 | namespace movit { 13 | 14 | TEST(LumaMixEffectTest, HardWipe) { 15 | float data_a[] = { 16 | 0.0f, 0.25f, 17 | 0.75f, 1.0f, 18 | }; 19 | float data_b[] = { 20 | 1.0f, 0.5f, 21 | 0.65f, 0.6f, 22 | }; 23 | float data_luma[] = { 24 | 0.0f, 0.25f, 25 | 0.5f, 0.75f, 26 | }; 27 | 28 | EffectChainTester tester(data_a, 2, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 29 | Effect *input1 = tester.get_chain()->last_added_effect(); 30 | Effect *input2 = tester.add_input(data_b, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 31 | Effect *input3 = tester.add_input(data_luma, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 32 | 33 | Effect *luma_mix_effect = tester.get_chain()->add_effect(new LumaMixEffect(), input1, input2, input3); 34 | ASSERT_TRUE(luma_mix_effect->set_float("transition_width", 100000.0f)); 35 | 36 | float out_data[4]; 37 | ASSERT_TRUE(luma_mix_effect->set_float("progress", 0.0f)); 38 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 39 | expect_equal(data_a, out_data, 2, 2); 40 | 41 | // Lower right from B, the rest from A. 42 | float expected_data_049[] = { 43 | 0.0f, 0.25f, 44 | 0.75f, 0.6f, 45 | }; 46 | ASSERT_TRUE(luma_mix_effect->set_float("progress", 0.49f)); 47 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 48 | expect_equal(expected_data_049, out_data, 2, 2); 49 | 50 | // Lower two from B, the rest from A. 51 | float expected_data_051[] = { 52 | 0.0f, 0.25f, 53 | 0.65f, 0.6f, 54 | }; 55 | ASSERT_TRUE(luma_mix_effect->set_float("progress", 0.51f)); 56 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 57 | expect_equal(expected_data_051, out_data, 2, 2); 58 | 59 | ASSERT_TRUE(luma_mix_effect->set_float("progress", 1.0f)); 60 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 61 | expect_equal(data_b, out_data, 2, 2); 62 | } 63 | 64 | TEST(LumaMixEffectTest, SoftWipeHalfWayThrough) { 65 | float data_a[] = { 66 | 0.0f, 0.25f, 67 | 0.75f, 1.0f, 68 | }; 69 | float data_b[] = { 70 | 1.0f, 0.5f, 71 | 0.65f, 0.6f, 72 | }; 73 | float data_luma[] = { 74 | 0.0f, 0.25f, 75 | 0.5f, 0.75f, 76 | }; 77 | // At this point, the luma range and the mix range should exactly line up, 78 | // so we get a straight-up fade by luma. 79 | float expected_data[] = { 80 | data_a[0] + (data_b[0] - data_a[0]) * data_luma[0], 81 | data_a[1] + (data_b[1] - data_a[1]) * data_luma[1], 82 | data_a[2] + (data_b[2] - data_a[2]) * data_luma[2], 83 | data_a[3] + (data_b[3] - data_a[3]) * data_luma[3], 84 | }; 85 | float out_data[4]; 86 | 87 | EffectChainTester tester(data_a, 2, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 88 | Effect *input1 = tester.get_chain()->last_added_effect(); 89 | Effect *input2 = tester.add_input(data_b, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 90 | Effect *input3 = tester.add_input(data_luma, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 91 | 92 | Effect *luma_mix_effect = tester.get_chain()->add_effect(new LumaMixEffect(), input1, input2, input3); 93 | ASSERT_TRUE(luma_mix_effect->set_float("transition_width", 1.0f)); 94 | ASSERT_TRUE(luma_mix_effect->set_float("progress", 0.5f)); 95 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 96 | expect_equal(expected_data, out_data, 2, 2); 97 | } 98 | 99 | TEST(LumaMixEffectTest, Inverse) { 100 | float data_a[] = { 101 | 0.0f, 0.25f, 102 | 0.75f, 1.0f, 103 | }; 104 | float data_b[] = { 105 | 1.0f, 0.5f, 106 | 0.65f, 0.6f, 107 | }; 108 | float data_luma[] = { 109 | 0.0f, 0.25f, 110 | 0.5f, 0.75f, 111 | }; 112 | 113 | EffectChainTester tester(data_a, 2, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 114 | Effect *input1 = tester.get_chain()->last_added_effect(); 115 | Effect *input2 = tester.add_input(data_b, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 116 | Effect *input3 = tester.add_input(data_luma, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 117 | 118 | Effect *luma_mix_effect = tester.get_chain()->add_effect(new LumaMixEffect(), input1, input2, input3); 119 | ASSERT_TRUE(luma_mix_effect->set_float("transition_width", 100000.0f)); 120 | ASSERT_TRUE(luma_mix_effect->set_int("inverse", 1)); 121 | 122 | // Inverse is not the same as reverse, so progress=0 should behave identically 123 | // as HardWipe, ie. everything should be from A. 124 | float out_data[4]; 125 | ASSERT_TRUE(luma_mix_effect->set_float("progress", 0.0f)); 126 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 127 | expect_equal(data_a, out_data, 2, 2); 128 | 129 | // Lower two from A, the rest from B. 130 | float expected_data_049[] = { 131 | 1.0f, 0.5f, 132 | 0.75f, 1.0f, 133 | }; 134 | ASSERT_TRUE(luma_mix_effect->set_float("progress", 0.49f)); 135 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 136 | expect_equal(expected_data_049, out_data, 2, 2); 137 | } 138 | 139 | } // namespace movit 140 | -------------------------------------------------------------------------------- /mix_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for MixEffect. 2 | 3 | #include 4 | 5 | #include "effect_chain.h" 6 | #include "gtest/gtest.h" 7 | #include "image_format.h" 8 | #include "input.h" 9 | #include "mix_effect.h" 10 | #include "test_util.h" 11 | 12 | namespace movit { 13 | 14 | TEST(MixEffectTest, FiftyFiftyMix) { 15 | float data_a[] = { 16 | 0.0f, 0.25f, 17 | 0.75f, 1.0f, 18 | }; 19 | float data_b[] = { 20 | 1.0f, 0.5f, 21 | 0.75f, 0.6f, 22 | }; 23 | float expected_data[] = { 24 | 0.5f, 0.375f, 25 | 0.75f, 0.8f, 26 | }; 27 | float out_data[4]; 28 | EffectChainTester tester(data_a, 2, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 29 | Effect *input1 = tester.get_chain()->last_added_effect(); 30 | Effect *input2 = tester.add_input(data_b, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 31 | 32 | Effect *mix_effect = tester.get_chain()->add_effect(new MixEffect(), input1, input2); 33 | ASSERT_TRUE(mix_effect->set_float("strength_first", 0.5f)); 34 | ASSERT_TRUE(mix_effect->set_float("strength_second", 0.5f)); 35 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 36 | 37 | expect_equal(expected_data, out_data, 2, 2); 38 | } 39 | 40 | TEST(MixEffectTest, OnlyA) { 41 | float data_a[] = { 42 | 0.0f, 0.25f, 43 | 0.75f, 1.0f, 44 | }; 45 | float data_b[] = { 46 | 1.0f, 0.5f, 47 | 0.75f, 0.6f, 48 | }; 49 | float out_data[4]; 50 | EffectChainTester tester(data_a, 2, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 51 | Effect *input1 = tester.get_chain()->last_added_effect(); 52 | Effect *input2 = tester.add_input(data_b, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR); 53 | 54 | Effect *mix_effect = tester.get_chain()->add_effect(new MixEffect(), input1, input2); 55 | ASSERT_TRUE(mix_effect->set_float("strength_first", 1.0f)); 56 | ASSERT_TRUE(mix_effect->set_float("strength_second", 0.0f)); 57 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); 58 | 59 | expect_equal(data_a, out_data, 2, 2); 60 | } 61 | 62 | TEST(MixEffectTest, DoesNotSumToOne) { 63 | float data_a[] = { 64 | 1.0f, 0.5f, 0.75f, 0.333f, 65 | }; 66 | float data_b[] = { 67 | 1.0f, 0.25f, 0.15f, 0.333f, 68 | }; 69 | 70 | // The fact that the RGB values don't sum but get averaged here might 71 | // actually be a surprising result, but when you think of it, 72 | // it does make physical sense. 73 | float expected_data[] = { 74 | 1.0f, 0.375f, 0.45f, 0.666f, 75 | }; 76 | 77 | float out_data[4]; 78 | EffectChainTester tester(data_a, 1, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 79 | Effect *input1 = tester.get_chain()->last_added_effect(); 80 | Effect *input2 = tester.add_input(data_b, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 81 | 82 | Effect *mix_effect = tester.get_chain()->add_effect(new MixEffect(), input1, input2); 83 | ASSERT_TRUE(mix_effect->set_float("strength_first", 1.0f)); 84 | ASSERT_TRUE(mix_effect->set_float("strength_second", 1.0f)); 85 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 86 | 87 | expect_equal(expected_data, out_data, 4, 1); 88 | } 89 | 90 | TEST(MixEffectTest, AdditiveBlendingWorksForBothTotallyOpaqueAndPartiallyTranslucent) { 91 | float data_a[] = { 92 | 0.0f, 0.5f, 0.75f, 1.0f, 93 | 1.0f, 1.0f, 1.0f, 0.2f, 94 | }; 95 | float data_b[] = { 96 | 1.0f, 0.25f, 0.15f, 1.0f, 97 | 1.0f, 1.0f, 1.0f, 0.5f, 98 | }; 99 | 100 | float expected_data[] = { 101 | 1.0f, 0.75f, 0.9f, 1.0f, 102 | 1.0f, 1.0f, 1.0f, 0.7f, 103 | }; 104 | 105 | float out_data[8]; 106 | EffectChainTester tester(data_a, 1, 2, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 107 | Effect *input1 = tester.get_chain()->last_added_effect(); 108 | Effect *input2 = tester.add_input(data_b, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 109 | 110 | Effect *mix_effect = tester.get_chain()->add_effect(new MixEffect(), input1, input2); 111 | ASSERT_TRUE(mix_effect->set_float("strength_first", 1.0f)); 112 | ASSERT_TRUE(mix_effect->set_float("strength_second", 1.0f)); 113 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 114 | 115 | expect_equal(expected_data, out_data, 4, 2); 116 | } 117 | 118 | TEST(MixEffectTest, MixesLinearlyDespitesRGBInputsAndOutputs) { 119 | float data_a[] = { 120 | 0.0f, 0.25f, 121 | 0.75f, 1.0f, 122 | }; 123 | float data_b[] = { 124 | 0.0f, 0.0f, 125 | 0.0f, 0.0f, 126 | }; 127 | float expected_data[] = { // sRGB(0.5 * inv_sRGB(a)). 128 | 0.00000f, 0.17349f, 129 | 0.54807f, 0.73536f, 130 | }; 131 | float out_data[4]; 132 | EffectChainTester tester(data_a, 2, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB); 133 | Effect *input1 = tester.get_chain()->last_added_effect(); 134 | Effect *input2 = tester.add_input(data_b, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB); 135 | 136 | Effect *mix_effect = tester.get_chain()->add_effect(new MixEffect(), input1, input2); 137 | ASSERT_TRUE(mix_effect->set_float("strength_first", 0.5f)); 138 | ASSERT_TRUE(mix_effect->set_float("strength_second", 0.5f)); 139 | tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB); 140 | 141 | expect_equal(expected_data, out_data, 2, 2); 142 | } 143 | 144 | } // namespace movit 145 | -------------------------------------------------------------------------------- /lift_gamma_gain_effect_test.cpp: -------------------------------------------------------------------------------- 1 | // Unit tests for LiftGammaGainEffect. 2 | 3 | #include 4 | 5 | #include "effect_chain.h" 6 | #include "gtest/gtest.h" 7 | #include "image_format.h" 8 | #include "lift_gamma_gain_effect.h" 9 | #include "test_util.h" 10 | 11 | namespace movit { 12 | 13 | TEST(LiftGammaGainEffectTest, DefaultIsNoop) { 14 | float data[] = { 15 | 0.0f, 0.0f, 0.0f, 1.0f, 16 | 0.5f, 0.5f, 0.5f, 0.3f, 17 | 1.0f, 0.0f, 0.0f, 1.0f, 18 | 0.0f, 1.0f, 0.0f, 0.7f, 19 | 0.0f, 0.0f, 1.0f, 1.0f, 20 | }; 21 | 22 | float out_data[5 * 4]; 23 | EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 24 | tester.get_chain()->add_effect(new LiftGammaGainEffect()); 25 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 26 | 27 | expect_equal(data, out_data, 4, 5); 28 | } 29 | 30 | TEST(LiftGammaGainEffectTest, Gain) { 31 | float data[] = { 32 | 0.0f, 0.0f, 0.0f, 1.0f, 33 | 0.5f, 0.5f, 0.5f, 0.3f, 34 | 1.0f, 0.0f, 0.0f, 1.0f, 35 | 0.0f, 1.0f, 0.0f, 0.7f, 36 | 0.0f, 0.0f, 1.0f, 1.0f, 37 | }; 38 | float gain[3] = { 0.8f, 1.0f, 1.2f }; 39 | float expected_data[] = { 40 | 0.0f, 0.0f, 0.0f, 1.0f, 41 | 0.4f, 0.5f, 0.6f, 0.3f, 42 | 0.8f, 0.0f, 0.0f, 1.0f, 43 | 0.0f, 1.0f, 0.0f, 0.7f, 44 | 0.0f, 0.0f, 1.2f, 1.0f, 45 | }; 46 | 47 | float out_data[5 * 4]; 48 | EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 49 | Effect *lgg_effect = tester.get_chain()->add_effect(new LiftGammaGainEffect()); 50 | ASSERT_TRUE(lgg_effect->set_vec3("gain", gain)); 51 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 52 | 53 | expect_equal(expected_data, out_data, 4, 5); 54 | } 55 | 56 | TEST(LiftGammaGainEffectTest, LiftIsDoneInApproximatelysRGB) { 57 | float data[] = { 58 | 0.0f, 0.0f, 0.0f, 1.0f, 59 | 0.5f, 0.5f, 0.5f, 0.3f, 60 | 1.0f, 0.0f, 0.0f, 1.0f, 61 | 0.0f, 1.0f, 0.0f, 0.7f, 62 | 0.0f, 0.0f, 1.0f, 1.0f, 63 | }; 64 | float lift[3] = { 0.0f, 0.1f, 0.2f }; 65 | float expected_data[] = { 66 | 0.0f, 0.1f , 0.2f, 1.0f, 67 | 0.5f, 0.55f, 0.6f, 0.3f, 68 | 1.0f, 0.1f, 0.2f, 1.0f, 69 | 0.0f, 1.0f, 0.2f, 0.7f, 70 | 0.0f, 0.1f, 1.0f, 1.0f, 71 | }; 72 | 73 | float out_data[5 * 4]; 74 | EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB); 75 | Effect *lgg_effect = tester.get_chain()->add_effect(new LiftGammaGainEffect()); 76 | ASSERT_TRUE(lgg_effect->set_vec3("lift", lift)); 77 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB); 78 | 79 | // sRGB is only approximately gamma-2.2, so loosen up the limits a bit. 80 | expect_equal(expected_data, out_data, 4, 5, 0.03, 0.003); 81 | } 82 | 83 | TEST(LiftGammaGainEffectTest, Gamma22IsApproximatelysRGB) { 84 | float data[] = { 85 | 0.0f, 0.0f, 0.0f, 1.0f, 86 | 0.5f, 0.5f, 0.5f, 0.3f, 87 | 1.0f, 0.0f, 0.0f, 1.0f, 88 | 0.0f, 1.0f, 0.0f, 0.7f, 89 | 0.0f, 0.0f, 1.0f, 1.0f, 90 | }; 91 | float gamma[3] = { 2.2f, 2.2f, 2.2f }; 92 | 93 | float out_data[5 * 4]; 94 | EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB); 95 | Effect *lgg_effect = tester.get_chain()->add_effect(new LiftGammaGainEffect()); 96 | ASSERT_TRUE(lgg_effect->set_vec3("gamma", gamma)); 97 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 98 | 99 | expect_equal(data, out_data, 4, 5); 100 | } 101 | 102 | TEST(LiftGammaGainEffectTest, OutOfGamutColorsAreClipped) { 103 | float data[] = { 104 | -0.5f, 0.3f, 0.0f, 1.0f, 105 | 0.5f, 0.0f, 0.0f, 1.0f, 106 | 0.0f, 1.5f, 0.5f, 0.3f, 107 | }; 108 | float expected_data[] = { 109 | 0.0f, 0.3f, 0.0f, 1.0f, // Clipped to zero. 110 | 0.5f, 0.0f, 0.0f, 1.0f, 111 | 0.0f, 1.5f, 0.5f, 0.3f, 112 | }; 113 | 114 | float out_data[3 * 4]; 115 | EffectChainTester tester(data, 1, 3, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR); 116 | tester.get_chain()->add_effect(new LiftGammaGainEffect()); 117 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR); 118 | 119 | expect_equal(expected_data, out_data, 4, 3); 120 | } 121 | 122 | TEST(LiftGammaGainEffectTest, NegativeLiftIsClamped) { 123 | float data[] = { 124 | 0.0f, 0.0f, 0.0f, 1.0f, 125 | 0.5f, 0.5f, 0.5f, 0.3f, 126 | 1.0f, 0.0f, 0.0f, 1.0f, 127 | 0.0f, 1.0f, 0.0f, 0.7f, 128 | 0.0f, 0.0f, 1.0f, 1.0f, 129 | }; 130 | float lift[3] = { 0.0f, -0.1f, -0.2f }; 131 | float expected_data[] = { 132 | 0.0f, 0.0f , 0.0f, 1.0f, // Note: Clamped below zero. 133 | 0.5f, 0.45f, 0.4f, 0.3f, 134 | 1.0f, 0.0f, 0.0f, 1.0f, // Unaffected. 135 | 0.0f, 1.0f, 0.0f, 0.7f, 136 | 0.0f, 0.0f, 1.0f, 1.0f, 137 | }; 138 | 139 | float out_data[5 * 4]; 140 | EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB); 141 | Effect *lgg_effect = tester.get_chain()->add_effect(new LiftGammaGainEffect()); 142 | ASSERT_TRUE(lgg_effect->set_vec3("lift", lift)); 143 | tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB); 144 | 145 | // sRGB is only approximately gamma-2.2, so loosen up the limits a bit. 146 | expect_equal(expected_data, out_data, 4, 5, 0.03, 0.003); 147 | } 148 | 149 | } // namespace movit 150 | -------------------------------------------------------------------------------- /ycbcr_422interleaved_input.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "effect_util.h" 7 | #include "resource_pool.h" 8 | #include "util.h" 9 | #include "ycbcr.h" 10 | #include "ycbcr_422interleaved_input.h" 11 | 12 | using namespace Eigen; 13 | using namespace std; 14 | 15 | namespace movit { 16 | 17 | YCbCr422InterleavedInput::YCbCr422InterleavedInput(const ImageFormat &image_format, 18 | const YCbCrFormat &ycbcr_format, 19 | unsigned width, unsigned height) 20 | : image_format(image_format), 21 | ycbcr_format(ycbcr_format), 22 | width(width), 23 | height(height), 24 | resource_pool(nullptr) 25 | { 26 | pbo = 0; 27 | texture_num[0] = texture_num[1] = 0; 28 | 29 | assert(ycbcr_format.chroma_subsampling_x == 2); 30 | assert(ycbcr_format.chroma_subsampling_y == 1); 31 | assert(width % ycbcr_format.chroma_subsampling_x == 0); 32 | 33 | widths[CHANNEL_LUMA] = width; 34 | widths[CHANNEL_CHROMA] = width / ycbcr_format.chroma_subsampling_x; 35 | pitches[CHANNEL_LUMA] = width; 36 | pitches[CHANNEL_CHROMA] = width / ycbcr_format.chroma_subsampling_x; 37 | 38 | pixel_data = nullptr; 39 | 40 | register_uniform_sampler2d("tex_y", &uniform_tex_y); 41 | register_uniform_sampler2d("tex_cbcr", &uniform_tex_cbcr); 42 | } 43 | 44 | YCbCr422InterleavedInput::~YCbCr422InterleavedInput() 45 | { 46 | for (unsigned channel = 0; channel < 2; ++channel) { 47 | if (texture_num[channel] != 0) { 48 | resource_pool->release_2d_texture(texture_num[channel]); 49 | } 50 | } 51 | } 52 | 53 | void YCbCr422InterleavedInput::set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num) 54 | { 55 | for (unsigned channel = 0; channel < 2; ++channel) { 56 | glActiveTexture(GL_TEXTURE0 + *sampler_num + channel); 57 | check_error(); 58 | 59 | if (texture_num[channel] == 0) { 60 | // (Re-)upload the texture. 61 | GLuint format, internal_format; 62 | if (channel == CHANNEL_LUMA) { 63 | format = GL_RG; 64 | internal_format = GL_RG8; 65 | } else { 66 | assert(channel == CHANNEL_CHROMA); 67 | format = GL_RGBA; 68 | internal_format = GL_RGBA8; 69 | } 70 | 71 | texture_num[channel] = resource_pool->create_2d_texture(internal_format, widths[channel], height); 72 | glBindTexture(GL_TEXTURE_2D, texture_num[channel]); 73 | check_error(); 74 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 75 | check_error(); 76 | glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, pbo); 77 | check_error(); 78 | glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 79 | check_error(); 80 | glPixelStorei(GL_UNPACK_ROW_LENGTH, pitches[channel]); 81 | check_error(); 82 | glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, widths[channel], height, format, GL_UNSIGNED_BYTE, pixel_data); 83 | check_error(); 84 | glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); 85 | check_error(); 86 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 87 | check_error(); 88 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 89 | check_error(); 90 | } else { 91 | glBindTexture(GL_TEXTURE_2D, texture_num[channel]); 92 | check_error(); 93 | } 94 | } 95 | 96 | glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, 0); 97 | check_error(); 98 | 99 | // Bind samplers. 100 | uniform_tex_y = *sampler_num + 0; 101 | uniform_tex_cbcr = *sampler_num + 1; 102 | 103 | *sampler_num += 2; 104 | } 105 | 106 | string YCbCr422InterleavedInput::output_fragment_shader() 107 | { 108 | float offset[3]; 109 | Matrix3d ycbcr_to_rgb; 110 | compute_ycbcr_matrix(ycbcr_format, offset, &ycbcr_to_rgb); 111 | 112 | string frag_shader; 113 | 114 | frag_shader = output_glsl_mat3("PREFIX(inv_ycbcr_matrix)", ycbcr_to_rgb); 115 | frag_shader += output_glsl_vec3("PREFIX(offset)", offset[0], offset[1], offset[2]); 116 | 117 | float cb_offset_x = compute_chroma_offset( 118 | ycbcr_format.cb_x_position, ycbcr_format.chroma_subsampling_x, widths[CHANNEL_CHROMA]); 119 | float cr_offset_x = compute_chroma_offset( 120 | ycbcr_format.cr_x_position, ycbcr_format.chroma_subsampling_x, widths[CHANNEL_CHROMA]); 121 | frag_shader += output_glsl_float("PREFIX(cb_offset_x)", cb_offset_x); 122 | frag_shader += output_glsl_float("PREFIX(cr_offset_x)", cr_offset_x); 123 | 124 | char buf[256]; 125 | sprintf(buf, "#define CB_CR_OFFSETS_EQUAL %d\n", 126 | (fabs(ycbcr_format.cb_x_position - ycbcr_format.cr_x_position) < 1e-6)); 127 | frag_shader += buf; 128 | 129 | frag_shader += read_file("ycbcr_422interleaved_input.frag"); 130 | return frag_shader; 131 | } 132 | 133 | void YCbCr422InterleavedInput::invalidate_pixel_data() 134 | { 135 | for (unsigned channel = 0; channel < 2; ++channel) { 136 | if (texture_num[channel] != 0) { 137 | resource_pool->release_2d_texture(texture_num[channel]); 138 | texture_num[channel] = 0; 139 | } 140 | } 141 | } 142 | 143 | bool YCbCr422InterleavedInput::set_int(const std::string& key, int value) 144 | { 145 | if (key == "needs_mipmaps") { 146 | // We currently do not support this. 147 | return (value == 0); 148 | } 149 | return Effect::set_int(key, value); 150 | } 151 | 152 | } // namespace movit 153 | -------------------------------------------------------------------------------- /ycbcr.cpp: -------------------------------------------------------------------------------- 1 | // Note: These functions are tested in ycbcr_input_test.cpp; both through some 2 | // direct matrix tests, but most of all through YCbCrInput's unit tests. 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "ycbcr.h" 9 | 10 | using namespace Eigen; 11 | 12 | namespace movit { 13 | 14 | // OpenGL has texel center in (0.5, 0.5), but different formats have 15 | // chroma in various other places. If luma samples are X, the chroma 16 | // sample is *, and subsampling is 3x3, the situation with chroma 17 | // center in (0.5, 0.5) looks approximately like this: 18 | // 19 | // X X 20 | // * 21 | // X X 22 | // 23 | // If, on the other hand, chroma center is in (0.0, 0.5) (common 24 | // for e.g. MPEG-4), the figure changes to: 25 | // 26 | // X X 27 | // * 28 | // X X 29 | // 30 | // In other words, (0.0, 0.0) means that the chroma sample is exactly 31 | // co-sited on top of the top-left luma sample. Note, however, that 32 | // this is _not_ 0.5 texels to the left, since the OpenGL's texel center 33 | // is in (0.5, 0.5); it is in (0.25, 0.25). In a sense, the four luma samples 34 | // define a square where chroma position (0.0, 0.0) is in texel position 35 | // (0.25, 0.25) and chroma position (1.0, 1.0) is in texel position (0.75, 0.75) 36 | // (the outer border shows the borders of the texel itself, ie. from 37 | // (0, 0) to (1, 1)): 38 | // 39 | // --------- 40 | // | | 41 | // | X---X | 42 | // | | * | | 43 | // | X---X | 44 | // | | 45 | // --------- 46 | // 47 | // Also note that if we have no subsampling, the square will have zero 48 | // area and the chroma position does not matter at all. 49 | float compute_chroma_offset(float pos, unsigned subsampling_factor, unsigned resolution) 50 | { 51 | float local_chroma_pos = (0.5 + pos * (subsampling_factor - 1)) / subsampling_factor; 52 | if (fabs(local_chroma_pos - 0.5) < 1e-10) { 53 | // x + (-0) can be optimized away freely, as opposed to x + 0. 54 | return -0.0; 55 | } else { 56 | return (0.5 - local_chroma_pos) / resolution; 57 | } 58 | } 59 | 60 | // Given , compute the values needed to turn Y'CbCr into R'G'B'; 61 | // first subtract the returned offset, then left-multiply the returned matrix 62 | // (the scaling is already folded into it). 63 | void compute_ycbcr_matrix(YCbCrFormat ycbcr_format, float* offset, Matrix3d* ycbcr_to_rgb, GLenum type, double *scale_factor) 64 | { 65 | double coeff[3], scale[3]; 66 | 67 | switch (ycbcr_format.luma_coefficients) { 68 | case YCBCR_REC_601: 69 | // Rec. 601, page 2. 70 | coeff[0] = 0.299; 71 | coeff[1] = 0.587; 72 | coeff[2] = 0.114; 73 | break; 74 | 75 | case YCBCR_REC_709: 76 | // Rec. 709, page 19. 77 | coeff[0] = 0.2126; 78 | coeff[1] = 0.7152; 79 | coeff[2] = 0.0722; 80 | break; 81 | 82 | case YCBCR_REC_2020: 83 | // Rec. 2020, page 4. 84 | coeff[0] = 0.2627; 85 | coeff[1] = 0.6780; 86 | coeff[2] = 0.0593; 87 | break; 88 | 89 | default: 90 | assert(false); 91 | } 92 | 93 | int num_levels = ycbcr_format.num_levels; 94 | if (num_levels == 0) { 95 | // For the benefit of clients using old APIs, but still zeroing out the structure. 96 | num_levels = 256; 97 | } 98 | if (ycbcr_format.full_range) { 99 | offset[0] = 0.0 / (num_levels - 1); 100 | offset[1] = double(num_levels / 2) / (num_levels - 1); // E.g. 128/255. 101 | offset[2] = double(num_levels / 2) / (num_levels - 1); 102 | 103 | scale[0] = 1.0; 104 | scale[1] = 1.0; 105 | scale[2] = 1.0; 106 | } else { 107 | // Rec. 601, page 4; Rec. 709, page 19; Rec. 2020, page 5. 108 | // Rec. 2020 contains the most generic formulas, which we use here. 109 | const double s = num_levels / 256.0; // 2^(n-8) in Rec. 2020 parlance. 110 | offset[0] = (s * 16.0) / (num_levels - 1); 111 | offset[1] = (s * 128.0) / (num_levels - 1); 112 | offset[2] = (s * 128.0) / (num_levels - 1); 113 | 114 | scale[0] = double(num_levels - 1) / (s * 219.0); 115 | scale[1] = double(num_levels - 1) / (s * 224.0); 116 | scale[2] = double(num_levels - 1) / (s * 224.0); 117 | } 118 | 119 | // Matrix to convert RGB to YCbCr. See e.g. Rec. 601. 120 | Matrix3d rgb_to_ycbcr; 121 | rgb_to_ycbcr(0,0) = coeff[0]; 122 | rgb_to_ycbcr(0,1) = coeff[1]; 123 | rgb_to_ycbcr(0,2) = coeff[2]; 124 | 125 | float cb_fac = 1.0 / (coeff[0] + coeff[1] + 1.0f - coeff[2]); 126 | rgb_to_ycbcr(1,0) = -coeff[0] * cb_fac; 127 | rgb_to_ycbcr(1,1) = -coeff[1] * cb_fac; 128 | rgb_to_ycbcr(1,2) = (1.0f - coeff[2]) * cb_fac; 129 | 130 | float cr_fac = 1.0 / (1.0f - coeff[0] + coeff[1] + coeff[2]); 131 | rgb_to_ycbcr(2,0) = (1.0f - coeff[0]) * cr_fac; 132 | rgb_to_ycbcr(2,1) = -coeff[1] * cr_fac; 133 | rgb_to_ycbcr(2,2) = -coeff[2] * cr_fac; 134 | 135 | // Inverting the matrix gives us what we need to go from YCbCr back to RGB. 136 | *ycbcr_to_rgb = rgb_to_ycbcr.inverse(); 137 | 138 | // Fold in the scaling. 139 | *ycbcr_to_rgb *= Map(scale).asDiagonal(); 140 | 141 | if (type == GL_UNSIGNED_SHORT) { 142 | // For 10-bit or 12-bit packed into 16-bit, we need to scale the values 143 | // so that the max value goes from 1023 (or 4095) to 65535. We do this 144 | // by folding the scaling into the conversion matrix, so it comes essentially 145 | // for free. However, the offset is before the scaling (and thus assumes 146 | // correctly scaled values), so we need to adjust that the other way. 147 | double scale = 65535.0 / (ycbcr_format.num_levels - 1); 148 | offset[0] /= scale; 149 | offset[1] /= scale; 150 | offset[2] /= scale; 151 | *ycbcr_to_rgb *= scale; 152 | if (scale_factor != nullptr) { 153 | *scale_factor = scale; 154 | } 155 | } else if (scale_factor != nullptr) { 156 | *scale_factor = 1.0; 157 | } 158 | } 159 | 160 | } // namespace movit 161 | --------------------------------------------------------------------------------