Projeto Virtual Worlds

Computação Gráfica 3D em C

Arquivos Mensais: julho 2014

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!

Anúncios

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…