Search code examples
glslsfmlblur

Making a glow effect - Problems with alpha values


I want to create a glow effect for my game. In order to keep this minimalistic let's say I want to glow an Image. :)

Starting with this one:

default picture, no effect

To get something like this:desired output, picture with glow effect

It's a three step way.

  • save all bright pixels from the scene (= luminescence)
  • Apply a blur effect on those pixels (= blur)
  • draw original picture and the blur texture ontop (= assemble)

Step 1 and 3 are no Problem. The blur part just doesn't want to work correctly.

Before I explain further, here's my luminescence result: the picture, but all darker pixels are transparent (threshold = 0.67f)

An now when I blur this, I get some unlucky results:enter image description here This black edge comes from the fact, that any transparent color is black vec4(0.0, 0.0, 0.0, 0.0). It's not an unkown Problem within SFML/GLSL, and the suggestion was to use SFML's sf::BlendMode for this and multiply the .rgb value of the final pixel color in the fragment shader with its alpha value. So I did and now this my result:

blurred, but overall too dark, especially the edges

It's better, but definetely not good. The blur shader now also avarages out the surrounding pixels of the luminescence mask. After assembling it's just a blurry picture:

Blurry Picture, not even close to resemble a glow effect.. I tried "fixing" this in the shader files by checking if the pixel's alpha is zero. This way I don't value them when avaraging out. But since sf::BlendMode is activated, I don't know how alpha behaves now - So I deactivated the blendmode but I still have weird results. (at the very of this question I provided the code and a result from this attempt)


none of my attempts to fix this work. I really could use some help here. Maybe I'm doing something fundamentally wrong in the shaders.. here's the full code - If you want to compile it, make a folder resources with the 2 Fragment shaders and the background.jpg (in 1280x720).

luminescence.frag

#version 120

uniform sampler2D texture;
uniform float threshold;

void main(void){
    vec3 current_color = texture2D(texture, gl_TexCoord[0].xy).rgb;
    vec4 pixel =  vec4(current_color.rgb, 0.0);
    float brightness = dot(current_color.rgb, vec3(0.2126, 0.7152, 0.0722));
    if (brightness >= threshold){
        pixel = texture2D(texture, gl_TexCoord[0].xy);
    }
    gl_FragColor = pixel;
}

boxblur.frag

#version 120

uniform sampler2D texture;
uniform float texture_inverse;
uniform int blur_radius;
uniform vec2 blur_direction;

void main(void){
    vec4 sum = texture2D(texture, gl_TexCoord[0].xy);

    for (int i = 0; i < blur_radius; ++i){
        sum += texture2D(texture, gl_TexCoord[0].xy + (i * texture_inverse) * blur_direction);
        sum += texture2D(texture, gl_TexCoord[0].xy - (i * texture_inverse) * blur_direction);
    }
    vec4 pixel = vec4(sum / (blur_radius * 2 + 1));
    pixel.rgb *= pixel.a;
    gl_FragColor = pixel;
}

main.cpp

#include <SFML/Graphics.hpp>
#include <iostream>
#include <exception>

void run() {
    const sf::Vector2f SIZE(1280, 720);

    sf::Texture background_tex;
    background_tex.loadFromFile("resources/background.jpg");
    sf::Sprite background(background_tex);



    sf::Shader luminescence;
    luminescence.loadFromFile("resources/luminescence.frag", sf::Shader::Fragment);
    luminescence.setUniform("texture", sf::Shader::CurrentTexture);
    luminescence.setUniform("threshold", 0.67f);

    sf::Shader blur;
    blur.loadFromFile("resources/boxblur.frag", sf::Shader::Fragment);
    blur.setUniform("texture", sf::Shader::CurrentTexture);
    blur.setUniform("texture_inverse", 1.0f / SIZE.x);




    sf::RenderStates shader_states;
    shader_states.blendMode = sf::BlendMode(sf::BlendMode::One, sf::BlendMode::OneMinusSrcAlpha);



    sf::ContextSettings context_settings;
    context_settings.antialiasingLevel = 12;

    //draws background
    sf::RenderTexture scene_render;
    scene_render.create(SIZE.x, SIZE.y, context_settings);

    //draws luminescence and blur
    sf::RenderTexture shader_render;
    shader_render.create(SIZE.x, SIZE.y, context_settings);



    sf::RenderWindow window(sf::VideoMode(SIZE.x, SIZE.y), "glsl fun", sf::Style::Default, context_settings);

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
        }

        scene_render.clear();
        scene_render.draw(background);
        scene_render.display();


        //apply luminescence
        shader_states.shader = &luminescence;
        shader_render.clear(sf::Color::Transparent);
        shader_render.draw(sf::Sprite(scene_render.getTexture()), shader_states);
        shader_render.display();

        //apply two pass gaussian blur 3 times to simulate gaussian blur.
        shader_states.shader = &blur;
        float blur_radius = 30.0f;
        for (int i = 0; i < 3; ++i) {
            blur.setUniform("blur_radius", static_cast<int>(blur_radius));

            //vertical blur
            blur.setUniform("blur_direction", sf::Glsl::Vec2(1.0, 0.0));
            shader_render.draw(sf::Sprite(shader_render.getTexture()), shader_states);
            shader_render.display();

            //horizontal blur
            blur.setUniform("blur_direction", sf::Glsl::Vec2(0.0, 1.0));
            shader_render.draw(sf::Sprite(shader_render.getTexture()), shader_states);
            shader_render.display();

            //decrease blur_radius to simulate a gaussian blur
            blur_radius *= 0.45f;
        }

        //assembly
        window.clear();
        window.draw(sf::Sprite(scene_render.getTexture()));
        window.draw(sf::Sprite(shader_render.getTexture()));
        window.display();
    }
}

int main() {
    try {
        run();
    }
    catch (std::exception e) {
        std::cerr << "caught exception - - - " << e.what() << '\n';
        return 1;
    }
    return 0;
}

This is the boxblur.frag where I tried to exclude zero alpha values: (I removed shader_states.blendMode = sf::BlendMode(sf::BlendMode::One, sf::BlendMode::OneMinusSrcAlpha);on line 29 in main.cpp of course):

#version 120

uniform sampler2D texture;
uniform float texture_inverse;
uniform int blur_radius;
uniform vec2 blur_direction;

void main(void){
    float div = 0.0;
    vec4 sum = vec4(0.0, 0.0, 0.0, 0.0);
    vec4 temp_color = texture2D(texture, gl_TexCoord[0].xy);
    if (temp_color.a > 0.0){
        sum += temp_color;
        div += 1.0;
    }


    for (int i = 0; i < blur_radius; ++i){
        temp_color = texture2D(texture, gl_TexCoord[0].xy + (i * texture_inverse) * blur_direction);
        if (temp_color.a > 0.0){
            sum += temp_color;
            div += 1.0;
        }
        temp_color = texture2D(texture, gl_TexCoord[0].xy - (i * texture_inverse) * blur_direction);
        if (temp_color.a > 0.0){
            sum += temp_color;
            div += 1.0;
        }
    }
    vec4 pixel;
    if (div == 0.0){
        pixel = vec4(texture2D(texture, gl_TexCoord[0].xy).rgb, 0.0);
    }
    else{
        pixel = vec4(sum / div);
    }
    gl_FragColor = pixel;
}

Resulting in: mostly bright values. but too blocky and not smooth at all

[I am using Visual Studio 2017 Community] - Thanks for any help!


Solution

  • I posted this question also on en.sfml-dev.org (here) and fallahn showed me the correct approach. Before tackling that, here are the picture results:

    Luminescence (threshold = 0.24f): luminescence picture

    Blur (4 layers): blur

    Assembled: glowing picture

    Yay! The solution is to set all transparent pixels to solid black vec4(0.0, 0.0, 0.0, 1.0) and than after they've been blurred, just add them ontop the Scene:

    vec4 tex_color = texture2D(texture, gl_TexCoord[0].xy);
    vec4 add_color = texture2D(add_texture, gl_TexCoord[0].xy);
    gl_FragColor = tex_color + add_color;
    

    This way, if add_color is black ("transparent"), we add tex_color + vec4(0.0, 0.0, 0.0, 1.0) which results in no change!

    This is great, because now you can ignore the alpha channel completely.

    To understand why I find this so great, you can read this little rant here (feel free to skip it):

    Without worrying about alpha you can ignore any sf::BlendMode like the confusing sf::BlendMode::OneMinusSrcAlpha which caused me headaches for 2 solid days. Try calculating any reasonable "true" alpha value when you know they are all premultiplied. Of course you also have to multiply all rgb values with the pixel's alpha to reverse the prumultiplication… the formulas escalate quite quickly from here. Also subtract 1 from alpha because it's OneMinusSrcAlpha... and don't forget to check for cases where the sum of all alphas (yes, you need to sum that) is 0 (or in OneMinusSrcAlpha matter, something else), because otherwise you get division by 0 (or in OneMinusSrcAlpha matter a division by 0 when all surrounding pixels are solid). Also sometimes weird alpha values might work, but only for a single pass of blur, but in my case I have multiple passes.. etc.

    Here's the final code:

    luminescence.frag

    #version 120
    
    uniform sampler2D texture;
    uniform float threshold;
    
    void main(void){
        vec3 current_color = texture2D(texture, gl_TexCoord[0].xy).rgb;
        vec4 pixel =  vec4(0.0, 0.0, 0.0, 1.0);
        float brightness = dot(current_color.rgb, vec3(0.2126, 0.7152, 0.0722));
        if (brightness >= threshold){
            pixel = texture2D(texture, gl_TexCoord[0].xy);
        }
        gl_FragColor = pixel;
    }
    

    boxblur.frag

    #version 120
    
    uniform sampler2D texture;
    uniform float texture_inverse;
    uniform int blur_radius;
    uniform vec2 blur_direction;
    
    void main(void){
        vec4 sum = texture2D(texture, gl_TexCoord[0].xy);
    
        for (int i = 0; i < blur_radius; ++i){
            sum += texture2D(texture, gl_TexCoord[0].xy + (i * texture_inverse) * blur_direction);
            sum += texture2D(texture, gl_TexCoord[0].xy - (i * texture_inverse) * blur_direction);
        }
        gl_FragColor = sum / (blur_radius * 2 + 1);
    }
    

    multiply.frag

    #version 120
    
    uniform sampler2D texture;
    uniform float multiply;
    
    void main(void){
        gl_FragColor = texture2D(texture, gl_TexCoord[0].xy) * multiply;
    }
    

    assemble.frag

    #version 120
    
    uniform sampler2D texture;
    uniform sampler2D add_texture;
    uniform float add_weight;
    
    void main(void){
        vec4 tex_color = texture2D(texture, gl_TexCoord[0].xy);
        vec4 add_color = texture2D(add_texture, gl_TexCoord[0].xy) * add_weight;
        gl_FragColor = tex_color + add_color;
    }
    

    main.cpp

    #include <SFML/Graphics.hpp>
    #include <iostream>
    #include <array>
    
    void run() {
        const sf::Vector2f SIZE(1280, 720);
    
        sf::Texture background_tex;
        background_tex.loadFromFile("resources/background.jpg");
        sf::Sprite background(background_tex);
    
        sf::Shader luminescence_shader;
        luminescence_shader.loadFromFile("resources/luminescence.frag", sf::Shader::Fragment);
        luminescence_shader.setUniform("texture", sf::Shader::CurrentTexture);
        luminescence_shader.setUniform("threshold", 0.24f);
    
        sf::Shader blur_shader;
        blur_shader.loadFromFile("resources/boxblur.frag", sf::Shader::Fragment);
        blur_shader.setUniform("texture", sf::Shader::CurrentTexture);
        blur_shader.setUniform("texture_inverse", 1.0f / SIZE.x);
    
        sf::Shader assemble_shader;
        assemble_shader.loadFromFile("resources/assemble.frag", sf::Shader::Fragment);
        assemble_shader.setUniform("texture", sf::Shader::CurrentTexture);
    
        sf::Shader multiply_shader;
        multiply_shader.loadFromFile("resources/multiply.frag", sf::Shader::Fragment);
        multiply_shader.setUniform("texture", sf::Shader::CurrentTexture);
    
    
        sf::RenderStates shader_states;
        //no blendmode! we make our own - assemble.frag
    
        sf::ContextSettings context_settings;
        context_settings.antialiasingLevel = 12;
    
        //draws background
        sf::RenderTexture scene_render;
        scene_render.create(SIZE.x, SIZE.y, context_settings);
    
        sf::RenderTexture luminescence_render;
        luminescence_render.create(SIZE.x, SIZE.y, context_settings);
    
        //draws luminescence and blur
        sf::RenderTexture assemble_render;
        assemble_render.create(SIZE.x, SIZE.y, context_settings);
    
    
    
        //addding multiple boxblurs with different radii looks really nice! in this case 4 layers
        std::array<sf::RenderTexture, 4> blur_renders;
        for (int i = 0; i < blur_renders.size(); ++i) {
            blur_renders[i].create(SIZE.x, SIZE.y, context_settings);
        }
        const int BLUR_RADIUS_VALUES[] = { 250, 180, 125, 55 };
        float blur_weight = blur_renders.empty() ? 0.0 : 1.0 / blur_renders.size();
    
        sf::RenderWindow window(sf::VideoMode(SIZE.x, SIZE.y), "glsl fun", sf::Style::Default, context_settings);
    
        while (window.isOpen()) {
            sf::Event event;
            while (window.pollEvent(event)) {
                if (event.type == sf::Event::Closed) {
                    window.close();
                }
            }
    
            //first draw the scene
            scene_render.clear();
            scene_render.draw(background);
            scene_render.display();
    
    
            //apply luminescence
            shader_states.shader = &luminescence_shader;
            luminescence_render.clear();
            luminescence_render.draw(sf::Sprite(scene_render.getTexture()), shader_states);
            luminescence_render.display();
    
            //apply two pass gaussian blur n times to simulate gaussian blur.
            shader_states.shader = &blur_shader;
            for (int i = 0; i < blur_renders.size(); ++i) {
                blur_shader.setUniform("blur_radius", BLUR_RADIUS_VALUES[i]);
    
                blur_renders[i].clear();
                blur_renders[i].draw(sf::Sprite(luminescence_render.getTexture()));
                blur_renders[i].display();
    
                //vertical blur
                blur_shader.setUniform("blur_direction", sf::Glsl::Vec2(1.0, 0.0));
                blur_renders[i].draw(sf::Sprite(blur_renders[i].getTexture()), shader_states);
                blur_renders[i].display();
    
                //horizontal blur
                blur_shader.setUniform("blur_direction", sf::Glsl::Vec2(0.0, 1.0));
                blur_renders[i].draw(sf::Sprite(blur_renders[i].getTexture()), shader_states);
                blur_renders[i].display();
            }
    
            //load blur_renders[0] into assemble_render so we can add the other blurs ontop of it
            shader_states.shader = &multiply_shader;
            multiply_shader.setUniform("multiply", blur_weight);
            assemble_render.clear();
            assemble_render.draw(sf::Sprite(blur_renders[0].getTexture()), shader_states);
            assemble_render.display();
    
            //adding the rest ontop creating a final blur
            shader_states.shader = &assemble_shader;
            assemble_shader.setUniform("add_weight", blur_weight);
            for (int i = 1; i < blur_renders.size(); ++i) {
                assemble_shader.setUniform("add_texture", blur_renders[i].getTexture());
                assemble_render.draw(sf::Sprite(assemble_render.getTexture()), shader_states);
                assemble_render.display();
            }
    
            //final result; scene + blur
            assemble_shader.setUniform("add_weight", 1.0f);
            assemble_shader.setUniform("add_texture", assemble_render.getTexture());
            assemble_render.draw(sf::Sprite(scene_render.getTexture()), shader_states);
            assemble_render.display();
    
            window.clear();
            window.draw(sf::Sprite(assemble_render.getTexture()));
            window.display();
        }
    }
    
    int main() {
        try {
            run();
        }
        catch (std::exception e) {
            std::cerr << "caught exception - - - " << e.what() << '\n';
            return 1;
        }
        return 0;
    }