effect_chain_test
gamma_compression_effect_test
gamma_expansion_effect_test
+alpha_multiplication_effect_test
+alpha_division_effect_test
colorspace_conversion_effect_test
mix_effect_test
+overlay_effect_test
saturation_effect_test
deconvolution_sharpen_effect_test
blur_effect_test
TESTS += gamma_expansion_effect_test
TESTS += gamma_compression_effect_test
TESTS += colorspace_conversion_effect_test
+TESTS += alpha_multiplication_effect_test
+TESTS += alpha_division_effect_test
TESTS += saturation_effect_test
TESTS += deconvolution_sharpen_effect_test
TESTS += blur_effect_test
LIB_OBJS += gamma_expansion_effect.o
LIB_OBJS += gamma_compression_effect.o
LIB_OBJS += colorspace_conversion_effect.o
+LIB_OBJS += alpha_multiplication_effect.o
+LIB_OBJS += alpha_division_effect.o
LIB_OBJS += saturation_effect.o
LIB_OBJS += vignette_effect.o
LIB_OBJS += mirror_effect.o
ImageFormat inout_format;
inout_format.color_space = COLORSPACE_sRGB;
inout_format.gamma_curve = GAMMA_sRGB;
- FlatInput *input = knew FlatInput(inout_format, FORMAT_BGRA, GL_UNSIGNED_BYTE, 1280, 720));
+ FlatInput *input = knew FlatInput(inout_format, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, GL_UNSIGNED_BYTE, 1280, 720));
chain.add_input(input);
Effect *saturation_effect = chain.add_effect(new SaturationEffect());
const float gain[] = { 0.8f, 1.0f, 1.0f };
lift_gamma_gain_effect->set_vec3("gain", &gain);
- chain.add_output(inout_format);
+ chain.add_output(inout_format, OUTPUT_POSTMULTIPLIED_ALPHA);
chain.finalize();
for ( ;; ) {
virtual bool needs_texture_bounce() const { return true; }
virtual bool needs_mipmaps() const { return true; }
virtual bool needs_srgb_primaries() const { return false; }
+ virtual AlphaHandling alpha_handling() const { return INPUT_AND_OUTPUT_ALPHA_PREMULTIPLIED; }
virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height);
std::string output_fragment_shader();
virtual bool needs_srgb_primaries() const { return false; }
+ virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
private:
Colorspace source_space, destination_space;
float temp_data[4 * 6], out_data[4 * 6];
{
- EffectChainTester tester(data, 1, 6, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 6, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
tester.run(temp_data, GL_RGBA, COLORSPACE_REC_601_525, GAMMA_LINEAR);
}
{
- EffectChainTester tester(temp_data, 1, 6, FORMAT_RGBA, COLORSPACE_REC_601_525, GAMMA_LINEAR);
+ EffectChainTester tester(temp_data, 1, 6, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_REC_601_525, GAMMA_LINEAR);
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
}
};
float out_data[4 * 5];
- EffectChainTester tester(data, 1, 5, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
tester.run(out_data, GL_RGBA, COLORSPACE_XYZ, GAMMA_LINEAR);
// Black should stay black.
};
float out_data[4 * 5];
- EffectChainTester tester(data, 1, 5, FORMAT_RGBA, COLORSPACE_REC_601_525, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_REC_601_525, GAMMA_LINEAR);
tester.run(out_data, GL_RGBA, COLORSPACE_XYZ, GAMMA_LINEAR);
// Black should stay black.
};
float out_data[4 * 5];
- EffectChainTester tester(data, 1, 5, FORMAT_RGBA, COLORSPACE_REC_601_625, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_REC_601_625, GAMMA_LINEAR);
tester.run(out_data, GL_RGBA, COLORSPACE_XYZ, GAMMA_LINEAR);
// Black should stay black.
};
float out_data[4 * 6];
- EffectChainTester tester(data, 1, 6, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 6, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
tester.run(out_data, GL_RGBA, COLORSPACE_REC_601_525, GAMMA_LINEAR);
expect_equal(expected_data, out_data, 4, 6);
#include "lift_gamma_gain_effect.h"
#include "saturation_effect.h"
#include "diffusion_effect.h"
+#include "overlay_effect.h"
+#include "resample_effect.h"
+#include "resize_effect.h"
unsigned char result[WIDTH * HEIGHT * 4];
SDL_FreeSurface(img);
+ unsigned char *x = (unsigned char *)converted->pixels;
+ for (int i = 0; i < img->w * img->h; ++i) {
+ if (x[i * 4 + 3] == 0) {
+ x[i * 4 + 0] = 255;
+ x[i * 4 + 1] = 255;
+ x[i * 4 + 2] = 255;
+ }
+ }
+
return (unsigned char *)converted->pixels;
}
ImageFormat inout_format;
inout_format.color_space = COLORSPACE_sRGB;
- inout_format.gamma_curve = GAMMA_sRGB;
+ inout_format.gamma_curve = GAMMA_LINEAR;
- FlatInput *input = new FlatInput(inout_format, FORMAT_BGRA, GL_UNSIGNED_BYTE, img_w, img_h);
+ FlatInput *input = new FlatInput(inout_format, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, GL_UNSIGNED_BYTE, img_w, img_h);
chain.add_input(input);
- Effect *lift_gamma_gain_effect = chain.add_effect(new LiftGammaGainEffect());
- Effect *saturation_effect = chain.add_effect(new SaturationEffect());
- Effect *diffusion_effect = chain.add_effect(new DiffusionEffect());
+
+ unsigned char *src_overlay1 = load_image("overlay1.png", &img_w, &img_h);
+#if 0
+ float *src_bleh = new float[img_w * img_h * 4];
+ for (int i = 0; i < img_w * img_h; ++i) {
+ float r = src_overlay1[i * 4 + 0] / 255.0f;
+ float g = src_overlay1[i * 4 + 1] / 255.0f;
+ float b = src_overlay1[i * 4 + 2] / 255.0f;
+ float a = src_overlay1[i * 4 + 3] / 255.0f;
+ // src_bleh[i * 4 + 0] = r * a;
+ // src_bleh[i * 4 + 1] = g * a;
+ // src_bleh[i * 4 + 2] = b * a;
+ src_bleh[i * 4 + 0] = r;
+ src_bleh[i * 4 + 1] = g;
+ src_bleh[i * 4 + 2] = b;
+ src_bleh[i * 4 + 3] = a;
+ }
+ FlatInput *overlay1 = new FlatInput(inout_format, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, GL_FLOAT, img_w, img_h);
+#endif
+ FlatInput *overlay1 = new FlatInput(inout_format, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, GL_UNSIGNED_BYTE, img_w, img_h);
+ chain.add_input(overlay1);
+
+ unsigned char *src_overlay2 = load_image("overlay2.png", &img_w, &img_h);
+ FlatInput *overlay2 = new FlatInput(inout_format, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, GL_UNSIGNED_BYTE, img_w, img_h);
+ chain.add_input(overlay2);
+
+ Effect *mix1 = chain.add_effect(new OverlayEffect(), overlay2, overlay1);
+ //Effect *mix1_resized = chain.add_effect(new ResampleEffect(), mix1);
+ Effect *mix1_resized = chain.add_effect(new ResizeEffect(), mix1);
+ CHECK(mix1_resized->set_int("width", 1280));
+ CHECK(mix1_resized->set_int("height", 720));
+ Effect *mix2 = chain.add_effect(new OverlayEffect(), input, mix1_resized);
+// Effect *mix2 = chain.add_effect(new OverlayEffect(), input, overlay1);
+
+ //Effect *lift_gamma_gain_effect = chain.add_effect(new LiftGammaGainEffect());
+ //Effect *saturation_effect = chain.add_effect(new SaturationEffect());
+ //Effect *diffusion_effect = chain.add_effect(new DiffusionEffect());
//Effect *vignette_effect = chain.add_effect(new VignetteEffect());
//Effect *sandbox_effect = chain.add_effect(new SandboxEffect());
//sandbox_effect->set_float("parm", 42.0f);
//chain.add_effect(new MirrorEffect());
- chain.add_output(inout_format);
+ chain.add_output(inout_format, OUTPUT_ALPHA_POSTMULTIPLIED);
chain.set_dither_bits(8);
chain.finalize();
++frame;
- update_hsv(lift_gamma_gain_effect, saturation_effect);
+ //update_hsv(lift_gamma_gain_effect, saturation_effect);
//vignette_effect->set_float("radius", radius);
//vignette_effect->set_float("inner_radius", inner_radius);
//vignette_effect->set_vec2("center", (float[]){ 0.7f, 0.5f });
- CHECK(diffusion_effect->set_float("radius", blur_radius));
- CHECK(diffusion_effect->set_float("blurred_mix_amount", blurred_mix_amount));
+ //CHECK(diffusion_effect->set_float("radius", blur_radius));
+ //CHECK(diffusion_effect->set_float("blurred_mix_amount", blurred_mix_amount));
input->set_pixel_data(src_img);
+ overlay1->set_pixel_data(src_overlay1);
+ //overlay1->set_pixel_data(src_bleh);
+ overlay2->set_pixel_data(src_overlay2);
chain.render_to_screen();
glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, pbo);
virtual std::string effect_type_id() const { return "DitherEffect"; }
std::string output_fragment_shader();
+ // Note that if we did error diffusion, we'd actually want to diffuse the
+ // premultiplied error. However, we need to do dithering in the same
+ // space as quantization, whether that be pre- or postmultiply.
+ virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
+
void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num);
private:
// in a linear fashion.
virtual bool needs_srgb_primaries() const { return true; }
+ // How this effect handles alpha, ie. what it outputs in its
+ // alpha channel. The choices are basically blank (alpha is always 1.0),
+ // premultiplied and postmultiplied.
+ //
+ // Premultiplied alpha is when the alpha value has been be multiplied
+ // into the three color components, so e.g. 100% red at 50% alpha
+ // would be (0.5, 0.0, 0.0, 0.5) instead of (1.0, 0.0, 0.0, 0.5)
+ // as it is stored in most image formats (postmultiplied alpha).
+ // The multiplication is taken to have happened in linear light.
+ // This is the most natural format for processing, and the default in
+ // most of Movit (just like linear light is).
+ //
+ // If you set INPUT_AND_OUTPUT_ALPHA_PREMULTIPLIED, all of your inputs
+ // (if any) are guaranteed to also be in premultiplied alpha.
+ // Otherwise, you can get postmultiplied or premultiplied alpha;
+ // you won't know. If you have multiple inputs, you will get the same
+ // (pre- or postmultiplied) for all inputs, although most likely,
+ // you will want to combine them in a premultiplied fashion anyway
+ // in that case.
+ enum AlphaHandling {
+ // Always outputs blank alpha (ie. alpha=1.0). Only appropriate
+ // for inputs that do not output an alpha channel.
+ // Blank alpha is special in that it can be treated as both
+ // pre- and postmultiplied.
+ OUTPUT_BLANK_ALPHA,
+
+ // Always outputs premultiplied alpha. As noted above,
+ // you will then also get all inputs in premultiplied alpha.
+ // If you set this, you should also set needs_linear_light().
+ INPUT_AND_OUTPUT_ALPHA_PREMULTIPLIED,
+
+ // Always outputs postmultiplied alpha. Only appropriate for inputs.
+ OUTPUT_ALPHA_POSTMULTIPLIED,
+
+ // Keeps the type of alpha unchanged from input to output.
+ // Usually appropriate if you process all color channels
+ // in a linear fashion, and do not change alpha.
+ //
+ // Does not make sense for inputs.
+ DONT_CARE_ALPHA_TYPE,
+ };
+ virtual AlphaHandling alpha_handling() const { return INPUT_AND_OUTPUT_ALPHA_PREMULTIPLIED; }
+
// Whether this effect expects its input to come directly from
// a texture. If this is true, the framework will not chain the
// input from other effects, but will store the results of the
#include "gamma_expansion_effect.h"
#include "gamma_compression_effect.h"
#include "colorspace_conversion_effect.h"
+#include "alpha_multiplication_effect.h"
+#include "alpha_division_effect.h"
#include "dither_effect.h"
#include "input.h"
#include "init.h"
return input;
}
-void EffectChain::add_output(const ImageFormat &format)
+void EffectChain::add_output(const ImageFormat &format, OutputAlphaFormat alpha_format)
{
output_format = format;
+ output_alpha_format = alpha_format;
}
Node *EffectChain::add_node(Effect *effect)
node->effect_id = effect_id;
node->output_color_space = COLORSPACE_INVALID;
node->output_gamma_curve = GAMMA_INVALID;
+ node->output_alpha_type = ALPHA_INVALID;
node->output_texture = 0;
nodes.push_back(node);
break;
}
+ switch (nodes[i]->output_alpha_type) {
+ case ALPHA_INVALID:
+ labels.push_back("alpha[invalid]");
+ break;
+ case ALPHA_BLANK:
+ labels.push_back("alpha[blank]");
+ break;
+ case ALPHA_POSTMULTIPLIED:
+ labels.push_back("alpha[postmult]");
+ break;
+ default:
+ break;
+ }
+
if (labels.empty()) {
fprintf(fp, " n%ld -> n%ld;\n", (long)nodes[i], (long)nodes[i]->outgoing_links[j]);
} else {
Input *input = static_cast<Input *>(node->effect);
node->output_color_space = input->get_color_space();
node->output_gamma_curve = input->get_gamma_curve();
+
+ Effect::AlphaHandling alpha_handling = input->alpha_handling();
+ switch (alpha_handling) {
+ case Effect::OUTPUT_BLANK_ALPHA:
+ node->output_alpha_type = ALPHA_BLANK;
+ break;
+ case Effect::INPUT_AND_OUTPUT_ALPHA_PREMULTIPLIED:
+ node->output_alpha_type = ALPHA_PREMULTIPLIED;
+ break;
+ case Effect::OUTPUT_ALPHA_POSTMULTIPLIED:
+ node->output_alpha_type = ALPHA_POSTMULTIPLIED;
+ break;
+ case Effect::DONT_CARE_ALPHA_TYPE:
+ default:
+ assert(false);
+ }
}
}
}
}
}
+// Propagate alpha information as far as we can in the graph.
+// Similar to propagate_gamma_and_color_space().
+void EffectChain::propagate_alpha()
+{
+ // We depend on going through the nodes in order.
+ sort_nodes_topologically();
+
+ for (unsigned i = 0; i < nodes.size(); ++i) {
+ Node *node = nodes[i];
+ if (node->disabled) {
+ continue;
+ }
+ assert(node->incoming_links.size() == node->effect->num_inputs());
+ if (node->incoming_links.size() == 0) {
+ assert(node->output_alpha_type != ALPHA_INVALID);
+ continue;
+ }
+
+ // The alpha multiplication/division effects are special cases.
+ if (node->effect->effect_type_id() == "AlphaMultiplicationEffect") {
+ assert(node->incoming_links.size() == 1);
+ assert(node->incoming_links[0]->output_alpha_type == ALPHA_POSTMULTIPLIED);
+ node->output_alpha_type = ALPHA_PREMULTIPLIED;
+ continue;
+ }
+ if (node->effect->effect_type_id() == "AlphaDivisionEffect") {
+ assert(node->incoming_links.size() == 1);
+ assert(node->incoming_links[0]->output_alpha_type == ALPHA_PREMULTIPLIED);
+ node->output_alpha_type = ALPHA_POSTMULTIPLIED;
+ continue;
+ }
+
+ // GammaCompressionEffect and GammaExpansionEffect are also a special case,
+ // because they are the only one that _need_ postmultiplied alpha.
+ if (node->effect->effect_type_id() == "GammaCompressionEffect" ||
+ node->effect->effect_type_id() == "GammaExpansionEffect") {
+ assert(node->incoming_links.size() == 1);
+ if (node->incoming_links[0]->output_alpha_type == ALPHA_BLANK) {
+ node->output_alpha_type = ALPHA_BLANK;
+ } else if (node->incoming_links[0]->output_alpha_type == ALPHA_POSTMULTIPLIED) {
+ node->output_alpha_type = ALPHA_POSTMULTIPLIED;
+ } else {
+ node->output_alpha_type = ALPHA_INVALID;
+ }
+ continue;
+ }
+
+ // Only inputs can have unconditional alpha output (OUTPUT_BLANK_ALPHA
+ // or OUTPUT_ALPHA_POSTMULTIPLIED), and they have already been
+ // taken care of above. Rationale: Even if you could imagine
+ // e.g. an effect that took in an image and set alpha=1.0
+ // unconditionally, it wouldn't make any sense to have it as
+ // e.g. OUTPUT_BLANK_ALPHA, since it wouldn't know whether it
+ // got its input pre- or postmultiplied, so it wouldn't know
+ // whether to divide away the old alpha or not.
+ Effect::AlphaHandling alpha_handling = node->effect->alpha_handling();
+ assert(alpha_handling == Effect::INPUT_AND_OUTPUT_ALPHA_PREMULTIPLIED ||
+ alpha_handling == Effect::DONT_CARE_ALPHA_TYPE);
+
+ // If the node has multiple inputs, check that they are all valid and
+ // the same.
+ bool any_invalid = false;
+ bool any_premultiplied = false;
+ bool any_postmultiplied = false;
+
+ for (unsigned j = 0; j < node->incoming_links.size(); ++j) {
+ switch (node->incoming_links[j]->output_alpha_type) {
+ case ALPHA_INVALID:
+ any_invalid = true;
+ break;
+ case ALPHA_BLANK:
+ // Blank is good as both pre- and postmultiplied alpha,
+ // so just ignore it.
+ break;
+ case ALPHA_PREMULTIPLIED:
+ any_premultiplied = true;
+ break;
+ case ALPHA_POSTMULTIPLIED:
+ any_postmultiplied = true;
+ break;
+ default:
+ assert(false);
+ }
+ }
+
+ if (any_invalid) {
+ node->output_alpha_type = ALPHA_INVALID;
+ continue;
+ }
+
+ // Inputs must be of the same type.
+ if (any_premultiplied && any_postmultiplied) {
+ node->output_alpha_type = ALPHA_INVALID;
+ continue;
+ }
+
+ if (alpha_handling == Effect::INPUT_AND_OUTPUT_ALPHA_PREMULTIPLIED) {
+ // If the effect has asked for premultiplied alpha, check that it has got it.
+ if (any_postmultiplied) {
+ node->output_alpha_type = ALPHA_INVALID;
+ } else {
+ // In some rare cases, it might be advantageous to say
+ // that blank input alpha yields blank output alpha.
+ // However, this would cause a more complex Effect interface
+ // an effect would need to guarantee that it doesn't mess with
+ // blank alpha), so this is the simplest.
+ node->output_alpha_type = ALPHA_PREMULTIPLIED;
+ }
+ } else {
+ // OK, all inputs are the same, and this effect is not going
+ // to change it.
+ assert(alpha_handling == Effect::DONT_CARE_ALPHA_TYPE);
+ if (any_premultiplied) {
+ node->output_alpha_type = ALPHA_PREMULTIPLIED;
+ } else if (any_postmultiplied) {
+ node->output_alpha_type = ALPHA_POSTMULTIPLIED;
+ } else {
+ node->output_alpha_type = ALPHA_BLANK;
+ }
+ }
+ }
+}
+
bool EffectChain::node_needs_colorspace_fix(Node *node)
{
if (node->disabled) {
}
char filename[256];
- sprintf(filename, "step3-colorspacefix-iter%u.dot", ++colorspace_propagation_pass);
+ sprintf(filename, "step5-colorspacefix-iter%u.dot", ++colorspace_propagation_pass);
output_dot(filename);
assert(colorspace_propagation_pass < 100);
} while (found_any);
}
}
+bool EffectChain::node_needs_alpha_fix(Node *node)
+{
+ if (node->disabled) {
+ return false;
+ }
+
+ // propagate_alpha() has already set our output to ALPHA_INVALID if the
+ // inputs differ or we are otherwise in mismatch, so we can rely on that.
+ return (node->output_alpha_type == ALPHA_INVALID);
+}
+
+// Fix up alpha so that there are no ALPHA_INVALID nodes left in
+// the graph. Similar to fix_internal_color_spaces().
+void EffectChain::fix_internal_alpha(unsigned step)
+{
+ unsigned alpha_propagation_pass = 0;
+ bool found_any;
+ do {
+ found_any = false;
+ for (unsigned i = 0; i < nodes.size(); ++i) {
+ Node *node = nodes[i];
+ if (!node_needs_alpha_fix(node)) {
+ continue;
+ }
+
+ // If we need to fix up GammaExpansionEffect, then clearly something
+ // is wrong, since the combination of premultiplied alpha and nonlinear inputs
+ // is meaningless.
+ assert(node->effect->effect_type_id() != "GammaExpansionEffect");
+
+ AlphaType desired_type = ALPHA_PREMULTIPLIED;
+
+ // GammaCompressionEffect is special; it needs postmultiplied alpha.
+ if (node->effect->effect_type_id() == "GammaCompressionEffect") {
+ assert(node->incoming_links.size() == 1);
+ assert(node->incoming_links[0]->output_alpha_type == ALPHA_PREMULTIPLIED);
+ desired_type = ALPHA_POSTMULTIPLIED;
+ }
+
+ // Go through each input that is not premultiplied alpha, and insert
+ // a conversion before it.
+ for (unsigned j = 0; j < node->incoming_links.size(); ++j) {
+ Node *input = node->incoming_links[j];
+ assert(input->output_alpha_type != ALPHA_INVALID);
+ if (input->output_alpha_type == desired_type ||
+ input->output_alpha_type == ALPHA_BLANK) {
+ continue;
+ }
+ Node *conversion;
+ if (desired_type == ALPHA_PREMULTIPLIED) {
+ conversion = add_node(new AlphaMultiplicationEffect());
+ } else {
+ conversion = add_node(new AlphaDivisionEffect());
+ }
+ conversion->output_alpha_type = desired_type;
+ insert_node_between(input, conversion, node);
+ }
+
+ // Re-sort topologically, and propagate the new information.
+ propagate_gamma_and_color_space();
+ propagate_alpha();
+
+ found_any = true;
+ break;
+ }
+
+ char filename[256];
+ sprintf(filename, "step%u-alphafix-iter%u.dot", step, ++alpha_propagation_pass);
+ output_dot(filename);
+ assert(alpha_propagation_pass < 100);
+ } while (found_any);
+
+ for (unsigned i = 0; i < nodes.size(); ++i) {
+ Node *node = nodes[i];
+ if (node->disabled) {
+ continue;
+ }
+ assert(node->output_alpha_type != ALPHA_INVALID);
+ }
+}
+
// Make so that the output is in the desired color space.
void EffectChain::fix_output_color_space()
{
CHECK(conversion->effect->set_int("destination_space", output_format.color_space));
conversion->output_color_space = output_format.color_space;
connect_nodes(output, conversion);
+ propagate_alpha();
+ propagate_gamma_and_color_space();
+ }
+}
+
+// Make so that the output is in the desired pre-/postmultiplication alpha state.
+void EffectChain::fix_output_alpha()
+{
+ Node *output = find_output_node();
+ assert(output->output_alpha_type != ALPHA_INVALID);
+ if (output->output_alpha_type == ALPHA_BLANK) {
+ // No alpha output, so we don't care.
+ return;
+ }
+ if (output->output_alpha_type == ALPHA_PREMULTIPLIED &&
+ output_alpha_format == OUTPUT_ALPHA_POSTMULTIPLIED) {
+ Node *conversion = add_node(new AlphaDivisionEffect());
+ connect_nodes(output, conversion);
+ propagate_alpha();
+ propagate_gamma_and_color_space();
+ }
+ if (output->output_alpha_type == ALPHA_POSTMULTIPLIED &&
+ output_alpha_format == OUTPUT_ALPHA_PREMULTIPLIED) {
+ Node *conversion = add_node(new AlphaMultiplicationEffect());
+ connect_nodes(output, conversion);
+ propagate_alpha();
propagate_gamma_and_color_space();
}
}
}
// Re-sort topologically, and propagate the new information.
+ propagate_alpha();
propagate_gamma_and_color_space();
found_any = true;
find_color_spaces_for_inputs();
output_dot("step2-input-colorspace.dot");
+ propagate_alpha();
+ output_dot("step3-propagated-alpha.dot");
+
propagate_gamma_and_color_space();
- output_dot("step3-propagated.dot");
+ output_dot("step4-propagated-all.dot");
fix_internal_color_spaces();
+ fix_internal_alpha(6);
fix_output_color_space();
- output_dot("step4-output-colorspacefix.dot");
+ output_dot("step7-output-colorspacefix.dot");
+ fix_output_alpha();
+ output_dot("step8-output-alphafix.dot");
// Note that we need to fix gamma after colorspace conversion,
// because colorspace conversions might create needs for gamma conversions.
// Also, we need to run an extra pass of fix_internal_gamma() after
- // fixing the output gamma, as we only have conversions to/from linear.
- fix_internal_gamma_by_asking_inputs(5);
- fix_internal_gamma_by_inserting_nodes(6);
+ // fixing the output gamma, as we only have conversions to/from linear,
+ // and fix_internal_alpha() since GammaCompressionEffect needs
+ // postmultiplied input.
+ fix_internal_gamma_by_asking_inputs(9);
+ fix_internal_gamma_by_inserting_nodes(10);
fix_output_gamma();
- output_dot("step7-output-gammafix.dot");
- fix_internal_gamma_by_asking_inputs(8);
- fix_internal_gamma_by_inserting_nodes(9);
+ output_dot("step11-output-gammafix.dot");
+ propagate_alpha();
+ output_dot("step12-output-alpha-propagated.dot");
+ fix_internal_alpha(13);
+ output_dot("step14-output-alpha-fixed.dot");
+ fix_internal_gamma_by_asking_inputs(15);
+ fix_internal_gamma_by_inserting_nodes(16);
- output_dot("step10-before-dither.dot");
+ output_dot("step17-before-dither.dot");
add_dither_if_needed();
- output_dot("step11-final.dot");
+ output_dot("step18-final.dot");
// Construct all needed GLSL programs, starting at the output.
construct_glsl_programs(find_output_node());
- output_dot("step12-split-to-phases.dot");
+ output_dot("step19-split-to-phases.dot");
// If we have more than one phase, we need intermediate render-to-texture.
// Construct an FBO, and then as many textures as we need.
class EffectChain;
class Phase;
+// For internal use within Node.
+enum AlphaType {
+ ALPHA_INVALID = -1,
+ ALPHA_BLANK,
+ ALPHA_PREMULTIPLIED,
+ ALPHA_POSTMULTIPLIED,
+};
+
+// Whether you want pre- or postmultiplied alpha in the output
+// (see effect.h for a discussion of pre- versus postmultiplied alpha).
+enum OutputAlphaFormat {
+ OUTPUT_ALPHA_PREMULTIPLIED,
+ OUTPUT_ALPHA_POSTMULTIPLIED,
+};
+
// A node in the graph; basically an effect and some associated information.
class Node {
public:
// Used during the building of the effect chain.
Colorspace output_color_space;
GammaCurve output_gamma_curve;
+ AlphaType output_alpha_type;
friend class EffectChain;
};
}
Effect *add_effect(Effect *effect, const std::vector<Effect *> &inputs);
- void add_output(const ImageFormat &format);
+ void add_output(const ImageFormat &format, OutputAlphaFormat alpha_format);
// Set number of output bits, to scale the dither.
// 8 is the right value for most outputs.
// Used during finalize().
void find_color_spaces_for_inputs();
+ void propagate_alpha();
void propagate_gamma_and_color_space();
Node *find_output_node();
void fix_internal_color_spaces();
void fix_output_color_space();
+ bool node_needs_alpha_fix(Node *node);
+ void fix_internal_alpha(unsigned step);
+ void fix_output_alpha();
+
bool node_needs_gamma_fix(Node *node);
void fix_internal_gamma_by_asking_inputs(unsigned step);
void fix_internal_gamma_by_inserting_nodes(unsigned step);
float aspect_nom, aspect_denom;
ImageFormat output_format;
+ OutputAlphaFormat output_alpha_format;
std::vector<Node *> nodes;
std::map<Effect *, Node *> node_map;
InvertEffect() {}
virtual std::string effect_type_id() const { return "InvertEffect"; }
std::string output_fragment_shader() { return read_file("invert_effect.frag"); }
+
+ // A real invert would actually care about its alpha,
+ // but in this unit test, it only complicates things.
+ virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
};
// Like IdentityEffect, but rewrites itself out of the loop,
expect_equal(data, out_data, 256, 1);
}
+// The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
+TEST(EffectChainTest, IdentityThroughAlphaConversions) {
+ const int size = 3;
+ float data[4 * size] = {
+ 0.8f, 0.0f, 0.0f, 0.5f,
+ 0.0f, 0.2f, 0.2f, 0.3f,
+ 0.1f, 0.0f, 1.0f, 1.0f,
+ };
+ float out_data[6];
+ EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ tester.get_chain()->add_effect(new IdentityEffect());
+ tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ expect_equal(data, out_data, 4, size);
+}
+
+TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
+ const int size = 3;
+ float data[4 * size] = {
+ 0.8f, 0.0f, 0.0f, 0.5f,
+ 0.0f, 0.2f, 0.2f, 0.3f,
+ 0.1f, 0.0f, 1.0f, 1.0f,
+ };
+ float expected_data[4 * size] = {
+ 0.1f, 0.0f, 1.0f, 1.0f,
+ 0.0f, 0.2f, 0.2f, 0.3f,
+ 0.8f, 0.0f, 0.0f, 0.5f,
+ };
+ float out_data[4 * size];
+ EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ RewritingToMirrorEffect *effect = new RewritingToMirrorEffect();
+ tester.get_chain()->add_effect(effect);
+ tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ Node *node = effect->mirror_node;
+ ASSERT_EQ(1, node->incoming_links.size());
+ EXPECT_EQ(0, node->outgoing_links.size());
+ EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
+
+ expect_equal(expected_data, out_data, 4, size);
+}
+
+// An input that outputs only blue, which has blank alpha.
+class BlueInput : public Input {
+public:
+ BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
+ virtual std::string effect_type_id() const { return "IdentityEffect"; }
+ std::string output_fragment_shader() { return read_file("blue.frag"); }
+ virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
+ virtual void finalize() {}
+ virtual bool can_output_linear_gamma() const { return true; }
+ virtual unsigned get_width() const { return 1; }
+ virtual unsigned get_height() const { return 1; }
+ virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
+ virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
+
+private:
+ int needs_mipmaps;
+};
+
+// Like RewritingToInvertEffect, but splicing in a BlueInput instead,
+// which outputs blank alpha.
+class RewritingToBlueInput : public Input {
+public:
+ RewritingToBlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
+ virtual std::string effect_type_id() const { return "RewritingToBlueInput"; }
+ std::string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
+ virtual void rewrite_graph(EffectChain *graph, Node *self) {
+ Node *blue_node = graph->add_node(new BlueInput());
+ graph->replace_receiver(self, blue_node);
+ graph->replace_sender(self, blue_node);
+
+ self->disabled = true;
+ this->blue_node = blue_node;
+ }
+
+ // Dummy values that we need to implement because we inherit from Input.
+ // Same as BlueInput.
+ virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
+ virtual void finalize() {}
+ virtual bool can_output_linear_gamma() const { return true; }
+ virtual unsigned get_width() const { return 1; }
+ virtual unsigned get_height() const { return 1; }
+ virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
+ virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
+
+ Node *blue_node;
+
+private:
+ int needs_mipmaps;
+};
+
+TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
+ const int size = 3;
+ float data[4 * size] = {
+ 0.0f, 0.0f, 1.0f, 1.0f,
+ 0.0f, 0.0f, 1.0f, 1.0f,
+ 0.0f, 0.0f, 1.0f, 1.0f,
+ };
+ float out_data[4 * size];
+ EffectChainTester tester(NULL, size, 1);
+ RewritingToBlueInput *input = new RewritingToBlueInput();
+ tester.get_chain()->add_input(input);
+ tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_PREMULTIPLIED);
+
+ Node *node = input->blue_node;
+ EXPECT_EQ(0, node->incoming_links.size());
+ EXPECT_EQ(0, node->outgoing_links.size());
+
+ expect_equal(data, out_data, 4, size);
+}
+
// Effectively scales down its input linearly by 4x (and repeating it),
// which is not attainable without mipmaps.
class MipmapNeedingEffect : public Effect {
if (pixel_format == FORMAT_RGB) {
format = GL_RGB;
bytes_per_pixel = 3;
- } else if (pixel_format == FORMAT_RGBA) {
+ } else if (pixel_format == FORMAT_RGBA_PREMULTIPLIED_ALPHA ||
+ pixel_format == FORMAT_RGBA_POSTMULTIPLIED_ALPHA) {
format = GL_RGBA;
bytes_per_pixel = 4;
} else if (pixel_format == FORMAT_BGR) {
format = GL_BGR;
bytes_per_pixel = 3;
- } else if (pixel_format == FORMAT_BGRA) {
+ } else if (pixel_format == FORMAT_BGRA_PREMULTIPLIED_ALPHA ||
+ pixel_format == FORMAT_BGRA_POSTMULTIPLIED_ALPHA) {
format = GL_BGRA;
bytes_per_pixel = 4;
} else if (pixel_format == FORMAT_GRAYSCALE) {
#ifndef _FLAT_INPUT_H
#define _FLAT_INPUT_H 1
+#include <assert.h>
+
#include "input.h"
#include "init.h"
(image_format.gamma_curve == GAMMA_LINEAR ||
image_format.gamma_curve == GAMMA_sRGB));
}
+ virtual AlphaHandling alpha_handling() const {
+ switch (pixel_format) {
+ case FORMAT_RGBA_PREMULTIPLIED_ALPHA:
+ case FORMAT_BGRA_PREMULTIPLIED_ALPHA:
+ return INPUT_AND_OUTPUT_ALPHA_PREMULTIPLIED;
+ case FORMAT_RGBA_POSTMULTIPLIED_ALPHA:
+ case FORMAT_BGRA_POSTMULTIPLIED_ALPHA:
+ return OUTPUT_ALPHA_POSTMULTIPLIED;
+ case FORMAT_RGB:
+ case FORMAT_BGR:
+ case FORMAT_GRAYSCALE:
+ return OUTPUT_BLANK_ALPHA;
+ default:
+ assert(false);
+ }
+ }
std::string output_fragment_shader();
};
float out_data[4 * size];
- EffectChainTester tester(data, 1, size, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, size, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
expect_equal(expected_data, out_data, 4, size);
float out_data[4 * size];
EffectChainTester tester(NULL, 1, size);
- tester.add_input(data, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
+ tester.add_input(data, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
expect_equal(expected_data, out_data, 4, size);
};
float out_data[4 * size];
- EffectChainTester tester(data, 1, size, FORMAT_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, size, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
expect_equal(expected_data, out_data, 4, size);
virtual bool needs_srgb_primaries() const { return false; }
+ // Actually needs postmultiplied input as well as outputting it.
+ // EffectChain will take care of that.
+ virtual AlphaHandling alpha_handling() const { return OUTPUT_ALPHA_POSTMULTIPLIED; }
+
private:
GammaCurve destination_curve;
float compression_curve[COMPRESSION_CURVE_SIZE];
virtual bool needs_linear_light() const { return false; }
virtual bool needs_srgb_primaries() const { return false; }
+ // Actually processes its input in a nonlinear fashion,
+ // but does not touch alpha, and we are a special case anyway.
+ virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
+
private:
GammaCurve source_curve;
float expansion_curve[EXPANSION_CURVE_SIZE];
0.0f, 0.0f, 0.0f, 1.0f,
};
float out_data[5 * 4];
- EffectChainTester tester(data, 5, 1, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
+ EffectChainTester tester(data, 5, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
expect_equal(data, out_data, 5, 1);
0.0f, 0.0f, 0.0f, 1.0f,
};
float out_data[5 * 4];
- EffectChainTester tester(data, 5, 1, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_REC_709);
+ EffectChainTester tester(data, 5, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_REC_709);
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
expect_equal(data, out_data, 5, 1);
// Glow: Cut out the highlights of the image (everything above a certain threshold),
// blur them, and overlay them onto the original image.
+//
+// FIXME: This might be broken after MixEffect started working in premultiplied alpha.
+// We need to think about how this is going to work, and then add a test.
#include "effect.h"
#ifndef _IMAGE_FORMAT_H
#define _IMAGE_FORMAT_H 1
-enum MovitPixelFormat { FORMAT_RGB, FORMAT_RGBA, FORMAT_BGR, FORMAT_BGRA, FORMAT_GRAYSCALE };
+enum MovitPixelFormat {
+ FORMAT_RGB,
+ FORMAT_RGBA_PREMULTIPLIED_ALPHA,
+ FORMAT_RGBA_POSTMULTIPLIED_ALPHA,
+ FORMAT_BGR,
+ FORMAT_BGRA_PREMULTIPLIED_ALPHA,
+ FORMAT_BGRA_POSTMULTIPLIED_ALPHA,
+ FORMAT_GRAYSCALE
+};
enum Colorspace {
COLORSPACE_INVALID = -1, // For internal use.
vec4 FUNCNAME(vec2 tc) {
vec4 x = INPUT(tc);
+ x.rgb /= x.aaa;
x.rgb = pow(x.rgb, vec3(1.0/2.2));
x.rgb += PREFIX(lift) * (vec3(1) - x.rgb);
x.rgb = pow(x.rgb, PREFIX(inv_gamma_22));
x.rgb *= PREFIX(gain_pow_inv_gamma);
+ x.rgb *= x.aaa;
return x;
}
// rest of the curve relatively little. Thus, we actually convert to gamma 2.2
// before lift, and then back again afterwards. (Gain and gamma are,
// up to constants, commutative with the de-gamma operation.)
+//
+// Also, gamma is a case where we would not want premultiplied alpha.
+// Thus, we have to divide away alpha first, and then re-multiply it back later.
#include "effect.h"
};
float out_data[5 * 4];
- EffectChainTester tester(data, 1, 5, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
tester.get_chain()->add_effect(new LiftGammaGainEffect());
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
};
float out_data[5 * 4];
- EffectChainTester tester(data, 1, 5, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *lgg_effect = tester.get_chain()->add_effect(new LiftGammaGainEffect());
ASSERT_TRUE(lgg_effect->set_vec3("gain", gain));
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
};
float out_data[5 * 4];
- EffectChainTester tester(data, 1, 5, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
+ EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
Effect *lgg_effect = tester.get_chain()->add_effect(new LiftGammaGainEffect());
ASSERT_TRUE(lgg_effect->set_vec3("lift", lift));
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
float gamma[3] = { 2.2f, 2.2f, 2.2f };
float out_data[5 * 4];
- EffectChainTester tester(data, 1, 5, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
+ EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
Effect *lgg_effect = tester.get_chain()->add_effect(new LiftGammaGainEffect());
ASSERT_TRUE(lgg_effect->set_vec3("gamma", gamma));
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
virtual bool needs_linear_light() const { return false; }
virtual bool needs_srgb_primaries() const { return false; }
+ virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
};
#endif // !defined(_MIRROR_EFFECT_H)
#ifndef _MIX_EFFECT_H
#define _MIX_EFFECT_H 1
-// Combine two images: a*x + b*y. (If you set a within [0,1] and b=1-a, you will get a fade.)
+// Combine two images: a*x + b*y. If you set a within [0,1] and b=1-a,
+// you will get a fade; if not, you may get surprising results (consider alpha).
#include "effect.h"
TEST(MixEffectTest, DoesNotSumToOne) {
float data_a[] = {
- 1.0f, 0.5f,
- 0.75f, 1.0f,
+ 1.0f, 0.5f, 0.75f, 0.333f,
};
float data_b[] = {
- 1.0f, 0.25f,
- 0.15f, 0.6f,
+ 1.0f, 0.25f, 0.15f, 0.333f,
};
+
+ // The fact that the RGB values don't sum but get averaged here might
+ // actually be a surprising result, but when you think of it,
+ // it does make physical sense.
float expected_data[] = {
- 0.0f, 0.25f,
- 0.6f, 0.4f,
+ 1.0f, 0.375f, 0.45f, 0.666f,
};
+
float out_data[4];
- EffectChainTester tester(data_a, 2, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data_a, 1, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *input1 = tester.get_chain()->last_added_effect();
- Effect *input2 = tester.add_input(data_b, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
+ Effect *input2 = tester.add_input(data_b, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *mix_effect = tester.get_chain()->add_effect(new MixEffect(), input1, input2);
ASSERT_TRUE(mix_effect->set_float("strength_first", 1.0f));
- ASSERT_TRUE(mix_effect->set_float("strength_second", -1.0f));
- tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+ ASSERT_TRUE(mix_effect->set_float("strength_second", 1.0f));
+ tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
- expect_equal(expected_data, out_data, 2, 2);
+ expect_equal(expected_data, out_data, 4, 1);
}
TEST(MixEffectTest, MixesLinearlyDespitesRGBInputsAndOutputs) {
-// If we didn't have to worry about alpha in the bottom layer,
-// this would be a simple mix(). However, since people might
-// compose multiple layers together and we don't really have
-// any control over the order, it's better to do it right.
+// It's actually (but surprisingly) not correct to do a mix() here;
+// it would be if we had postmultiplied alpha and didn't have to worry
+// about alpha in the bottom layer, but given that we use premultiplied
+// alpha all over, top shouldn't actually be multiplied by anything.
//
// These formulas come from Wikipedia:
//
// http://en.wikipedia.org/wiki/Alpha_compositing
+//
+// We use the associative version given. However, note that since we want
+// _output_ to be premultiplied, C_o from Wikipedia is not what we want,
+// but rather c_o (which is not explicitly given, but obviously is just
+// C_o without the division by alpha_o).
vec4 FUNCNAME(vec2 tc) {
vec4 bottom = INPUT1(tc);
vec4 top = INPUT2(tc);
+#if 0
+ // Postmultiplied version.
float new_alpha = mix(bottom.a, 1.0, top.a);
if (new_alpha < 1e-6) {
// new_alpha = 0 only if top.a = bottom.a = 0, at least as long as
vec3 color = premultiplied_color / new_alpha;
return vec4(color.r, color.g, color.b, new_alpha);
}
+#else
+ return top + (1.0 - top.a) * bottom;
+#endif
}
virtual bool needs_srgb_primaries() const { return false; }
virtual unsigned num_inputs() const { return 2; }
+
+ // Actually, if either image has blank alpha, our output will have
+ // blank alpha, too. However, understanding that would require changes
+ // to EffectChain, so postpone that optimization for later.
+ virtual AlphaHandling alpha_handling() const { return INPUT_AND_OUTPUT_ALPHA_PREMULTIPLIED; }
};
#endif // !defined(_OVERLAY_EFFECT_H)
0.5f, 0.5f, 0.5f, 0.0f,
};
float out_data[4];
- EffectChainTester tester(data_a, 1, 1, FORMAT_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data_a, 1, 1, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *input1 = tester.get_chain()->last_added_effect();
- Effect *input2 = tester.add_input(data_b, FORMAT_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ Effect *input2 = tester.add_input(data_b, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
tester.get_chain()->add_effect(new OverlayEffect(), input1, input2);
tester.run(out_data, GL_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
0.0f, 0.0f, 0.0f, 0.0f
};
float out_data[4];
- EffectChainTester tester(data_a, 1, 1, FORMAT_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data_a, 1, 1, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *input1 = tester.get_chain()->last_added_effect();
- Effect *input2 = tester.add_input(data_b, FORMAT_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ Effect *input2 = tester.add_input(data_b, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
tester.get_chain()->add_effect(new OverlayEffect(), input1, input2);
tester.run(out_data, GL_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
179.0f/255.0f, 153.0f/255.0f, 51.0f/255.0f, 0.625f
};
float out_data[4];
- EffectChainTester tester(data_a, 1, 1, FORMAT_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data_a, 1, 1, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *input1 = tester.get_chain()->last_added_effect();
- Effect *input2 = tester.add_input(data_b, FORMAT_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ Effect *input2 = tester.add_input(data_b, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
tester.get_chain()->add_effect(new OverlayEffect(), input1, input2);
tester.run(out_data, GL_BGRA, COLORSPACE_sRGB, GAMMA_LINEAR);
public:
SaturationEffect();
virtual std::string effect_type_id() const { return "SaturationEffect"; }
+ virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
std::string output_fragment_shader();
private:
1.0f, 0.5f, 0.75f, 0.6f,
};
float out_data[4];
- EffectChainTester tester(data, 1, 1, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *saturation_effect = tester.get_chain()->add_effect(new SaturationEffect());
ASSERT_TRUE(saturation_effect->set_float("saturation", 1.0f));
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
};
float out_data[5 * 4];
- EffectChainTester tester(data, 5, 1, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 5, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *saturation_effect = tester.get_chain()->add_effect(new SaturationEffect());
ASSERT_TRUE(saturation_effect->set_float("saturation", 0.0f));
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
};
float out_data[3 * 4];
- EffectChainTester tester(data, 3, 1, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 3, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *saturation_effect = tester.get_chain()->add_effect(new SaturationEffect());
ASSERT_TRUE(saturation_effect->set_float("saturation", 2.0f));
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
return input;
}
-void EffectChainTester::run(float *out_data, GLenum format, Colorspace color_space, GammaCurve gamma_curve)
+void EffectChainTester::run(float *out_data, GLenum format, Colorspace color_space, GammaCurve gamma_curve, OutputAlphaFormat alpha_format)
{
if (!finalized) {
- finalize_chain(color_space, gamma_curve);
+ finalize_chain(color_space, gamma_curve, alpha_format);
}
chain.render_to_fbo(fbo, width, height);
vertical_flip(out_data, width, height);
}
-void EffectChainTester::run(unsigned char *out_data, GLenum format, Colorspace color_space, GammaCurve gamma_curve)
+void EffectChainTester::run(unsigned char *out_data, GLenum format, Colorspace color_space, GammaCurve gamma_curve, OutputAlphaFormat alpha_format)
{
if (!finalized) {
- finalize_chain(color_space, gamma_curve);
+ finalize_chain(color_space, gamma_curve, alpha_format);
}
chain.render_to_fbo(fbo, width, height);
vertical_flip(out_data, width, height);
}
-void EffectChainTester::finalize_chain(Colorspace color_space, GammaCurve gamma_curve)
+void EffectChainTester::finalize_chain(Colorspace color_space, GammaCurve gamma_curve, OutputAlphaFormat alpha_format)
{
assert(!finalized);
ImageFormat image_format;
image_format.color_space = color_space;
image_format.gamma_curve = gamma_curve;
- chain.add_output(image_format);
+ chain.add_output(image_format, alpha_format);
chain.finalize();
finalized = true;
}
EffectChain *get_chain() { return &chain; }
Input *add_input(const float *data, MovitPixelFormat pixel_format, Colorspace color_space, GammaCurve gamma_curve);
Input *add_input(const unsigned char *data, MovitPixelFormat pixel_format, Colorspace color_space, GammaCurve gamma_curve);
- void run(float *out_data, GLenum format, Colorspace color_space, GammaCurve gamma_curve);
- void run(unsigned char *out_data, GLenum format, Colorspace color_space, GammaCurve gamma_curve);
+ void run(float *out_data, GLenum format, Colorspace color_space, GammaCurve gamma_curve, OutputAlphaFormat alpha_format = OUTPUT_ALPHA_POSTMULTIPLIED);
+ void run(unsigned char *out_data, GLenum format, Colorspace color_space, GammaCurve gamma_curve, OutputAlphaFormat alpha_format = OUTPUT_ALPHA_POSTMULTIPLIED);
private:
- void finalize_chain(Colorspace color_space, GammaCurve gamma_curve);
+ void finalize_chain(Colorspace color_space, GammaCurve gamma_curve, OutputAlphaFormat alpha_format);
EffectChain chain;
GLuint fbo, texnum;
std::string output_fragment_shader();
virtual bool needs_srgb_primaries() const { return false; }
+ virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num);
public:
WhiteBalanceEffect();
virtual std::string effect_type_id() const { return "WhiteBalanceEffect"; }
+ virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
std::string output_fragment_shader();
void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num);
};
float out_data[5 * 4];
- EffectChainTester tester(data, 1, 5, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 5, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *white_balance_effect = tester.get_chain()->add_effect(new WhiteBalanceEffect());
ASSERT_TRUE(white_balance_effect->set_vec3("neutral_color", neutral));
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
};
float out_data[3 * 4];
- EffectChainTester tester(data, 1, 3, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 3, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *white_balance_effect = tester.get_chain()->add_effect(new WhiteBalanceEffect());
ASSERT_TRUE(white_balance_effect->set_vec3("neutral_color", neutral));
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
};
float out_data[2 * 4];
- EffectChainTester tester(data, 1, 2, FORMAT_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
+ EffectChainTester tester(data, 1, 2, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
Effect *white_balance_effect = tester.get_chain()->add_effect(new WhiteBalanceEffect());
ASSERT_TRUE(white_balance_effect->set_float("output_color_temperature", 10000.0f));
tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
void finalize();
virtual bool can_output_linear_gamma() const { return false; }
+ virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
std::string output_fragment_shader();