Projeto Virtual Worlds

Computação Gráfica 3D em C

Arquivos da Categoria: Dicas

Rotinas que foram retiradas do OpenGL 3+

O modo core profile do OpenGL retirou uma série de rotinas do seu repertório. Dentre elas, as rotinas de manipulação de matrizes (que foram colocadas à cargo dos shaders) e as rotinas da GLU (GL Utilities).

Lidar com matrizes é algo muito útil em computação gráfica. Permite uma maneira unificada e centralizada de realizar transformações geométricas, por exemplo. Como OpenGL “jogou fora” essas rotinas, provavelmente porque é mais fácil que o desenvolvedor use alguma biblioteca externa para evitar as limitações das pilhas de matrizes, temos que implementar nossas próprias. A primeira é a multiplicação de matrizes.

Vale lembrar que OpenGL lida, por padrão, com matrizes orientadas por coluna. Ou seja, diferente do padrão de C, onde matrizes são orientadas por linha (o incremento de um ponteiro que aponta para uma matriz apontará para o próximo item da linha). Uma maneira de visualizar a multiplicação de matrizes orientadas por coluna (column major) é este:

Coluna de A e linha de B, multiplicadas, dá item em C.

Coluna de A e linha de B, multiplicadas, dá item em C.

E, também, só para lembrar, a ordem da multiplicação é da direita para esquerda. Isso é assim porque a multiplicação de uma matriz por um vetor é feita assim:

\displaystyle \begin{bmatrix}x'\\y'\\z'\\w'\end{bmatrix}=\begin{bmatrix}m_1&m_5&m_9&m_{13}\\m_2&m_6&m_{10}&m_{14}\\m_3&m_7&m_{11}&m_{15}\\m_4&m_8&m_{12}&m_{16}\end{bmatrix}\cdot\begin{bmatrix}x\\y\\z\\w\end{bmatrix}

E matrizes podem ser encaradas como sendo um vetor de vetores. Cada coluna (mas matrizes column major) é um vetor.

A rotina de multiplicação de matrizes “padrão” é algo mais ou menos assim:

void Matrix4x4Multiply(float *mOut, const float *m1, const float *m2)
{
  int i, c, r;

  for (c = 0; c < 4; c++)
    for (r = 0; r < 4; r++)
    {
      mOut[c*4+r] = 0.0f;
      for (i = 0; i < 4; i++)
        mOut[c*4+r] += m1[c*4+i] * m2[i*4+r];
    }
}

Isso ai funciona muito bem, mas eis uma rotina um pouco mais veloz, usando SSE. Se você debugar a função abaixo, ela faz exatamente o que a função acima faz, mas usa 2 loops, ao invés de 3 – a rotina acima realiza 16 inicializações, 64 multiplicações e 64 adições. A rotina abaixo faz 16 adições e 16 multiplicações (de blocos de 4 floats):

void Matrix4x4Multiply(float *mOut, const float *m1, const float *m2)
{
  int i, j;
  __m128 a_column, b_column, r_column;

  for (i = 0; i < 16; i += 4)
  {
    b_column = _mm_set1_ps(b[i]);
    a_column = _mm_load_ps(a);
    r_column = _mm_mul_ps(a_column, b_column);

    for (j = 1; j < 4; j++)
    {
      b_column = _mm_set1_ps(b[i+j]);
      a_column = _mm_load_ps(&amp;a[j*4]);
      r_column = _mm_add_ps(_mm_mul_ps(a_column, b_column), r_column);
    }

    _mm_store_ps(&amp;out[i], r_column);
  }
}

Eis a rotina que substitui a glLoadIdentity(). Trata-se de uma simples cópia de 4 blocos de 4 floats, usando SSE:

void Matrix4x4Identity(float *m)
{
  static __m128 t[4] = { { 1.0f, 0.0f, 0.0f, 0.0f },
                         { 0.0f, 1.0f, 0.0f, 0.0f },
                         { 0.0f, 0.0f, 1.0f, 0.0f },
                         { 0.0f, 0.0f, 0.0f, 1.0f } };
  __m128 *p = (__m128 *)m;
  int i;

  for (i = 0; i < 4; i++)
    p[i] = t[i];
}

Uma função que não tem equivalente direto nas rotinas do OpenGL, mas é útil na manipulação de operações com matrizes, é a transposição. Transposição é uma transformação de uma matriz onde linhas viram colunas (e vice versa). A rotina pode ser implemtnada com dois loops, mas, usando SSE, e um pequeno macete, fica mais rápido:

void Matrix4x4Transpose(float *mOut, const float *mIn)
{
  __m128 m[4], t[4];

  m[0] = _mm_load_ps(mIn);
  m[1] = _mm_load_ps(mIn + 4);
  m[2] = _mm_load_ps(mIn + 8);
  m[3] = _mm_load_ps(mIn + 12);

  t[0] = _mm_unpacklo_ps(m[0], m[1]);
  t[1] = _mm_unpacklo_ps(m[2], m[3]);
  t[2] = _mm_unpackhi_ps(m[0], m[1]);
  t[3] = _mm_unpackhi_ps(m[2], m[3]);

  _mm_store_ps(mOut,      _mm_movelh_ps(t[0], t[1]));
  _mm_store_ps(mOut + 4,  _mm_movehl_ps(t[1], t[0]));
  _mm_store_ps(mOut + 8,  _mm_movelh_ps(t[2], t[3]));
  _mm_store_ps(mOut + 12, _mm_movehl_ps(t[3], t[2]));
}

As rotinas glFrustum, gluPerspective e gluLookAt também sumiram do OpenGL (do GL Utilities). Como todas as três são, de fato, manipulação de matrizes (as duas primeiras manipulam a matriz de projeção, a última, a matriz ModelView), ei-las:

/* Frustum projection Matrix. */
void Matrix4x4Frustum(float *mOut, float l, float r, float b, float t, float n, float f)
{
  float rml = r - l;
  float tmb = t - b;
  float fmn = f - n;
  float n2 = 2.0f * n;

  mOut[1] = mOut[2] = mOut[3] =
  mOut[4] = mOut[6] = mOut[7] =
  mOut[12] = mOut[13] = mOut[15] = 0.0f;

  mOut[0] = n2 / rml;

  mOut[5] = n2 / tmb;

  mOut[8] = (r+l) / rml;
  mOut[9] = (t+b) / tmb;
  mOut[10] = -(f+n) / fmn;
  mOut[11] = -1.0f;

  mOut[14] = -(f * n2) / fmn;
}

/* Substitui gluPerspective (cria um frustum 'simétrico'). */
void Matrix4x4Perspective(float *mOut, float fov, float aspectratio, float n, float f)
{
  float xmax, ymax;

  ymax = n * tanf(DEG2RAD(fov));
  xmax = ymax * aspectratio;
  Matrix4x4Frustum(mOut, -xmax, xmax, -ymax, ymax, n, f);
}

/* Substitui gluLookAt */
void Matrix4x4LookAt(float *mOut, const float *mIn, const float *eye, const float *target, const float *up)
{
  float fw[3], side[3], _up[3];
  float matrix2[16], resultMatrix[16];

  Vector3Sub(fw, target, eye);
  Vector3SelfNormalize(fw);

  /* o vetor 'side' aponta para a esquerda da câmera. */
  Vector3Cross(side, fw, up);
  Vector3SelfNormalize(side);

  /* _up é, necessariamente, unitário! E é recalculado aqui
     para evitar o inconveniente de termos um vetor 'up' não
     perpendicular aos vetores 'forward' e 'side'. */
  Vector3Cross(_up, side, fw);

  /* Poderia inserir os 3 vetores via função Vector3Copy, sobre uma
     matriz de identidade, mas assim parece ser mais rápido. */
  matrix2[0] = side[0];
  matrix2[4] = side[1];
  matrix2[8] = side[2];

  matrix2[1] = _up[0];
  matrix2[5] = _up[1];
  matrix2[9] = _up[2];

  /* Note que se a câmera está orientada para (0,-1,0), então 'target' terá
     um componnte z menor que o de 'eye'. O vetor 'forward' será negativo,
     por isso temos que invertê-lo. */
  matrix2[2] = -fw[0];
  matrix2[6] = -fw[1];
  matrix2[10] = -fw[2];

  matrix2[3] = matrix2[7] = matrix2[11] = 
  matrix2[12] = matrix2[13] = matrix2[14] = 0.0f;
  matrix2[15] = 1.0f;

  Matrix4x4Multiply(resultMatrix, mIn, matrix2);

  /* Aproveita 'matrix2' para criar a matriz de translação. */
  Matrix4x4Identity(matrix2);
  matrix2[3] = -eye[0];
  matrix2[7] = -eye[1];
  matrix2[11] = -eye[2];
  Matrix4x4Multiply(mOut, resultMatrix, matrix2);
}

A função Matrix4x4Frustum simplesmente implementa a matriz padrão do frustum genérico do OpenGL:

\displaystyle \begin{bmatrix}\frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix}

A função Matrix4x4Perspective implementa exatamente o que gluPerspective faz: Ela cria um frustum simétrico, baseado no campo de visão (field of view), em graus, e no “aspect ratio” do plano “near” (4:3, 16:9, etc). Essa função calcula os valores do retângulo visível do plano near [os pontos (left, bottom) e (right, top)], onde a câmera aponta exatamente para o centro do retângulo, e chama Matrix4x4Frustum.

A função Matrix4x4LookAt é um pouco mais complicada… Ela calcula a translação para um novo sistema de coordenadas baseado nos 3 eixos onde a câmera está. Primeiro, calculamos o vetor que vai da posição da câmera (coordenada eye) para o alvo (target) e o chamamos de fw (de “forward”). Esse vetor é normalizado porque precisamos de um vetor unitário para compor o novo sistema de coordenadas. Neste ponto temos os vetores fwup e precisamos do terceiro vetor,, perpendicular a esses dois, que chamaremos de side. Por isso os produtos vetoriais. De posse dos 3 vetores, montamos a matriz transposta da matriz abaixo:

\displaystyle M_{lookAt}=\begin{bmatrix}s_x & up_x & -fw_x & 0 \\s_y & up_y & -fw_y & 0 \\s_z & up_z & -fw_z & 0 \\ 0 & 0 & 0 & 1\end{bmatrix}

Essa matriz (ou, melhor, sua transposta) “reorienta” o sistema de coordenadas de visualização. Precisamos afastar a câmera para a posição desejada e, por isso, uma matriz de translação final é usada:

\displaystyle T=\begin{bmatrix}1&0&0&-eye_x\\ 0&1&0&-eye_y\\ 0&0&1&-eye_z\\ 0&0&0&1\end{bmatrix}\cdot T_{view}\cdot\begin{bmatrix}s_x&s_y&s_z&0\\up_x&up_y&up_z&0\\-fw_x&-fw_y&-fw_z&0\\0&0&0&1\end{bmatrix}

A matriz T_{view}, inicial, geralmente é uma identidade.

Notou também que recalculamos o vetor \vec{up}? Isso torna a precaução de usar a dica da projeção de \vec{up} sobre \vec{forward} (aqui) desnecessária.

Anúncios

Instalando o GLFW 3 no Debian

Não é novidade aqui que eu uso o GLFW como framework base para incializar o contexto para o OpenGL e não ter que construir algumas rotinas básicas, como leitura de eventos de teclado, mouse e joystick. O último GLFW (3.0.4, no momento desta postagem) já suporta o padrão OpenGL 3 ou superior, não somente no modo “compatível” com o 2.1 e, por isso, acho interessante usá-lo. Infelizmente essa nova versão (do GLFW) não consta dos repositórios do Debian ou do Ubuntu, então temos que compilá-lo e, ao usá-lo, linka-lo ao nosso código. Prefiro, no caso do GLFW, a linkagem estática. O pequeno “script” abaixo mostrac como compilar e instalar o GLFW 3.0.4:

$ sudo apt-get -y install cmake libglu1-mesa-dev xorg-dev
$ cd ~/Work/glfw3/; # ou o diretório onde você vai baixar o GLFW3
$ git clone https://github.com/glfw/glfw.git
$ cmake .
$ make
$ sudo make install

Isso vai instalar as dependências, baixar o GLFW do GitHub, compilar e instalar tudo.

Existem algumas diferenças entre essa nova versão e o GLFW 2 (disponível nos repositórios de ambos Debian e Ubuntu), mas deixo isso pra depois.

UPDATE: Tive tantos problemas com o GLFW 3 que resolvi retornar ao 2 mesmo, fazendo alguns tweaks para obter o contexto do OpenGL 4.3.

Acumulando coisas num buffer…

Um dos passos finais da renderização de um frame, no OpenGL, é o uso do que é conhecido como accumulation buffer. Nesse buffer podemos desenhar diversas cenas com pesos diferentes e, no fim das contas, mostrar o resultado. Isso possibilita a criação, fácil (nem tanto!) de diversos efeitos fotográficos. Dois deles, bem úteis, são motion blur (um “embaçamento” causado por movimento) e depth of field (que é também um “embaçamento” causado por um ponto focal na cena).

Usar o buffer de acumulação, no OpenGL, é bem simples:

glAccum(GL_ACCUM, factor1);
/* Desenha a cena */
glAccum(GL_ACCUM, factor2);
/* Desenha a cena de novo */
glAccum(GL_RESULT, final_factor);

A primeira chamada a glAccum diz que as cores dos próximos “desenhos” serão multiplicados pelo fator factor1 e adicionados ao conteúdo atual do buffer. A segunda chamada faz o mesmo, mas usa o fator factor2. Finalmente a chamada a glAccum com o parâmetro GL_RESULT pega o conteúdo do buffer, multiplica pelo final_factor e o copia para o framebuffer.

Isso é uma super-simplificação do uso do buffer de acumulação, mas dá uma idéia de como ele funciona.

O efeito depth of field, por exemplo, pode ser obtido desenhando a cena de um ponto central e, depois, deslocando a câmera em diversas direções próximas ao ponto central, desenhar a cena outras vezes, acumulando-a com fatores diferentes. O efeito final pode ser mais ou menos como este:

O foco central está entrs os dois robôs, na frente. Os mais “nítidos” na cena.

Isso dá uma idéia de profundidade melhor do que desenhar os robozinhos sem o efeito:

Imagem sem o efeito…

A primeira imagem parece mais “foto-realista”, huh? Pelo menos ela fica melhor numa animação… Repare como as animações da Pixar usam esse recurso extensivamente!

É claro que, antes de poder usar o buffer de acumulação, devemos pedir ao OpenGL que o crie (no meu caso, usando a função glfwOpenWindow), informar as características do buffer (tamanho dos componentes RGB e A, usando a mesma função) e quando quisermos usá-lo, habilitá-lo usando a função glEnable.

No próximo post discutirei algumas implementações do efeito…

UPDATE: Infelizmente (?!) desde o OpenGL 3.0 não existe mais um accumulation buffer. Isso tem que ser feito por shaders!

Dica: Aprendendo WebGL

Topei com esse link hoje: Learning WebGL. Vale a pena conferir…