13 января 2012

QuesoGLC

Привет. Я хочу рассказать о такой библиотеке как QuesoGLC. Это свободная реализация OpenGL Character Renderer (GLC). Библиотека основана на Free Type и поддерживает unicode. Если совсем коротко, то это то что очень облегчает жизнь при выводе текста в OpenGL.

Вывод текста в OpenGL всегда было самым тормозящим для меня событием когда я начинал что-то делать с ним. Обычно все начиналось каждый раз с чистого листа. Почему? Потому что писать велосипеды очень полезно в образовательных целях. Хотя конечно велосипеды это зло. Вот потому я и рад, что есть QuesoGLC.

Для начала давайте определимся чего мы хотим. Лично я хочу вывести симпатичный текст который будет показывать количество fps и что-нибудь еще. Выводить текст можно разными способами, например в виде растрового изображения или в виде текстуры, можно даже построить его из набора полигонов и вывести как трехмерный объект. Я хочу получить красивый плоский текст, и для этого лучше всего подходит текстуры. Почему? В отличие от растрового он сглажен, а от трехмерного отличается меньшей сложностью при визуализации.

Для начало можно познакомиться со спецификацией. Можно конечно и не знакомиться, но поглядывать в нее вполне можно и иногда даже нужно.

GLC как и OpenGL представляет из себя конечный автомат, а следовательно требует наличия контекста который будет хранить текущее состояние.

GLint ctx = glcGenContext();
glcContext(ctx);

Как было сказано выше, GLC предполагает наличие возможности вывести текст не только как текстуру:

  • GLC_BITMAP - ввиде растра
  • GLC_LINE - ввиде линий
  • GLC_TEXTURE - ввиде текстуры
  • GLC_TRIANGLE - ввиде набора треугольников

Для вывода текста в виде текстуры нужно указать GLC_TEXTURE в качестве параметра соответствующей функции:

glcRenderStyle(GLC_TEXTURE);

Что бы получить возможность вывода юникода, нужно сказать GLC явно что мы будем использовать именно его (а именно utf-8).

glcStringType(GLC_UTF8_QSO);

Это все настройки контекста что могут нам пока понадобиться. Осталось загрузить какой нибудь шрифт. В GLC это делается легко, так как он знает где у нас лежат системные шрифты и мы можем не беспокоиться о том что нам нужно будет указать к ним путь.

myFont = glcGenFontID();
glcNewFontFromFamily(myFont, "Liberation Sans");
glcFontFace(myFont, "Regular");
glcFont(myFont);

Вот так легко мы можем использовать Liberation Sans в своей программе.

Еще несколько не маловажных действий для нормальной работы GLC нужно сделать уже с OpenGL. Нам необходимо включить текстуры, иначе мы увидим только набор прямоугольников вместо текста, и на момент вывода текста необходимо отключать проверку на глубину (так как это всего лишь пример, я отключил тест на глубину сразу).

glDisable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);

Если оставить буфер глубины включенным, то мы получим артефакты при выводе текста из-за того что полигоны на которые наложены текстуры с буквами перекрывают друг друга.

Текстом можно манипулировать обычными командами OpenGL: glTranslate[fd], glRotate[fd], glScale[fd]. После вращений, перемещений и масштабирования строку можно вывести используя уже сам GLC: glcRenderString("Строка");.

По мимо вывода самого текста GLC позволяет узнать немного о его метриках (bounding box, baseline):

  1. GLC_BOUNDS для bounding box
  2. GLC_BASELINE для baseline

Первое представляет из себя массив из восьми точек описывающий прямоугольник в который вписан текст. Второе это базовая линия текста (вспомните тетрадки в линейку).

Узнать о метриках строки можно с помощью вызова двух функций GLC:

GLfloat bline[4] = {0.f, 0.f, 0.f, 0.f};
GLfloat bbox[8] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f};
glcMeasureString(GL_FALSE, "строка для которой хочу знать метрики");
glcGetStringMetric(GLC_BASELINE, bline);
glcGetStringMetric(GLC_BOUNDS, bbox);

Для символа все проще:

GLfloat bline[4] = {0.f, 0.f, 0.f, 0.f};
GLfloat bbox[8] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f};
glcGetCharMetric('c', GLC_BASELINE, bline);
glcGetCharMetric('c', GLC_BOUNDS, bbox);

Мы еще вернемся к теме вывода текста когда будем говорить о построении интерфейсов и будем разбирать как выводить сложный много строчный текст.

Полный листинг примера:

#if 0
FILE=$(basename $0)
DIR=$(dirname $0)
FLAGS=$(pkg-config --libs --cflags quesoglc gl glu sdl)
gcc -o $DIR/demo $0 $FLAGS --std=c99 -O2 -lm
#clang -Wall -Wextra -g -O0 -o $DIR/demo $0 $FLAGS --std=c99 -lm
exit;
#endif

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>

#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glc.h>
#include <SDL/SDL.h>

#define WIDTH  640
#define HEIGHT 480
#define BPP    32

#define PRINT_STRING "Привет, world! :)"

void
all_init();

void
loop();

void
quit();

int
main(int argc, char **argv)
{
  (void)argc;
  (void)argv;

  all_init();
  loop();
  quit();

  return EXIT_SUCCESS;
}

void
scene_view()
{
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(40., (float)WIDTH/(float)HEIGHT, 1.0, 300.0);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}

void
gui_view()
{
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluOrtho2D(0.0f, WIDTH, 0.0f, HEIGHT);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}

void
all_init()
{
  GLint ctx = 0;
  GLint myFont = 0;

  /* SDL { */
  SDL_Init(SDL_INIT_VIDEO);
  SDL_SetVideoMode(WIDTH, HEIGHT, BPP, SDL_OPENGL);
  SDL_WM_SetCaption("QuesoGLC Texture Text", NULL);
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, true);
  /* SDL } */

  /* OpenGL { */
  glDisable(GL_DEPTH_TEST);
  glEnable(GL_TEXTURE_2D);

  glClearColor(0., 0., 0., 0.);
  glViewport(0, 0, WIDTH, HEIGHT);
  /* OpenGL } */

  /* QuesoGLC { */
  ctx = glcGenContext();
  glcContext(ctx);

  glcRenderStyle(GLC_TEXTURE);
  glcStringType(GLC_UTF8_QSO);

  myFont = glcGenFontID();
  glcNewFontFromFamily(myFont, "Liberation Sans");
  glcFontFace(myFont, "Regular");
  glcFont(myFont);
  /* QuesoGLC } */
}

void
loop()
{
  SDL_Event    event;
  bool         done      = false;
  unsigned int dt        = 0;
  unsigned int old       = 0;
           int fps_timer = 1000;
  unsigned int fps       = 1000;
  char buffer[50] = "FPS: ?";
  GLfloat bbox[8] = {0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f};
  int i = 0;
  float rotate = 0.0;

  while (!done)
    {
      dt = SDL_GetTicks() - old;
      old += dt;
      fps_timer -= dt;

      while (SDL_PollEvent(&event))
        {
          if (event.type == SDL_QUIT
              || (event.type == SDL_KEYDOWN
                  && event.key.keysym.sym == SDLK_ESCAPE))
            done = true;
        }

      /* draw { */
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

      scene_view();
      glLoadIdentity();
      glTranslatef(0, 0, -20);
      glRotatef(25,1,0,0);
      rotate += 0.5f;
      glRotatef(rotate,0,1,0);

      glPushMatrix();
      glcMeasureString(GL_FALSE, PRINT_STRING);
      glcGetStringMetric(GLC_BASELINE, bbox);

      glLineWidth(1);

      glBegin(GL_LINES);
        {
          glColor3f(1.0, 1.0, 0.0);
          glVertex3f(bbox[0], bbox[1], 0.0);
          glVertex3f(bbox[2], bbox[3], 0.0);
        }
      glEnd();

      glcMeasureString(GL_FALSE, PRINT_STRING);
      glcGetStringMetric(GLC_BOUNDS, bbox);
      glColor3f(0.0, 1.0, 1.0);
      glBegin(GL_LINE_LOOP);
        {
          for (i = 0; i < 4; i++)
            glVertex2fv(&bbox[2*i]);
        }
      glEnd();

      glColor3f(1.0f, 1.0f, 1.0f);
      glcRenderString(PRINT_STRING);
      glPopMatrix();

      glBegin(GL_LINES);
        {
          glColor3f(1.0, 0.0, 0.0);
          glVertex3f(0.0, 0.0, 0.0);
          glVertex3f(5.0, 0.0, 0.0);

          glColor3f(0.0, 1.0, 0.0);
          glVertex3f(0.0, 0.0, 0.0);
          glVertex3f(0.0, 5.0, 0.0);

          glColor3f(0.0, 0.0, 1.0);
          glVertex3f(0.0, 0.0, 0.0);
          glVertex3f(0.0, 0.0, 5.0);
        }
      glEnd();

      glColor3f(1.0, 0.0, 0.0);
      glPushMatrix();
      glcGetCharMetric('x', GLC_BOUNDS, bbox);
      glTranslatef(5.0f - fabs(bbox[4] - bbox[6]), -bbox[5] - 0.1f, 0.0f);
      glcRenderChar('x');
      glPopMatrix();

      glColor3f(0.0, 1.0, 0.0);
      glPushMatrix();
      glcGetCharMetric('y', GLC_BOUNDS, bbox);
      glTranslatef(0.1f, -bbox[5] + 5.0, 0.0f);
      glcRenderChar('y');
      glPopMatrix();

      glColor3f(0.0, 0.0, 1.0);
      glPushMatrix();
      glcGetCharMetric('z', GLC_BOUNDS, bbox);
      glTranslatef(0.0f, -bbox[5] - 0.1f, 5.0f);
      glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      glcRenderChar('z');
      glPopMatrix();

      /* fps draw { */
      gui_view();
      glTranslatef(5.0f, 5.0f, 0);
      glScalef(20.0f, 20.0f, 1.0f);
      glColor3f(1.0f, 1.0f, 1.0f);
      glcRenderString(buffer);
      /* fps draw } */

      glFlush();
      /* draw } */

      SDL_GL_SwapBuffers();

      /* fps calculation { */
      dt = SDL_GetTicks() - old;
      old += dt;
      fps_timer -= dt;

      if (fps_timer < 0)
        {
          sprintf(buffer, "FPS: %d", fps);
          fps = 0;
          fps_timer = 1000;
        }
      /* fps calculation } */

      SDL_Delay(16);

      fps++;
    }
}

void
quit()
{
  SDL_Quit();
}