Projeto Virtual Worlds

Computação Gráfica 3D em C

Arquivos da Categoria: Comentário

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.

Anúncios

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

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

Dois casos de clipping que parecem complicados (mas não são!)

Um amigo me chama a atenção para os dois casos abaixo:

Caso 1

Caso 1

Caso 2

Caso 2

Em ambos os casos acabaremos com 3 triângulos depois dos recortes. Notem que existem 2 planos de clipping (n1n2). Isso significa que a explicação que dei no último post sobre esse assunto (este aqui) continua válido, basta aplicar as regras um plano por vez. Verifique que no caso 1, por exemplo, se escolhermos primeiro o plano n1, obteremos 2 triângulos. Depois, ambos terão que ser recortados pelo plano n2, (experimente!).

É claro que poderíamos tornar o caso 1 especial. Afinal, basta dividir o quadrilátero formado no primeiro quadrante em dois triângulos. Mas, neste caso, teríamos outros casos “especiais” a considerar (por exemplo, coloque o ponto A, do triângulo do caso 2, no terceiro quadrante, ou seja, atrás de ambos os planos, mantendo os pontos B e C no lugar onde estão!).

 

Um padrão para o estilo desenvolvimento do projeto

Olá, gente! Quanto tempo! O projeto não morreu não, só deu uma parada para refrescar a cabeça e para mais estudos. Não me lembro se eu já disse isso aqui, mas estou estudando mais à fundo o GLSL e OpenCL.

Para não parecer que eu sumi de vez, eis um padrão que estou usando no desenvolvimento da biblioteca base do Virtual Worlds.

  • Todo o código será construído em C, não em C++;
  • O código será totalmente modular, ou seja, existem pacotes separados. Até o momento tenho: MathLib, Graphics, MemoryManager, um pedaço do NetworkIO e um pedacinho do Effects (depth of field, motion blur, shadows, bump e parallax mapping, etc). Ainda faltam muitos módulos, com o Shaders e SceneGraph, Lighting, entre outros;
  • Todas as funções são nomeadas a partir do nome do módulo.

Com relação aos nomes das funções. Eis um exemplo de como os headers ficaram:

/* module: vec3.h */
#ifndef _VEC3_H_
#define _VEC3_H_

#include <stdbool.h>

typedef struct vec3_s {
  float x, y, z;
} vec3_t;

void vec3_zero(vec3_t *v_p);
void vec3_assign(vec3_t *vout_p, float x, float y, float z);
void vec3_copy(vec3_t *vout_p, vec3_t *v_p);
void vec3_insert(vec3_p *vout_p, vec3_t *v_p);

float vec3_length(vec3_t *v_p);

void vec3_add(vec3_t *vout_p, vec3_t *v1_p, vec3_t *v2_p);
void vec3_sub(vec3_t *vout_p, vec3_t *v1_p, vec3_t *v2_p);
void vec3_scale(vec3_t *vout_p, vec3_t *v_p, float scale);

float vec3_dot(vec3_t *v1_p, vec3_t *v2_p);
void  vec3_cross(vec3_t *vout_p, vec3_t *v1_p, vec3_t *v2_p);

/* Recebe o vector e retorna-o nomrmalizado */
void vec3_normalize(vec3_t *v_p);

bool vec3_is_equal(vec3_t *v1_p, vec3_t *v2_p);
bool vec3_is_oposite(vec3_t *v1_p, vec3_t *v2_p);
bool vec3_is_invserted(vec3_t *v1_p, vec3_t *v2_p);

#endif /* _VEC3_H_ */

Diferente de códigos como o do Quake III, eu uso o padrão de C para assinalamento. Quer dizer, os parâmetros que receberão a alterações, se houverem mais que um, são os primeiros da lista. Isso me permitirá usar número de parâmetros variáveis (usar “…” nas funções), se necessário.

Ao invés de trabalhar com arrays como tipo primário para os vetores, defino typedefs específicos, como mostrado acima. De fato, para cada tipo de “objeto”, defino um tipo e o nomeio com sufixo “_t”, que denota typedefs. O sufixo “_s” é usado para structs. Do mesmo jeito, “_e” significa enums.

Nomes de variáveis são sempre uma descrição abreviada e, se houverem espaços, estes são substituídos por um undescore (“_”), como no exemplo abaixo:

int num_vertices;
vec3_t vnormal;
void frustum_get_from_opengl(plane_t *pout_p);

Note que o sufixo “_p” não é usado para tipos, mas para variáveis do tipo ponteiro. Se for necessário usar ponteiros para ponteiros, adiciono um ‘p’ extra:

object_t **objects_list_pp;

Faço isso porque, sinceramente, a tal da Notação Húngara, usada ad nauseum pela Microsoft já encheu o meu pobre e velho saquinho. Mas ela não é inútil… assim, uso uma variação que me agrada mais, colocando a descrição da variável no início…

E, falando em nomes de variáveis, aquelas que uso como iterators são nomeadas, na maioria das vezes, como um único caracter: i, j, k, w, etc… exceto quando o código ficar confuso…

Outra prática é evitar a supressão de variáveis auxiliares. Por exemplo, o fragmento abaixo ilustra o jeito “errado” e depois o “certo”, no meu código:

/* ERRADO */
if (plane_distance_from_point(plane_p, pt_p) < 0.0f)
{
  ...
}

/* CERTO */
float distance;

distance = plane_distance_from_point(plane_p, pt_p);
if (distance < 0.0f)
{
  ...
}

Essa variável extra ai não faz mal algum. O compilador provavelmente se livrará dela…

É isso… depois coloco aqui mais informações sobre o estilo de desenvolvimento do projeto…

Shaders

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

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

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

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

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

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

SDL, GLUT ou GLFW?

Para começar, de fato, a brincadeira é interessante que você aprenda como inicializar o contexto do OpenGL, bem como ajusta o framebuffer e outras coisinhas. Isso pode ser uma tarefa complicada, especialmente se quiser fazê-lo para multiplas plataformas. Para te ajudar um pouco existem algumas bibliotecas: SDL (Simple Direct Layer), GLUT (GL Utitilities Library), GLFW (GL FrameWork), etc.

SDL é uma das bibliotecas mais usadas por ai, mas possui, até a versão 1.2, alguns drawbacks, especialmente no Linux. Pela minha experiência, SDL é lenta, especialmente se você pretende usar muito cópia de blocos (texturas). O GLUT é genérico e usado, inclusive, nos exemplos da documentação da Kronos (“dona” do OpenGL), só que ajustar o contexto do OpenGL e o framebuffer é algo que o GLUT não faz muito bem (até a versão 3.7…). Esperimente ajustar o modo fullscreen numa resolução desejada sem usar funções “não-documentadas” dp GLUT!

A melhor biblioteca minimalista que pude achar, até agora, é o GLFW 2.7 (O 2.6 é muito bem e disponível nos repositórios do Ubuntu, mas não implementa FSAA — Full Scene AntiAliasing — muito bem). A outra vantagem do GLFW sobre o SDL e o GLUT é que ele um projeto open source atualizado com mais frequẽncia que os demais. Dêem uma olhada na rotina de inicialização de meus projetos:

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

void quit(void);

int main(void)
{
  if (glfwInit() != GL_TRUE)
  { fprintf(stderr, "ERROR initializing GLFW.\n"); return 1; }

  atexit(quit);

  /* Vou usar 4 samples para antialiasing! */
  glfwOpenWindowHint( GLFW_FSAA_SAMPLES, 4);

  if (glfwOpenWindow( 1024, 768,   /* Width, Height */
                      8, 8, 8, 8,  /* RGBA bit sizes */
                      24,          /* Depth buffer bit size */
                      0,           /* Stencil buffer size */
                      GLFW_FULLSCREEN ) != GL_TRUE)
  { fprintf(stderr, "ERROR creating framebuffer\n"); return 1; }

  oglInit();

  loop();

  return 0;
}

void quit(void) { glfwTerminate(); }

Eu acho o GLFW mais simples que o SDL porque ele não tem funções para BitBliting, por exemplo. Se vocẽ está usando OpenGL, então use glDrawPixels ou glCopyPixels, pôxa!

Problema de resolução (complicada?!)

Uma das coisas que tenho visto em códigos-exemplo usando OpenGL é que não há quaisquer preocupações com o aspect ratio do monitor. Isto é, pouca atenção é dada ao fato do monitor ser wide screen ou letter box. A diferença entre os dois formatos é muito grande.

Letter box sobre Wide screen

Wide screen sobre Letter box


Num monitor com geometria letter box, o pixel é quadrado para o formato letter box. Mas, um formato wide screen embutido numa geometria letter box causa uma deformação nos pixels, achatando-os no sentido horizontal… Isso porque, no formato wide screen, o pixel é quadrado para a geometria wide screen, mas, no formato letter box, embutido, o pixel fica mais “alto” do que largo.

As figuras assumem que os pixels serão quadrados em ambas as geometrias e formatos, mas, para isso, parte da área útil do monitor é “perdida”. Você já deve ter visto esse efeito quando assiste DVDs numa TV com geometria letter box e o filme é wide screen (causando o que é mostrado na segunda figura) ou quando assiste canais de TV aberta, não HD, numa TV wide screen (causando o que é mostrado na primeira figura).

O Virtual Worlds tentará, sempre que possível, usar o formato da segunda figura – isso porque, neste caso, a “perda” da área é menor que no primeiro caso… No segundo caso (Wide screen over Letter Box), há uma perda de 25% da área e este caso é mais familiar para o usuário do que o primeiro. No primeiro caso, a perda chega próxima à 33% da tela (repare na barra escura mais “grossa” – as figuras estão em escala!).

A rotina, em pseudo-código, ficaria assim:

Obtém o aspect-ratio da geometria do monitor;
Se é Wide screen, usa a tela toda;
Se é Letter box, usa 75% da tela, como mostrado na segunda figura.

Para manter o código portável, teremos 3 rotinas diferentes para determinar a geometria do monitor: Uma para o Linux, uma para o Windows e outra para o OS/X (supondo que o BSD use método diferente do Linux).

Além disso, dois métodos podem ser usados para desenhar, quando a geometria for Letter Box: Uma é usando valores apropriados para a rotina glViewport; Outra é usando o stencil buffer, mascarando as áreas superior e inferior da tela.

Assim que eu tiver essas rotinas up and running, posto aqui pra vocês verem…

Primeiras decisões

O projeto surgiu com a idéia de desenvolver um ambiente 3D onde eu pudesse modelar o “mundo real” e vender um “jogo” baseado neste modelo. Uso a palavra jogo aqui como ponto de entendimento comum do que o sistema fará. Quer dizer, o usuário poderá passear pelos mundos virtuais (daí o nome do projeto), interagindo como se estivesse num jogo de tiro em primeira pessoa (FPS).

Inicialmente o engine não tem suporte para web. Na medida que HTML5 vai amadurecendo e o WebGL for sendo suportado, poderei pensar em maneiras de portar a engine. A decisão inicial é esta: A engine funcionará como um jogo, sem suporte para web.

A segunda decisão importante é o ambiente o desenvolvimento do sistema será feito. Há anos venho tentando encontrar um ambiente que possa ser altamente portável. Java e outras linguagens gerenciáveis não me atraem (lembrem-se que sou um HPJ – High Performance Junkie), embora pudesse ser uma escolha óbvia do ponto de vista da portabilidade. Também não me sinto atraído pelo ambiente de janelas da Microsoft ou seus compiladores porque seria uma maneira de manter o produto final restrito a um único ambiente, indo contra a corrente de minha intenção.

O ambiente Linux (especialemnte o Ubuntu, derivado da distribuição Debian) me permite desenvolver para as três maiores plataformas atuais: Linux, Windows e OS/X, através de um conjunto de ferramentas que foram originalmente desenvolvidas para um port do Unix chamado GNU/Hurd e que foi adotado pelo Linux com perfeição. Sendo assim, a segunda decisão é óbvia: O ambiente de desenvolvimento será o Linux, por causa do projeto GNU, especialmente do compilador GCC (que é multiplataforma!).

Meu ambiente de software para o desenvolvimento do projeto é bem singelo e pode ser usado por qualquer pessoa:

  • Linux (Ubuntu);
  • GCC 4.5.2 ou superior;
  •  GDB 7.2 ou superior;
  • Placa de vídeo nVidia série 8 ou superior (com suporte, pelo menos, ao OpenGL 2.1);
  • Editor: VIM 7,3 iy superior;

Neste início de projeto usarei apenas uma biblioteca auxiliar para o gerenciamento de contextos do OpenGL e de janelas. Trata-se do GLFW (GL FrameWork), que é bem mais simples do que o SDL, pode ser linkado estaticamente e apresenta algumas rotinas simples para o manuseio de janelas (ou seja, do framebuffer), teclado, mouse e joystick. Na minha opinião o SDL é muito lento e tem alguns bugs esquisitos com o Linux. Exceto por essa interface simples com o hardware, toda a interação gráfica entre o engine e a placa de vídeo será feita via OpenGL.

Outras bibliotecas serão usadas no projeto: ZLib, libpng, libjpeg, libxml2, … Mas, à medida que forem sendo usadas, apresentarei os requisitos mínimos e as alternativas no Windows e no OS/X (quando possível).

Outra coisa: Este não é um projeto de curta duração. Existe muita estrada para trilhar. A intenção é, como em qualquer projeto complexo, começar simples e ir adicionando grandes features na medida que eu for andando, mas isso leva tempo, esforço e estudos. E uma dose cavalar de paciência também. Sem contar que este é, até o momento, um projeto de um homem só (eu!). Tenho que lidar com um monte de coisas ao mesmo tempo só para este projeto: Aprender a modelar objetos (no Blender 2.5), dissecar códigos feitos por outros (que é uma excelente maneira de aprender), rever disciplinas como Algebra Linear, Geometria Analítica, Equações diferenciais (alguns efeitos interessantes, em 3D, precisam dessa base), rever/reaprender algorítimos clássicos como árvores binárias (e seus diversos tipos)… Dá pra dar uma canseira!

Em que ponto do projeto estou?! Ainda no início… por isso ainda não “soltei” nenhum código-fonte ainda (ahhhh…. sim… pretendo que o projeto seja Open Source!). No momento preciso arrumar um jeito de importar uma cena, usando o Blender, agregando os planos divisores e portais numa BSP Tree. Estou estudando modelamento de objetos e ainda decidindo sobre o padrão que usarei para exportar as cenas (Collada? X3D? Quake MAP? ou será proprietário?!).

Nos próximos posts falarei do embasamento matemático necessário para você começar a pensar em 3D. Um pouquinho de algebra vetorial…