Projeto Virtual Worlds

Computação Gráfica 3D em C

Arquivos da Categoria: OpenGL

Usando GLFW3

Ok… faz tempo que não escrevo por aqui… Quero só mostrar que o mudei o framework para inicilização do OpenGL para o GLFW3, que tem alguns recursos interessantes (suporte a múltiplos monitores, por exemplo). O código principal para inicialização do projeto é, agora, este:

/* glfw3_fullscreen.c */
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <GLFW/glfw3.h>
#include <GL/gl.h>

// Defina o "nome" de sua janela aqui...
#define WINDOW_TITLE "My Window"

// Usando FULLHD.
#define WINDOW_WIDTH 1920
#define WINDOW_HEIGHT 1080

// Se formos usar FSAA (ver abaixo).
#define FSAA_SAMPLES 16

// Callbacks.
static void error_callback(int, const char *);
static void key_callback(GLFWwindow *, int, int, int, int);

// A função de desenho de quadros.
static void draw(void);

int main(int argc, char *argv[])
{
  GLFWmonitor *primaryMonitor;
  GLFWwindow *mainWindow;

  glfwSetErrorCallback(error_callback);

  if (!glfwInit())
    return EXIT_FAILURE;

  // Ao sair, certifica-se de chamar glfwTerminate().
  atexit(glfwTerminate);

  // GLFW3 agora exige a criação ou uso do objeto "monitor".
  // Aqui seleciono o monitor principal.
  primaryMonitor = glfwGetPrimaryMonitor();

  // Vamos usar RGBA com 8 bits cada (32 bit planes).
  glfwWindowHint(GLFW_RED_BITS,   8);
  glfwWindowHint(GLFW_GREEN_BITS, 8);
  glfwWindowHint(GLFW_BLUE_BITS,  8);
  glfwWindowHint(GLFW_ALPHA_BITS, 8);

  // 60 fps, double buffering, janela not-resizeable e not decorated, 
  // OpenGL 3.3 context (4.3 não suportado em uma de minhas
  // máquinas de teste), core profile.
  //
  // Alguns desses parâmetros são default, mas ajusto assim mesmo.
  glfwWindowHint(GLFW_REFRESH_RATE,          60);
  glfwWindowHint(GLFW_DOUBLEBUFFER,          true);
  glfwWindowHint(GLFW_RESIZABLE,             false);
  glfwWindowHint(GLFW_DECORATED,             false);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  glfwWindowHint(GLFW_OPENGL_PROFILE,        GLFW_OPENGL_CORE_PROFILE);

  // Se você for usar FULL SCREEN ANTI-ALIASING, ajuste o número de
  // samples aqui:
  //glfwWindowHint(GLFW_SAMPLES, FSAA_SAMPLES);

  // Tenta criar a janela.
  mainWindow = glfwCreateWindow(
                WINDOW_WIDTH, WINDOW_HEIGHT,
                WINDOW_TITLE,
                primaryMonitor,
                NULL);

  if (mainWindow)
  { 
    glfwSetKeyCallback(mainWindow, key_callback);

    glfwMakeContextCurrent(mainWindow);

    glfwShowWindow(mainWindow);

    // Esconde o cursor do mouse.
    glfwSetInputMode(mainWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // Loop principal...
    while (!glfwWindowShouldClose(mainWindow))
    {
      // Desenha um quadro aqui.
      draw();
      glfwSwapBuffers(mainWindow);

      // Verifica se há eventos (teclado, mouse, joystick, ...)
      // pendentes.
      glfwPollEvents();
    }

    // Destrói a janela.
    glfwDestroyWindow(mainWindow);
  }
  else
    return EXIT_FAILURE;

  return EXIT_SUCCESS;
}

void error_callback(int error, const char *description)
{
  fprintf(stderr, "Error: %s\n", description);
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
  // Termina o loop principal se apertar ESC.
  if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
    glfwSetWindowShouldClose(window, true);
}

// Função simples: Limpa o buffer.
void draw(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glFlush();
}

Não mudou grandes coisas em relação ao GLFW2. Para compilar esse cara é necessário instalar o pacote libglfw3-dev e usar o seguinte Makefile:

CFLAGS=-O2 -fno-stack-protector -march=native `pkg-config --cflags glfw3 gl`
LDFLAGS=-O2 -s
LIBS=`pkg-config --libs glfw3 gl`

all: glfw3_fullscreen

glfw3_fullscreen: glfw3_fullscreen.o
	$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)

glfw3_fullscreen.o: glfw3_fullscreen.c

Obviamente que as opções -fno-stack-protector e -march=native são opcionais e só as coloquei ai para gerar código mais otimizado.

Um código de testes para VAOs, VBOs e Shaders mais ‘definitivo’…

Daqui para frente os códigos serão direcionados ao OpenGL e GLSL 4.3, core profile. Então, apresento a vocês uns códigos de exemplo que usarei como base, daqui pra frente. Primeiro, eis os shaders:

// Vertex Shader: test.vert
#version 430 core

layout(location=0) in vec3 vpos;
layout(location=1) in vec3 vcolor;

smooth out vec3 color;

void main()
{
  color = vcolor;
  gl_Position = vec4(vpos, 1.0);
}
// Fragment Shader: test.frag
#version 430 core

smooth in vec3 color;

void main()
{
  gl_FragColor = vec4(color,1);
}

Eis o makefile:

# Makefile
CC=gcc
CFLAGS=-O3 -march=native `pkg-config --cflags glfw2 gl` 
LDFLAGS=-O3 -s -lm `pkg-config --libs glfw2 gl`

test: test.o
  $(CC) -o $@ $^ $(LDFLAGS)

test.o: test.c

.PHONY: clean
clean:
  rm -f test.o

E, abaixo, o código de exemplo em C “genérico”, usando VAOs, VBOs e os Shaders acima, para mostrar um triângulo equilatero:

/* ==================================================
   example.c

   Código básico de uso de VBOs, VAOs e Shaders.
   ================================================== */
#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <math.h>
#include <sys/time.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <GL/glfw.h>

#define WINDOW_WIDTH  1280
#define WINDOW_HEIGHT 720

static int finished = 0;

/* Vertex Array Object, Vertex Buffer Object e
   Program Object para OpenGL. */ < / span >
GLuint vao, vbo, prgo;

static void initGLFW(void);
static void initGL(void);
static void initModel(void);
static void initShaders(void);
static void destroyGLFW(void);
static void drawScene(void);
static void processEvents(void);

int main(int argc, char *argv[])
{
  initGLFW();
  initGL();
  initShaders();
  initModel();

  while (!finished)
  {
    drawScene();
    processEvents();
    glfwSwapBuffers();
  }

  destroyGLFW();

  return 0;
}

void processEvents(void)
{
  glfwPollEvents();

  if (glfwGetKey(GLFW_KEY_ESC))
    finished = 1;
}

/* Inicializa o GLFW. */
void initGLFW(void)
{
  if (!glfwInit())
  {
    fputs("Error initializing GLFW.\n", stderr);
    exit(1);
  }

  glfwDisable( GLFW_AUTO_POLL_EVENTS );

  glfwOpenWindowHint( GLFW_OPENGL_VERSION_MAJOR, 4 );
  glfwOpenWindowHint( GLFW_OPENGL_VERSION_MINOR, 3 );

  if (glfwExtensionSupported( "GLX_ARB_create_context_profile" ))
    glfwOpenWindowHint( GLFW_OPENGL_PROFILE,
                        GLFW_OPENGL_CORE_PROFILE );

  glfwOpenWindowHint( GLFW_FSAA_SAMPLES, 4 );

  if (!glfwOpenWindow( WINDOW_WIDTH, WINDOW_HEIGHT,
                       8, 8, 8, 8,
                       24,
                       0,
                       GLFW_WINDOW ))
  {
    glfwTerminate();
    fputs("Cannot create Window.\n", stderr);
    exit(1);
  }

  glfwDisable( GLFW_MOUSE_CURSOR );

  glfwSetWindowTitle( "GLSL Test" );
}

void destroyGLFW(void) { glfwTerminate(); }

void initGL(void)
{
  glViewport( 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT );

  glClearColor( 0, 0, 0, 1 );
  glClearDepth( 1.0f );

  glFrontFace( GL_CCW );
  glCullFace( GL_BACK );
  glEnable( GL_CULL_FACE );

  glDepthFunc( GL_LESS );
  glEnable( GL_DEPTH_TEST );
}

void initModel(void)
{
  struct tris_s
  {
    float pos[3];
    float color[3];
  };

  struct tris_s tris[] =
  {
    {{ 0.0f,  M_SQRT1_2, -1.0f}, {1.0f, 0.0f, 0.0f}},
    {{ -0.5f, -0.5f,      -1.0f}, {0.0f, 1.0f, 0.0f}},
    {{ 0.5f, -0.5f,      -1.0f}, {0.0f, 0.0f, 1.0f}}
  };

  glGenVertexArrays( 1, &amp; vao );
  glBindVertexArray( vao );

  glGenBuffers( 1, &amp; vbo );
  glBindBuffer( GL_ARRAY_BUFFER, vbo );
  glBufferData( GL_ARRAY_BUFFER, sizeof(tris), tris, GL_STATIC_DRAW );

  glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 
                         sizeof(struct tris_s), 
                         (void *)offsetof(struct tris_s, pos) );
  glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 
                         sizeof(struct tris_s), 
                         (void *)offsetof(struct tris_s, color) );

  glEnableVertexAttribArray( 0 );
  glEnableVertexAttribArray( 1 );

  glBindVertexArray( 0 );
  glBindBuffer( GL_ARRAY_BUFFER, 0 );
}

static char *LoadTextFile(const char *filename)
{
  char *buffer;
  FILE *fin;
  long size;
  jmp_buf jb;

  if ((fin = fopen(filename, "rb")) == NULL)
    return NULL;

  if (setjmp(jb))
  {
    fclose(fin);
    return NULL;
  }

  fseek(fin, 0L, SEEK_END);

  if ((size = ftell(fin)) < 0)
    longjmp(jb, 1);

  fseek(fin, 0L, SEEK_SET);

  if ((buffer = malloc(size + 1)) == NULL)
    longjmp(jb, 1);

  if (fread(buffer, size, 1, fin) < 1)
  {
    free(buffer);
    longjmp(jb, 1);
  }

  buffer[size] = '\0';

  fclose(fin);
  return buffer;
}

static GLuint LoadAndCompileShader(const char *filename, GLenum shadertype)
{
  GLuint shobj;

  if ((shobj = glCreateShader( shadertype )) != 0)
  {
    GLint status;
    GLint len;
    char *src;

    if ((src = LoadTextFile(filename)) == NULL)
    {
      glDeleteShader( shobj );
      return 0;
    }

    len = strlen(src);

    glShaderSource( shobj, 1, &amp; src, &amp; len );
    glCompileShader( shobj );

    free(src);

    glGetShaderiv( shobj, GL_COMPILE_STATUS, &amp; status );

    if (status == GL_FALSE)
    {
      GLint logsize;
      char *log;

      glGetShaderiv( shobj, GL_INFO_LOG_LENGTH, &amp; logsize );

      if ((log = malloc(logsize + 1)) != NULL)
      {
        glGetShaderInfoLog( shobj, logsize, &amp; len, log );
        log[len] = '\0';
        fprintf(stderr, "%s: %s\n", filename, log);
        free(log);
      }

      glDeleteShader( shobj );
      return 0;
    }
  }

  return shobj;
}

// Esta estrutura será usada para montar a tabela de
// shaders que usaremos para montar o programa.
struct shader_info_s
{
  char *filename;
  GLenum shadertype;
  GLuint shobj;
};

static void DeleteAllShadersOnList(GLuint prgobj, struct shader_info_s *shinf_ptr)
{
  for (; shinf_ptr - > filename; shinf_ptr++)
    if (shinf_ptr - > shobj)
    {
      if (prgobj)
        glDetachShader( prgobj, shinf_ptr - > shobj );

      glDeleteShader( shinf_ptr - > shobj );
      shinf_ptr - >
      shobj = 0;
    }
}

static GLuint CreateShadersProgram(struct shader_info_s *shinf_ptr)
{
  GLuint prgobj;
  struct shader_info_s *sptr = shinf_ptr;

  for (sptr = shinf_ptr; sptr - > filename; sptr++)
    if ((sptr - > shobj = LoadAndCompileShader(sptr - > filename, sptr - > shadertype)) == 0)
    {
      DeleteAllShadersOnList( 0, shinf_ptr );
      return 0;
    }

  if ((prgobj = glCreateProgram()) != 0)
  {
    GLint status;

    for (sptr = shinf_ptr; sptr - > shobj; sptr++)
      glAttachShader( prgobj, sptr - > shobj );

    glLinkProgram( prgobj );

    DeleteAllShadersOnList( prgobj, shinf_ptr );

    glGetProgramiv( prgobj, GL_LINK_STATUS, &amp; status );

    if (status == GL_FALSE)
    {
      GLint logsize, len;
      char *log;

      glGetProgramiv( prgobj, GL_INFO_LOG_LENGTH, &amp; logsize );

      if ((log = malloc(logsize + 1)) != NULL)
      {
        glGetProgramInfoLog( prgobj, logsize, &amp; len, log );
        log[len] = '\0';
        fprintf(stderr, "Link Error: %s\n", log);
        free(log);
      }

      glDeleteProgram( prgobj );

      return 0;
    }
  }

  return prgobj;
}

void initShaders(void)
{
  struct shader_info_s shaders[] =
  {
    { "test.vert", GL_VERTEX_SHADER, 0 },
    { "test.frag", GL_FRAGMENT_SHADER, 0 },
    { NULL, 0, 0 }
  };

  if ((prgo = CreateShadersProgram( shaders )) == 0)
  {
    fputs("Error creating shaders.\n", stderr);
    exit(1);
  }
}

void drawScene(void)
{
  glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

  glBindBuffer( GL_ARRAY_BUFFER, vbo );
  glBindVertexArray( vao );

  glUseProgram( prgo );
  glDrawArrays( GL_TRIANGLES, 0, 3 );
  glUseProgram( 0 );

  glBindVertexArray( 0 );
  glBindBuffer( GL_ARRAY_BUFFER, 0 );
}

Passando valores para os shaders (Uniforms)

Agora que já vimos como criar os programas com os shaders, eis o método de passar dados, além dos atributos de vértices, para os shaders. Existe um “tipo” de variável chamado uniform. Eles são constantes (no que concerne o shader) que são passadas para o programa. Considere o seguinte vertex shader:

// simple.vert
#version 330 core

layout(location=0) in vec3 vpos;

uniform mat4 projMat;
uniform mat4 modelviewMat;

void main()
{
  gl_Position = projMat * modelviewMat * vec4(vpos, 1.0);
}

Esse vertex shader precisa das matrizes de projeção e modelview para fazer a sua mágica. Só que essas matrizes não são passadas para o shader juntamente com os atributos dos vértices (no caso, vpos). O que temos que fazer é, depois de criar o programa (depois de “linkado”), obter a “localização” das variáveis “uniformes” e, de posse delas, inicializar as variáveis antes de chamar glDrawArays:

GLuint prgobj;
GLuint projMatLoc, modelviewMatLoc;

// Esses dois arrays contém as matrizes, inicializadas em algum outro lugar...</span>
float projectionMatrix[16], modelviewMatrix[16];

// Cria programa, conforme mostrado no post anterior.
if ((prgobj = CreateShadersProgram(myshaders)) == 0)
{
  // Trata erro aqui...
}

// Pega a localização dos uniforms no programa.
projMatLoc = glGetUniformLocation(prgobj, "projMat");
modelviewMatLoc = glGetUniformLocation(prgobj, "modelviewMat");

...
// Antes de usar glDrawArrays devemos incializar os uniforms:
...
glUseProgram(prgobj);
glUniformMatrix4x4fv(projMatLoc, 1, GL_FALSE, projectionMatrix);
glUniformMatrix4x4fv(modelviewMatLoc, 1, GL_FALSE, modelviewMatrix);
glDrawArrays( GL_TRIANGLES, 0, num_vertices );
glUseProgram(0);
...

Note que existe uma grande diferença entre tipos primitivos normais (float, por exemplo), vetores, matrizes, blocos e estruturas. Cada um desses “tipos” usa uma função de inicialização diferente. Existem várias funções do tipo glUniform*() no OpenGL, todas funcionam mais ou menos do mesmo jeito. Cabe a você escolher a mais adequada (aqui).

Carregando, compilando e usando shaders

Antes do OpenGL 4 os shaders precisam ser carregados em modo texto, compilados e linkados em “programas”. Este é o método que usarei, por enquanto, para manter alguma compatibilidade com OpenGL 3.3+. Eis a rotina, simples, para carga de um arquivo texto em uma string:

char *LoadTextFile(const char *filename)
{
  char *buffer;
  FILE *fin;
  long size;
  jmp_buf jb;

  if ((fin = fopen(filename, "rb")) == NULL)
    return NULL;
  
  if (setjmp(jb))
  {
    fclose(fin);
    return NULL;
  }
  
  fseek(fin, 0L, SEEK_END);
  if ((size = ftell(fin)) &lt; 0)
    longjmp(jb, 1);
  fseek(fin, 0L, SEEK_SET);
  
  if ((buffer = malloc(size + 1)) == NULL)
    longjmp(jb, 1);
  
  if (fread(buffer, size, 1, fin) &lt; 1)
  {
    free(buffer);
    longjmp(jb, 1);
  }
  
  buffer[size] = '\0';

  fclose(fin);
  return buffer;
}

Essa será a rotina usada para carregar nossos arquivos com os shaders.

Todo shader precisa ser compilado e associado aos outros shaders através de linkagem para compor um programa. Uma vez obtido o programa, os objetos dos shaders podem ser descartados. O código abaixo carrega e compila um shader:

// A função retorna o 'handle' do shader object ou 0, em caso de erro.
// 'shadertype' é, até agora, GL_VERTEX_SHADER ou GL_FRAGMENT_SHADER,
// mas podemos usar GL_GEOMETRY_SHADER, GL_TESS_CONTROL_SHADER,
// GL_TESS_EVALUATION_SHADER e GL_COMPUTE_SHADER no OpenGL 4+.
GLuint LoadAndCompileShader(const char *filename, GLenum shadertype)
{
  GLuint shobj;

  // Tenta criar um shader object.
  if ((shobj = glCreateShader(shadertype)) != 0)
  {
    GLint status;
    GLint len;
    char *src;

    // Tenta carregar o código do shader.
    if ((src = LoadTextFile(filename)) == NULL)
    {
      glDeleteShader(shobj);
      return 0;
    }
    len = strlen(src);

    // Insere o código no shader object e tenta compilá-lo.
    glShaderSource(shobj, 1, &src, &len);
    glCompileShader(shobj);

    // Não precisamos mais do código fonte!
    free(src);

    // Verifica se a compilação foi bem sucedida.
    glGetShaderiv(shobj, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE)
    {
      GLint logsize;
      char *log;

      glGetShaderiv(shobj, GL_INFO_LOG_LENGTH, &logsize);
      log = malloc(logsize+1);
      glGetShaderInfoLog(shobj, logsize, &len, log);
      log[len] = '\0';
      fprintf(stderr, "%s: %s\n", filename, log);
      free(log);

      glDeleteShader(shobj);
      return 0;
    }
  }
  
  return shobj;
}

A função de montagem (linkage) do programa ficará assim:

// shaders que usaremos para montar o programa.
struct shader_info_s {
  char *filename;
  GLenum shadertype;
  GLuint shobj;
};

// Rotina auxiliar local: Apaga os shaders objects da lista.
static void DeleteAllShadersOnList(GLuint prgobj, struct shader_info_s *shinf_ptr)
{
  for (;shinf_ptr->filename; shinf_ptr++)
    if (shinf_ptr->shobj)
    {
      // Só 'destaca' os shaders objects se um program object for fornecido.
      if (prgobj)
        glDetachShader(prgobj, shinf_ptr->shobj);
      glDeleteShader(shinf_ptr->shobj);
      shinf_ptr->shobj = 0;
    }
}

// Cria um program object a partir da lista de shaders.
GLuint CreateShadersProgram(struct shader_info_s *shinf_ptr)
{
  GLuint prgobj;
  struct shader_info_s *sptr = shinf_ptr;

  // Tenta carregar e compilar todos os shaders da lista.
  for (sptr = shinf_ptr; sptr->filename; sptr++)
    if ((sptr->shobj = LoadAndCompileShader(sptr->filename, sptr->shadertype)) == 0)
    {
      DeleteAllShadersOnList(0, shinf_ptr);
      return 0;
    }

  // Tenta criar um programa.
  if ((prgobj = glCreateProgram()) != 0)
  {
    GLint status;

    // Associa os shaders objects ao programa e os linka.
    for (sptr = shinf_ptr; sptr->shobj; sptr++)
      glAttachShader(prgobj, sptr->shobj);
    glLinkProgram(prgobj);

    // Depois de linkados, não precisamos mais dos shaders objects.
    DeleteAllShadersOnList(prgobj, shinf_ptr);

    // Verifica se a linkagem foi bem sucedida.
    glGetProgramiv(prgobj, GL_LINK_STATUS, &status);
    if (status == GL_FALSE)
    {
      GLint logsize, len;
      char *log;

      glGetProgramiv(prgobj, GL_INFO_LOG_LENGTH, &logsize);
      log = malloc(logsize + 1);
      glGetProgramInfoLog(prgobj, logsize, &len, log);
      log[len] = 0;
      fprintf(stderr, "Link Error: %s\n", log);
      free(log);

      // Já que temos um erro, livramo-nos do programa 'vazio'.
      glDeleteProgram(prgobj);

      return 0;
    }
  }

  return prgobj;
}

Um exemplo de uso da função acima e de como usar um programa contendo os shaders (assumindo que o programa não possui ‘uniforms’ e outras inicializações adicionais):

// Eis a lista de shaders para esse programa simples.
struct shader_info_s simple_shaders[] = {
  { "simple.vert", GL_VERTEX_SHADER, 0 },
  { "simple.frag", GL_FRAGMENT_SHADER, 0 },
  { NULL, 0, 0 }
};

// Tenta criar o programa.
if ((prgobj = CreateShadersProgram(simple_shaders)) == 0)
{
  // tratamento de erro aqui.
}

...
// Antes de chamar glDrawArrays() temos que "usar" o programa:
...
glUseProgram(prgobj);
glDrawArrays( GL_TRIANGLES, 0, num_vertices );
glUseProgram(0);    // 0 diz para não usar programa algum.
...

E é assim que shaders são usados! Sobre coisas como ‘uniforms’, falo depois…

Lá no início do post falei que antes da versão 4 do OpenGL os shaders poderiam ser carregados apenas em sua forma textual. Isso implica que, na versão 4, os programas podem ser carregados em seu formato binário, ou seja, já compilados. Isso pode ser feito pelas funções glGetProgramBinaryglProgramBinary, onde a primeira obtém o programa compilado e linkado e o coloca num buffer. A segunda função carrega o programa a partir de um buffer.

De fato, esse é um recurso interessante, mas tenha em mente que placas de vídeo diferentes podem ter GPUs diferentes e drivers diferentes. Daí, o formato do binário pode ser válido apenas para a sua máquina, tornando sua aplicação não-portável. Manter a carga de shaders via arquivos texto, acredito, ainda é o melhor jeito.

Modos Immediate versus Retained

Até agora tínhamos visto como enviar dados de vértices para o OpenGL através do modo conhecido como immediate. Neste modo, cada informação do vértice é informada através de uma chamada de função. Por exemplo, para desenhar um triângulo de coordenadas (0, 0.5, 0); (-0.5, -0.5, 0) e (0.5, -0.5, 0), nesta ordem, faziamos:

glBegin( GL_TRIANGLE );
  glVertex3f( 0, 0.5, 0 );
  glVertex3f( -0.5, -0.5, 0 );
  glVertex3f( 0.5, -0.5, 0 );
glEnd();

O modo é chamado immediate porque os dados são enviados “imediatamente”. Em contraste, o modo retained, usa buffers para “reter” as informações dos vértices e enviá-las de uma tacada só para o OpenGL (e para a placa de vídeo!).

No modo retained, no OpenGL, existe o conceito de Vertex Buffer Objects. São “objetos” que contém buffers de dados (não necessariamente de “vértices”, como o nome sujere). A idéia é que podemos copiar o conjunto de dados do ambiente “local” (nosso programa em C) para o ambiente do “servidor” (OpenGL). Eis como isso é feito:

// Neste array temos os trios de floats que compõem cada vértice.
GLfloat buffer[] =
{
  0,  0.5, 0,
  -0.5, -0.5, 0,
  0.5, -0.5, 0
};

// Este é o 'ponteiro' para o VBO.
GLuint vbo;

// Cria e liga o VBO ao contexto do OpenGL.
glGenBuffers( 1, &vbo );
glBindBuffer( GL_ARRAY_BUFFER, vbo );

// Copia o array dos vértices para o VBO.
glBufferData( GL_ARRAY_BUFFER, sizeof(buffer), buffer, GL_STATIC_DRAW );

// Informa ao OpenGL qual é a estrutura de um atributo do VBO.
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, NULL );

// Informa ao OpenGL que vamos usar o atributo 0.
glEnableVertexAttribArray(0);

// Finalmente, manda desenhar o buffer.
glDrawArrays( GL_ARRAY_BUFFER, 0, 3 );

O código acima é “quase” completo, mas falta um detalhe que explicarei adiante. Ele é bom para ilustrar como funciona o modo retained. Primeiro, precisamos criar, ligar e preencher um VBO. Depois, precisamos informar como o VBO é organizado, ou seja, quais são os atributos contidos no VBO. Finalmente, mandamos o OpenGL desenhar o array de 3 vértices, começando pelo vértice 0.

Para um simples triângulo isso ai não parece ser muito melhor que o modo immediate, mas para objetos com centenas ou milhares de vértices, faz muita diferença. Ao chamar glBufferData, passando o parâmetro GL_STATIC_DRAW, é quase garantido que todo o buffer será colocado na memória de vídeo ou, pelo menos, numa região de memória pertencente ao servidor” do OpenGL. No modo immediate isso é feito vértice por vértice.

O que falta na listagem acima? Outro conceito do OpenGL: Vertex Array Objects, que ao contrário do que nome diz, não são “arrays”, mas containers para estados relativos aos atributos de vértices. A idéia é armazenar os atributos de vértices em um objeto e os buffers em outros. Para desenhar o “modelo” basta então selecionar o VBO, o VAO e outros objetos relacionados a ele e chamar glDrawArrays. O código, para uma estrutura de modelo mais complexa, ficaria mais ou menos assim:

struct vertex_s
{
  float pos[3];
  float normal[3];
  float texcoord[2];
  float color[4];
};

#define VERTEX_SIZE sizeof(struct vertex_s)
#define ATTRIB_OFS(a) (void *)offsetof(struct vertex_s, (a))

// Assuma que o buffer 'data' será carregado em algum outro lugar.
struct vertex_s * data;
size_t num_vertices;

GLuint vao, vbo;

...

// Essa é a rotina que cria o VAO e o VBO.
glGenBuffers( 1, &vbo );
glBindBuffer( GL_ARRAY_BUFFER, vbo );
glBufferData( GL_ARRAY_BUFFER, num_versices *VERTEX_SIZE, data, GL_STATIC_DRAW );

glGenVertexArrays( 1, &vao );
glBindVertexArray( vao );
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, VERTEX_SIZE, ATTRIB_OFS(pos) );
glVertexAttribPointer( 1, 3, GL_FLOAT, GL_TRUE,  VERTEX_SIZE, ATRRIB_OFS(normal) );
glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, VERTEX_SIZE, ATRRIB_OFS(texcoord) );
glVertexAttribPointer( 3, 4, GL_COLOR, GL_FALSE, VERTEX_SIZE, ATTRIB_OFS(color) );
glEnableVertexAttribArray( 0 );
glEnableVertexAttribArray( 1 );
glEnableVertexAttribArray( 2 );
glEnableVertexAttribArray( 3 );

glBindBuffer( GL_ARRAY_BUFFER, 0 );  // desfaz o binding.
glBindVertexArray( 0 ); // desfaz o binding.
...

// Em alguma outra função, desenha o 'modelo':
glBindVertexArray( vao );
glBindBuffer( GL_ARRAY_BUFFER, vbo );
// glBindTexture( ... );
glDrawArrays( GL_TRIANGLES, 0, num_vertices );
glBindBuffer( GL_ARRAY_BUFFER, 0 );
glBindVertexArray( 0 );

Uma coisa digna de nota: Durante o ajuste dos atributos dos vértices, enquanto o VAO é definido, é necessário que o VBO esteja bounded! Senão, quando for chamar glDrawArrays, provavelmente você tomará um Segmentation Fault na cara. O motivo é que, embora o VAO não mantenha o estado de VBOs, ele precisa de um para fazer o binding dos atributos…

Shaders!

Você já ouviu falar muito sobre eles. Shaders nada mais são do que pequenos programas que são executados em paralelo no interior de sua placa de vídeo. Toda placa de vídeo moderna possui uma unidade de processamento gráfica (GPU), capaz de processar n rotinas simultâneamente. A placa de vídeo de meu desktop, por exemplo, é uma NVIDIA GeForce GT 635, capaz de processar 384 rotinas em paralelo ( especificação aqui ). Em essência, é isto, somada ao clock, texture fill rate e capacidade de memória que definem o poder de sua placa de vídeo… Memória, sozinha, só te diz o quanto de texturas e buffers a placa suporta, nada mais…

Daqui para frente veremos apenas as características de shaders relativos ao padrão OpenGL. Não muda lá grandes coisas com relação ao DirectX 10, só muda no nome dado aos tipos de shaders.

OpenGL lida com alguns tipos diferentes de shaders:

Vertex Shaders

São programinhas que lidam com os vértices que são fornecidos para a placa de vídeo, via vertex buffer objects, por exemplo. Sempre que receebe um vértice (ou um conjunto deles), a placa de vídeo executa um vertex shader.

Geometry Shader

Um geometry shader é um programinha que lida com um conjunto de vértices simultaneamente. Uma analogia é esta: Enquanto o vertex shader lida com os vértices separadamente, o geometry shader lida com triângulos, separadamente.

Tesselation Shaders (Control & Evaluation)

Esses são dois shaders, adicionados ao OpenGL 4.0, e especializados na operação de tesselation. Lembra-se que falei sobre clipping anteriormente, onde, ao recortar um triângulo o transformamos em dois triângulos? Isso pode ser feito por esses shaders.

Fragment Shader

Depois que um triângulo é rasterizado, ou seja, “preenchido”, o fragment shader é chamado para cada candidato à pixel… Essa é uma distinção importante: Pixels são artefatos visuais, você os vê na tela… fragmentos são pixels que ainda não são visíveis (e podem ser modificados ou descartados!). Este é o shader mais importante de todos. Graças ao paralelismo, minha placa de vídeo pode processar, potencialmente, 384 “fragmentos” de uma vez só…

Compute Shader

Este shader foi adicionado ao OpenGL 4.3. Trata-se de shaders genéricos, disassociados de operações gráficas específicas (pelo menos, em teoria).

Desses shaders os de uso obrigatório são vertex e fragment shaders.

Eis códigos de exemplo para ambos os shaders numa pequena aplicação que desenha um triângulo na tela:

/* simple.vert */
#version 130

in vec3 vrtx;
in vec4 clr;

out vec4 color;

void main()
{
  gl_Position = vec4(vrtx, 1.0);
  color = clr;
}
/* simple.frag */
#version 130

in vec4 color;

void main()
{
  gl_FragColor = color;
}

Os códigos lembram um bocado C, não é? Essa é uma das belezas de GLSL… Além de parecer muito com C, tem alguns atalhos para tipos mais complexos como vetores e matrizes (e também funções especializadas como derivação, produto escalar, vetorial, normalização, etc).

Cada shader tem o seu conjunto de “variáveis intrínsecas”, todas começadas com “gl_”. Vertex shaders são feitos para modificarem variáveis como gl_Position… Os fragment shaders modificam variáveis diferentes (um exemplo é gl_FragColor).

No exemplo acima, o vertex shader com nome “simple.vert” obtém dois atributos do vertex buffer object: A posição do vértice e a cor do mesmo. A função main() simplemente copia o vértice para gl_Position e repassa a cor para a variável de saída color (que será usada como entrada para o fragment shader). Esse é um exemplo simples. Num código mais “real” poderíamos passar matrizes de transformação e criamos algo assim:

gl_Position = ProjectionMatrix * ModelViewMatrix * vrtx;

Com isso poderíamos transladar os vértices, escaloná-los, rotacioná-los, etc… Matrizes e outras constantes são passadas ao shader via tipos especiais de variáveis “in” chamadas uniforms. Essencialmente, são constantes. Atributos sofrem interpolação, como você verá abaixo…

No nosso exemplo, o fragment shader, por sua vez, pega a cor e a copia para gl_FragColor, que será usada para acender um pixel no framenbuffer. Poderíamos passar para o fragment shader informações sobre o vetor de visualização, o vetor de irradiância de fontes luminosas, um intervalo de tempo, etc… tudo para calcular a cor do fragmento (pixel).

É importante notar que do vertex shader para o fragment shader existem interpolações. Ou seja, a cor do vértice inicial e final são respeitadas, mas as cores intermediárias são calculadas tomando esses dois como base. E, sim, os vértices também são “interpolados”… eles transformam-se em “linhas” que definem os “cantos” do triângulo! Abaixo temos o resultado com base no código de exemplo (que desenha um triângulo com vértices de cores diferentes). Leia as descrições nos comentários para melhor entendimento do código… No final deste artigo coloco também um link para uma página interessante sobre fragment shaders…

Saída do código de exemplo.

Saída do código de exemplo.

/* Para compilar o exemplo:
$ gcc -O3 -march=native `pkg-config --cflags glfw2 gl` -c exemplo.c
$ gcc -O3 -o exemplo exemplo.o -lm `pkg-config --libs glfw2 gl`
*/
#include <stdlib.h>
#include <stdio.h>
#include <setjmp.h>
#include <GL/glfw.h>
#include <GL/gl.h>

/* Tamanho padrão de nossa janela (framebuffer). */
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480

// Macro usada para atribuir offsets aos Vertex Attribute Pointers.
#define BUFFER_OFFSET(ofs) (NULL + (ofs))

// "Handle" do programa contendo shaders.
static unsigned int shaders_program;

static void InitOpenGL(void);
static char *LoadText(const char *filename);
static int LoadShaders(unsigned int *program);
static unsigned int CreateVAO(void);
static void DrawVAO(int vao, size_t triangles);

int main(void)
{
  /* *** INICIALIZAÇÃO do contexto gráfico usando GLFW 2 *** */
  /* OBS: Por que não uso GLFW 3? Por que não está disponível, por
          default, nos repositórios do Debian ou Ubuntu! E já tive
          problemas ao compilar o GLFW 3 para esses ambientes! */
  if (glfwInit() == GL_FALSE)
  {
    fprintf(stderr, "initializing graphics library");
    exit(EXIT_FAILURE);
  }

  /* O tratamento de eventos é feito "manualmente", daqui pra frente. */
  glfwDisable(GLFW_AUTO_POLL_EVENTS);

  /* Usando o core profile do OpenGL 3.0 (uma de minhas máquinas de teste
     só suporta a versão 3!) */
  glfwOpenWindowHint(GLFW_OPENGL_VERSION_MAJOR, 3);
  glfwOpenWindowHint(GLFW_OPENGL_VERSION_MINOR, 0);

  /* Se a extensão GLX_ARB_create_context_profile não estiver disponível,
     então: ajustar o profile é parcialmente suportado e;
     Se GLX_ARB_create_context_profile não estiver disponível, então usar
     GLFW_OPENGL_CORE_PROFILE fará glfwOpenWindow() falhar! */
  if (glfwExtensionSupported("GLX_ARB_create_context_profile"))
    glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

  /* Se a extensão GLX_ARB_multisample_extension não estiver disponível,
     então ajustar GLFW_FSAA_SAMPLES não tem efeito. */
  glfwOpenWindowHint(GLFW_FSAA_SAMPLES, 4);

  /* Cria a janela. */
  if ( !glfwOpenWindow(WINDOW_WIDTH, WINDOW_HEIGHT,
                       8, 8, 8, 8, /* rgba bits (8 para cada componente) */
                       24,         /* depth buffer bits (prefiro 24, é mais
                                      seguro que seja suportado) */
                       0,          /* Não vamos usar stencil buffer. */
                       GLFW_WINDOW) )
  {
    glfwTerminate();
    fprintf(stderr, "cannot open window");
    exit(EXIT_FAILURE);
  }

  /* Vamos sumir com o mouse. */
  glfwDisable(GLFW_MOUSE_CURSOR);

  InitOpenGL();

  if (LoadShaders(&shaders_program) != 0)
    exit(EXIT_FAILURE);

  CreateVAO();
  glUseProgram(shaders_program);

  for (;;)
  {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    /* NOTE: VAO já está "bound", neste ponto. */
    glDrawArrays(GL_TRIANGLES, 0, 3); /* count é o número de vertices, não o de
                                         triângulos! GL_TRIANGLES só indica que
                                         o OpenGL irá desenhar triangulos! */

    glfwPollEvents();

    /* Sai do loop se apertar ESC. */
    if (glfwGetKey(GLFW_KEY_ESC) == GLFW_PRESS)
      break;

    glfwSwapBuffers();
  }

  glfwTerminate();
  return 0;
}

// Initializa OpenGL.
static void InitOpenGL(void)
{
  glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);

  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  glClearDepth(1.0f);
  glEnable(GL_CULL_FACE);
  glFrontFace(GL_CCW);
  glCullFace(GL_BACK);

  /* Liga o teste de profundidade.
     Desnecessário neste teste! */
  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
}

// Função auxiliar: Apenas lê um arquivo texto e o coloca num buffer. */
static char *LoadText(const char *filename)
{
  long filesize;
  char *buffer = NULL;
  FILE *fin;
  jmp_buf jb;

  if ((fin = fopen(filename, "rb")) != NULL)
  {
    if (setjmp(jb))
    {
      fclose(fin);
      return NULL;
    }

    fseek(fin, 0L, SEEK_END);

    if ((filesize = ftell(fin)) <= 0)
      longjmp(jb, 1);

    fseek(fin, 0L, SEEK_SET);

    if ((buffer = malloc(filesize + 1)) == NULL)
      longjmp(jb, 1);

    if (fread(buffer, filesize, 1, fin) != 1)
    {
      free(buffer);
      longjmp(jb, 1);
    }

    *(buffer + filesize) = '\0';

    fclose(fin);
  }

  return buffer;
}

/* Carrega, compila e linka os shaders.

   Cada 'shader' precisa ter seu código fonte passado para
   o OpenGL, precisa ser compilado, separadamente, e depois linkado
   um 'programa'.

   Repare que isso é exatamente o que um compilador faz. Ou seja,
   GLSL é uma linguagem compilada. Código específico para a GPU
   é gerado e instalado na placa de vídeo.
*/
static int LoadShaders(unsigned int *program)
{
  char *vertex_shader;
  char *fragment_shader;
  int status;
  int len;
  char log[256];
  unsigned int vs, fs;

  /* Carrega vertex shader do disco. */
  if ((vertex_shader = LoadText("simple.vert")) == NULL)
  {
    fprintf(stderr, "Erro carregando Vertex Shader.\n");
    return -1;
  }

  /* Cria e compila o vertex shader. */
  vs = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vs, 1, &vertex_shader, NULL);
  free(vertex_shader); /* Não precisamos mais do código fonte. */
  glCompileShader(vs);

  /* Verifica se houveram erros de compilação. */
  glGetShaderiv(vs, GL_COMPILE_STATUS, &status);

  if (!status)
  {
    glGetShaderInfoLog(vs, sizeof(log) - 1, &len, log);
    fprintf(stderr, "Error compiling vertex shader:\n\t%s\n", log);
    return -1;
  }

  /* Mesma coisa para o fragment shader. */
  if ((fragment_shader = LoadText("simple.frag")) == NULL)
  {
    fprintf(stderr, "Erro carregando Fragment Shader.\n");
    return -1;
  }

  fs = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fs, 1, &fragment_shader, NULL);
  free(fragment_shader); /* Não precisamos mais do texto carregado. */
  glCompileShader(fs);
  glGetShaderiv(fs, GL_COMPILE_STATUS, &status);

  if (!status)
  {
    glGetShaderInfoLog(fs, sizeof(log) - 1, &len, log);
    fprintf(stderr, "Error compiling fragment shader:\n\t%s\n", log);
    return -1;
  }

  /* Cria um 'programa', adiciona os shaders a ele e os 'linka'. */
  *program = glCreateProgram();
  glAttachShader(*program, vs);
  glAttachShader(*program, fs);

  /* Como GLSL 1.3 (do OpenGL 3.0), aparentemente, não tem "layout"
     nos shaders, então é preciso definir a localização de variáveis
     globais (atributos) dos shaders aqui. Note que 'vrtx' e 'clr' são
     os atributos 'in' do vertex shader, na ordem que eles aparentecem.
  */
  glBindAttribLocation(*program, 0, "vrtx"); /* vertex */
  glBindAttribLocation(*program, 1, "clr"); /* color */

  glLinkProgram(*program);

  /* Verifica se houve erro de 'linkagem'. */
  glGetProgramiv(*program, GL_LINK_STATUS, &status);

  if (!status)
  {
    glGetProgramInfoLog(vs, sizeof(log) - 1, &len, log);
    fprintf(stderr, "Error linking shaders program:\n\t%s\n", log);
    return -1;
  }

  return 0;
}

/* Cria o <em>Vertex Array Object</em> e os <em>Vertex Buffer Objects</em>.
   Para esse exemplo escolhi criar buffers diferentes para
   os vértices e as cores dos vértices. Poderíamos ter criado
   um buffer só com uma estrutura contendo as duas informações. */
static unsigned int CreateVAO(void)
{
  /* Esses arrays vão desaparecer no final da função. */
  float points[] =
  {
    0.0f, 0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f
  };

  float colors[] =
  {
    0.0f, 0.0f, 1.0f, 1.0f,
    0.0f, 1.0f, 0.0f, 1.0f,
    1.0f, 0.0f, 0.0f, 1.0f
  };

  /* NOTE: Os vértices são definidos no sentido anti-horário, bem como
           a ordem das cores dos vértices. */

  unsigned int vao, vbo[2];

  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

  /* Cria 2 buffers objects. Um para conter os vértices e outro para
     as cores dos vértices. */
  glGenBuffers(2, vbo);

  /* Carrega o vbo com os vertices e associa os atributos. */
  glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, 0, 0, BUFFER_OFFSET(0));

  /* Carrega o vbo com as cores e associa os atributos. */
  glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 4, GL_FLOAT, 0, 0, BUFFER_OFFSET(0));

  /* Neste ponto o Vertex Array Object contém as referências aos buffer
     objects e aos atributos de cada buffer. Se fizessemos:
     glBindVertexArray(0), poderíamos recuperar todas as referências,
     refazendo o bind. Daí, é só chamar glDrawArrays para desenhar.

     Se não me engano, um Vertex Array também contém os binds
     de texturas. */

  /* NOTE: Neste exemplo, ao sair, o VAO continuará "bounded". */

  return vao;
}

Visite a página GLSL Sandbox Galery. lá você encontrará uma série de exemplos usando fragment shaders, usando a especificação OpenGL ES, existentes em browsers capazes de renderizar, via WebGL. Ou seja, funciona com qualquer um exceto o Internet Explorer! O que os autores fazem é desenhar um retângulo (QUAD) no framebuffer (canvas) e usar um fragment shader para obter os efeitos desejados (alguns muito interessantes!).

Mudança de base e “bump mapping”

Eis mais uma demonstração do poder das matrizes. Especialmente quanto falamos de mudança de base do sistema de coordenadas (ou, no jargão da álgebra linear: do subespaço vetorial).

Dê uma olhada nas 3 texturas abaixo:

Textura original, mapa de normais e textura modificada.

Textura original, mapa de normais e textura modificada.

A textura da esquerda é a original, parece com um muro de pedra, mas é “achatada” (se você rotacioná-la verá o efeito). A textura do meio, azulada, é, na verdade, um mapa com os vetores normais de cada pixel da textura, usada para causar o efeito final, o da textura da direita, onde o “rejunte” está  num nível abaixo do das pedras.

Cada componente da textura do meio equivale ao comprimento de um vetor numa base bem definida: R corresponde ao vetor associado ao eixo X; G ao Y; e B ao Z. A imagem é azulada porque o vetor Z é o mesmo do vetor normal original. O vetor normal original é o mais proeminente. Os outros dois são usados para modificar o vetor normal original. Por isso as bordas das pedras, nessa textura, têm cores tendendo ao violeta (componente R grande) ou ao “ciano” (componente G grande).

Visualmente, esses vetores correspondem ao diagrama abaixo:

Visualizando os vetores tangente e binormal.

Visualizando os vetores tangente e binormal.

Essa textura central é baseada, ou seja, tem como base do sistema de coordenadas, 3 vetores unitários: O vetor normal original e dois outros, chamados de tangente e binormal (ou bitangente). Tudo o que um shader tem que fazer para obter o vetor original é saber qual é a base e aplicar a mudança de base abaixo:

\displaystyle \vec{N}' = \begin{bmatrix} \vec{t}^T \\ \vec{b}^T \\ \vec{n}^T \end{bmatrix} \cdot \vec{N}

Onde \vec{t} é o vetor tangente, \vec{b} é o binormal e \vec{n} é o normal; obtidos do vertex buffer, junto com os outros atributos do vértice. \vec{N} são as 3 coordenadas obtidas da textura do meio e \vec{N}' é o vetor normal recalculado. Como quem fará esse calculo é o fragment shader, o vetor \vec{N} é obtido da textura via um Sampler. Ou seja, cada pixel da textura é um vetor. Mas, diferente da figura anterior, cada um dos vetores “tangentes” e o vetor “normal” são definidos para cada um dos vértices.

Note que a equação acima, na verdade, é uma conversão de base do sistema de coordenadas original, onde o vetor normal está para outro, baseado na superfície do objeto e \vec{N} “perturba” o vetor normal original… Ainda existe um passo intermediário: Os valores de \vec{N} precisam estar na faixa entre [-1,1], ao invés de [0,1], como acontece com os valores RGB numa textura. Isso pode ser facilmente feito escalonando \vec{N} duas vezes (multiplicando por 2) e subtraindo 1.

Calculando a iluminação:

Ainda não chegamos lá, mas um modelo para calcular iluminação (especialmente iluminação “ambiente”) é através do produto escalar entre o vetor normal e o vetor diretor da fonte de luz. Já que estamos “dando um solavanco” (bumping) os vetores normais de cada pixel, a impressão que ficará é a de que a superfície do objeto texturizado é, de fato, parecida com um muro, com rejunte e tudo. Pode-se enxergar esses “solavancos” na figura simplificada abaixo:

Antes e depois do "solavanco".

Antes e depois do “solavanco”.

O problema do bump mapping:

Nem tudo é perfeito… Bump Mapping tem um problema: A ilusão de profundidade só pode ser mantida em alguns casos. Se a superfície estiver voltada diretamente para a câmera, a ilusão ficará perfeita, mas rotacione a superfície e o encanto se quebra. A figura abaixo ilustra:

Bump Mapping e Parallax Mapping, lado a lado.

Bump Mapping e Parallax Mapping, lado a lado.

Do lado esquerdo temos um cubo texturizado e usando a técnica descrita acima. Do lado direito, temos o mesmo cubo, usando uma técnica diferente, mais complexa, chamada Steep Parallax Mapping.

Em ambos os casos temos o mesmo cubo (liso) com a mesma textura aplicada, mas com diferentes tipos de mapas de vetores normais, usando diferentes técnicas. Descreverei Steep Parallax Mapping assim que entendê-la melhor…

Isso não significa que bump mapping seja ruim… Dê uma olhada, por exemplo, no jogo Watch Dogs. Especialmente nos teclados que aparecem no jogo, sobre mesas… Eles são colocados lá por bump mapping e, ás vezes, dá para perceber o efeito acima… mas, só às vezes! :)

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.

Manipulação da câmera (viewpoint).

Existe uma maneira simples de lidar com a analogia da câmera, em OpenGL. Na biblioteca GLU (OpenGL Utilities) existe a função gluLookAt, que toma 3 vetores:

void gluLookAt(float eyeX, float eyeY, float eyeZ,
               float centerX, float centerY, float centerZ,
               float upX, float upY, float upZ);

O problema é que os vetores eye e up tem que ser perpendiculares.

Mas, se mudarmos o ponto de visão (o vetor “eye”), o vetor “up”, com toda certeza, não será mais perpendicular ao primeiro. E como não temos o terceiro vetor perpendicular como referência, não podemos usar um produto vetorial para calcular o novo vetor “up”. Poderíamos colocar um quarto vetor na jogada (além do vetor “eye” − que dá o ponto para onde estamos olhando; o vetor “center” − que nos dá a posição no espaço da câmera; e do vetor “up” − que nos diz onde é que nossa câmera interpreta “para cima”), mas isso só iria causar mais complicações e diminuiria a performance, já que teríamos que lidar com mais um vetor. Existe um jeito mais fácil.

Dado um vetor “eye” e um vetor “up”, não perpendicular, podemos simplesmente calcular a projeção do vetor “up” num vetor unitário baseado em “eye” e subtrair do vetor “up” essa projeção, lembrando de normalizar o vetor “up” no fim das contas. “Projeção” é “produto escalar”. A coisa fica assim:

static void RecalcUpVector(float *up, const float *eye)
{
  float w[3], v[4];

  /* w = normalize(eye); */
  Vector3Normalize(w, eye);

  /* up = up - (w . up)w */
  Vector3Scale(v, w, Vector3Dot(w, up));
  Vector3Sub(up, up, v);
  Vector3Normalize2(up);
}

Todo vetor tem 3 componentes. Para facilitar o entendimento eis como funciona com vetores 2D: Todo vetor 2D tem componentes X e Y. Ao projetar o vetor “up”, não perpendicular, sobre o vetor unitário colinear ao vetor “eye” temos um vetor apenas com o tamanho X de “up”. Sabendo que \vec{v}_{up} é dado por:

\vec{v}_{up} = \vec{v}_{up_x} + \vec{v}_{up_y}

vector

Para obter o vetor \vec{v}_{up_y}, que é perpendicular ao vetor unitário \vec{w} basta subtrair o vetor \vec{v}_{up_x} (a projeção de \vec{v}_{up} sobre \vec{w}) do próprio \vec{v}_{up}.

Ok, toda essa história de “projeção” e “normalização” pode ser resumida assim: A rotina acima substrai o componente do vetor “up” da parte que se encontra “sobre” o vetor “eye” e certifica-se que o novo vetor “up” tem tamnho igual a 1.

Se nossa câmera é definida somente com esses 3 vetores (eye, center e up), então o código para ajustar a matriz de transformação do sistema de coordenadas VIEW fica:

void SetupViewspaceMatrix(struct camera_s *camera)
{
  RecalcUpVector(camera->up, camera->eye);

  gluLookAt(camera->eye[0], camera->eye[1], camera->eye[2],
            camera->center[0], camera->center[1], camera->center[2],
            camera->up[0], camera->up[1], camera->up[2]);
}

Simples assim…

Para mover a cãmera, basta atualizar o vetor “center” (que é uma coordenada). Para rotacionar a câmera podemos usar a função Vector3RotateAroundAxis() e depois recalcular o vetor “up”. É claro que temos que ter cuidado para que os vetores “eye” e “up” não sejam colineares!

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!