FBO Drawing App

I’m trying to create a simple drawing app with an FBO.
resetFbo() is supposed to fill the FBO with green.
When mouse is down, a circle brush is supposed to draw into FBO.
When I draw, the green background disappears (becomes black), the brush strokes stay.
Can anyone tell me what I’m doing wrong?
Thanks for your help

#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/Camera.h"
#include "cinder/gl/gl.h"
#include "cinder/ImageIo.h"
#include "cinder/Utilities.h"
#include "cinder/params/Params.h"
#include "Resources.h"

#define SIZE 512

using namespace ci;
using namespace ci::app;
using namespace std;

class DrawingCanvasApp : public App {
  public:
    static void    prepareSettings( Settings *settings );
    void    setup() override;
    void    update() override;
    void    draw() override;
    void    mouseMove( MouseEvent event ) override;
    void    mouseDown( MouseEvent event ) override;
    void    mouseDrag( MouseEvent event ) override;
    void    mouseUp( MouseEvent event ) override;
  private:
    void    resetFbo();
            
    gl::FboRef       mFBO;
    
    vec2             mMouse;
    bool             mMousePressed;
    
    static const int        FBO_WIDTH = SIZE, FBO_HEIGHT = SIZE;
};

void DrawingCanvasApp::prepareSettings( Settings *settings )
{
    settings->setWindowSize( SIZE, SIZE );
    settings->setFrameRate( 60.0f );
}

void DrawingCanvasApp::mouseDown( MouseEvent event )
{
    mMousePressed = true;
}

void DrawingCanvasApp::mouseUp( MouseEvent event )
{
    mMousePressed = false;
}

void DrawingCanvasApp::mouseMove( MouseEvent event )
{
    mMouse = event.getPos();
}

void DrawingCanvasApp::mouseDrag( MouseEvent event )
{
    mMouse = event.getPos();
}

void DrawingCanvasApp::setup()
{
    mMousePressed = false;
    
    mFBO = gl::Fbo::create( FBO_WIDTH, FBO_HEIGHT, gl::Fbo::Format().colorTexture() );
    resetFbo();
}

void DrawingCanvasApp::update()
{
    gl::setMatricesWindow( mFBO->getSize() );
    gl::viewport( mFBO->getSize() );
    
    mFBO->bindFramebuffer();
    
    gl::enableAlphaBlending();
    
    if( mMousePressed )
    {
        ci::gl::color( ColorA( 1, 0, 0, 1 ) );
        gl::drawSolidCircle( mMouse, 25.0f, 32 );
    }
    
    mFBO->unbindFramebuffer();
}

void DrawingApp::draw()
{
    gl::clear( Color( 0.0, 0.0, 1.0 ) );
    gl::enableAlphaBlending();
    gl::setMatricesWindow( getWindowSize() );
    gl::viewport( getWindowSize() );
    
    gl::draw( mFBO->getColorTexture() );
}

void DrawingCanvasApp::resetFbo()
{
    gl::setMatricesWindow( mFBO->getSize() );
    gl::viewport( mFBO->getSize() );
    
    mFBO->bindFramebuffer();
    
    gl::enableAlphaBlending();
            
    ci::gl::color( ColorA( 0, 1, 0, 1 ) );
    gl::drawSolidRect( mFBO->getBounds() );
    
    mFBO->unbindFramebuffer();
}

CINDER_APP( DrawingCanvasApp, RendererGl, DrawingCanvasApp::prepareSettings )

Here’s a couple of changes that will fix the problem you’re seeing. The main problem you had was that you weren’t being mindful of OpenGL being one big state machine. Your call to gl::color to draw the red circle persists until another call is made to override it, which meant you were blitting that FBO to the screen modulated with red.

For this reason, cinder has a whole bunch of gl::Scoped* structs that automatically save and restore the affected state using RAII to try and minimize this issue. Here’s the code i’ve changed to make it work, along with a few short annotations to help you along. Hope this makes sense.

void DrawingCanvasApp::update()
{
    if ( mMousePressed ) // Don't bother binding the fbo if we're not going to draw anything
    {
        gl::ScopedMatrices matrices; // Automatically restore the current matrices when we exit this scope
        gl::setMatricesWindow( mFBO->getSize() );
        gl::ScopedViewport vp( ivec2 ( 0 ), mFBO->getSize() );  // Automatically restore the current viewport when we exit this scope
        gl::ScopedFramebuffer buffer { mFBO }; // Automatically unbind the framebuffer when we exit this scope.
        gl::ScopedBlendAlpha blend;
        
        {
            gl::ScopedColor color ( ColorA( 1, 0, 0, 1 ) ); // Automatically restore the color when we exit this scope. Seeing a pattern here? ;)
            gl::drawSolidCircle( mMouse, 25.0f, 32 );
        }
    }
    
}

void DrawingCanvasApp::draw()
{
    // There's a few places in here where some gl::Scoped* calls would be good hygiene, but i'll leave that up to you.
    gl::clear( Color( 0.0, 0.0, 1.0 ) );
    gl::enableAlphaBlending();
    gl::setMatricesWindow( getWindowSize() );
    gl::viewport( getWindowSize() );
    
    gl::draw( mFBO->getColorTexture() );
}

void DrawingCanvasApp::resetFbo()
{
    gl::setMatricesWindow( mFBO->getSize() );
    gl::viewport( mFBO->getSize() );
    gl::ScopedFramebuffer buffer { mFBO };
    gl::clear ( ColorA( 0, 1, 0, 1 ) );
}
2 Likes

Thanks for your help, lithium! This solves my problem and is really informative.

Hi @lithium, back again!
I’m trying to add an elaboration to my code - using nanovg to render…
With your corrections, this code seems to work properly with the standard cinder calls, but not with the nanovg versions shown below.
As I understand it, “nvgBeginFrame() defines the size of the window to render to in relation currently set viewport (i.e. glViewport on GL backends)”
But for whatever reason, I have some sort of viewport issue.
Any thoughts? Thanks again!

class Painting {
public:
    
    gl::FboRef  mFbo;
    ci::vec2    mDim;
    
    void setup(const ci::vec2& dim)
    {
        mDim = dim;
        mFbo = ci::gl::Fbo::create( mDim.x, mDim.y, ci::gl::Fbo::Format().colorTexture() );
    }
    
    void render(nvg::ContextGL& vg)
    {
        gl::ScopedMatrices matrices;
        gl::setMatricesWindow( mFbo->getSize() );
        gl::ScopedViewport vp( ivec2 ( 0 ), mFbo->getSize() );
        gl::ScopedFramebuffer buffer { mFbo };
        gl::ScopedBlendAlpha blend;
        
        ci::gl::clear( ColorA( 0, randFloat(), 0, 1 ) );
        ci::gl::clear( GL_STENCIL_BUFFER_BIT );
        
        vg.beginFrame( mDim, getWindow()->getContentScale() );
        vg.translate( ci::vec2( 0.0f, 0.0f ) );
        vg.fillColor( ColorA( ci::randFloat(),0,0,1 ) );
        vg.circle( mDim.x * 0.5f, mDim.y * 0.5f, 50.0f );
        vg.fill();
        vg.endFrame();
        
        /*
        {
            gl::ScopedColor color ( ColorA( randFloat(), 0, 0, 1 ) );
            gl::drawSolidCircle( mDim * 0.5f, 50.0f, 32 );
        }
        */
    }
};

class TestApp : public App {
  public:
	void setup() override;
	void draw() override;
    
    shared_ptr<ci::nvg::ContextGL>  mCtx;
    std::shared_ptr<Painting>       mPainting;
};

void TestApp::setup()
{
    mCtx = make_shared<ci::nvg::ContextGL>(nvg::createContextGL());
    
    mPainting = std::make_shared<Painting>();
    mPainting->setup( ci::vec2( 200, 200 ) );
}

void TestApp::draw()
{
    if( ! mPainting )
        return;
    
    mPainting->render( *mCtx );
    
    gl::clear( Color( 1, 0, 1 ) );
    ci::gl::clear( GL_STENCIL_BUFFER_BIT );
    
    gl::enableAlphaBlending();
    
    {
        gl::ScopedMatrices matrices;
        gl::setMatricesWindow( getWindowSize() );
        gl::ScopedViewport vp( ivec2 ( 0 ), getWindowSize() );
        
        gl::draw( mPainting->mFbo->getColorTexture() );
        
        /*
        {
            gl::ScopedColor color ( ColorA( 0, 0, 1, 1 ) );
            gl::drawSolidCircle( ci::vec2( getWindowSize() ) * 0.5f, 100.0f, 32 );
        }
        */

        auto& vg = *mCtx;

        vg.beginFrame( ci::vec2( getWindowSize() ), getWindow()->getContentScale() );
        vg.translate( ci::vec2( 0.0f, 0.0f ) );
        vg.fillColor( ColorA( 0,0,1,1 ) );
        vg.circle( getWindowWidth() * 0.5f, getWindowHeight() * 0.5f, 100.0f );
        vg.fill();
        vg.endFrame();
    }
}

What’s it supposed to look like? I’m getting this (with the green bit flashing like crazy), which seems in line with what you’ve written

The strobing is a product of the randFloat() in the colors just for testing, sorry about that.

Here’s what it should look like…


Thanks for your help!

This seems like a problem with nanovg itself. i know it pokes at a bit of gl state (which is mentioned in the docs) but i don’t think it cleans up perfectly. For example it leaves a different vao bound, which confuses cinder’s internal state (call gl::context()->sanityCheck() to see what the problems are)

It’s likely not a viewport issue at all, but an errant depth mode, stencil state, or something like that. Fire up your favourite OpenGL debugger and have a look at the GL state before and after nanovg gets involved and hopefully something will jump out at you.

Hmm. Will poke around and see what I find. Thanks for your help!