05 февраля 2012

OpenGL и SVG

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

К тому же пользовательские интерфейсы, как правило при увеличении разрешения либо уменьшались, либо становились более размытыми. Проблема тут в том что растровое изображение нельзя маштабировать без потерь.

Так о чем я? Я о том что использовать SVG для текстур, особенно когда предполагается не фото реалистичный рендеринг, идея очень классная.

Что бы загрузить SVG текстуру в OpenGL нам понадобиться:

Вот и все. Давайте сначала разберем загрузку SVG и превращение его в текстуру OpenGL.

  handle = rsvg_handle_new_from_file("gztest.svg.gz", &error);

Код выше открывает SVG файл, даже если он обработан gzip (gzip gztest.svg), что очень даже круто, учитывая насколько меньше тот становиться меньше.

  rsvg_handle_get_dimensions(handle, &dimension_data);

  int width = dimension_data.width * size;
  int height = dimension_data.height * size;

Выше мы узнаем размер SVG файла.

  cairo_surface_t *cairo_surface =
    cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);

  cairo_t *cr = cairo_create(cairo_surface);

Создается холст (cairo_surface) и кисть (cr).

  cairo_scale(cr, size, size);
  cairo_save(cr);

Изменяем размер SVG в соответствии с размером текстуры.

  rsvg_handle_render_cairo(handle, cr);

Рисуем файл SVG на холсте с помощью кисти и мы готовы создать текстуру.

  unsigned char* cairo_data = cairo_image_surface_get_data(cairo_surface);
  glGenTextures(1,&texture);
  glBindTexture(GL_TEXTURE_2D, texture);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D,
               0,
               GL_RGBA,
               width, height,
               0,
               GL_BGRA,
               GL_UNSIGNED_INT_8_8_8_8_REV,
               cairo_data);

Именно так мы создаем текстуру. Что бы CAIRO_FORMAT_ARGB32 был понятен OpenGL, мы используем GL_BGRA, а в качестве типа данных для cairo_data используем GL_UNSIGNED_INT_8_8_8_8_REV.

  cairo_surface_destroy (cairo_surface);
  cairo_destroy(cr);
  g_object_unref(handle);

Освобождаем ресурсы выделенные под cairo и librsvg.

А вот и весь пример:

#if 0
FILE=$(basename $0)
DIR=$(dirname $0)
FLAGS=$(pkg-config --libs --cflags quesoglc gl glu sdl cairo librsvg-2.0 glib-2.0)
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>

#include <librsvg/rsvg.h>
#include <librsvg/rsvg-cairo.h>
#include <cairo.h>
#include <glib-object.h>

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

void
all_init();

GLuint
load_texture(double size);

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("SVG", 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);
  glcDisable(GLC_GL_OBJECTS);
  glcEnable(GLC_HINTING_QSO);

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

GLuint
load_texture(double size)
{
  RsvgHandle *handle;
  RsvgDimensionData dimension_data;
  GError *error = NULL;
  GLuint texture;

  g_type_init();

  handle = rsvg_handle_new_from_file("gztest.svg.gz", &error);
  if(error != NULL)
    {
      printf("%s\n", error->message);
      abort();
    }

  rsvg_handle_get_dimensions(handle, &dimension_data);

  int width = dimension_data.width * size;
  int height = dimension_data.height * size;

  cairo_surface_t *cairo_surface =
    cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);

  cairo_t *cr = cairo_create(cairo_surface);
  cairo_scale(cr, size, size);
  cairo_save(cr);

  rsvg_handle_render_cairo(handle, cr);

  unsigned char* cairo_data = cairo_image_surface_get_data(cairo_surface);
  glGenTextures(1,&texture);
  glBindTexture(GL_TEXTURE_2D, texture);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D,
               0,
               GL_RGBA,
               width, height,
               0,
               GL_BGRA,
               GL_UNSIGNED_INT_8_8_8_8_REV,
               cairo_data);

  cairo_surface_destroy (cairo_surface);
  cairo_destroy(cr);
  g_object_unref(handle);

  return texture;
}

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: ?";
  GLuint texture_one;
  GLuint texture_two;

  texture_one = load_texture(1.0);
  texture_two = load_texture(64.0 / 512.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(2, -2, -20);
      glRotatef(25,1,0,0);

      glPushMatrix();
      glColor3f(1.0f, 1.0f, 1.0f);
      glTranslatef(-8.2, 5, 0);
      glcRenderString("512x512");
      glPopMatrix();

      glPushMatrix();
      glColor3f(1.0f, 1.0f, 1.0f);
      glTranslatef(-8.3, 0, 0);
      glScalef(4.0, 4.0, 0.0);
      glBindTexture(GL_TEXTURE_2D, texture_one);
      glBegin(GL_QUADS);
        {
          glColor3f(1.0, 1.0, 1.0);
          glTexCoord2f(1, -1); glVertex3f(1.0, 1.0, 0.0);
          glTexCoord2f(1,  0); glVertex3f(1.0, 0.0, 0.0);
          glTexCoord2f(0,  0); glVertex3f(0.0, 0.0, 0.0);
          glTexCoord2f(0, -1); glVertex3f(0.0, 1.0, 0.0);
        }
      glEnd();
      glPopMatrix();

      glPushMatrix();
      glColor3f(1.0f, 1.0f, 1.0f);
      glTranslatef(-0.2, 5, 0);
      glcRenderString("64x64");
      glPopMatrix();

      glPushMatrix();
      glColor3f(1.0f, 1.0f, 1.0f);
      glTranslatef(-0.8, 0, 0);
      glScalef(4.0, 4.0, 0.0);
      glBindTexture(GL_TEXTURE_2D, texture_two);
      glBegin(GL_QUADS);
        {
          glColor3f(1.0, 1.0, 1.0);
          glTexCoord2f(1, -1); glVertex3f(1.0, 1.0, 0.0);
          glTexCoord2f(1,  0); glVertex3f(1.0, 0.0, 0.0);
          glTexCoord2f(0,  0); glVertex3f(0.0, 0.0, 0.0);
          glTexCoord2f(0, -1); glVertex3f(0.0, 1.0, 0.0);
        }
      glEnd();
      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();
}

Тут могут быть ошибки, особенно это касается работы с librsvg и cairo, не совсем уверен что правильно делаю освобождение выделенной памяти. Я конечно долго смотрел в документацию, но вполне мог что-то упустить.


Полезное:

  • Документация к librsvg
  • Документация к Cairo
  • Немного о GObject
  • Тестовый SVG файл (md5: 5982572c5fe62b9c8a0d27c15944f263)