I have a QOpenGLWidget in which I draw text with a QPainter. I would like to implement a snapshot feature by rendering the widget content into a QOpenGLFrameBuffer and converting the frame buffer into a QImage.
Unfortunately, if I set the font's point size too high (> 46), the text appears as a black bar in the snapshot, while in the widget it is displayed correctly. See below an example snapshot where the block over the line is supposed to be text with font size > 46.
Here is the simplified code to render the image (it should work, because it is correctly displayed in the QOpenGLWidget):
void renderSomething(const int x, const int y, const QString & str, const int fontSize) {
// 1. draw the line
// (calculate min_x, max_x, y and z values)
glLineWidth(3);
glColor3f(0., 0., 0.);
glBegin(GL_LINES);
glVertex3f(min_x, y, z);
glVertex3f(max_x, y, z);
glEnd();
// 2. draw the text
GLint gl_viewport[4];
glGetIntegerv(GL_VIEWPORT, gl_viewport);
backup_gl_state();
QOpenGLPaintDevice paintDevice(gl_viewport[2], gl_viewport[3]);
QPainter painter(&paintDevice);
painter.setFont(QFont(painter.font().family(), fontSize);
painter.setPen(Qt::black);
painter.drawText(x, y, str);
painter.end();
restore_gl_state();
}
Here is the code for storing a snapshot:
void Viewport::takeSnapshot(const QString & path, const int size) {
glPushAttrib(GL_VIEWPORT_BIT);
glViewport(0, 0, size, size); // since size varies I want the text to scale with it
QOpenGLFramebufferObject fbo(size, size, QOpenGLFramebufferObject::Depth);
fbo.bind();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderer.renderSomething(100, 100, "Test", 50); // Here is the render call!
QImage fboImage(fbo.toImage());
QImage image(fboImage.constBits(), fboImage.width(), fboImage.height(), QImage::Format_RGB32);
image.save(path);
glPopAttrib();
fbo.bindDefault();
fbo.release();
}
EDIT
I found a workaround by using QPainter to directly draw the text on the QImage instead of drawing into the FBO. No black bar anymore, but it complicates my code and I would be happy to find a real solution...
The solution was simple: Re-read the documentation on QOpenGLFrameBufferObject in which it says:
Create the QOpenGLFrameBufferObject instance with the CombinedDepthStencil attachment if you want QPainter to render correctly.
I only attached a depth buffer. The correct initialization would be:
QOpenGLFramebufferObject fbo(size, size, QOpenGLFramebufferObject::CombinedDepthStencil);
Related
This is the code as it is but I'm having trouble making SDL_Rect work or cairo move to / line to. It produces a blank black window. I found out that it is possible for cairo to draw on an SDL2 window but don't know how I would go about making it work. Majority of the code I see uses GTK+.
SDL_Window* mainWindow;
SDL_Renderer* mainRenderer;
SDL_CreateWindowAndRenderer(1280, 960, SDL_WINDOW_SHOWN, &mainWindow, &mainRenderer);
cairo_surface_t* surface;
cairo_t* cr;
surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1200, 900);
cr = cairo_create(surface);
cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
cairo_set_line_width(cr, 25);
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
cairo_move_to(cr, 100.0, 100.0);
cairo_line_to(cr, 500, 500);
cairo_stroke(cr);
unsigned char* data;
data = cairo_image_surface_get_data(surface);
SDL_Texture* texture;
SDL_Rect rect = {0, 0, 100, 100};
texture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ABGR8888,
SDL_TEXTUREACCESS_STREAMING, 100, 200);
SDL_UpdateTexture(texture, &rect, data, 400);
// Main program loop
while (1)
{
SDL_Event event;
if (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT)
{
SDL_DestroyRenderer(mainRenderer);
SDL_DestroyWindow(mainWindow);
SDL_Quit();
break;
}
}
SDL_RenderClear(mainRenderer);
SDL_RenderCopy(mainRenderer, texture, NULL, NULL);
SDL_RenderPresent(mainRenderer);
}
// Cleanup and quit
cairo_destroy(cr);
cairo_surface_destroy(surface);
Your texture is 100x200 and you're only updating its (0,0)(100,100) rectangle from cairo image data, but with cairo you only started drawing at (100,100), so entire area is black. In addition, your pitch when updating texture is incorrect - it is byte length of source data line; your cairo image have width of 1200 and its format requires 4 bytes per pixel; neglecting padding it is 1200*4, not 400 (note - if format is different, e.g. 3 bytes per pixel, padding may be important - refer to cairo documentation to check if it pads its rows if you're going to use that format). So there are two solutions:
Use cairo to produce full image you want, e.g. don't use (100,100) offset with move_to, or copy entire image to SDL texture. Then only correcting pitch is enough.
Copy part of cairo data to texture,
e.g.
const unsigned int bpp = 4;
const unsigned int pitch = 1200*bpp;
SDL_UpdateTexture(texture, &rect,
data // offset pointer to start at 'first' pixel you want to copy
+ 100*pitch // skip first 100 rows
+ 100*bpp, // and first 100 pixels
pitch // pitch is the same - it refers to source image, not destination
);
I apologize if this isn't exact. I'm doing the best I can to copy code by hand from one computer to another, and the destination computer doesn't have a compiler (don't ask).
Header file
#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H
#include <qopenglwidget.h>
class MyOpenGlWidget : public QOpenGLWidget
{
Q_OBJECT
public:
explicit MyOpenGlWidget(QWidget *parent = 0, Qt::WindowFlags f = Qt::WindowFlags());
virtual ~MyOpenGlWidget();
protected:
// these are supposed to be overridden, so use the "override" keyword to compiler check yourself
virtual void initializeGL() override;
virtual void resizeGL(int w, int h) override;
virtual void paintGL() override;
private:
QPixmap *_foregroundPixmap;
}
#endif
Source file
QOpenGLFunctions_2_1 *f = 0;
MyOpenGlWidget::MyOpenGlWidget(QWidget *parent, Qt::WindowFlags f) :
QOpenGLWidget(parent, f)
{
_foregroundPixmap = 0;
QPixmap *p = new QPixmap("beveled_texture.tiff");
if (!p->isNull())
{
_foregroundPixmap = p;
}
}
MyOpenGlWidget::~MyOpenGlWidget()
{
delete _foregroundPixmap;
}
void MyOpenGlWidget::initializeGL()
{
// getting a deprecated set of functions because such is my work environment
// Note: Also, QOpenGLWidget doesn't support these natively.
f = QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_2_1>();
f->glClearColor(0.0f, 1.0f, 0.0f, 1.0f); // clearing to green
f->glEnable(GL_DEPTH_TEST);
f->glEnable(GL_CULL_FACE); // implicitly culling front face
f->glEnable(GL_SCISSOR_TEST);
// it is either copy the matrix and viewport code from resizeGL or just call the method
this->resizeGL(this->width(), this->height());
}
void MyOpenGlWidget::resizeGL(int w, int h)
{
// make the viewport square
int sideLen = qMin(w, h);
int x = (w - side) / 2;
int y = (h - side) / 2;
// the widget is 400x400, so this random demonstration square will show up inside it
f->glViewport(50, 50, 100, 100);
f->glMatrixMode(GL_PROJECTION);
f->glLoadIdentity();
f->glOrtho(-2.0f, +2.0f, -2.0f, +2.0f, 1.0f, 15.0f); // magic numbers left over from a demo
f->glMatrixMode(GL_MODELVIEW);
// queue up a paint event
// Note: QGLWidget used updateGL(), but QOpenGLWidget uses update().
this->update();
}
void MyOpenGlWidget::paintGL()
{
f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// I want to draw a texture with beveled edges the size of this widget, so I can't
// have the background clearing all the way to the edges
f->glScissor(50, 50, 200, 200); // more magic numbers just for demonstration
// clears to green in just scissored area (unless QPainter is created)
f->glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
// loading identity matrix, doing f->glTranslatef(...) and f->glRotatef(...)
// pixmap loaded earlier in another function
if (_foregroundPixmap != 0)
{
// QPixmap apparently draws such that culling the back face will cull the entire
// pixmap, so have to switch culling for duration of pixmap drawing
f->glCullFace(GL_FRONT);
QPainter(this);
painter.drawPixmap(0, 0, _foregroundPixmap->scaled(this->size()));
// done, so switch back to culling the front face
f->glCullFace(GL_BACK);
}
QOpenGLFunctions_2_1 *f = 0;
void MyOpenGlWidget::initializeGL()
{
// getting a deprecated set of functions because such is my work environment
// Note: Also, QOpenGLWidget doesn't support these natively.
f = QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_2_1>();
f->glClearColor(0.0f, 1.0f, 0.0f, 1.0f); // clearing to green
f->glEnable(GL_DEPTH_TEST);
f->glEnable(GL_CULL_FACE); // implicitly culling front face
f->glEnable(GL_SCISSOR_TEST);
// it is either copy the matrix and viewport code from resizeGL or just call it directly
this->resizeGL(this->width(), this->height());
}
void MyOpenGlWidget::resizeGL(int w, int h)
{
// make the viewport square
int sideLen = qMin(w, h);
int x = (w - side) / 2;
int y = (h - side) / 2;
// the widget is 400x400, so this random demonstration square will show up inside it
f->glViewport(50, 50, 100, 100);
f->glMatrixMode(GL_PROJECTION);
f->glLoadIdentity();
f->glOrtho(-2.0f, +2.0f, -2.0f, +2.0f, 1.0f, 15.0f); // magic numbers left over from a demo
f->glMatrixMode(GL_MODELVIEW);
// queue up a paint event
// Note: QGLWidget used updateGL(), but QOpenGLWidget uses update().
this->update();
}
void MyOpenGlWidget::paintGL()
{
f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// I want to draw a texture with beveled edges the size of this widget, so I can't
// have the background clearing all the way to the edges
f->glScissor(50, 50, 200, 200); // more magic numbers just for demonstration
// clears to green in just scissored area (unless QPainter is created)
f->glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
// loading identity matrix, doing f->glTranslatef(...) and f->glRotatef(...), drawing triangles
// done drawing, so now draw the beveled foreground
if (_foregroundPixmap != 0)
{
// QPixmap apparently draws such that culling the back face will cull the entire
// pixmap, so have to switch culling for duration of pixmap drawing
f->glCullFace(GL_FRONT);
QPainter(this);
painter.drawPixmap(0, 0, _foregroundPixmap->scaled(this->size()));
// done, so switch back to culling the front face
f->glCullFace(GL_BACK);
}
}
The problem is this code from paintGL():
QPainter(this);
As soon as a QPainter object is created, the glScissor(...) call that I made earlier in the function is overrun and some kind of glClearColor(...) call is made (possibly from QPainter's constructor) that clears the entire viewport to the background color that I set just after glScissor(...). Then the pixmap draws my beveled texture just fine.
I don't want QPainter to overrun my scissoring.
The closest I got to an explanation was two QPainter methods, beginNativePainting() and endNativePainting(). According to the documentation, scissor testing is disabled between these two, but in their example they re-enable it. I tried using this "native painting" code, but I couldn't stop QPainter's mere existence from ignoring GL's scissoring and clearing my entire viewport.
Why is this happening and how do I stop this?
Note: This work computer has network policies to prevent me from going to entertainment sites like imgur to upload "what I want" and "what I get" pictures, so I have to make due with text.
Why is this happening
The OpenGL context is a shared resource and you have to share it with other players.
and how do I stop this?
You can't. Just do the proper thing and set viewport, scissor rectangle and all the other drawing related state at the right moment: Right before you are going to draw something that relies on these settings. Don't set them aeons (in computer terms) before, somewhere in some "initialization" or a reshape handler. And be expected that in drawing code any function you call that makes use of OpenGL will leave some garbage behind.
I have a texture drawn in a GLcontrol and I want to draw points on top of it. Instead, I get the full texture set to the colour of the point I want to draw. I guess that I have to disable the texture format and enable the points drawings, but cant reach the solution...
Here is the draw function:
Basically the point to draw is ROI[0], but instead drawing just the point I got the image shown below (the image is grayscale before drawing "the point").
private: void drawImg(int img){
int w=this->glControl_create_grid->Width;
int h=this->glControl_create_grid->Height;
GL::MatrixMode(MatrixMode::Projection);
GL::LoadIdentity();
GL::Ortho(0, w, 0, h, -1, 1); // Bottom-left corner pixel has coordinate (0, 0)
GL::Viewport(0, 0, w, h); // Use all of the glControl painting area
GL::Clear(ClearBufferMask::ColorBufferBit | ClearBufferMask::DepthBufferBit);
GL::ClearColor(Color::LightGray);
GL::MatrixMode(MatrixMode::Modelview);
GL::LoadIdentity();
GL::Enable(EnableCap::Texture2D);
GL::BindTexture(TextureTarget::Texture2D, img);
OpenTK::Graphics::OpenGL::ErrorCode error=GL::GetError();
GL::Begin(BeginMode::Quads);
GL::TexCoord2(0, 0);
GL::Vertex2(0 ,h);
GL::TexCoord2(1, 0);
GL::Vertex2(w, h);
GL::TexCoord2(1, 1);
GL::Vertex2(w, 0);
GL::TexCoord2(0, 1);
GL::Vertex2(0, 0);
GL::End();
GL::Disable(EnableCap::Texture2D);
if (ROI[0].x!=0||ROI[0].y!=0){
GL::Color3(Color::Red);
GL::Begin(BeginMode::Points);
GL::Vertex2(ROI[0].x,ROI[0].y);
GL::End();
}
}
What should I change in my code? I can't seem to achieve it....
I found the answer. It seems that the color also applies to textures when binding them so I just needed to add GL::Color3(Color::White) before drawing the texture.
I'm using glReadPixels to create a copy of the final frame of a state in my game (e.g. in-game, in-menu, etc...) for use in a transitional effect. I get that copy but it seems darker than the original:
here is the relevant code:
void GameStateManager::GetLastFrame()
{
if (last_frame_buffer.id != 0)
glDeleteTextures(1,&last_frame_buffer.id);
uint8* data = new uint8[Constants::SCREEN_WIDTH * Constants::SCREEN_HEIGHT*3];
glReadPixels(0,0,Constants::SCREEN_WIDTH,Constants::SCREEN_HEIGHT,GL_RGB,GL_UNSIGNED_BYTE,data);
last_frame_buffer.target = GL_TEXTURE_RECTANGLE_ARB;
glGenTextures(1,&last_frame_buffer.id);
glBindTexture(last_frame_buffer.target,last_frame_buffer.id);
glTexParameterf(last_frame_buffer.target,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameterf(last_frame_buffer.target,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexImage2D(last_frame_buffer.target,0,GL_RGB,Constants::SCREEN_WIDTH,Constants::SCREEN_HEIGHT,0,GL_RGB,GL_UNSIGNED_BYTE,data);
glBindTexture(last_frame_buffer.target,0);
delete[] data;
}
this is the actual render code:
glPushMatrix();
glTranslatef(0,last_frame_pos,0);
glCallList(last_frame_quad);
glPopMatrix();
and this is the display list:
glBegin(GL_QUADS);
glTexCoord2f(0 , 0); glVertex2f(0 , height);
glTexCoord2f(0 , height); glVertex2f(0 , 0);
glTexCoord2f(width, height); glVertex2f(width, 0);
glTexCoord2f(width, 0); glVertex2f(width, height);
glEnd();
Here are the things I've tried so far:
change buffer format : BYTE (made image darker), UNSIGNED_SHORT, UNSIGNED_INT, FLOAT
enable/disable : TEXTURE_2D, TEXTURE_RECTANGLE, BLEND
manually brighten pixels (failed miserably)
I have the following code to render text in my app, first i get the mouse coordinates in the world, then use those coordinates to place my text in the world, so it will follow my mouse position:
Edit: added buildfont() function in code example:
GLvoid BuildFont(GLvoid) // Build Our Bitmap Font
{
HFONT font; // Windows Font ID
HFONT oldfont; // Used For Good House Keeping
base = glGenLists(96); // Storage For 96 Characters
font = CreateFont( -12, // Height Of Font
0, // Width Of Font
0, // Angle Of Escapement
0, // Orientation Angle
FW_NORMAL, // Font Weight
FALSE, // Italic
FALSE, // Underline
FALSE, // Strikeout
ANSI_CHARSET, // Character Set Identifier
OUT_TT_PRECIS, // Output Precision
CLIP_DEFAULT_PRECIS, // Clipping Precision
ANTIALIASED_QUALITY, // Output Quality
FF_DONTCARE|DEFAULT_PITCH, // Family And Pitch
"Verdana"); // Font Name (if not found, its using some other font)
oldfont = (HFONT)SelectObject(hDC, font); // Selects The Font We Want
wglUseFontBitmaps(hDC, 32, 96, base); // Builds 96 Characters Starting At Character 32
SelectObject(hDC, oldfont); // Selects The Font We Want
DeleteObject(font); // Delete The Font
}
GLvoid glPrint(const char *fmt, ...){
char text[256];
va_list ap;
if (fmt == NULL) return;
va_start(ap, fmt);
vsprintf(text, fmt, ap);
va_end(ap);
glPushAttrib(GL_LIST_BIT);
glListBase(base - 32);
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);
glPopAttrib();
}
...
glPushMatrix();
glColor4f(0,0,0,1);
// X-1 wont work because these are the world coordinates:
glRasterPos2d(MousePosX-1, MousePosY);
glPrint("TEST");
glColor4f(1,1,0,1);
glRasterPos2d(MousePosX, MousePosY);
glPrint("TEST");
glPopMatrix();
But i want to render multiline texts, or texts with "borders" (like in above code i tried) or text with background (so i could distinguish them better from the background)
So how i do this?
I just need to know how i can move it in pixels, so i could precisely modify the position on my screen WITHOUT using 2d projection view on top of my 3d render projection... i just want to make it as simple as possible.
I tried to draw a quad under the text, but of course it doesnt work since its still using the world coordinates... so when i rotate my camera the text doesnt move along the background of the text... i am afraid the only solution is to create another projection on top of the 3d projection...
Here's a small snippet of code that I use to render some debug text in a small application:
void
renderText(float x, float y, const char* text) {
int viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glOrtho(viewport[0], viewport[2], viewport[1], viewport[3], -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glRasterPos2f(x, viewport[3] - y);
const int length = (int)strlen(text);
for (int i = 0; i < length; ++i) {
glutBitmapCharacter(GLUT_BITMAP_9_BY_15, text[i]);
}
glMatrixMode( GL_PROJECTION );
glPopMatrix();
glMatrixMode( GL_MODELVIEW );
glPopMatrix();
}
xand y is the desired window coordinates of the string. glutBitmapCharacter(GLUT_BITMAP_9_BY_15, ...) is just a utility function from GLUT that renders a 9 x 15 pixels large bitmap character.