Projeto Virtual Worlds

Computação Gráfica 3D em C

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.

Anúncios

VAOs e VBOs

Quando você lê sobre os Vertex Array Objects na especificação do OpenGL, a coisa pode ficar meio confusa. Em alguns casos você é levado a acreditar que esse objeto mantém o estado apenas dos mapas de atributos de um array de vértices (daí o nome), mas, de fato, ele mantém o estado, inclusive, dos Buffer Objects usados no mapeamento. Até 16 conjuntos de mapas de atributos e até 16 VBOs podem ter seus estados mantidos num único VAO.

VBOs, como já descrevi, existem como containers de dados que serão hospedados no servidor OpenGL, por exemplo, na memória da sua placa de vídeo, ao invés da memória do sistema. Isso acelera um bocado o processamento gráfico, mas cria uma complicação: As posições onde cada componente de um vértice contido nesse buffer precisam ser informadas para o shader através de funções como glVertexAttribPointer. Ou seja, “shader, a coordenada x,y,z está na posição 0 do VBO nº 2 e é composta com 3 floats… mas a cor do vértice está na posição 0 do VBO nº 3 (ou na posição 12 da mesma VBO nº 2) e tem tamanho…”. Esses são os “atributos” do vértice que glVerterAttribPointer dispobiliza e armazena na VAO.

Eis a primeira parte da lista de itens que uma VAO mantém:

Estado Tipo Tamanho Default
VERTEX_ATTRIB_ARRAY_ENABLED bool 16 false
VERTEX_ATTRIB_ARRAY_NORMALIZED bool 16 false
VERTEX_ATTRIB_ARRAY_INTEGER bool 16 false
VERTEX_ATTRIB_ARRAY_LONG bool 16 false
VERTEX_ATTRIB_ARRAY_SIZE uint 16 4
VERTEX_ATTRIB_ARRAY_STRIDE uint 16 0
VERTEX_ATTRIB_ARRAY_TYPE GLenum 16 GL_FLOAT
VERTEX_ATTRIB_ARRAY_POINTER void * 16 NULL
VERTEX_ATTRIB_ARRAY_DIVISOR uint 16 0

Repare que até 16 itens podem ser armazenados. A mesma coisa acontece com outros estados, exceto para o ELEMENT_ARRAY_BUFFER_BINDING, que refere-se ao VBO que contém a lista de índices que montam as faces do objeto a ser desenhado.

Quanto aos VBOs ligados ao VAO, seus estados são mantidos por 5 possíveis estados:

Estado Tipo Tamanho Default
VERTEX_ATTRIB_BUFFER_BINDING uint 16 0
VERTEX_ATTRIB_BINDING* uint 16 ?
VERTEX_ATTRIB_REL_ACTIVE_OFFSET uint 16 0
VERTEX_BINDING_OFFSET uint 16 0
VERTEX_BINDING_STRIDE uint 16 16

O VERTEX_ATTRIB_BINDING é um caso especial e vale ler a documentação da função glVertexAttrib (note a ausência de “Pointer” no final), mas, em resumo, os VBOs usados na VAO são mapeados.

Isso significa que, numa inicialização, você fez o binding de um VAO recém criado, e ajustou os atributos da estrutura de seu vértice a ser passada para o vertex shader usando até 16 VBOs, não será necessário refazer os bindings dos VBOs quando você re-selecionar a VAO antes da chamada a glDrawElements, por exemplo. O VAO mantém o binding de cada uma das 16 possíveis locations.

PS:

Existe um estado único (não são 16) extra chamado LABEL que é uma string. Mas isso é primariamente usado para debugging e não tem lá grande importância.

O que é e o que não é o OpenGL

Antes de continuar debulhando algorítmos e funções é interessante definir o que OpenGL pode fazer o que não pode. Alguns de vocês pode ter o conteito de que OpenGL é uma biblioteca mágica que engloba todas as funcionalidades possíveis para o desenho, em tempo real, de qualquer efeito ou recurso necessário para obter uma animação realista tridimensional. Se for assim, ficará desapontado… Grande parte do que pode ser visto em jogos, por exemplo, é criado por rotinas independentes do OpenGL (ou do DirectX, no caso do Windows)…

Em resumo, OpenGL é uma biblioteca que permite posicionar pontos no espaço tridimensional e projetá-los no espaço bidimensional correspondente a uma janela ou a tela do computador. Ele também contém alguns recursos interessantes como texturas. Ele não contém rotinas para renderizar sombras, iluminação, detecção de colisões, “radiosidade”, oclusão, … Tudo isso é feito fora do contexto do OpenGL ou, no máximo, no interior de rotinas especializadas chamadas de shaders − mesmo assim, são rotinas definidas pelo programador!

Você viu, anteriormente, que existem diversos sistemas de coordenadas usadas pelo OpenGL. Mas, o que ocorre, na realidade, é que só existem 2 sistemas: World CoordinatesScreen Coordinates. O segundo é a projeção do primeiro no plano xy. Para facilitar a vida criam-se outros sistemas com o auxílio de transformações lineares:

Transformações lineares.

Transformações lineares.

No OpenGL anterior à versão 3, as matrizes de transformação mostradas acima eram codificadas em funções do próprio OpenGL… Isso não existe mais. Para posicionar e transformar os objetos definidos no object space para o Screen Space temos que, explicitamente, definir as matrizes ModelView e Projection e passá-las para o vertex shader. Isso significa que esses “espaços” são construções meramente matemáticas e já fazem parte do OpenGL. Um vertex shader típico seria:

#version 430 core

layout(location=0) in vec3 vpos;

uniform mat4 modelviewMatrix;
uniform mat4 projectionMatrix;

void main()
{
  gl_Position = projectionMatrix * modelviewMatrix * vec4(vpos, 1.0);
}

Onde modelviewMatrixprojectionMatrix são definidas no programa host. A primeira transforma o objeto definido no object space para o view space (ou camera space) e a segunda faz a projeção do espaço tridimensional do view space para o espaço bidimensional do Screen space.

Note também que OpenGL não lida, diretamente, com a figura geométrica como um todo… Ao definir um triângulo, definimos apenas as características dos vértices. As informações de preenchimento e contorno dos lados são interpolações lineares baseadas nos atributos dos vértices. Interpolações lineares seguem a equação abaixo:

\displaystyle \dot{p}'=(1-n)\dot{p}_1+n\dot{p}_2,\quad n \in [0,1]

Por causa dessa interpolação, OpenGL não garante que duas placas de vídeo desenharão triângulos do mesmo jeito. Praticamente tudo o que OpenGL garante é que a sequência dos comendos será obedecida… Ao mandar OpenGL desenhar um triângulo ele o fará, mas se as bordas serão desenhadas antes do preenchimento, ou vice-versa, isso não é garantido.

Outra garantia do OpenGL: Uma vez definido o viewport (a área retangular que será visível no plano da tela), os limites de coordenadas são padronizados em [-1, 1]. Ou seja, o canto inferior esquedo da área visível é (-1,-1) e o canto superior direito (1,1). Ainda, o centro da área visível é a origem do sistema de coordenadas (0,0)… Observe, de novo, como defini o triângulo no exemplo do post anterior… Lá, a sequência de coordenadas é definida no sentido anti-horário e a primeira coordenada é (0, 0.707, -1). Ou seja, o topo do triângulo está a pouco mais de 70% do caminho entre o centro da janela e o topo. Ao mesmo tempo, os outros dois pontos são definidos com suas coordenadas X como -0.5 e 0.5, respectivamente:

Saída do código de exemplo.

Saída do código de exemplo.

Assim, o escalonamento é importante. Ao definir seu mundo em uma escala, você será obrigado a modificá-la para caber no padrão do OpenGL (e isso pode ser feito na matriz modelview). E, é claro, dependendo do aspect ratio, esse exemplo ai em cima pode ficar distorcido… No exemplo criei uma janela de aspect ratio 16:9, já que esse é o formato de meu monitor de vídeo… Ou seja, nesse formato é garantido que o pixel terá o formato “quadrado” esperado. Se tivesse escolhido uma resolução de 640×480, ou seja, com aspect ratio de 4:3, o triângulo seria mais achatado, horizontalmente.

 

Organizando a “zona”…

Confesso que, do jeito que estão, todos os textos sobre manipulação de matrizes ficaram bem confusos. Tentei misturar dois conceitos completamente diversos num só lugar e é importante separá-los… No OpenGL as matrizes são column-major, ou seja, a sequência de valores é organizada por coluna, não por linha… Mas, na literatura, toda manipulação matricial é feita por linha, ou seja, o índice da linha vem antes do índice da coluna. Usarei a notação tradicional daqui por diante…

Um vetor \vec{v} deve ser escrito como:

\displaystyle \vec{v}=\begin{pmatrix}  v_x \\  v_y \\  v_z  \end{pmatrix}

Ou seja, temos uma coluna de valores x, y e z. Ao passo que, uma matriz pode ser entendida como sendo composta de 3 vetores:

\displaystyle M=\begin{bmatrix}  \vec{i} & \vec{j} & \vec{j}  \end{bmatrix} = \begin{bmatrix}  i_x & j_x & k_x \\  i_y & j_y & k_y \\  i_z & j_z & k_z  \end{bmatrix}

Anteriormente eu tinha dito que esses vetores eram “transpostos”… Agora, essa é a definição de uma matriz, de acordo com o método tradicional!

Neste caso, os vetores \vec{i}, \vec{j} e \vec{k} são chamados vetores diretores. Eles são usados naquilo que é conhecido como combinação linear. Isso quer dizer que numa multiplicação da matriz M por um vetor \vec{v}, obteremos:

\displaystyle \vec{w}=\begin{bmatrix}  \vec{i} & \vec{j} & \vec{k}  \end{bmatrix} \cdot  \begin{pmatrix}  v_x \\  v_y \\  v_z  \end{pmatrix} = \begin{bmatrix}  \vec{i}\cdot v_x & \vec{j}\cdot v_y & \vec{k}\cdot v_z  \end{bmatrix}

É como se cada componente do vetor \vec{v} aplicasse um “peso” a um vetor diretor contido na matriz. O vetor \vec{v} é transformado para o vetor \vec{w}, de acordo com os vetores diretores contidos na matriz M.

Quero dizer que, ao aplicar a matriz com os vetores diretores ortogonais (perpendiculares entre si) muda as coordenadas do vetor \vec{v} de acordo com o sisetma de coordenadas dado pelos vetores diretores. Se rotacionarmos o sistema de coordenadas em 45°, por exemplo, o vetor \vec{v} permanecerá na mesma posição no novo sistema de coordenadas, o que significa que que estamos transformando \vec{v}.

Outra maneira de encarar matrizes é usar os vetores diretores transpostos:

\displaystyle \vec{w}=\begin{bmatrix}  \vec{a^T}\\  \vec{b^T}\\  \vec{c^T}  \end{bmatrix}\cdot\vec{v}=\begin{pmatrix}  \vec{a^T}\cdot\vec{v}\\  \vec{b^T}\cdot\vec{v}\\  \vec{c^T}\cdot\vec{v}\end{pmatrix}

Isto é uma transformação de sistemas de coordenadas. O vetor \vec{v} permanece no mesmo lugar no sistema de coordenadas original, mas é alterado no novo sistema. Ao multiplicar pela matriz de vetores diretores transposta estamos transformando o sistema de coordenadas, não o vetor \vec{v}.

Técnicamente, a matriz não deve ser uma transposta os vetores diretores originais, mas sua inversa. Mas, como estamos trabalhando com sistemas de coordenadas ortogonais, a inversa da matriz e sua transposta são a mesma coisa…

Uma explicação para o uso de matrizes com vetores diretores “diretos” e “transpostos”…

Para ilustrar a diferença, suponha que tenhamos um ponto \vec{p} na posição (2,1) no sistema de coordenadas tradicional (em azul). Em vermelho temos um sistema de coordenadas diferente, dados pelos vetores diretores \vec{i}=(\frac{\sqrt{2}}{2},\frac{\sqrt{2}}{2}) e \vec{j}=(\frac{\sqrt{2}}{2},\frac{-\sqrt{2}}{2}). Ambos são unitários e perpendiculares entre si:

Sistemas de coordenadas diferentes.

Sistemas de coordenadas diferentes.

Isso nos dá uma matriz com os vetores diretores (para o sistema de coordenadas vermelho) regido pela seguinte equação:

\displaystyle M=\begin{bmatrix} \vec{i} & \vec{j}\end{bmatrix}=  \begin{bmatrix}  \begin{pmatrix} \frac{\sqrt{2}}{2} \\ \frac{\sqrt{2}}{2} \end{pmatrix} &  \begin{pmatrix} -\frac{\sqrt{2}}{2} \\ \frac{\sqrt{2}}{2} \end{pmatrix} \end{bmatrix}=  \frac{1}{2}\cdot\begin{bmatrix} \sqrt{2} & -\sqrt{2} \\ \sqrt{2} & \sqrt{2} \end{bmatrix}

Ao fazermos a multiplicação desta matriz pelo ponto \vec{p}, obteremos:

\displaystyle \vec{{p}'} = M \cdot \vec{p} =  \frac{1}{2}\cdot\begin{bmatrix} \sqrt{2} & -\sqrt{2} \\ \sqrt{2} & \sqrt{2} \end{bmatrix} \cdot \begin{pmatrix} 2 \\ 1 \end{pmatrix} \approx \begin{pmatrix} 0.7 \\ 2.12\end{pmatrix}

Se você plotar o novo ponto \vec{{p}'} verá que ele está na posição (2,1) no sistema de coordenadas vermelho!

O que acontece quando usamos M^{-1}? Obteremos o ponto \vec{{p}'}=(2.12, 0.7) e se plotarmos o novo ponto \vec{{p}'} no sistema de coordenadas vermelha, veremos que ele fica no mesmo lugar, no sistema de coordenadas azul. Estamos então mapeando o ponto \vec{p} no sistema de coordenadas vermelho, mas mantendo a mesma posição em que ele estava, no azul!

A diferença é sutil: No primeiro caso o ponto \vec{{p}'} mantém a posição relativa em ambos os sistemas de coordenadas, sendo transformado de uma para outra. No segundo caso ele mantém a posição relativa ao sistema de coordenadas original, sendo mapeado ao ponto correspondente no sistema de coordenadas modificado.

É por isso que, quando queremos mudar o sistema de coordenadas model-view para o sistema de coordenadas da câmera, via função gluLookAt, usamos a matriz transposta dos vetores diretores do novo sistema de coordenadas (o da câmera): Queremos que os pontos permanecam na mesma posição em relação ao sistema model-view, mas “aplicando” o sistema da câmera.

Sobre os cálculos matriciais…

Então, por favor, não confunda a ordenação dos itens de uma matriz, usada pelo OpenGL, com o modelo matemático… são coisas diferentes…

Internamente, a sequência de componentes de uma matriz corresponde a uma coluna, os componentes x,y e z de um vetor ficam numa coluna, na sequência em que aparecem. Assim, para multiplicar uma linha por um vetor teríamos que transpor a matriz. Porque OpenGL usa essa organização, me escapa completamente (DirectX usa row-major, só para citar!). Talvez seja porque fica mais fácil copiar vetores para uma matriz, mas certamente dificulta a manipulação da mesma (internamente).

Felizmente, com a depreciação de funções fixas do OpenGL e o uso intensivo de shaders, passar matrizes row major para o GLSL é muito simples, mesmo que o padrão seja column major.

Uma coisa óbvia… e “enchendo linguiça”…

Não sei se você reparou, mas não existem “superfícies sólidas” em computação gráfica!

Escrevo isso só para deixar isso claro: Qualquer aparência de “solidez” é ilusória, no processamento de imagens em 3D. Tudo o que você vê são polígonos (triângulos, na maioria das vezes) que estão voltados para o observador. Os outros triângulos, não visíveis, simplesmente não são desenhados. Ainda, às vezes, a aparência de superfícies irregulares é ilusória também! Dê uma olhada no tópico sobre “bump mapping”, por exemplo.

A tarefa de um graphics engine é, portanto, decidir quais triângulos serão desenhados e em que ordem, bem como “recortá-los”, se for necessário, ajustanto também as coordenadas de texturas para que “cantos” invisíveis não sejam processados pela placa de vídeo. A placa-de-vídeo e a biblioteca gráfica (como OpenGL), por sua vez, também suportam “recortes automáticos”, mas é interessante não abusar desse recurso.

Ok, estou “enchendo linguiça” aqui porque já faz um tempo que não ponho minhas mãos no projeto… Em breve, espero, mostrarei como montar uma Binary Spatial Partition Tree, para ordenar polígonos e desenhá-los numa ordem pré-definida, bem como o uso de “portais”… Segurem suas calças porque o assunto é um cadinho espinhoso…. mas, é interessante…

[]s
Fred

Uma diferença impressionante numa rotina básica.

A rotina de normalização de um vetor, aquela que transforma um vetor qualquer num vetor unitário, constitui-se em dividir os componentes x, y e z do vetor original pelo seu tamanho… Mas, essa é uma das rotinas fundamentais de todo o projeto e, por isso, tem que ser feita de forma rápida, bem rápida! Vejamos duas implementações possíveis:

void vec3_normalize_mul(float *v)
{
  float len = vec3_length(v);

  if (len != 0.0f)
  {
    len = 1.0f / len;

    v[0] *= len;
    v[1] *= len;
    v[2] *= len;
  }
}

void vec3_normalize_div(float *v)
{
  float len = vec3_length(v);

  if (len != 0.0f)
  {
    v[0] /= len;
    v[1] /= len;
    v[2] /= len;
  }
}

Essencialmente as duas rotinas fazem a mesma coisa. Mas, se você medir a quantidade de ciclos de clock gastos por ambas terá uma surpresa…  A primeira rotina gasta cerca de 350 ciclos, mas a segunda gasta cerca de 2500 ciclos! Ou seja, fazer uma divisão e 3 multiplicações é quase 615% mais rápido do que fazer três divisões!

Evite divisões!

Livro novo sendo feito….

Alguns de vocês já sabem que em 1994 eu publiquei um conjunto de textos que ficou sendo conhecido como “Curso de Assembly”. Esses textos são, hoje, meio que obsoletos – embora ainda sejam válidos. Desde essa época tenho pensado em escrever um livro “de verdade”, mas que seja um pouco mais abrangente e mais atualizado.

Well… estou escrevendo um com o título “C & Assembly para arquitetura x86-64” e o rascunho mais recente está disponível no grupo, de mesmo nome, no Facebook: aqui

Para quem não tem Facebook, a versão mais recente do rascunho está disponível no Google Drive: aqui.

Nem só de gráficos vive o 3D

Até agora tenho postado informações a respeito do uso do OpenGL para apresentar modelos tridimensionais. Como nem só de gráficos vive uma simulação, eis algumas informações sobre áudio em 3D… Provavelmente você já sabe que o ‘GL’, do OpenGL, significa Graphics Library. Vou mostrar aqui um pouco sobre o OpenAL (AL, de Audio Library).

No que se refere à biblioteca, seu computador tem um ou mais audio devices. Ao invés de falarmos “placa de som”, um device é mais genérico: Podemos usar um arquivo como um device (a saída do áudio será, por exemplo, um arquivo MP3, ao invés dos alto-falantes do seu headphone). Ainda, em teoria, você pode lidar com mais de um device. Na prática, você terá apenas um dispositivo.

Cada device suporta um ou mais contextos… Um contexto é um container de estados associados a um device. No caso do OpenAL, um contexto contém um listener, uma ou mais fontes de áudio e um ou mais buffers…

O Listener é o objeto relacionado ao ouvinte (você). Existem, ainda, 2 objetos que você pode criar à vontade (existem limites, claro!): Sources e Buffers. Onde, source é uma “fonte de áudio” e buffer é onde os dados do áudio são armazenados. Esse isolamento permite que duas fontes de áudio compartilhem um único buffer, por exemplo. Em resumo, temos um device, que possui um contexto, que incorpora um listener e um ou mais sources que, por sua vez, estão atrelados a buffers.

Tanto o listener quanto os sources são posicionados no espaço 3D através de coordenadas X,Y e Z, com os eixos orientados pela regra da mão direita (como no OpenGL):

Right Hand

Mas, diferente do OpenGL, não há múltiplos espaços vetoriais, apenas um que chamarei daqui por diante de sound space, para não confundir com os espaços vetoriais do OpenGL. Isso quer dizer que não precisamos nos preocupar com coordenadas homogêneas, por exemplo. O posicionamento dos objetos é feito de uma maneira bem simples:

alListener3f(AL_POSITION, x, y, z);

alSource3f(source, AL_POSITION, x, y, z);

Aqui, a variável source é um inteiro sem sinal, do tipo ALuint, que é “alocado” pela função alGetSources(), bem parecido com algumas funções do OpenGL:

ALuint source;

alGetSources(1, &source);

Veremos depois que um buffer é “alocado” de forma semelhante.

Além do posicionamento, tanto o listener quanto os sources têm conjuntos de atributos que podem ser alterados. O listener, por exemplo, tem o atributo AL_ORIENTATION que especifica dois vetores perpendiculares que apontam, respectivamente, para frente e para cima do ponto onde está posicionado o ouvinte. Isso permite à biblioteca definir onde estão seus ouvidos direito e esquerdo. Provavelmente não é necessário que ambos os vetores sejam perpendiculares e/ou que sejam unitários (é provável que OpenAL os normalize e “acerte” o vetor up com base no vetor fornecido e o vetor frontal, do mesmo jeito que a antiga função gluLookAt(), do OpenGL, fazia). Mesmo que isso seja verdade (não testei), é sempre bom dar uma mãozinha à biblioteca… Você poderá usar a dica da função RecalcUpVector citada aqui.

O listener têm os seguintes atributos: AL_GAIN, AL_POSITION, AL_VELOCITY e AL_ORIENTATION. O atributo AL_GAIN pode ser interpretado como um volume “master”. AL_VELOCITY (tanto do listener quanto dos sources) é usado nas rotinas que lidam com efeito doppler (que não cobrirei aqui, por enquanto).

Já para os sources, ajustar a posição e associar um buffer ao source parece ser o suficiente, mas os seguintes atributos estão disponíveis: AL_GAIN, AL_MIN_GAIN, AL_MAX_GAIN, AL_POSITION, AL_VELOCITY, AL_DIRECTION, AL_SOURCE_RELATIVE, AL_REFERENCE_DISTANCE, AL_MAX_DISTANCE, AL_ROLLOFF_FACTOR, AL_CONE_INNER_ANGLE, AL_CONE_OUTER_ANGLE, AL_PITCH, AL_LOOPING, AL_MSEC_OFFSET, AL_BYTE_OFFSET, AL_SAMPLE_OFFSET, AL_BUFFER, AL_SOURCE_STATE, AL_BUFFER_QUEUED, AL_BUFFERS_PROCESSED. A quantidade de atributos é maior porque queremos poder alterar mais as características das fontes do que as do listener. Alguns atributos são fáceis de entender:

AL_SOURCE_RELATIVE é um atributo do tipo TRUE ou FALSE. Se estiver ligado (AL_TRUE), significa que a posição da fonte de áudio é relativa ao listener. Tome o exemplo de fontes de ouvido: Você pode mexer a cabeça de um lado para outro, levantar e abaixar, que as posições relativa dos fontes continuarão as mesmas (eles se movem junto com a sua cabeça). Ao dizer que o source tem posição relativa, você informa que o vetor em AL_POSITION do source é sempre relativa ao AL_POSITION do listener, caso contrário, o source será colocado no sound space em relação à origem do sistema de coordenadas (0,0,0).

Por padrão, os sources são omni-direcionais. Isso é, não têm direção. Mas podemos direcionar o áudio usando o atributo AL_DIRECTION. Ele é um vetor (unitário, de preferência) que aponta para a direção em que o cone de áudio estará apontando. AL_CONE_INNER_ANGLE e AL_CONE_OUTER_ANGLE especificam o cone interno (100% do ganho) e o cone externo (atenuação, interpolada linearmente, até total silêncio), como mostrado abaixo:

openal3dcone2-4

Mas, o atributo mais importante dos sources é AL_BUFFER. Ele aceita um valor inteiro sem sinal que contém o identificador do buffer que contém os samples do áudio. Para tanto, é necessário criar um objeto buffer, carregá-lo e associá-lo ao source:

ALuint buffer;
void *data;
ALuint size, freq;

alGetBuffers(1, &buffer);
alBufferData(buffer, AL_MONO16, data, size, freq);
alSourcei(source, AL_BUFFER, buffer);

A função alBufferData aloca e inicializa um objeto buffer. Isso é necessário porque algumas placas de áudio possuem memória interna, como as antigas SoundBlaster AWE32 e a Gravis Ultrasound. Isso permite a aceleração de hardware, se houver alguma. Uma vez que o buffer foi criado e preenchido, associamos com o source usando o atributo AL_BUFFER, como mostrado acima.

No exemplo que mostrarei aqui, usei uma função da biblioteca ALUT (OpenAL UTilities) que carrega um áudio de um arquivo e cria o buffer: alutCreateBufferFromFile().

Uma informação importante é que as fontes de áudio são são capazes de posicionarem o áudio se esses forem MONO. OpenAL suporta buffers com áudio STEREO, mas não permite que sejam posicionados… Isso é útil quando você quer uma música de fundo: Posicione uma simples fonte de áudio no centro do sound space, associe-a com um buffer com a música STEREO, mude o AL_GAIN da fonte para um valor mais baixo que 1.0 e, voilà… musica de fundo. Se quiser música STEREO, ou com múltiplos canais, posicionada, precisará criar fontes isoladas para cada canal e posicioná-las onde quiser no sound space.

Quando o listener e os sources (lembre-se podem haver diversos!) forem posicionados, orientados, direcionados, …, basta mandar o OpenAL tocar os sources com alSourcePlay() ou alSourcePlayv(). O primeiro inicia um único source e o segundo permite o início de um array de sources.

Eis um exemplo… O arquivo wilhelm.wav está disponível no github (aqui), no meu sandbox, junto com este código e o Makefile, no diretório tests/openal:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

// Precisa do libopenal-dev e libalut-dev instalados.
// Ou o equivalente dessas libs, se for Windows.
#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alut.h>

int main(int argc, char *argv[])
{
  ALCdevice *device;
  ALCcontext *context;
  ALuint source;
  ALuint buffer;
  ALenum source_state;
  ALfloat listenerOrientation[] = { 0, 0, -1,   // Para Frente.
                                    0, 1,  0 }; // Para cima

  // Posições da fonte de áudio.
  ALfloat positions[] = { 0,  0,  1,   // Atrás
                          0,  0, -1,   // Na frente
                          1,  0,  0,   // Direita
                         -1,  0,  0 }; // Esquerda
  int i;

  // alutCreateBufferFromFile() precisa que o alut seja inicializado.
  // É preciso iniciar ALUT sem inicializar o contexto.
  if (alutInitWithoutContext(&argc, argv) == AL_FALSE)
  {
    fprintf(stderr, "Error initializing ALUT.\n");
    return 1;
  }

  // Pega o dispositivo default.
  if ((device = alcOpenDevice(NULL)) == NULL)
  {
    alutExit();
    fprintf(stderr, "Erro inicializando dispositivo.\n");
    return 1;
  }

  // cria o contexto default para o dispositivo.
  context = alcCreateContext(device, NULL);

  if ((context == NULL) || (alcMakeContextCurrent(context) == AL_FALSE))
  {
    fprintf(stderr, "Erro ajustando contexto.\n");
    alcCloseDevice(device);
    alutExit();
    return 1;
  }

  // Ajusta o listener.
  alListener3f(AL_POSITION, 0, 0, 0);
  alListenerfv(AL_ORIENTATION, listenerOrientation);

  // Cria source.
  alGenSources(1, &source);

  // Cria buffer e carrega áudio.
  // wilhelm.wav é um áudio em MONO16.
  if ((buffer = alutCreateBufferFromFile("./wilhelm.wav")) == 0)
  {
    alDeleteSources(1, &source);
    alcMakeContextCurrent(NULL);
    alcCloseDevice(device);
    alutExit();
    fprintf(stderr, "Error creating buffer.\n");
    return 1;
  }

  // Liga buffer ao source.
  alSourcei(source, AL_BUFFER, buffer);

  // Tenta tocar nas 4 posições.
  for (i = 0; i != 4; i++)
  {
    printf("Playing sample at position (%.1f, %.1f, %.1f)\n",
           positions[3 * i], positions[3 * i + 1], positions[3 * i + 2]);

    // Ajusta a posição do source e toca-o.
    alSourcefv(source, AL_POSITION, &positions[3 * i]);
    alSourcePlay(source);

    // Fica no loop, enquanto estiver tocando.
    do
    {
      alGetSourcei(source, AL_SOURCE_STATE, &source_state);
    } while (source_state == AL_PLAYING);

    sleep(1);
  }

  // Seja educado e limpe tudo antes de sair!
  alSourcei(source, AL_BUFFER, 0);
  alDeleteBuffers(1, &buffer);
  alDeleteSources(1, &source);
  alcMakeContextCurrent(NULL);
  alcDestroyContext(context);
  alcCloseDevice(device);
  alutExit();

  return 0;
}

Uma pequena observação:

Não há necessidade de esperar pelo término do áudio para ajustar novos parâmetros para o listener ou o source. O loop interno, que verifica o status do source, só está ai por conveniência, para ouvirmos o áudio em momentos distintos.

A documentação do OpenAL 1.1 (a mais recente, atualmente) pode ser encontrada aqui.

UPDATE

O código abaixo serve apenas para mostrar informações sobre a biblioteca OpenAL instalada no seu sistema:

// Compilar com:
// gcc -O3 -o alinfo alinfo.c -lopenal
//
#include <stdio.h>
#include <string.h>
#include <malloc.h>
#include <AL/al.h>
#include <AL/alc.h>

int main(int argc, char *argv[])
{
  ALCdevice *device;
  ALCcontext *context;
  const char *version, *renderer, *vendor, *extensions;
  char *e, *p;

  if ((device = alcOpenDevice(NULL)) == NULL)
  {
    fprintf(stderr, "ERRO ao tentar abrir dispositivo.\n");
    return 1;
  }

  if ((context = alcCreateContext(device, NULL)) == NULL)
  {
    alcCloseDevice(device);
    fprintf(stderr, "ERRO ao tentar criar contexto default.\n");
    return 1;
  }

  alcMakeContextCurrent(context);

  version = alGetString(AL_VERSION);
  renderer = alGetString(AL_RENDERER);
  vendor = alGetString(AL_VENDOR);
  extensions = alGetString(AL_EXTENSIONS);

  printf("Version: %s\n"
         "Renderer: %s\n"
         "Vendor: %s\n"
         "Extensions:\n",
         version, renderer, vendor);

  e = strdup(extensions);

  p = strtok(e, " ");

  while (p)
  {
    printf(" %s\n", p);
    p = strtok(NULL, " ");
  }

  free(e);

  alcMakeContextCurrent(NULL);
  alcDestroyContext(context);
  alcCloseDevice(device);

  return 0;
}

Em um dos ambientes em que tenho para testes, o código acima cuspiu a seguinte saída:

$ ./alinfo
Version: 1.1 ALSOFT 1.14
Renderer:
OpenAL Soft
Vendor:
OpenAL Community
Extensions:
AL_EXT_ALAW
AL_EXT_DOUBLE
AL_EXT_EXPONENT_DISTANCE
AL_EXT_FLOAT32
AL_EXT_IMA4
AL_EXT_LINEAR_DISTANCE
AL_EXT_MCFORMATS
AL_EXT_MULAW
AL_EXT_MULAW_MCFORMATS
AL_EXT_OFFSET
AL_EXT_source_distance_model
AL_LOKI_quadriphonic
AL_SOFT_buffer_samples
AL_SOFT_buffer_sub_data
AL_SOFTX_deferred_updates
AL_SOFT_direct_channels
AL_SOFT_loop_points

Procure informações no Google sobre essas extensões…

Array de estruturas ou Estrutura com arrays?

Existem algumas maneiras de montar o buffer contendo os atributos de vértices de um triângulo. Um vértice possui atributos como: coordenada, vetor normal, coordenada de textura, cor. Podemos agrupar esses atributos numa estrutura única e criar um array de estruturas (Array of Structures, AoS):

struct vertice_attrib_s {
  float pos[3];
  float normal[3];
  float texcoord[2];
  float color[4];     // RGBA
};

struct vertice_attrib_s buffer[NUM_VERTICES];

Ou podemos criar um grande buffer particionado (Structure of Arrays, SoA):

// Neste buffer os offsets definidos abaixo
// determinam o início de cada array de atributos.
#define POSITION_ATTRIB_OFFSET 0
#define NORMAL_ATTRIB_OFFSET (NUM_VERTICES*3)
#define TEXCOORD_ATTRIB_OFFSET (NUM_VERTICES*(3+3))
#define COLOR_ATTRIB_OFFSET (NUM_VERTICES*(3+3+2))

// Grande buffer contendo os 4 arrays.
float buffer[NUM_VERTICES*12];

Tenho a tendência a usar o primeiro modelo (AoS) pelo simples motivo que, para achar um atributo qualquer, basta calcular apenas um offset e obter o ponteiro para a estrutura. No segundo modelo teríamos que manter ponteiros auxiliares para cada array:

float *aux_vpos_ptr = buffer + POSITION_ATTRIB_OFFSET;
float *aux_normal_ptr = buffer + NORMAL_ATTRIB_OFFSET;
float *aux_texcoord_ptr = buffer + TEXCOORD_ATTRIB_OFFSET;
float *aux_color_ptr = buffer + COLOR_ATTRIB_OFFSET;

O motivo da minha preferência também é embasada em informação não muito bem documentadas: Parece que o VBO montado de maneira “intercalada” (AoS) apresenta melhor performance… Parece que OpenGL vai intercalar os atributos antes de apresentá-los aos shaders… Fato ainda a ser medido e confirmado, amigos!

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 );
}