c++openglsdlsdl-2sdl-ttf

Render Text using SDL2 TTF and glDrawPixels


I am currently adding support for a SDL2 windowing system to a library, that currently uses GLUT. This includes rendering text. The problematic part is that I don't have access to the Window or Renderer at the position where text rendering happens. Previously text was rendered with GLUT using the glutBitmapCharacter function.

I am very close to solving this, by using glDrawPixels, but the output looks like this: Output

It seems, that the texture is flipped and some noise is rendered above. I tried playing with the GL_UNPACK_XXX functions, but couldn't find out how to fix that. Do you have some idea?

Here is a sample application, that reproduces this issuse:

main.cpp:

#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <GL/glew.h>
#include <string>
#include <stdint.h>
#include <chrono>
#include <thread>

const int width = 400;
const int height = 400;

TTF_Font* font = nullptr;

// THIS FUNCTION IS THE ONLY THING I CAN CHANGE!
void drawText(std::string text) {
  SDL_Color color = {
    static_cast<Uint8>(255),
    static_cast<Uint8>(0),
    static_cast<Uint8>(255),
    static_cast<Uint8>(255)
  };

  SDL_Surface* textRender = TTF_RenderUTF8_Blended(font, text.c_str(), color);
  SDL_Surface* surface = SDL_ConvertSurfaceFormat(textRender, SDL_PIXELFORMAT_RGBA8888, 0);
  SDL_FreeSurface(textRender);

  glPushAttrib(GL_BLEND);
  glPushAttrib(GL_BLEND_EQUATION);
  glPushAttrib(GL_CLIENT_PIXEL_STORE_BIT);

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glPixelStorei(GL_UNPACK_SWAP_BYTES,  GL_FALSE);
  glPixelStorei(GL_UNPACK_LSB_FIRST,   GL_FALSE);
  glPixelStorei(GL_UNPACK_ROW_LENGTH,  surface->pitch);
  glPixelStorei(GL_UNPACK_SKIP_ROWS,   0);
  glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
  glPixelStorei(GL_UNPACK_ALIGNMENT,   1);

  glDrawPixels(surface->w, surface->h, GL_RGBA, GL_UNSIGNED_BYTE, surface->pixels);

  glPopAttrib();
  glPopAttrib();
  glPopAttrib();

  SDL_FreeSurface(surface);
}

// EVERYTHING BELOW HERE IS ONLY TO PROVIDE A MINIMUM REPRODUCABLE EXAMPLE!

void loop(int64_t frame) {
  glClearColor(1.0, 1.0, 1.0, 1.0);
  glClear(GL_COLOR_BUFFER_BIT);
  glRasterPos2f(-1.0, 0);
  drawText(std::string("Hello World!") + std::to_string(frame));
}

int main() {
  SDL_InitSubSystem(SDL_INIT_VIDEO);
  TTF_Init();

  auto* window = SDL_CreateWindow("sdl-text", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_OPENGL);
  auto* context = SDL_GL_CreateContext(window);
  glewInit();

  bool quit = false;
  int64_t frame = 0;

  font = TTF_OpenFont("arial.ttf", 42);
  if (!font) {
    quit = true;
  }

  while (!quit) {
    loop(frame++);

    SDL_GL_SwapWindow(window);

    SDL_Event e;
    while (SDL_PollEvent(&e)) {
      switch(e.type) {
        case SDL_QUIT:
          quit = true;
          break;
      }
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(16));
  }

  TTF_Quit();
  SDL_GL_DeleteContext(context);
  SDL_DestroyWindow(window);
  SDL_QuitSubSystem(SDL_INIT_VIDEO);

  return 0;
}

compiled with the following command:

g++ main.cpp -lSDL2 -lSDL2_ttf -lGLEW -lGL

Solution

  • Ok, it seems like OpenGL takes the row length in pixel and SDL provides it in bytes. This is easily solved.

    We also don't need to convert the surface to RGBA. We can just tell OpenGL to draw BGRA (although SDL says it provides AGBR, but it works).

    Finally we flip the image before drawing with glPixelZoom(1, -1). To offset the flip we have to move it up by the height.

    The solution is the following:

    void drawText(std::string text) {
      SDL_Color color = {
        static_cast<Uint8>(255),
        static_cast<Uint8>(0),
        static_cast<Uint8>(0),
      };
    
      // We dont't need to convert the surface format.
      SDL_Surface* surface = TTF_RenderUTF8_Blended(font, text.c_str(), color);
    
      glPushAttrib(GL_BLEND);
      glPushAttrib(GL_BLEND_EQUATION);
      glPushAttrib(GL_CLIENT_PIXEL_STORE_BIT);
    
      glEnable(GL_BLEND);
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
      // SDL uses bytes, while OpenGL uses pixels.
      glPixelStorei(GL_UNPACK_ROW_LENGTH, surface->pitch / surface->format->BytesPerPixel);
    
      // Get the current raster position.
      std::array<float, 4> pos;
      glGetFloatv(GL_CURRENT_RASTER_POSITION, pos.data());
    
      // Because the texture is upside down we move it up by the height and flip it.
      glWindowPos2f(pos[0], pos[1] + surface->h);
      glPixelZoom(1, -1);
    
      // Use BGRA as a format.
      glDrawPixels(surface->w, surface->h, GL_BGRA, GL_UNSIGNED_BYTE, surface->pixels);
    
      glPopAttrib();
      glPopAttrib();
      glPopAttrib();
    
      SDL_FreeSurface(surface);
    }