Projeto Virtual Worlds

Computação Gráfica 3D em C

Arquivos Mensais: agosto 2011

Problema de resolução (simples!)

Lembram-se deste post? Eu estava em dúvida sobre qual alternativa usar no caso da geometria do monitor seja diferente da geometra do retângulo condido no plano de projeção. Isso surgiu com a aquisição de um notebook que possui geometria wide screen (meu monitor, no desktop é 4:3, ou seja, letter box).

Deixei o assunto meio de lado e só recentemente, realizando um teste com mapeamento cúbico (outro dia posto por aqui), resolvi usar o formato letter box no monitor wide screen. O que obtive, é óbvio (depois que você pensa um pouco) foi este formato:

Letter Box sobre Widescreen

Óbvio porque a função glViewport assume que a origem do retângulo (o ponto 0,0) é o centro da tela. Quando fazemos:

glViewport(0, 0, 1024, 768);

Estamos definido um retângulo de 1024 por 768 pixels onde o lado esquerdo (left) começa na posição (width/2)+x e (height/2)+y — onde x e y são os 2 primeiros valores e ‘width’ e ‘height’ são os 2 outros.

Felizmente a biblioteca GLFW fornece uma função chamada glfwGetDesktopMode que nos fornece os tamanhos da geometria da resolução atual do vídeo. Assim eu posso determinar o aspect ratio e, se este estiver em torno de 4:3 (1.33333…), posso modificar o height para causar o efeito que eu queria inicialmente (em monitores 4:3):

Wide screen sobre Letter Box

Mantendo o aspact ratio de minha aplicação constante.

Anúncios

SDL, GLUT ou GLFW?

Para começar, de fato, a brincadeira é interessante que você aprenda como inicializar o contexto do OpenGL, bem como ajusta o framebuffer e outras coisinhas. Isso pode ser uma tarefa complicada, especialmente se quiser fazê-lo para multiplas plataformas. Para te ajudar um pouco existem algumas bibliotecas: SDL (Simple Direct Layer), GLUT (GL Utitilities Library), GLFW (GL FrameWork), etc.

SDL é uma das bibliotecas mais usadas por ai, mas possui, até a versão 1.2, alguns drawbacks, especialmente no Linux. Pela minha experiência, SDL é lenta, especialmente se você pretende usar muito cópia de blocos (texturas). O GLUT é genérico e usado, inclusive, nos exemplos da documentação da Kronos (“dona” do OpenGL), só que ajustar o contexto do OpenGL e o framebuffer é algo que o GLUT não faz muito bem (até a versão 3.7…). Esperimente ajustar o modo fullscreen numa resolução desejada sem usar funções “não-documentadas” dp GLUT!

A melhor biblioteca minimalista que pude achar, até agora, é o GLFW 2.7 (O 2.6 é muito bem e disponível nos repositórios do Ubuntu, mas não implementa FSAA — Full Scene AntiAliasing — muito bem). A outra vantagem do GLFW sobre o SDL e o GLUT é que ele um projeto open source atualizado com mais frequẽncia que os demais. Dêem uma olhada na rotina de inicialização de meus projetos:

#include <stdlib.h>
#include <stdio.h>
#include <GL/gl.h>
#include <GL/glfw.h>

void quit(void);

int main(void)
{
  if (glfwInit() != GL_TRUE)
  { fprintf(stderr, "ERROR initializing GLFW.\n"); return 1; }

  atexit(quit);

  /* Vou usar 4 samples para antialiasing! */
  glfwOpenWindowHint( GLFW_FSAA_SAMPLES, 4);

  if (glfwOpenWindow( 1024, 768,   /* Width, Height */
                      8, 8, 8, 8,  /* RGBA bit sizes */
                      24,          /* Depth buffer bit size */
                      0,           /* Stencil buffer size */
                      GLFW_FULLSCREEN ) != GL_TRUE)
  { fprintf(stderr, "ERROR creating framebuffer\n"); return 1; }

  oglInit();

  loop();

  return 0;
}

void quit(void) { glfwTerminate(); }

Eu acho o GLFW mais simples que o SDL porque ele não tem funções para BitBliting, por exemplo. Se vocẽ está usando OpenGL, então use glDrawPixels ou glCopyPixels, pôxa!

PNG Loader, de novo…

No agora extinto Lost in the e-Jungle eu havia postado a rotina para carga de arquivos PNG via libpng. Eu tinha um problema que não citei por lá: A função glTexImage2D precisa receber o formato específico da imagem carregada na memória. No meu caso, para facilitar a rotina, assumi que o formato seria GL_RGBA e fui em frente. Acontece que algumas imagens PNG têm formato diferente… Às vezes elas têm formato RGB, descartando o canal alpha. Noutras, são formatadas em grey scale com menos de 8 bits e, ainda, em outras vezes elas contém uma tabela de cores (palette), igualzinho ao que acontece quando vocẽ lida com arquivos GIF. Como lidar com todos esses formatos?

Felizmente a libpng nos ajuda. Abaixo, temos a rotina que carrega um arquivo PNG, mas que fornece um pixelmap sempre no formato RGBA:

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <malloc.h>
#include <png.h>
#include <setjmp.h>

#define PNG_SIG_BYTES 8

/* A rotina devolve um ponteiro para o pixelmap
   e também os tamanhos (width e height) da imagem.
   Retornará NULL se houve algum problema. */
uint8_t *load_png(char *name, int *width, int *height)
{
  FILE *png_file;
  uint8_t header[PNG_SIG_BYTES];
  png_structp png_ptr;
  png_infop info_ptr, end_info;
  uint32_t bit_depth, color_type;
  uint32_t rowbytes;
  uint32_t numbytes;
  uint8_t *pixels = NULL;
  uint8_t **row_ptrs = NULL;
  int i;

  if ((png_file = fopen(name, "rb")) == NULL)
    return NULL;

  if (fread(header, PNG_SIG_BYTES, 1, png_file) != 1)
  {
    fclose(png_file);
    return NULL;
  }

  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
                                   NULL, NULL, NULL);
  if (png_ptr == NULL)
  {
    fclose(png_file);
    return NULL;
  }

  info_ptr = png_create_info_struct(png_ptr);
  end_info = png_create_info_struct(png_ptr);

  if (setjmp(png_jmpbuf(png_ptr)))
  {
    if (pixels != NULL) free(pixels);
    if (row_ptrs != NULL) free(row_ptrs);

    png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
    fclose(png_file);

    return NULL;
  }

  png_init_io(png_ptr, png_file);
  png_set_sig_bytes(png_ptr, PNG_SIG_BYTES);
  png_read_info(png_ptr, info_ptr);

  *width = png_get_image_width(png_ptr, info_ptr);
  *height = png_get_image_height(png_ptr, info_ptr);

  /* Pega info sobre o pixelmap. */
  bit_depth = png_get_bit_depth(png_ptr, info_ptr);
  color_type = png_get_color_type(png_ptr, info_ptr);

  /* Se for grayscale e bits por componente < 8,
     converte para 8 bits por componente. */
  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
    png_set_gray_1_2_4_to_8(png_ptr);

  /* Se for 16 bits por componente, converte para 8. */
  if (bit_depth == 16)
      png_set_strip_16(png_ptr);

  /* Se tiver paleta, converte para RGB */
  if (color_type == PNG_COLOR_TYPE_PALETTE)
    png_set_palette_to_rgb(png_ptr);
  else
  {
    /* Senão, se for grayscale, coloca ALPHA */
    if (color_type == PNG_COLOR_TYPE_GRAY ||
        color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
      png_set_gray_to_rgb(png_ptr);
  }

  /* Tem transparência? */
  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
    /* Converte tRNS para ALPHA */
    png_set_tRNS_to_alpha(png_ptr);
  else
    /* Senão, coloca ALPHA = 1.0 */
    png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER);

  /* Atualiza a imagem lida. */
  png_read_update_info(png_ptr, info_ptr);

  /* Aloca espaço para a imagem e
     os ponteiros para as linhas. */
  rowbytes = png_get_rowbytes(png_ptr, info_ptr);
  numbytes = rowbytes*(*height);
  pixels   = malloc(numbytes);

  /* Esse longjmp() é um HACK, usando o jmpbuf
     da estrutura png_struct. */
  if (pixels == NULL)
    longjmp(png_ptr->jmpbuf, 1);

  row_ptrs = malloc((*height) * sizeof(uint8_t *));

  if (row_ptrs == NULL)
    longjmp(png_ptr->jmpbuf, 1);

  /* Prepara os ponteiros para as linhas
     de forma inversa em relação ao eixo y.
     OpenGL precisa disso! */
  for (i=0; i < *height; i++)
    row_ptrs[i] = pixels + ((*height) - 1 - i)*rowbytes;

  /* Lê as linhas */
  png_read_image(png_ptr, row_ptrs);

  /* Livra-se do vetor de ponteiros para as linhas. */
  free(row_ptrs);

  /* Destrói as estruturas da libpng e
     fecha o arquivo. */
  png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
  fclose(png_file);

  /* Retorna o pixelmap */
  return pixels;
}

Dessa forma vocẽ sempre terá um pixelmap RGBA, no final das contas.

É bom lembrar também que a função glTexImage2D espera pixelmaps com tamanho horizontal e vertical proporcionais a 2n nas versões do OpenGL inferiores a 2.0, incluindo a especificação OpenGL ES. Versões mais recentes suportam quaisquer tamanhos (até um certo limite, é claro), mas usar tamanhos diferentes de 2n tende a ser menos performático.

Samba do vetor doido…

Eu bem que avisei pra vocês que os posts aqui iam ser mais espaçados. Eis mais uma dica que ainda não testei.

Como rotacionar um ponto em torno de um eixo imaginário, indicado por um vetor unitário…

A técnica é bem simples: Rotacionamos o sistema de coordendas em torno do eixo x e y de acordo com a diretção do vetor unitário, depois rotacionamos o ponto em torno do eixo z, de acordo com o ângulo desejado, e desfazemos as rotações em torno de x e y. Em resumo: alinhamos o eixo do sistema de coordenadas com o vetor unitário, rotacionamos e voltamos ao alinhamento original.

Isso é feito multiplicando 5 matrizes de rotação (duas para a rotação dos eixos x e y; uma para rotação do eixo z e duas para desfazer a rotação em torno de x e y). Confiem em mim, a matriz final fica muito grande e nem é tão útil assim.

Para entender a listagem abaixo, dê uma olhada aqui:

void vector3RotateAroundNormal(const float v[3],
                               const float normal[3],
                               float angle,
                               float out[3])
{
  float d, cd, bd, n0bd, n0cd;
  float x, y, z, xt, yt, zt;
  float ca, sa; /* cos(angle) and sin(angle) */

  /* Tamanho do vetor normal projetado no plano xy */
  d = sqrtf((normal[1] * normal[1]) + (normal[2] * normal[2]));

  /* If normal isn't already on z axis, rotate x and y axis... */
  if (d != 0.0f)
  {
    cd = normal[2] / d;
    bd = normal[1] / d;
    n0bd = normal[0] * bd;
    n0cd = normal[0] * cd;

    xt = (d * v[0])          - (n0bd * v[1])      + (n0cd * v[2]);
    yt =                       (cd * v[1])        + (bd * v[2]);
    z  = (-normal[0] * v[0]) - (normal[1] * v[1]) + (normal[2] * v[2]);
  }
  else
  {
    xt = v[0];
    yt = v[1];
    z  = v[2];
  }

  /* Rotate around z axis */
  ca = cos(angle);
  sa = sin(angle);

  x =  xt*ca + yt*sa;
  y = -xt*sa + yt*ca;

  /* Rotate x and y axis back in place, if necessary */
  if (d != 0.0f)
  {
    zt = ( n0cd * x) + (bd * y) + (normal[2] * z);
    y  = (-n0bd * x) + (cd * y) - (normal[1] * z);
    x  = ( d*x)                 - (normal[0] * z);
    z  = zt;
  }

  out[0] = x;
  out[1] = y;
  out[2] = z;
}

As multiplcações das matrizes foram obtidas através do WolframAlpha (Eu até tentei fazê-las manualmente, mas o processo é um tanto quanto chato!). Eis a rotação em torno de x e y, com base no vetor unitário (a,b,c):

\displaystyle \begin{pmatrix}  1&0&0&0\\  0&\frac{c}{d}&-\frac{b}{d}&0\\  0&\frac{b}{d}&\frac{c}{d}&0\\  0&0&0&1  \end{pmatrix}\cdot\begin{pmatrix}  d&0&-a&0\\  0&1&0&0\\  a&0&d&0\\  0&0&0&1  \end{pmatrix}

Acima temos a rotação no eixo X e no eixo Y, com base nas coordendas do vetor normal. Multiplicando, obtemos:

\displaystyle \begin{pmatrix}  d&0&-a&0\\  -\frac{ab}{d}&\frac{c}{d}&-b&0\\  \frac{ac}{d}&\frac{b}{d}&c&0\\  0&0&0&1  \end{pmatrix}

Note que ‘d’ é o taamnho do vetor unitário projetado no plano xy:

d = sqrtf(normal[1]*normal[1] + normal[2]*normal[2])

Essa matriz é a primeira parte da rotina acima e deve ser calculada apenas se o tamanho do vetor unitário projetado for diferente de zero. Este é o caso do vetor unitário já estar alinhado com o eixo z… neste caso as rotações do sistema de coordenadas não é necessário. Se a o taamnho da projeção for zero então d será zero e teríamos “divisões-por-zero” que, é claro, é um erro.

Dai, basta executar a rotação em torno do eixo z (com a tradicional matriz usando senos e cossenos) e desfazer as rotações previamente feitas usando a matriz transposta da anterior (de novo, somente se d for diferente de zero!). Podemos usar a matriz transposta como sua inversa porque a matriz acima é ortogonal…

O que a rotina faz é simplesmente:

\displaystyle \vec{v}' = {R'}_{xy}\cdot R_z\cdot R_{xy}\cdot\vec{v}

Onde as rotações R_{xy} e sua inversa são feitas de acordo com a posição do vetor normal. A rotação R_z é o ângulo em torno do vetor normal.

É importante que a rotina receba, no parâmetro normal, apenas vetores normalizados, ou seja, unitários. Não coloquei a normalização do vetor na rotina para poupar tempo de processamento. Antes de calcular d poderíamos fazer:

VectorNormalize(normal);

Mas deixo isso para a função chamadora…

OBS: As matrizes acima são orientadas por linha (porque acho mais confortável pensar em matrizes assim), só que o OpenGL lida com matrizes orientadas por colunas. Assim, a matriz “inversa” (a última) é a de rotação e a inversa real é a anterior.