rendering - How to get correct SourceOver alpha compositing in SDL with OpenGL -
i using fbo (or "render texture") has alpha channel (32bpp argb) , clear color not opaque, example (r=1, g=0, b=0, a=0) (i.e. transparent). rendering translucent object, example rectangle color (r=1, g=1, b=1, a=0.5), on top of that. (all values normalized 0 1)
according common sense, imaging software such gimp , photoshop, several articles on porter-duff compositing, expect texture
- fully transparent outside of rectangle
- white (1.0, 1.0, 1.0) 50 % opacity inside rectangle.
like (you won't see on website):
instead, background color rgb values, (1.0, 0.0, 0.0) weighted overall (1 - sourcealpha) instead of (destalpha * (1 - sourcealpha)). actual result this:
i have verified behavior using opengl directly, using sdl's wrapper api, , using sfml's wrapper api. sdl , sfml have saved results image (with alpha channel) instead of merely rendering screen sure it's not problem final rendering step.
what need produce expected sourceover result, either sdl, sfml, or using opengl directly?
some sources:
w3 article on compositing, specifies co = αs x cs + αb x cb x (1 – αs), weight of cb should 0 if αb 0, no matter what.
english wiki shows destination ("b") being weighted according αb (as αs, indirectly).
german wiki shows 50% transparency examples, transparent background's original rgb values not interfere either green or magenta source, shows intersection asymmetric in favor of element "on top".
there several questions on seemingly deal @ first glance, not find talks abut specific issue. people suggest different opengl blending functions, general consensus seems glblendfuncseparate(gl_src_alpha, gl_one_minus_src_alpha, gl_one, gl_one_minus_src_alpha)
, both sdl , sfml use default. have tried different combinations no success.
another suggested thing premultiplying color destination alpha, since opengl can have 1 factor, needs 2 factors correct sourceover. however, cannot make sense of @ all. if i'm premultiplying (1, 0, 0) destination alpha value of, say, (0.1), (0.1, 0, 0) (as suggested here example). can tell opengl factor gl_one_minus_src_alpha
(and source gl_src_alpha
), i'm blending black, incorrect. though not specialist on topic, put fair amount of effort trying understand (and @ least got point managed program working pure software implementation of every compositing mode). understanding applying alpha value of 0.1 "via premultiplication" (1.0, 0.0, 0.0) not @ same treating alpha value correctly fourth color component.
here minimal , complete example using sdl. requires sdl2 compile, optionally sdl2_image if want save png.
// define save result image png (requires sdl2_image), undefine instead display in window #define save_image_as_png #include <sdl.h> #include <stdio.h> #ifdef save_image_as_png #include <sdl_image.h> #endif int main(int argc, char **argv) { if (sdl_init(sdl_init_video) != 0) { printf("init failed %s\n", sdl_geterror()); return 1; } #ifdef save_image_as_png if (img_init(img_init_png) == 0) { printf("img init failed %s\n", img_geterror()); return 1; } #endif sdl_window *window = sdl_createwindow("test", sdl_windowpos_centered, sdl_windowpos_centered, 800, 600, sdl_window_opengl | sdl_window_shown); if (window == null) { printf("window failed %s\n", sdl_geterror()); return 1; } sdl_renderer *renderer = sdl_createrenderer(window, 1, sdl_renderer_accelerated | sdl_renderer_targettexture); if (renderer == null) { printf("renderer failed %s\n", sdl_geterror()); return 1; } // texture render on sdl_texture *render_texture = sdl_createtexture(renderer, sdl_pixelformat_rgba8888, sdl_textureaccess_target, 300, 200); if (render_texture == null) { printf("rendertexture failed %s\n", sdl_geterror()); return 1; } sdl_settextureblendmode(render_texture, sdl_blendmode_blend); sdl_setrenderdrawblendmode(renderer, sdl_blendmode_blend); printf("init ok\n"); #ifdef save_image_as_png uint8_t *pixels = new uint8_t[300 * 200 * 4]; #endif while (1) { sdl_event event; while (sdl_pollevent(&event)) { if (event.type == sdl_quit) { return 0; } } sdl_rect rect; rect.x = 1; rect.y = 0; rect.w = 150; rect.h = 120; sdl_setrendertarget(renderer, render_texture); sdl_setrenderdrawcolor(renderer, 255, 0, 0, 0); sdl_renderclear(renderer); sdl_setrenderdrawcolor(renderer, 255, 255, 255, 127); sdl_renderfillrect(renderer, &rect); #ifdef save_image_as_png sdl_renderreadpixels(renderer, null, sdl_pixelformat_argb8888, pixels, 4 * 300); // masks fine system. might need randomly change ff parts around. sdl_surface *tmp_surface = sdl_creatergbsurfacefrom(pixels, 300, 200, 32, 4 * 300, 0xff0000, 0xff00, 0xff, 0xff000000); if (tmp_surface == null) { printf("surface error %s\n", sdl_geterror()); return 1; } if (img_savepng(tmp_surface, "t:\\sdltest.png") != 0) { printf("save image error %s\n", img_geterror()); return 1; } printf("image saved successfully\n"); return 0; #endif sdl_setrendertarget(renderer, null); sdl_setrenderdrawcolor(renderer, 255, 255, 255, 255); sdl_renderclear(renderer); sdl_rendercopy(renderer, render_texture, null, null); sdl_renderpresent(renderer); sdl_delay(10); } }
thanks @holyblackcat , @rabbid76 able shed light on entire thing. hope can out other people want know how correct alpha blending , details behind premultiplied alpha.
the basic problem correct "source over" alpha blending in not possible opengl's built-in blend functionality (that glenable(gl_blend)
, glblendfunc[separate](...)
, glblendequation[separate](...)
) (this same d3d way). reason following:
when calculating result color , alpha values of blending operation (according correct source over), 1 have use these functions:
each rgb color values (normalized 0 1):
rgb_f = ( alpha_s x rgb_s + alpha_d x rgb_d x (1 - alpha_s) ) / alpha_f
the alpha value (normalized 0 1):
alpha_f = alpha_s + alpha_d x (1 - alpha_s)
where
- sub f result color/alpha,
- sub s source (what on top) color/alpha,
- d destionation (what on bottom) color/alpha,
- alpha processed pixel's alpha value
- and rgb represents 1 of pixel's red, green, or blue color values
however, opengl can handle limited variety of additional factors go source or destination values (rgb_s , rgb_d in color equation) (see here), relevant ones in case being gl_one
, gl_src_alpha
, gl_one_minus_src_alpha
. can specify alpha formula correctly using options, best can rgb is:
rgb_f = alpha_s x rgb_s + rgb_d x (1 - alpha_s)
which lacks destination's alpha component (alpha_d). note formula equivalent correct 1 if \alpha_d = 1. in other words, when rendering onto framebuffer has no alpha channel (such window backbuffer), fine, otherwise produce incorrect results.
to solve problem , achieve correct alpha blending if alpha_d not equal 1, need gnarly workarounds. original (first) formula above can rewritten to
alpha_f x rgb_f = alpha_s x rgb_s + alpha_d x rgb_d x (1 - alpha_s)
if accept fact result color values dark (they multiplied result alpha color). gets rid of division already. correct rgb value, 1 have divide result rgb value result alpha value, however, turns out conversion never needed. introduce new symbol (pmargb) denotes rgb values dark because have been multiplied corresponding pixel's alpha value.
pmargb_f = alpha_s x rgb_s + alpha_d x rgb_d x (1 - alpha_s)
we can rid of problematic alpha_d factor ensuring of destination image's rgb values have been multiplied respective alpha values @ point. example, if wanted background color (1.0, 0.5, 0, 0.3), not clear framebuffer color, (0.3, 0.15, 0, 0.3) instead. in other words, doing 1 of steps gpu have in advance, because gpu can handle 1 factor. if rendering existing texture, have ensure created premultiplied alpha. result of our blending operations textures also have premultiplied alpha, can keep rendering things onto there , sure destination have premultiplied alpha. if rendering semi-transparent texture, semi-transparent pixels dark, depending on alpha value (0 alpha meaning black, 1 alpha meaning correct color). if rendering buffer has no alpha channel (like buffer use displaying things), alpha_f implicitly 1, premultiplied rgb values equal correctly blended rgb values. current formula:
pmargb_f = alpha_s x rgb_s + pmargb_d x (1 - alpha_s)
this function can used when source does not yet have premultiplied alpha (for example, if source regular image came out of image processing program, alpha channel correctly blended no premultiplied alpha).
there reason might want rid of \alpha_s well, , use premultiplied alpha source well:
pmargb_f = pmargb_s + pmargb_d x (1 - alpha_s)
this formula needs taken if source happens have premultiplied alpha - because source pixel values pmargb instead of rgb. going case if rending offscreen buffer alpha channel using above method. may reasonable have texture assets stored premultiplied alpha default formula can always taken.
to recap, calculate alpha value, use formula:
alpha_f = alpha_s + alpha_d x (1 - alpha_s)
, corresponds (gl_one, gl_one_minus_src_alpha
). calculate rgb color values, if source not have premultiplied alpha applied rgb values, use
pmargb_f = alpha_s x rgb_s + pmargb_d x (1 - alpha_s)
, corresponds (gl_src_alpha, gl_one_minus_src_alpha
). if does have premultiplied alpha applied it, use
pmargb_f = pmargb_s + pmargb_d x (1 - alpha_s)
, corresponds (gl_one, gl_one_minus_src_alpha
).
what practically means in opengl: when rendering framebuffer alpha channel, switch correct blending function accordingly , make sure fbo's texture has premultiplied alpha applied rgb values. note correct blending function may potentially different each rendered object, according whether or not source has premultiplied alpha. example: want background [1, 0, 0, 0.1], , render object color [1, 1, 1, 0.5] onto it.
// clear premultiplied version of real background color - texture (which destination in blending operations) complies "destination must have premultiplied alpha" convention. glclearcolor(0.1f, 0.0f, 0.0f, 0.1f); glclear(gl_color_buffer_bit | gl_depth_buffer_bit); // // option 1 - source either has premultiplied alpha whatever reason, or can ensure has // { // set drawing color premultiplied version of real drawing color. glcolor4f(0.5f, 0.5f, 0.5f, 0.5f); // set blending equation according "blending source premultiplied alpha". glenable(gl_blend); glblendfuncseparate(gl_one, gl_one_minus_src_alpha, gl_one, gl_one_minus_src_alpha); glblendequationseparate(gl_add, gl_add); } // // option 2 - source not have premultiplied alpha // { // set drawing color original version of real drawing color. glcolor4f(1.0f, 1.0f, 1.0f, 0.5f); // set blending equation according "blending source premultiplied alpha". glenable(gl_blend); glblendfuncseparate(gl_src_alpha, gl_one_minus_src_alpha, gl_one, gl_one_minus_src_alpha); glblendequationseparate(gl_add, gl_add); } // --- draw thing --- gldisable(gl_blend);
in either case, resulting texture has premultiplied alpha. here 2 possibilities might want texture:
if want export image correctly alpha blended (as per sourceover definition), need rgba data , explicitly divide each rgb value corresponding pixel's alpha value.
if want render onto backbuffer (whose background color shall (0, 0, 0.5)), proceed (for example, additionally want modulate texture (0, 0, 1, 0.8)):
// buffer has 100 % alpha. glclearcolor(0.0f, 0.0f, 0.5f, 1.0f); glclear(gl_color_buffer_bit | gl_depth_buffer_bit); // color texture drawn - modulating color's rgb values need premultiplied alpha glcolor4f(0.0f, 0.0f, 0.8f, 0.8f); // set blending equation according "blending source premultiplied alpha". glenable(gl_blend); glblendfuncseparate(gl_one, gl_one_minus_src_alpha, gl_one, gl_one_minus_src_alpha); glblendequationseparate(gl_add, gl_add); // --- draw texture --- gldisable(gl_blend);
technically, result have premultiplied alpha applied it. however, because result alpha 1 each pixel, premultiplied rgb values equal correctly blended rgb values.
to achieve same in sfml:
rendertexture.clear(sf::color(25, 0, 0, 25)); sf::rectangleshape rect; sf::renderstates rs; // assuming object has premultiplied alpha - or can make sure has { rs.blendmode = sf::blendmode(sf::blendmode::one, sf::blendmode::oneminussrcalpha); rect.setfillcolor(sf::color(127, 127, 127, 127)); } // assuming object not have premultiplied alpha { rs.blendmode = sf::blendalpha; // shortcut constructor correct blending parameters type rect.setfillcolor(sf::color(255, 255, 255, 127)); } // --- align rect --- rendertexture.draw(rect, rs);
and likewise draw rendertexture
onto backbuffer
// premultiplied modulation color rendertexture_sprite.setcolor(sf::color(0, 0, 204, 204)); window.clear(sf::color(0, 0, 127, 255)); sf::renderstates rs; rs.blendmode = sf::blendmode(sf::blendmode::one, sf::blendmode::oneminussrcalpha); window.draw(rendertexture_sprite, rs);
unfortunately, not possible sdl afaik (at least not on gpu part of rendering process). unlike sfml, exposes fine-grained control on blending mode user, sdl not allow setting individual blending function components - has sdl_blendmode_blend
hardcoded glblendfuncseparate(gl_src_alpha, gl_one_minus_src_alpha, gl_one, gl_one_minus_src_alpha)
.
Comments
Post a Comment