Projeto Virtual Worlds

Computação Gráfica 3D em C

Arquivos Mensais: setembro 2011

Escolhendo o frustum perfeito…

Temos que tomar cuidado com o ajuste do frustum porque quanto menor for a relação zNear/zFar, maior é o erro dos valores armazenados no buffer de profunciodade (depth buffer). Para ilustrar eis um gráfico mostrando o erro da coordenada Z para um zFar fixo em 1000.0, usando um depth bufferde 24 bits:

Quanto mais próximo do "olho", mais imprecisos ficam os pontos mais "longe".

Eu costumava usar zNear ajustado em 0.1 e o zFar em 1000. Gosto de pensar nas coordenadas do OpenGL como se fossem metros. Assim, se o pleno zNear estiver à 10 centímetros da câmera eu evito o problema de clipping de objetos muito próximos. Acontece que isso me dá um erro de quase 50% para objetos localizados no meio da faixa (uns 500 metros — note a linha verde, no gráfico). No entanto, ao colocar o plano zNear entre 0.25 e 1 “metro” de distância do observador, o erro cai drásticamente.

Daqui pra frente vou usar a distância do plano zNear unitária (1 metro). Pensando bem: meus olhos costumam estar afastados, pelo menos, 0.5 metros de qualquer objeto que você esteja vendo, então não faz mal recortar objetos mais próximos que isso. A distância de 0.5 “metros” me dá um erro máximo de cerca de 10%, mas a de 1 metro me dá cerca de 5% apenas.

Claro, que esse gráfico ai em cima foi gerado com o plano zFar relativamente próximo (1 km). Vejamos o que dá se aumentarmos a diferença para 10 km:

10 km?! Quem é que "enxerga" detalhes à 10 km de distância?!

Como a relação zNear/zFar diminuiu muito (zFar aumentou 10 vezes) é natural que o erro aumente. Minha escolha, nesse caso, de usar o plano zNear distanciado de apenas 1 “metro” do observador causa um erro tão grande no depth buffer, para objetos relativemente próximos em relação à faixa total, que torna impráticável usar um volume enorme para o frustum. Por isso, minha decisão, até o momento, é ficar com o plano zFar distanciado em “apenas” 1 “quilometro” e usar o recurso de fogging para “esconder”, gradualmente, qualquer coisa que se aproxime, digamos, a uns 30 metros deste limite, ou o ultrapasse.

Ficou curioso em como calculei o erro? E como plotei os gráficos? Veja aqui e use este script para o GNU Plot:

$ cat zplot.p
#
# script gnuplot para plotar erro de profundidade.
#
set title "Erro de profundidade (depth buffer error)"
set key outside title "zNear" box 
set xlabel "Z"
set ylabel "erro Z"

# Mude zFar para ver o erro para outros volumes
# do frustum.
zFar = 1000.0

# Tamanho do depth buffer.
bits = 24

set xrange [0:zFar]
set yrange [0:1]     # erros de 0 a 100%

# Erro(x) = z^2 / (zNear * (depth_buffer_bits - 1)).
#
f(z,n) = (z*z) / (n * (2**bits - 1))

plot f(x, 0.01) title "0.01", \
     f(x, 0.1)  title "0.1", \
     f(x, 0.25) title "0.25", \
     f(x, 0.5)  title "0.5", \
     f(x, 1)    title "1", \
     f(x, 10)   title "10"

$ gnuplot -p zplot.p

Na frente da onda

Eu reluto em usar orientação à objetos em meus códigos. Os motivos? São dois: Performance e tamanho do código. Se você usar os fundamentos da OO (herança, polimorfismo e encapsulamento) terá um monstrengo de código binário nas mãos. Em meus testes, usar C++ ao invés de C aumenta o código em, pelo menos, 30% do tamanho. Além disso, um conjunto de estruturas e rotinas são adicionadas ao código final que você não tem o mínimo controle… Ou seja, se o compilador resolver traduzir seu código da maneira que ele bem entender, estará adicionando bugs irreparáveis (ou muito difíceis de debugar).

Dito isso, como vocês podem ler neste artigo que escrevi para o Bit Is Myth, me rendi aos containers da STL. Uma demonstração da força e facilidade de uso é a leitura de modelos, no formato da Wavefront Technologies (criadora do Maya):

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

struct vec3 { float x, y, z; };
struct indexes { int v, n; };

GLuint readObj(const char *filename)
{
#define MAX_LINE_SIZE 256

  static char line[MAX_LINE_SIZE+1];
  static char auxBuffer[64];
  FILE *f;
  GLuint displayList;
  vec3 v;
  indexes i, j, k;
  std::vector<vec3> vertices;
  std::vector<vec3> normals;
  std::vector<indexes> idxs;
  std::vector<indexes>::iterator nIdx;

  if ((f = fopen(filename, "rt")) == NULL)
  {
    fprintf(stderr, "Unable to open file.\n");
    exit(EXIT_FAILURE);
  }

  /* Aqui eu leio a linha completa para depois
     separar, usando sscanf, os tokens e
     seus dados. */
  while (fgets(line, MAX_LINE_SIZE, f) != NULL)
  {
    /* NOTA: Não precisamos nos preocupar com
             o '\n' no final da string! */

    /* Pega o token. */
    if (sscanf(line, "%s", auxBuffer) == 1)
    {
      if (strcmp(auxBuffer, "v") == 0) /* vertices*/
      {
        sscanf(line, "v %f %f %f", &v.x, &v.y, &v.z);
        vertices.push_back(v);
      }
      else
      if (strcmp(auxBuffer, "vn") == 0) /* normais */
      {
        sscanf(line, "vn %f %f %f", &v.x, &v.y, &v.z);
        normals.push_back(v);
      }
      else
      if (strcmp(auxBuffer, "f") == 0) /* faces (índices) */
      {
        /* NOTA: Não estou usando coordenadas de texturas. */
        /* NOTA: Todas as faces precisam ser triangulares. */
        sscanf(line, "f %d//%d %d//%d %d//%d",
          &i.v, &i.n, &j.v, &j.n, &k.v, &k.n);

        /* Armazena os 3 índices */
        idxs.push_back(i);
        idxs.push_back(j);
        idxs.push_back(k);
      }
    }
  }

  fclose(f);

  /* Usando display lists pq o teste foi feito com
     OpenGL 1.4 (vertex buffers existem no 1.5+). */
  displayList = glGenLists(1);

  /* 0 significa erro! */
  if (displayList != 0)
  {
    glNewList(displayList, GL_COMPILE);
      glBegin( GL_TRIANGLES );

      /* Usa o iterador para o vector para obter os índices */
      for (nIdx = idxs.begin(); nIdx != idxs.end(); ++nIdx)
      {
        /* NOTE: indexes begin @ 1, instead of 0 */
        v = normals[nIdx->n - 1];
        glNormal3fv( (float *)&v );

        v = vertices[nIdx->v - 1];
        glVertex3fv( (float *)&v );
      }

      glEnd();
    glEndList();
  }

  return displayList;

#undef MAX_LINE_SIZE
}

Vale observar que não estou usando Vertex Buffer Objects porque este teste foi criado numa máquina que não implementa OpenGL 1.5 ou superior (Um LeNovo, com chipset Intel obsoleto, que só implementa OpenGL 1.4). Então, fui obrigado a usar display lists.

Mas, o que quero demonstrar é o uso do container sequencial vector e na leitura de um modelo qualquer gerado no Blender ou em qualquer software de modelamento 3D que preste.

Como os containers vector são declarados como locais à função, os arrays são alocados no heap e dealocados logo antes de saírem do escopo da função. É como se tivéssemos um garbage collector, sem a inconveniência de deixar o espaço alocado “no limbo” por algum tempo. a classe, por sí só, ocupa pouco mais de 8 bytes na pilha (experimente pegar o tamanho do objeto com sizeof)… O container não faz múltiplas realocações à medida que adicionamos itens e podemos pegar o ponteiro para o primeiro item com a garantia de que os próximos itens seguem o primeiro (por isso é possível usar as funções glNormal3fv e glVertex3fv).

Quanto ao formato do arquivo de modelos da Wavefront, nada mais simples: Todos os vértices do modelo são definidos em linhas separadas iniciadas com o token ‘v’, seguido das 3 coordenadas (x,y,z). Os vetores normais seguem o mesmo princípio, só que o token é ‘vn’. Com base nessas duas listas usamos a lista de índices que compõem os triângulos do objeto (token ‘f’, seguindo de 3 índices: vertice/textura/normal – Note que não gerei meus arquivos .OBJ com  coordenadas de texturas, portanto o componente central é vazio – se houvessem coordenadas de texturas, então uma lista com o token ‘vt’ estaria presente, bem como tokens ‘usemtl’, dentro da lista de faces).

Um cubo, por exemplo, pode ser definido, num arquivo .OBJ, assim:

# Lista de vértices
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
v 1.000000 1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000

# Lista de normais
vn 0.000000 0.000000 -1.000000
vn -1.000000 -0.000000 -0.000000
vn -0.000000 -0.000000 1.000000
vn 1.000000 -0.000000 0.000000
vn 1.000000 0.000000 0.000001
vn -0.000000 1.000000 0.000000
vn 0.000000 -1.000000 0.000000

# Lista de faces
f 5//1 1//1 8//1
f 1//1 4//1 8//1
f 3//2 7//2 8//2
f 3//2 8//2 4//2
f 2//3 6//3 3//3
f 6//3 7//3 3//3
f 1//4 5//4 2//4
f 5//5 6//5 2//5
f 5//6 8//6 7//6
f 5//6 7//6 6//6
f 1//7 2//7 3//7
f 1//7 3//7 4//7

Nada mais simples! A desvantagem é que precisamos ler as listas completamente para montarmos a geometria do objeto de acordo com as faces. A coisa seria mais fácil se os vértices já estivesse pré-ordenados e com seus vetores normais e de textura (se tivessem) associados a eles. Ficaria mais fácil de montar um Vertex Buffer Object… C’est la vie…

Como você deve ter observado, neste exemplo, tive que tomar cuidado com algumas coisas:

  1. Todas as faces têm que ser triangulares;
  2. Não usei texturas.

Assim, no Blender, tive que “triangulizar” o modelo (no Edit Mode, selecionar Quads to Tris, no menu Faces), depois pedi para recalcular os vetores normais (just in case!) e, finalmente, ao exportar, deselecionei a geração das coordenadas UV, selecionei Normals e High Quality Normals (porque não quis usar um vetor pré-calculado com vetores normais, como é usado em modelos MD2). Selecionei também “Triangulate” (também just in case!) e voilà!

O modelo do macaco, com a aplicação do modificador de subdivisão (para aumentar o número de triângulos) ficou porreta, depois de renderizado no OpenGL:

Monkey, renderizado a partir de um modelo OBJ, gerado no Blender.


UPDATE: Recentemente (2014) abandonei o formato Wavefront porque ele não atende minhas necessidades. Ainda, coloquei um código de teste mais “completo” e “confiável” em posts mais recentes.

Shaders

Uma dos avanços, nos últimos 10 anos, de bibliotecas como OpenGL e Direct3D, são os shaders. “Shade”, em inglês, têm muitos significados… “Óculos escuros” é um deles… “matiz” ou “tonalidade” são outros dois. Na disciplina de computação gráfica tem os dois últimos significados. Um “shader” é um programa que transforma um vértice ou um pixel.

Historicamente, o OpenGL e o Direct3D (para citar duas bibliotecas) usam blocos de rotinas fixas para lidar com transformações. Para realizar transformações geométricas existe a matriz MODELVIEW, para lidar com a projeção dos vértices no espaço tridimensional temos a matriz PROJECTION. Temos matrizes para os vetores normais e para as texturas também. Manipular essas matrizes significa transformar um conjunto de vetores ou fragmentos (pixels) em outra coisa. A “novidade” é que isso não precisa mais ser feito exclusivamente manipulando matrizes, podemos também criar pequenos programas (shader programs) para criar efeitos especiais ou para facilitar a manipulação matemática…

De tempos em tempos vou postar algum artigo aqui sobre uma linguagem de programação de shaders, padronizada, associada ao OpenGL, conhecida como GLSL (lê-se Gslang) — OpenGL Shading Language.

GLSL, hoje, implementa 3 tipos de shaders: Vertex, Geometry e Fragment shaders. O primeiro tipo transforma vértices fornecidos por funções como glVertex. Os “vertex shaders” não obtém quaisquer informações sobre a conexão dos vértices. Esse tipo de shader lida com um vértice por vez e não tem como saber quais são os demais vértices. Os fragment shaders lidam com os pixels ligados aos vértices e aqueles pixels que são interpolados entre vértices. Mas esse shader lida com “fragmentos” individuais… Geometry shaders são mais recentes e existem para incrementar o controle dos dois outros shaders. Como o nome sugere, lidam com a conexão entre vértices (pelo que entendi até o momento!).

Outra vantagem dos shaders é que eles usam a arquitetura de paralelismo das placas de vídeo. Um único shader pode ser executado em diversas “threads” da GPU, em paralelo. Um vertex shader pode, por exemplo, processar todos os vértices de um objeto de uma só vez. Um fragment shader não pode ser executado em paralelo com um vertex shader porque são estágios diferentes do workflow da biblioteca, mas diversos fragment shaders, assim como os vertex shaders, podem ser executados em paralelo entre si, acelerando em muitas vezes a renderização de uma cena.

Tudo isso é muito interessante, mas tenho que me acostumar com a distribuição de processamento e a independência da ordem do processamento (no caso dos vertex e fragment shaders). À medida que eu for entendendo essa excelente ferramenta vou postando minhas conclusões por aqui.

Mapeamento cúbico (Ambiental – do jeito simples)

Antes de mostrar o jeito mais performático de fazer um mapeamento cúbico ambiental, deixe-me mostrar um teste, usando 6 texturas. A idéia é a de que podemos encapsular o nosso “mundo” num grande cubo e texturizar suas faces de forma tal que pareça que estamos dentro de um ambiente. É como se fosse uma foto panorâmica.

Este é o método mais simples de impleentar, embora obter as texturas corretas é uma coisa bem complicada de fazer. Dêem uma olhada nas texturas, montadas sobre um cubo “planificado”, que usei como exemplo em meu teste:

De cima para baixo, da esquerda para direita: +Y, -X, +Z, +X, -Z e -Y

Obtive essas texturas do site Humus, diminuí o tamanho de cada uma das 6 texturas (de 2048 x 2048 para 512 x 512) porque tenho 3 ambientes de teste para minha viagem com OpenGL e, num deles, a versão do OpenGL é a 1.4, com sérias restrições de memória.

Eis o código para montar o mapa cúbico:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glfw.h>
#include "load_png.h"

#define WIDE_RATIO (16.0f/9.0f)
#define FIELD_OF_VIEW_ANGLE 55.0f

#define BOX_SIZE 512.0f

int texIDs[6] = { 0 };
float rx = 0.0f, ry = 0.0f;
int mousex, mousey;

GLFWvidmode mode;
int vph;

/* Protótipos */
void initGraphics(void);
void initOpenGL(void);
void mainLoop(void);

int main(void)
{
  initGraphics();
  initOpenGL();

  mainLoop();

  return 0;
}

static void quit(void)
{
  int i;

  if (texIDs[0] != 0)
    glDeleteTextures( 6, texIDs );
  glfwTerminate();
}

static void calculateHeight(GLFWvidmode *mode, int *h)
{
  float ratio;

  ratio = (float)mode->Width / (float)mode->Height;

  *h = mode->Height;
  if (ratio < WIDE_RATIO)
    *h /= WIDE_RATIO;
}

void initGraphics(void)
{
  if (glfwInit() != GL_TRUE)
  {
    fprintf(stderr, "ERROR: Cannot initialize GLFW.\n");
    exit(1);
  }

  glfwGetDesktopMode(&mode);

  calculateHeight(&mode, &vph);

  atexit(quit);

  if (glfwOpenWindow(mode.Width, mode.Height,
                     8, 8, 8, 8,
                     32,
                     0,
                     GLFW_FULLSCREEN) != GL_TRUE)
  {
    fprintf(stderr, "ERROR: Cannot open window.\n");
    exit(1);
  }

  glfwSwapInterval(0);

  glfwGetMousePos(&mousex, &mousey);
}

static void loadTextures(void)
{
  static char *files[] = { "textures/posx.png", "textures/negx.png",
                           "textures/posy.png", "textures/negy.png",
                           "textures/posz.png", "textures/negz.png" };
  void *data;
  int w, h;
  int i;

  glGenTextures(6, texIDs);

  for (i = 0; i < 6; i++)
  {
    glBindTexture( GL_TEXTURE_2D, texIDs[i]);

    if ((data = load_png(files[i], &w, &h)) == NULL)
    {
      fprintf(stderr, "ERROR: Unable to load texture (%s).\n", files[i]);
      exit(1);
    }

    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glTexEnvi( GL_TEXTURE_2D, GL_TEXTURE_ENV_MODE, GL_REPLACE );

    glTexImage2D( GL_TEXTURE_2D, 
                  0, 
                  GL_RGBA, 
                  w, h, 
                  0, 
                  GL_RGBA, 
                  GL_UNSIGNED_BYTE, data );

    free(data);
  }
}

void initOpenGL(void)
{
  glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );

  /* Posiciona strip se ratio não for Wide.
     OBS: Lembrar que a origem é no canto inferior esquerdo. */
  glViewport(0, (mode.Height - vph)/2.0f, mode.Width, vph);

  glMatrixMode( GL_PROJECTION );
  gluPerspective( FIELD_OF_VIEW_ANGLE, 
                  (float)mode.Width / (float)vph, 
                  0.1f, 1000.0f );
  glMatrixMode( GL_MODELVIEW );

  glShadeModel( GL_FLAT );

  glEnable( GL_DEPTH_TEST );

  loadTextures();

  glEnable( GL_TEXTURE_2D );
}

static void transform(void)
{
  glLoadIdentity();
  glRotatef( rx, 1, 0, 0 );
  glRotatef( ry, 0, 1, 0 );
}

static void draw(void)
{
  static float posx[4][5] = { { 0, 0,  BOX_SIZE/2, -BOX_SIZE/2, -BOX_SIZE/2 },
                              { 1, 0,  BOX_SIZE/2, -BOX_SIZE/2,  BOX_SIZE/2 },
                              { 1, 1,  BOX_SIZE/2,  BOX_SIZE/2,  BOX_SIZE/2 },
                              { 0, 1,  BOX_SIZE/2,  BOX_SIZE/2, -BOX_SIZE/2 } };
  static float negx[4][5] = { { 0, 0, -BOX_SIZE/2, -BOX_SIZE/2,  BOX_SIZE/2 },
                              { 1, 0, -BOX_SIZE/2, -BOX_SIZE/2, -BOX_SIZE/2 },
                              { 1, 1, -BOX_SIZE/2,  BOX_SIZE/2, -BOX_SIZE/2 },
                              { 0, 1, -BOX_SIZE/2,  BOX_SIZE/2,  BOX_SIZE/2 } };
  static float posy[4][5] = { { 0, 0, -BOX_SIZE/2,  BOX_SIZE/2, -BOX_SIZE/2 },
                              { 1, 0,  BOX_SIZE/2,  BOX_SIZE/2, -BOX_SIZE/2 },
                              { 1, 1,  BOX_SIZE/2,  BOX_SIZE/2,  BOX_SIZE/2 },
                              { 0, 1, -BOX_SIZE/2,  BOX_SIZE/2,  BOX_SIZE/2 } };
  static float negy[4][5] = { { 0, 0, -BOX_SIZE/2, -BOX_SIZE/2,  BOX_SIZE/2 },
                              { 1, 0,  BOX_SIZE/2, -BOX_SIZE/2,  BOX_SIZE/2 },
                              { 1, 1,  BOX_SIZE/2, -BOX_SIZE/2, -BOX_SIZE/2 },
                              { 0, 1, -BOX_SIZE/2, -BOX_SIZE/2, -BOX_SIZE/2 } };
  static float posz[4][5] = { { 0, 0,  BOX_SIZE/2, -BOX_SIZE/2,  BOX_SIZE/2 },
                              { 1, 0, -BOX_SIZE/2, -BOX_SIZE/2,  BOX_SIZE/2 },
                              { 1, 1, -BOX_SIZE/2,  BOX_SIZE/2,  BOX_SIZE/2 },
                              { 0, 1,  BOX_SIZE/2,  BOX_SIZE/2,  BOX_SIZE/2 } };
  static float negz[4][5] = { { 0, 0, -BOX_SIZE/2, -BOX_SIZE/2, -BOX_SIZE/2 },
                              { 1, 0,  BOX_SIZE/2, -BOX_SIZE/2, -BOX_SIZE/2 },
                              { 1, 1,  BOX_SIZE/2,  BOX_SIZE/2, -BOX_SIZE/2 },
                              { 0, 1, -BOX_SIZE/2,  BOX_SIZE/2, -BOX_SIZE/2 } };

  glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

  glColor3f( 1, 1, 1 );

  glBindTexture( GL_TEXTURE_2D, texIDs[0] );  /* POSX */
  glNormal3f(-1,  0,  0);
  glInterleavedArrays( GL_T2F_V3F, 0, posx );
  glDrawArrays( GL_QUADS, 0, 4 );

  glBindTexture( GL_TEXTURE_2D, texIDs[1] );  /* NEGX */
  glNormal3f( 1,  0,  0);
  glInterleavedArrays( GL_T2F_V3F, 0, negx );
  glDrawArrays( GL_QUADS, 0, 4 );

  glBindTexture( GL_TEXTURE_2D, texIDs[2] );  /* POSY */
  glNormal3f( 0, -1,  0);
  glInterleavedArrays( GL_T2F_V3F, 0, posy );
  glDrawArrays( GL_QUADS, 0, 4 );

  glBindTexture( GL_TEXTURE_2D, texIDs[3] ); /* NEGY */
  glNormal3f( 0,  1,  0);
  glInterleavedArrays( GL_T2F_V3F, 0, negy );
  glDrawArrays( GL_QUADS, 0, 4 );

  glBindTexture( GL_TEXTURE_2D, texIDs[4] ); /* NEGZ */
  glNormal3f( 0,  0,  1);
  glInterleavedArrays( GL_T2F_V3F, 0, negz );
  glDrawArrays( GL_QUADS, 0, 4 );

  glBindTexture( GL_TEXTURE_2D, texIDs[5] );  /* POSZ */
  glNormal3f( 0,  0, -1);
  glInterleavedArrays( GL_T2F_V3F, 0, posz );
  glDrawArrays( GL_QUADS, 0, 4 );
}

static int inputHandler(void)
{
  int x, y;

  if (glfwGetKey('Q') == GLFW_PRESS)
    return 0;

  if (glfwGetKey('W') == GLFW_PRESS) { rx -= 1.0f; }
  if (glfwGetKey('S') == GLFW_PRESS) { rx += 1.0f; }
  if (glfwGetKey('A') == GLFW_PRESS) { ry -= 1.0f; }
  if (glfwGetKey('D') == GLFW_PRESS) { ry += 1.0f; }

  glfwGetMousePos(&x, &y);

  ry += (x - mousex) * 0.1f;
  rx += (y - mousey) * 0.1f;

  mousex = x;
  mousey = y;

  return 1;
}

void mainLoop(void)
{
  for (;;)
  {
    transform();
    draw();
    if (!inputHandler())
      break;

    glfwSwapBuffers();
  }
}

Nada muito complicado. Preste atenção às funções loadTextures e draw. Em loadTexrures o importante é perceber o modo de empacotamento da textura sobre a superfície. Usei GL_CLAMP_TO_EDGE porque, se não o fizezesse, alguns “artefatos” aparecem na cena. Ou seja, você verá os cantos do cubo montado em draw. E, em draw, tudo o que fazemos é criar um grande cubo (poderia ser maior!) com as texturas carregadas.

Você pode ter percebido que os lados +Z e -Z foram invertidos. Isso porque as texturas são feitas para mapeamento cúbico de reflexão, ou seja, são feitas para projetar esse ambiente num objeto, dando a impressão de reflexo. Quando usamos para o mapeamento de ambientes temos que inverter os lados Z.

A textura +Y (a de cima, na figura anterior) deve ser colocada na face superior, por isso ela é chamada de +Y. As outras seguem o mesmo princípio, exceto pelas faces +Z e -Z, pelo motivo explicado acima.

Se você já futucou o OpenGL perceberá também que eu não estou usando a extensão GL_ARB_texture_cube_map (que já está incorporada no OpenGL 2.1). Usar essa extensão faz com que as coisas acelerem um cadinho, mas isso ai em cima é apenas um teste, ok?

O código completo e as texturas podem ser obtidas aqui. O código foi feito para Linux e depende das bibliotecas libglfw-dev e libpng12-dev (também das bibliotecas do mesa – que já devem estar instaladas no seu linux, se vocẽ tem uma placa gráfica decente com os drivers instalados)…

Problema de resolução (complicou, de novo!)

Usar uma faixa da tela quanto temos uma geometria do monitor 4:3 é bem fácil, como demonstrei no artigo anterior. Do ponto de vista do código basta fazer isto:

glfwGetDesktopMode(&mode);

if (glfwOpenWindow(mode.Width, mode.Height,
                   8, 8, 8, 8,
                   32,
                   GLFW_FULLSCREEN) != GL_TRUE)
{
  exit(1);
}

float ratio = (float)mode.Width / (float)mode.Height;

viewportHeight = mode.Height;
if (ratio < WIDE_RATIO)
  viewportHeight /= WIDE_RATIO;

float newOriginY = (mode.Height - viewportHeight) / 2.0f;

glViewport(0, newOriginY, mode.Width, viewportHeight);

glMatrixMode(GL_PROJECTION);
gluPerspective( FOV_ANGLE, (float)mode.Width / (float)viewportHeight, 0.1f, 1000.0f);
glMatrixMode(GL_MODELVIEW);

Só que alterar o Viewport dessa forma causa um problema: Um terço do espaço útil da tela ficará perdido para a aplicação!

OpenGL 3+ permite o uso de multiplas viewports, mas o OpenGL 2.1 não. A solução óbvia seria usar o stencil buffer para mascarar 1/3 da tela, no formato letter box para que os outros 1/3 possam ser reusados em algum momento (para colocarmos legendas, por exemplo). Só que para isso teríamos que manipular a matriz de projeção, para manter o aspect ratio em ambos os modos letter box e wide screen. Mantento a consistência da projeção em ambos os modos.

Ainda não sei se é possível manipular o viewport em passos diferentes de renderização. Tipo: No primeiro passo ajustamos o viewport para a região central da tela (1/3 dela) e desenhamos a cena. No segundo passo retornamos o viewport para toda a região da tela, mascaramos a parte da cena via stencil buffer, alteramos o aspect ratio de volta para 4:3 e desenhamos fora da cena, usando uma projeção ortogonal, ao invés de perspectiva.

Podemos fazer o contrário também… usar o stencil buffer para criar uma máscara da cena (assim, a área superior e inferior ficariam intocadas). Mas ai temos o problema do aspect ratio. Isso, essencialmente, recortará a cena nos cantos superior e inferior, causando efeitos diferentes nas geometrias 4:3 e 16:9. O que eu não quero!

Vou achar uma solução e, quando o fizer, posto por aqui…