Trouble chaining shaders on ofFbo

I’ve read the shader chapter of the oF book multiple times and read countless forum posts, but am still stuck. I’m trying to draw a ofVideoPlayer texture to an ofFbo and then apply multiple shaders, reusing the same ofFbo. However I’m finding that the ofFbo is layered with textures instead of the new draw replacing the contents of the last draw. Furthermore, if I ofClear, I’m also clearing the texture and I’m assuming copying to a buffer first would be costly. Also, I am receiving diagonal artifacts in the rendered image.

Here is setting up the ofFbo and ofPlanePrimitive I use to map coordinates

        fbo_.allocate(w, h);
        fbo_.begin();
        ofClear(255, 255, 255, 255);
        fbo_.end();

        plane_.set(w, h);
        plane_.setPosition(w/2, h/2, 0);
        plane_.mapTexCoords(0, 0, w, h);

This is the first texture being written:

        ofTexture tex = video_player_.getTexture();

        fbo_.begin();
        tex.draw(0, 0);
        fbo_.end();

Here is the chunk of code that gets repeated after the above texture is drawn, for different shaders:

        fbo.begin();
        ofTexture tex = fbo.getTexture();

        shader_.begin();
        
        shader_.setUniformTexture("tex0", tex, 0);
        shader_.setUniform2f("resolution", tex.getWidth(), tex.getHeight());
        shader_.setUniform1f("time", t);

        setupUniforms();

        tex.draw(0, 0);

        shader_.end();
        fbo.end();

Here is my vertex shader:

#version 330

uniform mat4 modelViewProjectionMatrix;

in vec4 position;
in vec2 texcoord;

out vec2 texture_coord;

void main(){
    gl_Position = modelViewProjectionMatrix * position;

    texture_coord = texcoord;
}

And my fragment shader:

#version 330

uniform sampler2DRect tex0;

uniform float time;
uniform vec2 resolution;

out vec4 outputColor;

in vec2 texture_coord;

vec3 rgb2hsv(vec3 c)
{
    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));

    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

vec3 hsv2rgb(vec3 c)
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

void main()
{
    vec4 color = texture(tex0, texture_coord);
    
    vec3 hsv = rgb2hsv(color.rgb);
    hsv.x = hsv.z;
    hsv.y = 1.0;
    hsv.z = 1.0;
    
    vec3 rgb = hsv2rgb(hsv);
 
    outputColor = vec4(rgb, color.w);
}

And the diagonal line:

Due to the help of pete on the slack channel, I got it working! What I needed was a ping pong buffer to switch between fbos and not try to write to the one I was reading from.

Here is the header:

    class PingPongBuffer {
        public:
            PingPongBuffer();
            void allocate(int width, int height, int internal_format=GL_RGBA);
            bool isAllocated();
            void swap();
            float getHeight();
            float getWidth();

            shared_ptr<ofFbo> src;
            shared_ptr<ofFbo> dest;

        private:
            vector<shared_ptr<ofFbo>> fbos_;
    };

The implementation

  PingPongBuffer::PingPongBuffer() : src(make_shared<ofFbo>()), dest(make_shared<ofFbo>()) {
        fbos_.push_back(src);
        fbos_.push_back(dest);
    }

    void PingPongBuffer::allocate(int width, int height, int internal_format) {
        for (auto fbo: fbos_) {
            fbo->allocate(width, height, internal_format);

            // Set magnification/minification algorithm
            fbo->getTexture().setTextureMinMagFilter(GL_LINEAR, GL_LINEAR);

            // Clear any lingering garbage
            fbo->begin();
            ofClear(255, 255, 255, 255);
            fbo->end();
        }
    }

    float PingPongBuffer::getWidth() {
        return src->getWidth();
    }

    float PingPongBuffer::getHeight() {
        return src->getHeight();
    }

    bool PingPongBuffer::isAllocated() {
        return src->isAllocated() && dest->isAllocated();
    }

    void PingPongBuffer::swap() {
        src.swap(dest);
    }

Me drawing to it

        ping_pong_.dest->begin();
        ofClear(0, 255);
        video_player_.draw(0, 0);
        ping_pong_.dest->end();

Then for each shader:

        ping_pong_.swap();
        ...
        ping_pong.dest->begin();
        ofClear(0, 255);

        ofTexture tex = ping_pong.src->getTexture();
        shader_.begin();
        
        shader_.setUniformTexture("tex0", tex, 0);
        shader_.setUniform2f("resolution", tex.getWidth(), tex.getHeight());
        shader_.setUniform1f("time", t);

        setupUniforms();

        tex.draw(0, 0);
        shader_.end();
        ping_pong.dest->end();

Then finally drew ping_pong_->dest to the screen.

3 Likes

Thanks for sharing.

In the past, I’ve done something similar to your first try (no ping pong) without problems. I’m wondering what happens in your case.

It appear to me that it’s weird to ask a shader to use the same texture as input and output, due to the parralel nature of the GPU work. I feel it’s normal to expect some strange results with some shaders, like convolution ones. But in your case, the shader only read the actual color of the pixel that it is modifying, so I think it must work. This is the same I’ve done in the past without problems.

Do you (or anybody) have some explanation about what’s wrong with this process ?

After repeatedly introducing bugs around swap timing and then eventually encountering a bug where whether or not I should swap was programmatically unknown, I decided to redesign it around swap() being private. The following is what I came up with:

Translation unit:

 class PingPongBuffer {
    public:
        PingPongBuffer();

        void allocate(int width, int height, int internal_format=GL_RGBA);
        bool isAllocated();

        float getHeight();
        float getWidth();

        void begin();
        void end();

        shared_ptr<ofFbo> drawable();

    private:
        void swap();

        shared_ptr<ofFbo> src_;
        shared_ptr<ofFbo> dest_;
        bool receiving_;
};

Implementation:

PingPongBuffer::PingPongBuffer() : src_(make_shared<ofFbo>()), dest_(make_shared<ofFbo>()) {
    receiving_ = false;
}

void PingPongBuffer::allocate(int width, int height, int internal_format) {
    vector<shared_ptr<ofFbo>> fbos = {src_, dest_};
    for (auto fbo: fbos) {
        fbo->allocate(width, height, internal_format);

        // Set magnification/minification algorithm
        fbo->getTexture().setTextureMinMagFilter(GL_LINEAR, GL_LINEAR);

        // Clear any lingering garbage
        fbo->begin();
        ofClear(255, 255, 255, 255);
        fbo->end();
    }
}

float PingPongBuffer::getWidth() {
    return src_->getWidth();
}

float PingPongBuffer::getHeight() {
    return src_->getHeight();
}

bool PingPongBuffer::isAllocated() {
    return src_->isAllocated() && dest_->isAllocated();
}

void PingPongBuffer::begin() {
    swap();
    receiving_ = true;
    dest_->begin();
}

void PingPongBuffer::end() {
    receiving_ = false;
    dest_->end();
}

shared_ptr<ofFbo> PingPongBuffer::drawable() {
    return receiving_ ? src_ : dest_;
}

void PingPongBuffer::swap() {
    src_.swap(dest_);
}

It’s pretty simple. When you want to draw to it, you call begin/end like you normally would do with src. When you want to draw it (including to itself) you call drawable() to retrieve the buffer that currently has been drawn to latest.

This is public domain, no credit or licensing needed if you use it.